diff --git a/.gitignore b/.gitignore index 27067dc22..63a113838 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ # Cargo / rustc artifacts /target +# Python artifacts +__pycache__/ + # Local config .env venv/ +.venv # JetBrains .idea diff --git a/Cargo.lock b/Cargo.lock index 68f6b9043..9a7481f0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -835,6 +846,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +[[package]] +name = "matrixmultiply" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +dependencies = [ + "rawpointer", +] + [[package]] name = "memchr" version = "2.5.0" @@ -901,6 +921,20 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", + "serde", +] + [[package]] name = "nom" version = "7.1.3" @@ -1011,6 +1045,21 @@ dependencies = [ "libc", ] +[[package]] +name = "numpy" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a462c1af5ba1fddec1488c4646993a23ae7931f9e170ccba23e9c7c834277797" +dependencies = [ + "ahash", + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", +] + [[package]] name = "once_cell" version = "1.17.0" @@ -1153,6 +1202,30 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.50" @@ -1303,9 +1376,11 @@ dependencies = [ "futures", "hex", "indexmap", + "itertools", "lazy_static", "log", "maplit", + "ndarray", "num", "qcs-api", "qcs-api-client-common", @@ -1320,6 +1395,7 @@ dependencies = [ "serde_json", "simple_logger", "tempfile", + "test-case", "thiserror", "tokio", "toml 0.5.11", @@ -1394,6 +1470,7 @@ dependencies = [ name = "qcs-sdk-python" version = "0.5.0-rc.6" dependencies = [ + "numpy", "pyo3", "pyo3-asyncio", "pyo3-build-config", @@ -1464,6 +1541,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1552,9 +1635,9 @@ dependencies = [ [[package]] name = "rigetti-pyo3" -version = "0.1.0-rc.4" +version = "0.1.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747db1eb6c058767a5528363b21e2a32eef3fa504238c7b643cac68d98b260c8" +checksum = "8a218a8bb8c01486e4c2e4f01db65fc01174898618512027b54b7c32c4d327bd" dependencies = [ "num-complex", "num-traits", @@ -1899,6 +1982,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "test-case" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d6cf5a7dffb3f9dceec8e6b8ca528d9bd71d36c9f074defb548ce161f598c0" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-macros" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26" +dependencies = [ + "cfg-if", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "1.0.38" diff --git a/Cargo.toml b/Cargo.toml index c9beaea7a..996f7854c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,23 @@ [workspace] members = ["crates/*"] +[workspace.dependencies] +qcs-api = "0.2.1" +qcs-api-client-common = "0.4.2" +qcs-api-client-grpc = "0.4.2" +qcs-api-client-openapi = "0.5.2" +quil-rs = "0.15" +serde_json = "1.0.86" +tokio = "1.24.2" + +# ndarray is used by the `qcs` crate, but it is also used in the `python` crate via a +# re-export through the numpy crate. They should be updated as a pair to keep both +# crates version of ndarray in sync. +# Similarly, pyo3 packages (`numpy`, `rigetti-pyo3`, `pyo3*`) track versions together +# and need to be updated together. +ndarray = { version = "0.15.6", features = ["serde"] } +numpy = "0.17" +pyo3 = { version = "0.17", features = ["extension-module"] } +pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] } +pyo3-build-config = { version = "0.17" } +rigetti-pyo3 = { version = "0.1.0-rc.4", features = ["extension-module", "complex"] } diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 76aaaaa11..66f7b489a 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -19,32 +19,35 @@ futures = "0.3.24" indexmap = "1.9.1" lazy_static = "1.4.0" log = "0.4.17" +ndarray.workspace = true num = { version = "0.4.0", features = ["serde"] } -qcs-api = "0.2.1" -qcs-api-client-common = "0.4.2" -qcs-api-client-openapi = "0.5.2" -qcs-api-client-grpc = "0.4.2" -quil-rs = "0.15" +qcs-api.workspace = true +qcs-api-client-common.workspace = true +qcs-api-client-openapi.workspace = true +qcs-api-client-grpc.workspace = true +quil-rs.workspace = true reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "json"] } rmp-serde = "1.1.1" serde = { version = "1.0.145", features = ["derive"] } serde_bytes = "0.11.7" -serde_json = "1.0.86" +serde_json.workspace = true thiserror = "1.0.37" -tokio = { version = "1.24.2", features = ["fs"] } +tokio = { workspace = true, features = ["fs"] } toml = "0.5.9" uuid = { version = "1.2.1", features = ["v4"] } tonic = { version = "0.8.2", features = ["tls", "tls-roots"] } zmq = { version = "0.9.2", features = ["vendored"] } +itertools = "0.10.5" [dev-dependencies] erased-serde = "0.3.23" float-cmp = "0.9.0" hex = "0.4.3" maplit = "1.0.2" -qcs-api-client-grpc = { version = "0.4.2", features = ["server"] } +qcs-api-client-grpc = { workspace = true, features = ["server"] } simple_logger = { version = "2.3.0", default-features = false } tempfile = "3.3.0" tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } warp = { version = "0.3.3", default-features = false } regex = "1.7.0" +test-case = "2.2.2" diff --git a/crates/lib/examples/execute.rs b/crates/lib/examples/execute.rs index fe628d97b..4c7d5b97d 100644 --- a/crates/lib/examples/execute.rs +++ b/crates/lib/examples/execute.rs @@ -17,7 +17,7 @@ async fn main() { let result = exe .with_parameter("theta", 0, PI) - .execute_on_qpu("Aspen-11") + .execute_on_qpu("Aspen-M-3") .await .expect("Program should execute successfully"); diff --git a/crates/lib/examples/local.rs b/crates/lib/examples/local.rs index 903f286b4..4b40e93ee 100644 --- a/crates/lib/examples/local.rs +++ b/crates/lib/examples/local.rs @@ -18,9 +18,9 @@ async fn main() { let result = exe .with_parameter("theta", 0, PI) - .execute_on_qpu("Aspen-11") + .execute_on_qpu("Aspen-M-3") .await .expect("Program should execute successfully"); - println!("{:?}", result); + println!("{result:?}"); } diff --git a/crates/lib/examples/parametric_compilation.rs b/crates/lib/examples/parametric_compilation.rs index f699cc8f6..eb62b4178 100644 --- a/crates/lib/examples/parametric_compilation.rs +++ b/crates/lib/examples/parametric_compilation.rs @@ -5,7 +5,6 @@ use std::f64::consts::PI; use std::time::Duration; use qcs::Executable; -use qcs_api_client_grpc::models::controller::{readout_values::Values, IntegerReadoutValues}; const PROGRAM: &str = r#" DECLARE ro BIT @@ -30,32 +29,30 @@ async fn main() { let theta = step * f64::from(i); let data = exe .with_parameter("theta", 0, theta) - .execute_on_qpu("Aspen-11") + .execute_on_qpu("Aspen-M-3") .await .expect("Executed program on QPU"); total_execution_time += data .duration - .expect("Aspen-11 should always report duration"); + .expect("Aspen-M-3 should always report duration"); - let ro_readout_data = data - .readout_data - .get_readout_values_for_field("ro") + let first_ro_values = data + .result_data + .to_register_map() + .expect("should be able to create a RegisterMap") + .get_register_matrix("ro") .expect("readout values should contain 'ro'") - .expect("'ro' should contain readout values"); - let first_ro_data = ro_readout_data - .first() - .expect("'ro' should contain at least one readout value") - .clone(); - let first_ro_values = first_ro_data - .expect("first readout value should ") - .values - .expect("'ro' should have readout values"); - if let Values::IntegerValues(IntegerReadoutValues { mut values }) = first_ro_values { - parametric_measurements.append(&mut values) + .as_integer() + .expect("'ro' should be a register of integer values") + .row(0) + .to_owned(); + + for value in &first_ro_values { + parametric_measurements.push(*value) } } - println!("Total execution time: {:?}", total_execution_time); + println!("Total execution time: {total_execution_time:?}"); for measurement in parametric_measurements { if measurement == 1 { diff --git a/crates/lib/examples/quil_t.rs b/crates/lib/examples/quil_t.rs index 3779bf4d6..a4bc53345 100644 --- a/crates/lib/examples/quil_t.rs +++ b/crates/lib/examples/quil_t.rs @@ -3,7 +3,6 @@ //! [pyquil]: https://pyquil-docs.rigetti.com/en/stable/quilt_getting_started.html#Another-example:-a-simple-T1-experiment use qcs::Executable; -use qcs_api_client_grpc::models::controller::{readout_values::Values, IntegerReadoutValues}; /// This program doesn't do much, the main point is that it will fail if quilc is invoked. const PROGRAM: &str = r#" @@ -20,17 +19,17 @@ async fn main() { let result = exe .compile_with_quilc(false) - .execute_on_qpu("Aspen-11") + .execute_on_qpu("Aspen-M-3") .await .expect("Program should execute successfully") - .readout_data - .get_readout_values("ro".to_string(), 0) - .expect("Readout data should include 'ro'") - .values - .expect("Readout data should include values"); + .result_data + .to_register_map() + .expect("should be able to convert execution data to RegisterMap") + .get_register_matrix("ro") + .expect("Register data should include 'ro'") + .as_integer() + .expect("ro should be a register of integer values") + .to_owned(); - match result { - Values::IntegerValues(IntegerReadoutValues { values }) => assert!(!values.is_empty()), - _ => panic!("expected IntegerReadoutValues, got {:?}", result), - } + println!("{result:?}"); } diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs index dd0b2acf7..553a43667 100644 --- a/crates/lib/src/api.rs +++ b/crates/lib/src/api.rs @@ -63,7 +63,7 @@ pub enum RewriteArithmeticError { Rewrite(#[from] rewrite_arithmetic::Error), } -/// The result of a call to [`rewrite_arithmetic`] which provides the +/// The result of a call to [`rewrite_arithmetic()`] which provides the /// information necessary to later patch-in memory values to a compiled program. #[derive(Clone, Debug, Serialize)] pub struct RewriteArithmeticResult { @@ -122,7 +122,7 @@ pub struct TranslationResult { /// /// # Errors /// -/// Returns a [`translation::Error`] if translation fails. +/// Returns a [`TranslationError`] if translation fails. pub async fn translate( native_quil: &str, shots: u16, @@ -255,7 +255,10 @@ impl From for ExecutionResult { c.values .iter() .map(|c| { - Complex::::new(c.real.unwrap_or(0.0), c.imaginary.unwrap_or(0.0)) + Complex::::new( + c.real.unwrap_or_default(), + c.imaginary.unwrap_or_default(), + ) }) .collect(), ), @@ -300,7 +303,7 @@ impl From for ExecutionResults { /// /// # Errors /// -/// May error if a [`gRPC`] client cannot be constructed, or a [`gRPC`] +/// May error if a [`Qcs`] client cannot be constructed, or if the `gRPC` /// call fails. pub async fn retrieve_results( job_id: &str, diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs index 78ea5c35a..8f272ef1c 100644 --- a/crates/lib/src/executable.rs +++ b/crates/lib/src/executable.rs @@ -9,7 +9,7 @@ use std::time::Duration; use qcs_api_client_common::configuration::LoadError; use qcs_api_client_common::ClientConfiguration; -use crate::execution_data; +use crate::execution_data::{self, ResultData}; use crate::qpu::client::Qcs; use crate::qpu::quilc::CompilerOpts; use crate::qpu::rewrite_arithmetic; @@ -25,7 +25,7 @@ use quil_rs::Program; /// /// ```rust /// use qcs_api_client_common::ClientConfiguration; -/// use qcs::{Executable, RegisterData}; +/// use qcs::Executable; /// /// /// const PROGRAM: &str = r##" @@ -41,12 +41,27 @@ use quil_rs::Program; /// #[tokio::main] /// async fn main() { /// let mut result = Executable::from_quil(PROGRAM).with_config(ClientConfiguration::default()).with_shots(4).execute_on_qvm().await.unwrap(); -/// // We know it's i8 because we declared the memory as `BIT` in Quil. /// // "ro" is the only source read from by default if you don't specify a .read_from() -/// let data = result.registers.remove("ro").expect("Did not receive ro data").into_i8().unwrap(); -/// // In this case, we ran the program for 4 shots, so we know the length is 4. -/// assert_eq!(data.len(), 4); -/// for shot in data { +/// +/// // We first convert the readout data to a [`RegisterMap`] to get a mapping of registers +/// // (ie. "ro") to a [`RegisterMatrix`], `M`, where M[`shot`][`index`] is the value for +/// // the memory offset `index` during shot `shot`. +/// // There are some programs where QPU readout data does not fit into a [`RegisterMap`], in +/// // which case you should build the matrix you need from [`QpuResultData`] directly. See +/// // the [`RegisterMap`] documentation for more information on when this transformation +/// // might fail. +/// let data = result.result_data +/// .to_register_map() +/// .expect("should convert to readout map") +/// .get_register_matrix("ro") +/// .expect("should have data in ro") +/// .as_integer() +/// .expect("should be integer matrix") +/// .to_owned(); +/// +/// // In this case, we ran the program for 4 shots, so we know the number of rows is 4. +/// assert_eq!(data.nrows(), 4); +/// for shot in data.rows() { /// // Each shot will contain all the memory, in order, for the vector (or "register") we /// // requested the results of. In this case, "ro" (the default). /// assert_eq!(shot.len(), 2); @@ -144,20 +159,30 @@ impl<'executable> Executable<'executable, '_> { /// .execute_on_qvm() /// .await /// .unwrap(); - /// let first = result - /// .registers - /// .remove("first") - /// .expect("Did not receive first buffer") - /// .into_f64() - /// .expect("Received incorrect data type for first"); - /// let second = result - /// .registers - /// .remove("second") - /// .expect("Did not receive second buffer") - /// .into_f64() - /// .expect("Received incorrect data type for second"); - /// assert_eq!(first[0][0], 3.141); - /// assert_eq!(second[0][0], 1.234); + /// let first_value = result + /// .result_data + /// .to_register_map() + /// .expect("qvm memory should fit readout map") + /// .get_register_matrix("first") + /// .expect("readout map should have 'first'") + /// .as_real() + /// .expect("should be real numbered register") + /// .get((0, 0)) + /// .expect("should have value in first position of first register") + /// .clone(); + /// let second_value = result + /// .result_data + /// .to_register_map() + /// .expect("qvm memory should fit readout map") + /// .get_register_matrix("second") + /// .expect("readout map should have 'second'") + /// .as_real() + /// .expect("should be real numbered register") + /// .get((0, 0)) + /// .expect("should have value in first position of first register") + /// .clone(); + /// assert_eq!(first_value, 3.141); + /// assert_eq!(second_value, 1.234); /// } /// ``` #[must_use] @@ -202,9 +227,27 @@ impl<'executable> Executable<'executable, '_> { /// .with_parameter("theta", 0, theta) /// .with_parameter("theta", 1, theta * 2.0) /// .execute_on_qvm().await.unwrap(); - /// let data = result.registers.remove("theta").expect("Could not read theta").into_f64().unwrap(); - /// assert_eq!(data[0][0], theta); - /// assert_eq!(data[0][1], theta * 2.0); + /// let theta_register = result + /// .result_data + /// .to_register_map() + /// .expect("should fit readout map") + /// .get_register_matrix("theta") + /// .expect("should have theta") + /// .as_real() + /// .expect("should be real valued register") + /// .to_owned(); + /// + /// let first = theta_register + /// .get((0, 0)) + /// .expect("first index, first shot of theta should have value") + /// .to_owned(); + /// let second = theta_register + /// .get((0, 1)) + /// .expect("first shot, second_index of theta should have value") + /// .to_owned(); + /// + /// assert_eq!(first, theta); + /// assert_eq!(second, theta * 2.0); /// } /// } /// ``` @@ -240,10 +283,8 @@ impl<'executable> Executable<'executable, '_> { } } -/// The [`Result`] from executing on the QVM. -pub type ExecuteResultQVM = Result; -/// The [`Result`] from executing on a QPU. -pub type ExecuteResultQPU = Result; +/// The [`Result`] from executing on a QPU or QVM. +pub type ExecutionResult = Result; impl Executable<'_, '_> { /// Specify a number of times to run the program for each execution. Defaults to 1 run or "shot". @@ -284,12 +325,12 @@ impl Executable<'_, '_> { /// /// # Returns /// - /// A `HashMap` where the key is the name of the register that was read from (e.g. "ro"). + /// An [`ExecutionResult`]. /// /// # Errors /// /// See [`Error`]. - pub async fn execute_on_qvm(&mut self) -> ExecuteResultQVM { + pub async fn execute_on_qvm(&mut self) -> ExecutionResult { let config = self.get_config().await?; let mut qvm = if let Some(qvm) = self.qvm.take() { @@ -303,8 +344,8 @@ impl Executable<'_, '_> { self.qvm = Some(qvm); result .map_err(Error::from) - .map(|registers| execution_data::Qvm { - registers, + .map(|registers| execution_data::ExecutionData { + result_data: ResultData::Qvm(registers), duration: None, }) } @@ -370,10 +411,10 @@ impl<'execution> Executable<'_, 'execution> { /// /// # Returns /// - /// A `HashMap` where the key is the name of the register that was read from (e.g. "ro"). + /// An [`ExecutionResult`]. /// /// # Errors - /// All errors are human readable by way of [`mod@eyre`]. Some common errors are: + /// All errors are human readable by way of [`mod@thiserror`]. Some common errors are: /// /// 1. You are not authenticated for QCS /// 1. Your credentials don't have an active reservation for the QPU you requested @@ -382,7 +423,7 @@ impl<'execution> Executable<'_, 'execution> { /// 1. Missing parameters that should be filled with [`Executable::with_parameter`] /// /// [quilc]: https://github.com/quil-lang/quilc - pub async fn execute_on_qpu(&mut self, quantum_processor_id: S) -> ExecuteResultQPU + pub async fn execute_on_qpu(&mut self, quantum_processor_id: S) -> ExecutionResult where S: Into>, { @@ -429,10 +470,7 @@ impl<'execution> Executable<'_, 'execution> { /// # Errors /// /// See [`Executable::execute_on_qpu`]. - pub async fn retrieve_results( - &mut self, - job_handle: JobHandle<'execution>, - ) -> ExecuteResultQPU { + pub async fn retrieve_results(&mut self, job_handle: JobHandle<'execution>) -> ExecutionResult { let qpu = self.qpu_for_id(job_handle.quantum_processor_id).await?; qpu.retrieve_results(job_handle.job_id, job_handle.readout_map) .await @@ -501,7 +539,7 @@ pub enum Error { /// [`Executable::retrieve_results`] can invalidate the handle. #[error("The job handle was not valid")] InvalidJobHandle, - /// Occurs when failing to construct a [`QcsClient`]. + /// Occurs when failing to construct a [`Qcs`] client. #[error("The QCS client configuration failed to load")] QcsConfigLoadFailure(#[from] LoadError), } diff --git a/crates/lib/src/execution_data.rs b/crates/lib/src/execution_data.rs index e5f602a37..6d3973841 100644 --- a/crates/lib/src/execution_data.rs +++ b/crates/lib/src/execution_data.rs @@ -1,21 +1,66 @@ -use std::collections::HashMap; +use enum_as_inner::EnumAsInner; +use num::complex::Complex64; +use quil_rs::program::SyntaxError; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; -use std::num::{ParseIntError, TryFromIntError}; +use std::num::TryFromIntError; use std::str::FromStr; use std::time::Duration; -use qcs_api_client_grpc::models::controller::ReadoutValues; -use quil_rs::instruction::MemoryReference; +use itertools::Itertools; +use ndarray::prelude::*; -use crate::RegisterData; +use crate::{ + qpu::{QpuResultData, ReadoutValues}, + qvm::QvmResultData, + RegisterData, +}; -/// The result of executing an [`Executable`](crate::Executable) via -/// [`Executable::execute_on_qvm`](crate::Executable::execute_on_qvm). +/// Represents the two possible types of data returned from either the QVM or a real QPU. +/// Each variant contains the original data returned from its respective executor. +/// +/// # Usage +/// +/// Your usage of [`ResultData`] will depend on the types of programs you are running and where. +/// The `to_register_map()` method will attempt to build a [`RegisterMap`] out of the data, where each +/// register name is mapped to a 2-dimensional rectangular [`RegisterMatrix`] where each row +/// represents the final values in each register index for a particular shot. This is often the +/// desired form of the data and it is _probably_ what you want. This transformation isn't always +/// possible, in which case `to_register_map()` will return an error. +/// +/// To understand why this transformation can fail, we need to understand a bit about how readout data is +/// returned from the QVM and from a real QPU: +/// +/// The QVM treats each `DECLARE` statement as initialzing some amount of memory. This memory works +/// as one might expect it to. It is zero-initialized on each shot, and subsequent writes to the same region +/// overwrite the previous value. The QVM returns memory at the end of every shot. This means +/// we get the last value in every memory reference for each shot, which is exactly the +/// representation we want for a [`RegisterMatrix`]. For this reason, `to_register_map()` should +/// always succeed for [`ResultData::Qvm`]. +/// +/// The QPU on the other hand doesn't use the same memory model as the QVM. Each memory reference +/// (ie. "ro\[0\]") is more like a stream than a value in memory. Every `MEASURE` to a memory +/// reference emits a new value to said stream. This means that the number of values per memory +/// reference can vary per shot. For this reason, it's not always clear what the final value in +/// each shot was for a particular reference. When this is the case, `to_register_map()` will return +/// an error as it's impossible to build a correct [`RegisterMatrix`] from the data without +/// knowing the intent of the program that was run. Instead, it's recommended to build the +/// [`RegisterMatrix`] you need from the inner [`QpuResultData`] data using the knowledge of your +/// program to choose the correct readout values for each shot. +#[derive(Debug, Clone, PartialEq, EnumAsInner)] +pub enum ResultData { + /// Data returned from the QVM, stored as [`QvmResultData`] + Qvm(QvmResultData), + /// Readout data returned from the QPU, stored as [`QpuResultData`] + Qpu(QpuResultData), +} + +/// The result of executing an [`Executable`](crate::Executable) #[derive(Debug, Clone, PartialEq)] -pub struct Qvm { - /// The readout data that was read from the [`Executable`](crate::Executable). - /// Key is the name of the register, value is the data of the register after execution. - pub registers: HashMap, +pub struct ExecutionData { + /// The [`ResultData`] that was read from the [`Executable`](crate::Executable). + pub result_data: ResultData, /// The time it took to execute the program on the QPU, not including any network or queueing /// time. If paying for on-demand execution, this is the amount you will be billed for. /// @@ -23,138 +68,344 @@ pub struct Qvm { pub duration: Option, } -/// A mapping of readout fields to their [`ReadoutValues`]. -#[derive(Debug, Clone, PartialEq)] +/// An enum representing every possible register type as a 2 dimensional matrix. +#[derive(Clone, Debug, EnumAsInner, PartialEq, Serialize, Deserialize)] +pub enum RegisterMatrix { + /// Integer register + Integer(Array2), + /// Real numbered register + Real(Array2), + /// Complex numbered register + Complex(Array2), +} + +/// A mapping of a register name (ie. "ro") to a [`RegisterMatrix`] containing the values for the +/// register. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[repr(transparent)] -pub struct ReadoutMap(HashMap); +pub struct RegisterMap(HashMap); + +/// Errors that may occur when trying to build a [`RegisterMatrix`] from execution data +#[allow(missing_docs)] +#[derive(Debug, thiserror::Error)] +pub enum RegisterMatrixConversionError { + /// The data could not be fit into a rectangular matrix + #[error("The data for register {register} does fit into a rectangular matrix")] + InvalidShape { register: String }, + + /// The memory reference had no associated readout values + #[error("The mapping of {memory_reference} to {alias} had no readout values")] + UnmappedAlias { + memory_reference: String, + alias: String, + }, + + /// A row of readout values for a register were missing + #[error("Missing readout values for {register}[{index}]")] + MissingRow { register: String, index: usize }, + + /// The memory reference could not be parsed + #[error("{0}")] + MemoryReferenceParseError(MemoryReferenceParseError), +} + +impl ResultData { + /// Convert [`ResultData`] from its inner representation as [`QvmResultData`] or + /// [`QpuResultData`] into a [`RegisterMap`]. The [`RegisterMatrix`] for each register will be + /// constructed such that each row contains all the final values in the register for a single shot. + /// + /// # Errors + /// + /// Returns a [`RegisterMatrixConversionError`] if the inner execution data for any of the + /// registers would result in a jagged matrix. [`QpuResultData`] data is captured per measure, + /// meaning a value is returned for every measure to a memory reference, not just once per shot. + /// This is often the case in programs that use mid-circuit measurement or dynamic control flow, + /// where measurements to the same memory reference might occur multiple times in a shot, or be + /// skipped conditionally. In these cases, building a rectangular [`RegisterMatrix`] would + /// necessitate making assumptions about the data that could skew the data in undesirable ways. + /// Instead, it's recommended to manually build a matrix from [`QpuResultData`] that accurately + /// selects the last value per-shot based on the program that was run. + pub fn to_register_map(&self) -> Result { + match self { + ResultData::Qvm(data) => RegisterMap::from_qvm_result_data(data), + ResultData::Qpu(data) => RegisterMap::from_qpu_result_data(data), + } + } +} + +impl RegisterMap { + /// Returns the [`RegisterMatrix`] for the given register, if it exists. + #[must_use] + pub fn get_register_matrix(&self, register_name: &str) -> Option<&RegisterMatrix> { + self.0.get(register_name) + } -impl ReadoutMap { - /// Given a known readout field name and index, return the result's [`ReadoutValues`], if any. + /// Returns a [`RegisterMap`] with the underlying [`RegisterMatrix`] data #[must_use] - pub fn get_readout_values(&self, field: String, index: u64) -> Option { - let readout_values = self.0.get(&MemoryReference { name: field, index })?; + pub fn from_hashmap(map: HashMap) -> Self { + Self(map) + } - Some(readout_values.clone()) + /// Returns a [`RegisterMap`] built from [`QvmResultData`] + fn from_qvm_result_data( + result_data: &QvmResultData, + ) -> Result { + Ok(Self( + result_data + .memory + .iter() + .map(|(name, register)| { + let register_matrix = match register { + RegisterData::I8(data) => Array::from_shape_vec( + (data.len(), data.first().map_or(0, Vec::len)), + data.iter().flatten().copied().map(i64::from).collect(), + ) + .map(RegisterMatrix::Integer), + RegisterData::I16(data) => Array::from_shape_vec( + (data.len(), data.first().map_or(0, Vec::len)), + data.iter().flatten().copied().map(i64::from).collect(), + ) + .map(RegisterMatrix::Integer), + RegisterData::F64(data) => Array::from_shape_vec( + (data.len(), data.first().map_or(0, Vec::len)), + data.iter().flatten().copied().collect(), + ) + .map(RegisterMatrix::Real), + RegisterData::Complex32(data) => Array::from_shape_vec( + (data.len(), data.first().map_or(0, Vec::len)), + data.iter() + .flatten() + .copied() + .map(|c| Complex64::new(c.re.into(), c.im.into())) + .collect(), + ) + .map(RegisterMatrix::Complex), + } + .map_err(|_| { + RegisterMatrixConversionError::InvalidShape { + register: name.to_string(), + } + })?; + Ok((name.clone(), register_matrix)) + }) + .collect::, RegisterMatrixConversionError>>( + )?, + )) } - /// Given a known readout field name, return the result's [`ReadoutValues`] for all indices, if any. - pub fn get_readout_values_for_field( - &self, - field: &str, - ) -> Result>>, TryFromIntError> { - let mut readout_values = Vec::new(); - for (memref, values) in &self.0 { - let MemoryReference { name, index } = memref; - let index = usize::try_from(*index)?; - if name == field { - if readout_values.len() <= index { - readout_values.resize(index + 1, None); + /// Attempts to build a [`RegisterMap`] from [`QpuResultData`]. + /// + /// # Errors + /// + /// This fails if the underlying [`QpuResultData`] data is jagged. See [`RegisterMap`] for more + /// detailed explanations of why and when this occurs. + fn from_qpu_result_data( + qpu_result_data: &QpuResultData, + ) -> Result { + let register_map = qpu_result_data + .mappings + .iter() + // Pair all the memory references with their readout values + .map(|(memory_reference, alias)| { + Ok(( + parse_readout_register(memory_reference).map_err(|e| { + RegisterMatrixConversionError::MemoryReferenceParseError(e) + })?, + qpu_result_data.readout_values.get(alias).ok_or_else(|| + RegisterMatrixConversionError::UnmappedAlias { + memory_reference: memory_reference.to_string(), + alias: alias.to_string(), + }, + )?, + )) + }) + // Collect into a type that will sort them by memory reference, this allows us + // to make sure indices are sequential. + .collect::, + RegisterMatrixConversionError, + >>()?; + + // Return an error if any group of memory references don't form a continuous sequence, indicating + // that a row is missing + let mut reference_windows = register_map.keys().tuple_windows().peekable(); + // Ensure the first window starts with a zero index + if let Some((reference_a, _)) = reference_windows.peek() { + if reference_a.index != 0 { + return Err(RegisterMatrixConversionError::MissingRow { + register: reference_a.name.clone(), + index: 0, + }); + } + } + for (reference_a, reference_b) in register_map.keys().tuple_windows() { + if reference_a.name == reference_b.name { + if reference_a.index + 1 != reference_b.index { + return Err(RegisterMatrixConversionError::MissingRow { + register: reference_a.name.clone(), + index: reference_a.index + 1, + }); } - readout_values[index] = Some(values.clone()); + } else if reference_b.index != 0 { + return Err(RegisterMatrixConversionError::MissingRow { + register: reference_b.name.clone(), + index: 0, + }); } } - Ok((!readout_values.is_empty()).then_some(readout_values)) - } + Ok(Self( + // Iterate over them in reverse so we can initialize each RegisterMatrix with the + // correct number of rows + register_map.into_iter().try_rfold( + HashMap::with_capacity(qpu_result_data.readout_values.len()), + |mut register_map, (reference, values)| { + let matrix = + register_map + .entry(reference.name.clone()) + .or_insert(match values { + ReadoutValues::Integer(v) => RegisterMatrix::Integer( + Array2::zeros((v.len(), reference.index + 1)), + ), + ReadoutValues::Complex(v) => RegisterMatrix::Complex( + Array2::zeros((v.len(), reference.index + 1)), + ), + ReadoutValues::Real(v) => RegisterMatrix::Real(Array2::zeros(( + v.len(), + reference.index + 1, + ))), + }); - /// `readout_values` maps program-defined readout to result-defined readout, e.g.: - /// { "ro[0]": "q0", "ro[1]": "q1" } - /// where `ro[0]` is defined in the original program, and `q0` is what comes back in execution results. - /// Here we map the result-defined readout values back to their original result-defined names. - pub(crate) fn from_mappings_and_values( - readout_mappings: &HashMap, - readout_values: &HashMap, - ) -> Self { - let result = readout_values - .iter() - .flat_map(|(readout_name, values)| { - readout_mappings - .iter() - .filter(|(_, program_alias)| *program_alias == readout_name) - .map(|(program_name, _)| program_name.as_ref()) - .map(parse_readout_register) - .map(|reference| Ok((reference?, values.clone()))) - .collect::, MemoryReferenceParseError>>() - }) - .flatten() - .collect::>() - .into(); - - result + // Insert the readout values as a column iff it fits within the + // dimensions of the matrix. Otherwise, the readout data must be + // jagged and we return an error. + match (matrix, values) { + (RegisterMatrix::Integer(m), ReadoutValues::Integer(v)) + if m.nrows() == v.len() => + { + m.column_mut(reference.index) + .assign(&Array::from_vec(v.clone())); + } + (RegisterMatrix::Real(m), ReadoutValues::Real(v)) + if m.nrows() == v.len() => + { + m.column_mut(reference.index) + .assign(&Array::from_vec(v.clone())); + } + (RegisterMatrix::Complex(m), ReadoutValues::Complex(v)) + if m.nrows() == v.len() => + { + m.column_mut(reference.index) + .assign(&Array::from_vec(v.clone())); + } + _ => { + return Err(RegisterMatrixConversionError::InvalidShape { + register: reference.name, + }) + } + } + Ok(register_map) + }, + )?, + )) } } -impl From> for ReadoutMap { - fn from(map: HashMap) -> Self { - Self(map) - } -} - -/// The result of executing an [`Executable`](crate::Executable) via -/// [`Executable::execute_on_qpu`](crate::Executable::execute_on_qpu). -#[derive(Debug, Clone, PartialEq)] -pub struct Qpu { - /// The data of all readout data that were read from - /// (via [`Executable::read_from`](crate::Executable::read_from)). Key is the name of the - /// register, value is the data of the register after execution. - pub readout_data: ReadoutMap, - /// The time it took to execute the program on the QPU, not including any network or queueing - /// time. If paying for on-demand execution, this is the amount you will be billed for. - /// - /// This will always be `None` for QVM execution. - pub duration: Option, +// This is a copy of [`quil_rs::instruction::MemoryReference`] that uses `usize` for the index +// instead of `u64` for compatibility with the containers we use for [`RegisterMap`]. +// It's possible `quil_rs` will use `usize` for its `MemoryReference` in the future. If so, we +// should use it to replace this. +// See https://github.com/rigetti/qcs-sdk-rust/issues/224 +#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] +struct MemoryReference { + name: String, + index: usize, } #[derive(Debug, thiserror::Error)] -pub(crate) enum MemoryReferenceParseError { - #[error("Could not parse memory reference: {reason}")] - InvalidFormat { reason: String }, +pub enum MemoryReferenceParseError { + #[error("{0}")] + InvalidFormat(#[from] SyntaxError), - #[error("Could not parse index from reference name: {0}")] - InvalidIndex(#[from] ParseIntError), + #[error("Could not convert index from u64 to a usize: {0}")] + OversizedIndex(#[from] TryFromIntError), } -// Note: MemoryReference may have a from_string in the fututre fn parse_readout_register( register_name: &str, ) -> Result { - let open_brace = - register_name - .find('[') - .ok_or_else(|| MemoryReferenceParseError::InvalidFormat { - reason: "Opening brace not found".into(), - })?; - let close_brace = - register_name - .find(']') - .ok_or_else(|| MemoryReferenceParseError::InvalidFormat { - reason: "Closing brace not found".into(), - })?; - + let reference = quil_rs::instruction::MemoryReference::from_str(register_name)?; Ok(MemoryReference { - name: String::from(®ister_name[..open_brace]), - index: u64::from_str(®ister_name[open_brace + 1..close_brace])?, + name: reference.name, + index: usize::try_from(reference.index)?, }) } #[cfg(test)] -mod describe_readout_map { +mod describe_register_map { use maplit::hashmap; + use ndarray::prelude::*; - use super::{ReadoutMap, ReadoutValues}; + use crate::qpu::QpuResultData; + use crate::qvm::QvmResultData; + + use super::{RegisterData, RegisterMap}; use qcs_api_client_grpc::models::controller::readout_values::Values; - use qcs_api_client_grpc::models::controller::IntegerReadoutValues; + use qcs_api_client_grpc::models::controller::{IntegerReadoutValues, ReadoutValues}; - fn dummy_readout_values(v: i32) -> ReadoutValues { + fn dummy_readout_values(v: Vec) -> ReadoutValues { ReadoutValues { - values: Some(Values::IntegerValues(IntegerReadoutValues { - values: vec![v], - })), + values: Some(Values::IntegerValues(IntegerReadoutValues { values: v })), } } #[test] - fn it_converts_from_translation_readout_mappings() { + fn it_converts_rectangular_qpu_result_data_to_register_map() { + let readout_mappings = hashmap! { + String::from("ro[1]") => String::from("qB"), + String::from("ro[2]") => String::from("qC"), + String::from("ro[0]") => String::from("qA"), + String::from("bar[0]") => String::from("qE"), + String::from("bar[1]") => String::from("qD") + }; + + let readout_values = hashmap! { + String::from("qA") => dummy_readout_values(vec![1, 2]), + String::from("qB") => dummy_readout_values(vec![3, 4]), + String::from("qC") => dummy_readout_values(vec![5, 6]), + String::from("qD") => dummy_readout_values(vec![0, 1]), + String::from("qE") => dummy_readout_values(vec![2, 3]), + }; + + let qpu_result_data = + QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values); + + let register_map = RegisterMap::from_qpu_result_data(&qpu_result_data) + .expect("Should be able to create RegisterMap from rectangular QPU readout"); + + let ro = register_map + .get_register_matrix("ro") + .expect("RegisterMap should have ro") + .as_integer() + .expect("Should be a register of integer values"); + + let expected_ro = arr2(&[[1, 3, 5], [2, 4, 6]]); + + assert_eq!(ro, expected_ro); + + let bar = register_map + .get_register_matrix("bar") + .expect("RegisterMap should have bar") + .as_integer() + .expect("Shout be a register of integer values"); + + let expected_bar = arr2(&[[2, 0], [3, 1]]); + + assert_eq!(bar, expected_bar); + } + + #[test] + fn it_fails_to_convert_missing_readout_indices_to_register_map() { let readout_mappings = hashmap! { String::from("ro[1]") => String::from("qA"), String::from("ro[2]") => String::from("qB"), @@ -164,42 +415,56 @@ mod describe_readout_map { }; let readout_values = hashmap! { - String::from("qA") => dummy_readout_values(11), - String::from("qB") => dummy_readout_values(22), - String::from("qD") => dummy_readout_values(33), - String::from("qE") => dummy_readout_values(44), + String::from("qA") => dummy_readout_values(vec![11]), + String::from("qB") => dummy_readout_values(vec![22]), + String::from("qD") => dummy_readout_values(vec![33]), + String::from("qE") => dummy_readout_values(vec![44]), + }; + + let qpu_result_data = + QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values); + + RegisterMap::from_qpu_result_data(&qpu_result_data) + .expect_err("Should not be able to create RegisterMap from QPU readout with missing indices for a register"); + } + + #[test] + fn it_fails_to_convert_jagged_qpu_result_data_to_register_map() { + let readout_mappings = hashmap! { + String::from("ro[1]") => String::from("qB"), + String::from("ro[2]") => String::from("qC"), + String::from("ro[0]") => String::from("qA"), + }; + + let readout_values = hashmap! { + String::from("qA") => dummy_readout_values(vec![1, 2]), + String::from("qB") => dummy_readout_values(vec![2]), + String::from("qC") => dummy_readout_values(vec![3]), }; - let readout_map = ReadoutMap::from_mappings_and_values(&readout_mappings, &readout_values); - let ro = readout_map - .get_readout_values_for_field("ro") - .unwrap() - .expect("ReadoutMap should have field `ro`"); - - assert_eq!( - ro, - vec![ - None, - Some(dummy_readout_values(11)), - Some(dummy_readout_values(22)), - ], - ); - - let bar = readout_map - .get_readout_values_for_field("bar") - .unwrap() - .expect("ReadoutMap should have field `bar`"); - - assert_eq!( - bar, - vec![ - None, - None, - None, - Some(dummy_readout_values(33)), - None, - Some(dummy_readout_values(44)) - ], - ); + let qpu_result_data = + QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values); + + RegisterMap::from_qpu_result_data(&qpu_result_data) + .expect_err("Should not be able to create RegisterMap from QPU readout with jagged data for a register"); + } + + #[test] + fn it_converts_from_qvm_result_data() { + let qvm_result_data = QvmResultData::from_memory_map(hashmap! { + String::from("ro") => RegisterData::I8(vec![vec![1, 0, 1]]), + }); + + let register_map = RegisterMap::from_qvm_result_data(&qvm_result_data) + .expect("Should be able to create RegisterMap from QvmResultData"); + + let ro = register_map + .get_register_matrix("ro") + .expect("RegisterMap should have ro") + .as_integer() + .expect("Should be a register of integers"); + + let expected = arr2(&[[1, 0, 1]]); + assert_eq!(ro, expected); } } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 0182bbbca..8337852f3 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -50,13 +50,15 @@ //! crate allows you to run Quil programs against real QPUs or a QVM //! using [`Executable`]. -pub use executable::{Error, Executable, ExecuteResultQPU, ExecuteResultQVM, JobHandle, Service}; -pub use execution_data::{Qpu, Qvm, ReadoutMap}; +pub use executable::{Error, Executable, ExecutionResult, JobHandle, Service}; +pub use execution_data::{ + ExecutionData, RegisterMap, RegisterMatrix, RegisterMatrixConversionError, ResultData, +}; pub use register_data::RegisterData; pub mod api; mod executable; mod execution_data; pub mod qpu; -mod qvm; +pub mod qvm; mod register_data; diff --git a/crates/lib/src/qpu/client.rs b/crates/lib/src/qpu/client.rs index a881253dd..fda3ca87c 100644 --- a/crates/lib/src/qpu/client.rs +++ b/crates/lib/src/qpu/client.rs @@ -1,5 +1,5 @@ //! This module provides methods for getting clients for the -//! desired API (e.g. ``gRPC`` or ``OpenAPI``) and will properly +//! desired API (e.g. `gRPC` or `OpenAPI`) and will properly //! initialize those clients (e.g. with authentication metadata). use qcs_api_client_common::ClientConfiguration; @@ -156,7 +156,7 @@ impl Qcs { } } -/// Errors that may occur while trying to resolve a ``gRPC`` endpoint +/// Errors that may occur while trying to resolve a `gRPC` endpoint #[derive(Debug, thiserror::Error)] pub enum GrpcEndpointError { /// Error due to a malformed URI @@ -176,7 +176,7 @@ pub enum GrpcEndpointError { NoEndpoint(String), } -/// Errors that may occur while trying to use a ``gRPC`` client +/// Errors that may occur while trying to use a `gRPC` client #[derive(Debug, thiserror::Error)] pub enum GrpcClientError { /// Error due to failure to resolve the endpoint @@ -191,12 +191,12 @@ pub enum GrpcClientError { #[error("Response body had missing data: {0}")] ResponseEmpty(String), - /// Error due to ``gRPC`` error + /// Error due to `gRPC` error #[error("gRPC error: {0}")] GrpcError(#[from] GrpcError), } -/// Errors that may occur while trying to use a [`OpenAPI`] client +/// Errors that may occur while trying to use an `OpenAPI` client #[derive(Debug, thiserror::Error)] pub enum OpenApiClientError { /// Error due to request failure diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs index 00606abfc..84ae138b6 100644 --- a/crates/lib/src/qpu/execution.rs +++ b/crates/lib/src/qpu/execution.rs @@ -12,15 +12,16 @@ use quil_rs::Program; use tokio::task::{spawn_blocking, JoinError}; use crate::executable::Parameters; -use crate::execution_data::{MemoryReferenceParseError, Qpu, ReadoutMap}; +use crate::execution_data::{MemoryReferenceParseError, ResultData}; use crate::qpu::{rewrite_arithmetic, runner::JobId, translation::translate}; -use crate::JobHandle; +use crate::{ExecutionData, JobHandle}; use super::client::{GrpcClientError, Qcs}; use super::quilc::{self, CompilerOpts, TargetDevice}; use super::rewrite_arithmetic::RewrittenProgram; use super::runner::{retrieve_results, submit}; use super::translation::EncryptedTranslationResult; +use super::QpuResultData; use super::{get_isa, IsaError}; /// Contains all the info needed for a single run of an [`crate::Executable`] against a QPU. Can be @@ -181,7 +182,7 @@ impl<'a> Execution<'a> { &self, job_id: JobId, readout_mappings: HashMap, - ) -> Result { + ) -> Result { let response = retrieve_results( job_id, self.quantum_processor_id.as_ref(), @@ -189,11 +190,11 @@ impl<'a> Execution<'a> { ) .await?; - Ok(Qpu { - readout_data: ReadoutMap::from_mappings_and_values( + Ok(ExecutionData { + result_data: ResultData::Qpu(QpuResultData::from_controller_mappings_and_values( &readout_mappings, &response.readout_values, - ), + )), duration: response .execution_duration_microseconds .map(Duration::from_micros), diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs index e4953ab5e..593438b81 100644 --- a/crates/lib/src/qpu/mod.rs +++ b/crates/lib/src/qpu/mod.rs @@ -12,6 +12,7 @@ use qcs_api_client_openapi::{ pub mod client; mod execution; pub mod quilc; +mod result_data; pub(crate) mod rewrite_arithmetic; pub(crate) mod rpcq; pub(crate) mod runner; @@ -19,6 +20,8 @@ pub(crate) mod translation; pub use client::Qcs; pub(crate) use execution::{Error as ExecutionError, Execution}; +#[allow(clippy::module_name_repetitions)] +pub use result_data::{QpuResultData, ReadoutValues}; /// Query QCS for the ISA of the provided `quantum_processor_id`. /// diff --git a/crates/lib/src/qpu/quilc/mod.rs b/crates/lib/src/qpu/quilc/mod.rs index c391aef6a..32290846d 100644 --- a/crates/lib/src/qpu/quilc/mod.rs +++ b/crates/lib/src/qpu/quilc/mod.rs @@ -190,7 +190,7 @@ impl NativeQuilRequest { } } -/// Description of a device to compile for, part of [`NativeQuilRequest`] +/// Description of a device to compile for. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "_type")] pub struct TargetDevice { @@ -268,6 +268,7 @@ MEASURE 1 ro[1] .await .expect("Could not run program on QVM"); for shot in results + .memory .remove("ro") .expect("Did not receive ro buffer") .into_i8() diff --git a/crates/lib/src/qpu/result_data.rs b/crates/lib/src/qpu/result_data.rs new file mode 100644 index 000000000..1b12b0497 --- /dev/null +++ b/crates/lib/src/qpu/result_data.rs @@ -0,0 +1,108 @@ +//! This modules provides types and functions for initializing and working with +//! data returned from the QPU +use enum_as_inner::EnumAsInner; +use num::complex::Complex64; +use quil_rs::instruction::MemoryReference; +use std::collections::HashMap; + +use qcs_api_client_grpc::models::controller::{ + readout_values as controller_readout_values, ReadoutValues as ControllerReadoutValues, +}; + +/// A row of readout values from the QPU. Each row contains all the values emitted to a +/// memory reference across all shots. +#[derive(Debug, Clone, EnumAsInner, PartialEq)] +pub enum ReadoutValues { + /// Integer readout values + Integer(Vec), + /// Real numbered readout values + Real(Vec), + /// Complex readout values + Complex(Vec), +} + +/// This struct encapsulates data returned from the QPU after executing a job. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, PartialEq)] +pub struct QpuResultData { + pub(crate) mappings: HashMap, + pub(crate) readout_values: HashMap, +} + +impl QpuResultData { + /// Builds a new [`QpuResultData`] from mappings of memory references to readout identifiers + /// and readout identifiers to [`ReadoutValues`] + #[must_use] + pub fn from_mappings_and_values( + mappings: HashMap, + readout_values: HashMap, + ) -> Self { + Self { + mappings, + readout_values, + } + } + + /// Creates a new [`QpuResultData`] using data returned from controller service. + pub(crate) fn from_controller_mappings_and_values( + mappings: &HashMap, + values: &HashMap, + ) -> Self { + Self { + mappings: mappings.clone(), + readout_values: values + .iter() + .map(|(key, readout_values)| { + ( + key.clone(), + match &readout_values.values { + Some(controller_readout_values::Values::IntegerValues(v)) => { + ReadoutValues::Integer( + v.values.iter().copied().map(i64::from).collect(), + ) + } + Some(controller_readout_values::Values::ComplexValues(v)) => { + ReadoutValues::Complex( + v.values + .iter() + .map(|c| { + Complex64::new( + c.real.unwrap_or(0.0).into(), + c.imaginary.unwrap_or(0.0).into(), + ) + }) + .collect(), + ) + } + None => ReadoutValues::Integer(Vec::new()), + }, + ) + }) + .collect(), + } + } + + /// Returns the [`ReadoutValues`] for a [`MemoryReference`], or `None` if a mapping to the + /// provided memory reference doesn't exist. + #[must_use] + pub fn get_values_for_memory_reference( + &self, + reference: &MemoryReference, + ) -> Option<&ReadoutValues> { + self.mappings + .get(&reference.to_string()) + .and_then(|key| self.readout_values.get(key)) + } + + /// Get mappings of a memory region (ie. "ro\[0\]") to it's key name in `readout_values` (ie. "q0") + #[must_use] + pub fn mappings(&self) -> &HashMap { + &self.mappings + } + + /// Get mapping of a readout values identifier (ie. "q0") to a set of [`ReadoutValues`] + #[must_use] + pub fn readout_values(&self) -> &HashMap { + &self.readout_values + } +} diff --git a/crates/lib/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs index a2e4182c7..ace1de629 100644 --- a/crates/lib/src/qvm/execution.rs +++ b/crates/lib/src/qvm/execution.rs @@ -1,5 +1,5 @@ +use std::borrow::Cow; use std::str::FromStr; -use std::{borrow::Cow, collections::HashMap}; use qcs_api_client_common::ClientConfiguration; use quil_rs::{ @@ -9,9 +9,8 @@ use quil_rs::{ }; use crate::executable::Parameters; -use crate::RegisterData; -use super::{Request, Response}; +use super::{QvmResultData, Request, Response}; /// Contains all the info needed to execute on a QVM a single time, with the ability to be reused for /// faster subsequent runs. @@ -61,7 +60,7 @@ impl Execution { readouts: &[Cow<'_, str>], params: &Parameters, config: &ClientConfiguration, - ) -> Result, Error> { + ) -> Result { if shots == 0 { return Err(Error::ShotsMustBePositive); } @@ -110,7 +109,7 @@ impl Execution { shots: u16, readouts: &[Cow<'_, str>], config: &ClientConfiguration, - ) -> Result, Error> { + ) -> Result { let request = Request::new(&self.program.to_string(true), shots, readouts); let client = reqwest::Client::new(); @@ -129,7 +128,9 @@ impl Execution { qvm_url: config.qvm_url().into(), source, }), - Ok(Response::Success(response)) => Ok(response.registers), + Ok(Response::Success(response)) => { + Ok(QvmResultData::from_memory_map(response.registers)) + } Ok(Response::Failure(response)) => Err(Error::Qvm { message: response.status, }), diff --git a/crates/lib/src/qvm/mod.rs b/crates/lib/src/qvm/mod.rs index 33dd3f8c9..ca0e45479 100644 --- a/crates/lib/src/qvm/mod.rs +++ b/crates/lib/src/qvm/mod.rs @@ -11,6 +11,27 @@ use crate::RegisterData; mod execution; +/// Encapsulates data returned after running a program on the QVM +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct QvmResultData { + pub(crate) memory: HashMap, +} + +impl QvmResultData { + #[must_use] + /// Build a [`QvmResultData`] from a mapping of register names to a [`RegisterData`] + pub fn from_memory_map(memory: HashMap) -> Self { + Self { memory } + } + + /// Get a map of register names (ie. "ro") to a [`RegisterData`] containing their values. + #[must_use] + pub fn memory(&self) -> &HashMap { + &self.memory + } +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(untagged)] pub(super) enum Response { diff --git a/crates/lib/src/register_data.rs b/crates/lib/src/register_data.rs index e29f62942..b5915858a 100644 --- a/crates/lib/src/register_data.rs +++ b/crates/lib/src/register_data.rs @@ -2,21 +2,26 @@ use enum_as_inner::EnumAsInner; use num::complex::Complex32; use serde::{Deserialize, Serialize}; -/// Data resulting from [`Executable::execute_on_qvm`](`crate::Executable::execute_on_qvm`) or -/// [`Executable::execute_on_qpu`](`crate::Executable::execute_on_qpu`). +/// Data resulting from [`Executable::execute_on_qvm`](`crate::Executable::execute_on_qvm`) /// /// This represents a single vector (or "register") of typed memory across some number of shots. /// The register corresponds to the usage of a `DECLARE` instruction in Quil, and the name of that /// register should be provided with [`Executable::read_from`](`crate::Executable::read_from`). /// -/// There is a variant of this enum for each type of data that a register could hold. -/// Any variant of an instance of `ExecutionResult` will contain a `Vec` with one entry for each shot, -/// where each entry represents the entire register. +/// There is a variant of this enum for each type of data that a register could hold. The register +/// is represented as a 2-dimensional array `M` where the value `M[shot_number][memory_index]` +/// represents the value at `memory_index` for `shot_number`. /// /// # Usage /// -/// Typically you will already know what type of data the `ExecutionResult` _should_ have, so you can -/// use the [`mod@enum_as_inner`] methods (e.g. [`ExecutionResult::into_i8`]) in order to +/// Typically, you will be interacting with this data through the [`crate::ResultData`] of an +/// [`crate::ExecutionData`] returned after running a program. In those cases, you'll probably +/// want to convert it to a readout map using [`crate::ResultData.to_register_map()`]. This +/// will give you each register in the form of a [`crate::RegisterMatrix`] which is similar +/// but backed by an [`ndarray::Array2`] and more convenient for working with matrices. +/// +/// If you are interacting with [`RegisterData`] directly, then you should already know what type of data it _should_ +/// have, so you can use the [`mod@enum_as_inner`] methods (e.g. [`RegisterData::into_i8`]) in order to /// convert any variant type to its inner data. #[derive(Clone, Debug, Deserialize, EnumAsInner, PartialEq, Serialize)] #[serde(untagged)] diff --git a/crates/lib/tests/basic_qvm.rs b/crates/lib/tests/basic_qvm.rs index c107c7591..dad161dd3 100644 --- a/crates/lib/tests/basic_qvm.rs +++ b/crates/lib/tests/basic_qvm.rs @@ -19,7 +19,7 @@ MEASURE 1 second async fn test_bell_state() { const SHOTS: u16 = 10; - let mut data = Executable::from_quil(PROGRAM) + let data = Executable::from_quil(PROGRAM) .with_config(ClientConfiguration::default()) .with_shots(SHOTS) .read_from("first") @@ -28,22 +28,30 @@ async fn test_bell_state() { .await .expect("Could not run on QVM"); - let first: Vec> = data - .registers - .remove("first") - .expect("Missing first buffer") - .into_i8() - .expect("Produced wrong data type"); - let second: Vec> = data - .registers - .remove("second") - .expect("Missing second buffer") - .into_i8() - .expect("Produced wrong data type"); + let first = data + .result_data + .to_register_map() + .expect("should convert to readout map") + .get_register_matrix("first") + .expect("should have first register") + .as_integer() + .expect("first register should be integers") + .to_owned(); + + let second = data + .result_data + .to_register_map() + .expect("should convert to readout map") + .get_register_matrix("second") + .expect("should have second register") + .as_integer() + .expect("second register should be integers") + .to_owned(); + + assert_eq!(first.shape(), [SHOTS.into(), 1]); + assert_eq!(second.shape(), [SHOTS.into(), 1]); for (first, second) in first.into_iter().zip(second) { - assert_eq!(first.len(), 1); - assert_eq!(second.len(), 1); - assert_eq!(first[0], second[0]); + assert_eq!(first, second); } } diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs index 6fa5b7676..a330eb2ba 100644 --- a/crates/lib/tests/mocked_qpu.rs +++ b/crates/lib/tests/mocked_qpu.rs @@ -3,11 +3,10 @@ use std::time::Duration; +use ndarray::arr2; + use qcs::Executable; use qcs_api_client_common::configuration::{SECRETS_PATH_VAR, SETTINGS_PATH_VAR}; -use qcs_api_client_grpc::models::controller::{ - readout_values::Values, IntegerReadoutValues, ReadoutValues, -}; const BELL_STATE: &str = r#" DECLARE ro BIT[2] @@ -19,7 +18,7 @@ MEASURE 0 ro[0] MEASURE 1 ro[1] "#; -const QPU_ID: &str = "Aspen-9"; +const QPU_ID: &str = "Aspen-M-3"; #[tokio::test] async fn successful_bell_state() { @@ -31,22 +30,14 @@ async fn successful_bell_state() { .expect("Failed to run program that should be successful"); assert_eq!( result - .readout_data - .get_readout_values_for_field("ro") + .result_data + .to_register_map() + .expect("should convert to RegisterMap") + .get_register_matrix("ro") .expect("should have values for `ro`") - .unwrap(), - vec![ - Some(ReadoutValues { - values: Some(Values::IntegerValues(IntegerReadoutValues { - values: vec![0, 0], - })), - }), - Some(ReadoutValues { - values: Some(Values::IntegerValues(IntegerReadoutValues { - values: vec![1, 1], - })), - }), - ], + .as_integer() + .expect("`ro` should have integer values"), + arr2(&[[0, 1], [0, 1],]), ); assert_eq!(result.duration, Some(Duration::from_micros(8675))); } @@ -131,7 +122,7 @@ mod mock_qcs { warp::reply::json(&isa) }); - let translate = warp::path(format!("{}:translateNativeQuilToEncryptedBinary", QPU_ID)) + let translate = warp::path(format!("{QPU_ID}:translateNativeQuilToEncryptedBinary")) .and(warp::post()) .and(warp::body::json()) .map(|_request: TranslateNativeQuilToEncryptedBinaryRequest| { diff --git a/crates/lib/tests/parametric_compilation.rs b/crates/lib/tests/parametric_compilation.rs index f6ffcd7b2..a5917d987 100644 --- a/crates/lib/tests/parametric_compilation.rs +++ b/crates/lib/tests/parametric_compilation.rs @@ -27,18 +27,23 @@ async fn basic_substitution() { for i in 0..=200 { let theta = step * f64::from(i); - let mut result = exe + let result = exe .with_parameter("theta", 0, theta) .execute_on_qvm() .await .expect("Executed on QPU"); - parametric_measurements.append( - &mut result - .registers - .remove("ro") - .expect("Found ro register") - .into_i8() - .unwrap()[0], + parametric_measurements.push( + result + .result_data + .to_register_map() + .expect("should convert to RegisterMap") + .get_register_matrix("ro") + .expect("should have `ro`") + .as_integer() + .expect("`ro` should have integer values") + .get((0, 0)) + .expect("ro register should have a value in the first index and shot") + .to_owned(), ) } diff --git a/crates/python/.flake8 b/crates/python/.flake8 new file mode 100644 index 000000000..98e11a2c0 --- /dev/null +++ b/crates/python/.flake8 @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 120 +# E203, E302 ignored for black compatibility +extend-ignore = E203, E302 + +per-file-ignores = + qcs_sdk/__init__.pyi:F401,F403 # Disable "unused" warning for top-level exports diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml index 048a2e5ca..b5dd6bfe2 100644 --- a/crates/python/Cargo.toml +++ b/crates/python/Cargo.toml @@ -17,16 +17,17 @@ crate-type = ["cdylib"] [dependencies] qcs = { path = "../lib" } -qcs-api-client-common = "0.4.2" -qcs-api-client-openapi = "0.5.2" -qcs-api-client-grpc = "0.4.2" -pyo3 = { version = "0.17", features = ["extension-module"] } -pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] } -quil-rs = "0.15" -tokio = "1.21" -qcs-api = "0.2.1" -rigetti-pyo3 = { version = "0.1.0-rc.4", features = ["extension-module", "complex"] } -serde_json = "1.0.86" +qcs-api.workspace = true +qcs-api-client-common.workspace = true +qcs-api-client-grpc.workspace = true +qcs-api-client-openapi.workspace = true +pyo3.workspace = true +pyo3-asyncio.workspace = true +quil-rs.workspace = true +serde_json.workspace = true +tokio.workspace = true +numpy.workspace = true +rigetti-pyo3.workspace = true [build-dependencies] -pyo3-build-config = { version = "0.17" } +pyo3-build-config.workspace = true diff --git a/crates/python/poetry.lock b/crates/python/poetry.lock index e92cf03ec..851218764 100644 --- a/crates/python/poetry.lock +++ b/crates/python/poetry.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "attrs" version = "22.2.0" @@ -5,14 +7,17 @@ description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] [package.extras] -cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs"] -docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["attrs", "zope.interface"] -tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] -tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "black" @@ -21,6 +26,20 @@ description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] [package.dependencies] click = ">=8.0.0" @@ -43,6 +62,10 @@ description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -54,6 +77,10 @@ description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "exceptiongroup" @@ -62,6 +89,10 @@ description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] [package.extras] test = ["pytest (>=6)"] @@ -73,6 +104,10 @@ description = "brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] name = "maturin" @@ -81,13 +116,27 @@ description = "Build and publish crates with pyo3, rust-cpython and cffi binding category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "maturin-0.13.7-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:bc58b0266a4c124f9afd69a987ac324ff35c0d963b41073ed64a32f94c226d5a"}, + {file = "maturin-0.13.7-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:424d53adc9684167cf89829275fe5072331a197de0ac08e7a893f9283c7df213"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:3137876c338eb7e551ba44a609b5f02d02454d1b3a8677ad6bf2121c5a92b2b7"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:a96f1b3ede71c0f76b8c7cfac18a9eec90174bdf434fa9aeff491be9a7ca5179"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0b6ac1219a809155057fd1f358f7ece03c3abd2e2991832ce5146825a9fa4160"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3c36f429adc3a8af8de9838399750742a86053f0031a953b48ee92932120dc0c"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:bb3a2830d64ae6a324571f694475b91111e827bc0ccc60a0c47f4fb596a46bd8"}, + {file = "maturin-0.13.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794f58d3103449f8cd8ab5f36fd05c31b8d8de3643cd0e3720fd5dc3c328dd5c"}, + {file = "maturin-0.13.7-py3-none-win32.whl", hash = "sha256:63586eb286866264ec62d29df6ab955360de6226128f67d14623ffe1a12d4963"}, + {file = "maturin-0.13.7-py3-none-win_amd64.whl", hash = "sha256:f50d62aca567fdbbb929771794f3c5c78048ef0efa4af7d83ed472a8b8d26454"}, + {file = "maturin-0.13.7-py3-none-win_arm64.whl", hash = "sha256:8c6225e7eba2885a0cd82a6cf898e74bb720796a5744e0450f3b1340d1ca97af"}, + {file = "maturin-0.13.7.tar.gz", hash = "sha256:c0a77aa0c57f945649ca711c806203a1b6888ad49c2b8b85196ffdcf0421db77"}, +] [package.dependencies] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -zig = ["ziglang (>=0.9.0,<0.10.0)"] patchelf = ["patchelf"] +zig = ["ziglang (>=0.9.0,<0.10.0)"] [[package]] name = "mypy-extensions" @@ -96,6 +145,48 @@ description = "Experimental type system extensions for programs checked with the category = "dev" optional = false python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "numpy" +version = "1.24.1" +description = "Fundamental package for array computing in Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7"}, + {file = "numpy-1.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9"}, + {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"}, + {file = "numpy-1.24.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398"}, + {file = "numpy-1.24.1-cp310-cp310-win32.whl", hash = "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2"}, + {file = "numpy-1.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2"}, + {file = "numpy-1.24.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8"}, + {file = "numpy-1.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032"}, + {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1"}, + {file = "numpy-1.24.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9"}, + {file = "numpy-1.24.1-cp311-cp311-win32.whl", hash = "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36"}, + {file = "numpy-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51"}, + {file = "numpy-1.24.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407"}, + {file = "numpy-1.24.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954"}, + {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36"}, + {file = "numpy-1.24.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7"}, + {file = "numpy-1.24.1-cp38-cp38-win32.whl", hash = "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1"}, + {file = "numpy-1.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c"}, + {file = "numpy-1.24.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6"}, + {file = "numpy-1.24.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7"}, + {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700"}, + {file = "numpy-1.24.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf"}, + {file = "numpy-1.24.1-cp39-cp39-win32.whl", hash = "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f"}, + {file = "numpy-1.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086"}, + {file = "numpy-1.24.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566"}, + {file = "numpy-1.24.1.tar.gz", hash = "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2"}, +] [[package]] name = "packaging" @@ -104,6 +195,10 @@ description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pathspec" @@ -112,6 +207,10 @@ description = "Utility library for gitignore style pattern matching of file path category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] [[package]] name = "platformdirs" @@ -120,10 +219,14 @@ description = "A small Python package for determining appropriate platform-speci category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -132,6 +235,10 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.extras] dev = ["pre-commit", "tox"] @@ -144,6 +251,10 @@ description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] [package.dependencies] attrs = ">=19.2.0" @@ -164,12 +275,16 @@ description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, + {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, +] [package.dependencies] pytest = ">=6.1.0" [package.extras] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "tomli" @@ -178,6 +293,10 @@ description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "typing-extensions" @@ -186,38 +305,12 @@ description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] [metadata] -lock-version = "1.1" +lock-version = "2.0" python-versions = "^3.8" -content-hash = "67c0952caa1ebf2881aec378850a3188a6650b6b9cc9cda838dec5d23ed15e72" - -[metadata.files] -attrs = [] -black = [] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [] -exceptiongroup = [] -iniconfig = [] -maturin = [] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [] -pathspec = [] -platformdirs = [] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pytest = [] -pytest-asyncio = [] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typing-extensions = [] +content-hash = "226706998c75145ffb0124fc12c73c08935e14c1872e1b0bd37f08c1c3e69cb5" diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml index 46f900f4e..14b8e2d3a 100644 --- a/crates/python/pyproject.toml +++ b/crates/python/pyproject.toml @@ -43,11 +43,17 @@ sdist-include = ["README.md"] python = "^3.8" [tool.poetry.dev-dependencies] +numpy = "^1.24.1" maturin = "^0.13.2" pytest = "^7.1.3" pytest-asyncio = "^0.19.0" black = "^22.8.0" +[tool.black] +line-length = 120 +target-version = ['py37', 'py38', 'py39', 'py310'] +include = '\.pyi?$' + [build-system] requires = ["maturin>=0.13,<0.14"] build-backend = "maturin" diff --git a/crates/python/qcs_sdk/__init__.pyi b/crates/python/qcs_sdk/__init__.pyi index cbd1666c4..8369f07dd 100644 --- a/crates/python/qcs_sdk/__init__.pyi +++ b/crates/python/qcs_sdk/__init__.pyi @@ -1,17 +1,16 @@ from .api import * -from .qpu.client import ( - QcsClient as QcsClient -) - +from .qpu.client import QcsClient as QcsClient from .qpu.isa import ( - get_instruction_set_architecture as get_instruction_set_architecture + get_instruction_set_architecture as get_instruction_set_architecture, ) from ._execution_data import ( - QPU as QPU, - QVM as QVM, - ReadoutMap as ReadoutMap, + ResultData as ResultData, + ExecutionData as ExecutionData, + RegisterMatrix as RegisterMatrix, + RegisterMap as RegisterMap, + RegisterMatrixConversionError as RegisterMatrixConversionError, ) from ._executable import ( diff --git a/crates/python/qcs_sdk/_executable.pyi b/crates/python/qcs_sdk/_executable.pyi index 2ec80827d..696f9f392 100644 --- a/crates/python/qcs_sdk/_executable.pyi +++ b/crates/python/qcs_sdk/_executable.pyi @@ -6,12 +6,12 @@ from enum import Enum from typing import Dict, List, Optional from .qpu.quilc import CompilerOpts -from ._execution_data import QVM, QPU +from ._execution_data import ExecutionData class QcsExecutionError(RuntimeError): """Error encounteted when executing programs.""" - ... + ... class Executable: def __new__( @@ -20,10 +20,9 @@ class Executable: parameters: Optional[List[ExeParameter]] = None, shots: Optional[int] = None, compile_with_quilc: Optional[bool] = None, - compiler_options: Optional[CompilerOpts] = None + compiler_options: Optional[CompilerOpts] = None, ) -> "Executable": ... - - async def execute_on_qvm(self) -> QVM: + async def execute_on_qvm(self) -> ExecutionData: """ Execute on a QVM which must be available at the configured URL (default http://localhost:5000). @@ -31,8 +30,7 @@ class Executable: - ``QcsExecutionError``: If the job fails to execute. """ ... - - async def execute_on_qpu(self, quantum_processor_id: str) -> QPU: + async def execute_on_qpu(self, quantum_processor_id: str) -> ExecutionData: """ Compile the program and execute it on a QPU, waiting for results. @@ -40,8 +38,7 @@ class Executable: - ``QcsExecutionError``: If the job fails to execute. """ ... - - async def retrieve_results(job_handle: JobHandle) -> QPU: + async def retrieve_results(self, job_handle: JobHandle) -> ExecutionData: """ Wait for the results of a job to complete. @@ -50,11 +47,10 @@ class Executable: """ ... - class JobHandle: """ The result of submitting a job to a QPU. - + Used to retrieve the results of a job. """ @@ -64,7 +60,6 @@ class JobHandle: Unique ID associated with a single job execution. """ ... - @property def readout_map(self) -> Dict[str, str]: """ @@ -72,38 +67,34 @@ class JobHandle: """ ... - class ExeParameter: """ Program execution parameters. Note: The validity of parameters is not checked until execution. """ + def __new__( cls: type["ExeParameter"], name: str, index: int, value: float, ) -> "ExeParameter": ... - @property def name(self) -> str: ... @name.setter def name(self, value: str): ... - @property def index(self) -> int: ... @index.setter def index(self, value: int): ... - @property def value(self) -> float: ... @value.setter def value(self, value: float): ... - class Service(Enum): - Quilc = "Quilc", - Qvm = "Qvm", - Qcs = "Qcs", - Qpu = "Qpu", + Quilc = "Quilc" + Qvm = "Qvm" + Qcs = "Qcs" + Qpu = "Qpu" diff --git a/crates/python/qcs_sdk/_execution_data.pyi b/crates/python/qcs_sdk/_execution_data.pyi index d64ae413f..cd339168c 100644 --- a/crates/python/qcs_sdk/_execution_data.pyi +++ b/crates/python/qcs_sdk/_execution_data.pyi @@ -4,69 +4,144 @@ It is only here to represent the structure of the rust source code 1:1 """ import datetime -from typing import List, Optional - -class _IntegerReadoutValues: - @property - def values(self) -> List[int]: ... - @values.setter - def values(self, value: List[int]): ... - - -class _ComplexReadoutValues: - @property - def values(self) -> List[complex]: ... - @values.setter - def values(self, value: List[complex]): ... - - -class _ReadoutValuesValues: - @property - def integer_values(self) -> _IntegerReadoutValues: ... - @integer_values.setter - def integer_values(self, value: _IntegerReadoutValues): ... - - @property - def complex_values(self) -> _ComplexReadoutValues: ... - @complex_values.setter - def complex_values(self, value: _ComplexReadoutValues): ... - - -class _ReadoutValues: +from typing import Optional + +import numpy as np +from numpy.typing import NDArray + +from .qpu import QPUResultData +from .qvm import QVMResultData + +class RegisterMatrixConversionError(ValueError): + """Error that may occur when building a ``RegisterMatrix`` from execution data.""" + + ... + +class RegisterMatrix: + """ + Values in a 2-dimensional ``ndarray`` representing the final shot value in each memory reference across all shots. + Each variant corresponds to the possible data types a register can contain. + + Variants: + ``integer``: Corresponds to the Quil `BIT`, `OCTET`, or `INTEGER` types. + ``real``: Corresponds to the Quil `REAL` type. + ``complex``: Registers containing complex numbers. + + 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. + + """ + + def is_integer(self) -> bool: ... + def is_real(self) -> bool: ... + def is_complex(self) -> bool: ... + def as_integer(self) -> Optional[NDArray[np.int64]]: ... + def as_real(self) -> Optional[NDArray[np.float64]]: ... + # In numpy `complex128` is a complex number made up of two `f64`s. + def as_complex(self) -> Optional[NDArray[np.complex128]]: ... + def to_integer(self) -> NDArray[np.int64]: ... + def to_real(self) -> NDArray[np.float64]: ... + def to_complex(self) -> NDArray[np.complex128]: ... + @staticmethod + def from_integer(inner: NDArray[np.int64]) -> "RegisterMatrix": ... + @staticmethod + def from_real(inner: NDArray[np.float64]) -> "RegisterMatrix": ... + @staticmethod + def from_complex(inner: NDArray[np.complex128]) -> "RegisterMatrix": ... + +class RegisterMap: + """A map of register names (ie. "ro") to a ``RegisterMatrix`` containing the values of the register.""" + + def get_register_matrix(self, register_name: str) -> Optional[RegisterMatrix]: ... + """Get the ``RegisterMatrix`` for the given register. Returns `None` if the register doesn't exist.""" + +class ResultData: + """ + Represents the two possible types of data returned from either the QVM or a real QPU. + Each variant contains the original data returned from its respective executor. + + Usage + ----- + + Your usage of ``ResultData`` will depend on the types of programs you are running and where. + The `to_register_map()` method will attempt to build ``RegisterMap`` out of the data, where each + register name is mapped to a 2-dimensional rectangular ``RegisterMatrix`` where each row + represents the final values in each register index for a particular shot. This is often the + desired form of the data and it is _probably_ what you want. This transformation isn't always + possible, in which case `to_register_map()` will return an error. + + To understand why this transformation can fail, we need to understand a bit about how readout data is + returned from the QVM and from a real QPU: + + The QVM treats each `DECLARE` statement as initialzing some amount of memory. This memory works + as one might expect it to. It is zero-initalized, and subsequent writes to the same region + overwrite the previous value. The QVM returns memory at the end of every shot. This means + we get the last value in every memory reference for each shot, which is exactly the + representation we want for a ``RegisterMatrix``. For this reason, `to_register_map()` should + always succeed for ``ResultData::Qvm``. + + The QPU on the other hand doesn't use the same memory model as the QVM. Each memory reference + (ie. "ro[0]") is more like a stream than a value in memory. Every `MEASURE` to a memory + reference emits a new value to said stream. This means that the number of values per memory + reference can vary per shot. For this reason, it's not always clear what the final value in + each shot was for a particular reference. When this is the case, `to_register_map()` will return + an error as it's impossible to build a correct ``RegisterMatrix`` from the data without + knowing the intent of the program that was run. Instead, it's recommended to build the + ``RegisterMatrix`` you need from the inner ``QPUResultData`` data using the knowledge of your + program to choose the correct readout values for each shot. + + Variants: + - ``qvm``: Data returned from the QVM, stored as ``QVMResultData`` + - ``qpu``: Data returned from the QPU, stored as ``QPUResultData`` + + 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. + + """ + + def to_register_map(self) -> RegisterMap: ... + """ + Convert ``ResultData`` from its inner representation as ``QVMResultData`` or + ``QPUResultData`` into a ``RegisterMap``. The ``RegisterMatrix`` for each register will be + constructed such that each row contains all the final values in the register for a single shot. + + Errors + ------ + + Raises a ``RegisterMatrixConversionError`` if the inner execution data for any of the + registers would result in a jagged matrix. ``QPUResultData`` data is captured per measure, + meaning a value is returned for every measure to a memory reference, not just once per shot. + This is often the case in programs that use mid-circuit measurement or dynamic control flow, + where measurements to the same memory reference might occur multiple times in a shot, or be + skipped conditionally. In these cases, building a rectangular ``RegisterMatrix`` would + necessitate making assumptions about the data that could skew the data in undesirable ways. + Instead, it's recommended to manually build a matrix from ``QPUResultData`` that accurately + selects the last value per-shot based on the program that was run. + """ + + def is_qvm(self) -> bool: ... + def is_qpu(self) -> bool: ... + def as_qvm(self) -> Optional[QVMResultData]: ... + def as_qpu(self) -> Optional[QPUResultData]: ... + def to_qvm(self) -> QVMResultData: ... + def to_qpu(self) -> QPUResultData: ... + @staticmethod + def from_qvm(inner: QVMResultData) -> "ResultData": ... + @staticmethod + def from_qpu(inner: QPUResultData) -> "ResultData": ... + +class ExecutionData: @property - def values(self) -> Optional[_ReadoutValuesValues]: ... - @values.setter - def values(self, value: Optional[_ReadoutValuesValues]): ... - - -class ReadoutMap: - def get_readout_values(self, field: str, index: int) -> Optional[_ReadoutValues]: - """Given a known readout field name and index, return the result's ``ReadoutValues``, if any.""" - ... - - def get_readout_values_for_field(self, field: str) -> Optional[List[Optional[_ReadoutValues]]]: - """Given a known readout field name, return the result's ``ReadoutValues`` for all indices, if any.""" - ... - -class QVM: - @property - def registers(self) -> dict: ... - @registers.setter - def registers(self, value: dict): ... - - @property - def duration(self) -> Optional[datetime.timedelta]: ... - @duration.setter - def duration(self, value: Optional[datetime.timedelta]): ... - - -class QPU: - @property - def readout_data(self) -> ReadoutMap: ... - @readout_data.setter - def readout_data(self, value: ReadoutMap): ... - + def result_data(self) -> ResultData: ... + @result_data.setter + def result_data(self, result_data: ResultData): ... @property def duration(self) -> Optional[datetime.timedelta]: ... @duration.setter - def duration(self, value: Optional[datetime.timedelta]): ... + def duration(self, duration: Optional[datetime.timedelta]): ... diff --git a/crates/python/qcs_sdk/api.pyi b/crates/python/qcs_sdk/api.pyi index 208f66678..f956603fb 100644 --- a/crates/python/qcs_sdk/api.pyi +++ b/crates/python/qcs_sdk/api.pyi @@ -1,9 +1,10 @@ """ The qcs_sdk module provides an interface to Rigetti Quantum Cloud Services. Allowing users to compile and run Quil programs on Rigetti quantum processors. """ -from typing import Dict, List, Optional from numbers import Number +from typing import Dict, List, Optional +from .qpu.isa import InstructionSetArchitecture from .qpu.client import QcsClient RecalculationTable = List[str] @@ -12,33 +13,33 @@ PatchValues = Dict[str, List[float]] class ExecutionError(RuntimeError): """Error encountered during program execution submission or when retrieving results.""" - ... + ... class TranslationError(RuntimeError): """Error encountered during program translation.""" - ... + ... class CompilationError(RuntimeError): """Error encountered during program compilation.""" - ... + ... class RewriteArithmeticError(RuntimeError): """Error encountered rewriting arithmetic for program.""" - ... + ... class DeviceIsaError(ValueError): """Error while building Instruction Set Architecture.""" - ... + ... class QcsGetQuiltCalibrationsError(RuntimeError): """Error while fetching Quil-T calibrations.""" - ... + ... class RewriteArithmeticResults: """ @@ -53,17 +54,15 @@ class RewriteArithmeticResults: ... @program.setter def program(self, value: str): ... - @property def recalculation_table(self) -> List[str]: - """ + """ The recalculation table stores an ordered list of arithmetic expressions, which are to be used when updating the program memory before execution. """ ... @recalculation_table.setter def recalculation_table(self, value: List[str]): ... - class TranslationResult: """ The result of a call to [`translate`] which provides information about the translated program. @@ -77,7 +76,6 @@ class TranslationResult: ... @program.setter def program(self, value: str): ... - @property def ro_sources(self) -> Optional[dict]: """ @@ -87,7 +85,6 @@ class TranslationResult: @ro_sources.setter def ro_sources(self, value: Optional[dict]): ... - class ExecutionResult: """Execution readout data from a particular memory location.""" @@ -97,14 +94,12 @@ class ExecutionResult: ... @shape.setter def shape(self, value: List[int]): ... - @property def data(self) -> List[Number | List[float]]: """The result data. Complex numbers are represented as [real, imaginary].""" ... @data.setter def data(self, value: List[Number | List[float]]): ... - @property def dtype(self) -> str: """The type of the result data (as a `numpy` `dtype`).""" @@ -112,7 +107,6 @@ class ExecutionResult: @dtype.setter def dtype(self, value: str): ... - class ExecutionResults: """Execution readout data for all memory locations.""" @@ -126,7 +120,6 @@ class ExecutionResults: ... @buffers.setter def buffers(self, value: Dict[str, ExecutionResult]): ... - @property def execution_duration_microseconds(self) -> Optional[int]: """The time spent executing the program.""" @@ -134,7 +127,6 @@ class ExecutionResults: @execution_duration_microseconds.setter def execution_duration_microseconds(self, value: Optional[int]): ... - class Register: """ Data from an individual register. @@ -153,25 +145,22 @@ class Register: - ``from_*``: wrap underlying values as this enum type. """ - + def is_i8(self) -> bool: ... def is_i16(self) -> bool: ... def is_i32(self) -> bool: ... def is_f64(self) -> bool: ... def is_complex64(self) -> bool: ... - def as_i8(self) -> Optional[List[int]]: ... def as_i16(self) -> Optional[List[int]]: ... def as_i32(self) -> Optional[List[int]]: ... def as_f64(self) -> Optional[List[float]]: ... def as_complex64(self) -> Optional[List[complex]]: ... - def to_i8(self) -> List[int]: ... def to_i16(self) -> List[int]: ... def to_i32(self) -> List[int]: ... def to_f64(self) -> List[float]: ... def to_complex64(self) -> List[complex]: ... - @staticmethod def from_i8(inner: List[int]) -> "Register": ... @staticmethod @@ -183,7 +172,6 @@ class Register: @staticmethod def from_complex64(inner: List[complex]) -> "Register": ... - class QuiltCalibrations: """Result of `get_quilt_calibrations`.""" @@ -193,7 +181,6 @@ class QuiltCalibrations: ... @quilt.setter def quilt(self, value: str): ... - @property def settings_timestamp(self) -> Optional[str]: """ISO8601 timestamp of the settings used to generate these calibrations.""" @@ -201,7 +188,6 @@ class QuiltCalibrations: @settings_timestamp.setter def settings_timestamp(self, value: Optional[str]): ... - async def compile( quil: str, target_device: str, @@ -230,7 +216,6 @@ async def compile( """ ... - def rewrite_arithmetic( native_quil: str, ) -> RewriteArithmeticResults: @@ -243,14 +228,13 @@ def rewrite_arithmetic( Returns: A dictionary with the rewritten program and recalculation table (see `RewriteArithmeticResults`). - + Raises: - ``TranslationError`` If the program could not be translated. - ``RewriteArithmeticError`` If the program arithmetic cannot be evaluated. """ ... - def build_patch_values( recalculation_table: RecalculationTable, memory: Memory, @@ -271,7 +255,6 @@ def build_patch_values( """ ... - async def translate( native_quil: str, num_shots: int, @@ -289,14 +272,13 @@ async def translate( Returns: An Awaitable that resolves to a dictionary with the compiled program, memory descriptors, and readout sources (see `TranslationResult`). - + Raises: - ``LoadError`` If there is an issue loading the QCS Client configuration. - ``TranslationError`` If the `native_quil` program could not be translated. """ ... - async def submit( program: str, patch_values: Dict[str, List[float]], @@ -314,14 +296,13 @@ async def submit( Returns: An Awaitable that resolves to the ID of the submitted job. - + Raises: - ``LoadError`` If there is an issue loading the QCS Client configuration. - ``ExecutionError`` If there was a problem during program execution. """ ... - async def retrieve_results( job_id: str, quantum_processor_id: str, @@ -344,7 +325,6 @@ async def retrieve_results( """ ... - async def get_quilc_version( client: Optional[QcsClient] = None, ) -> str: @@ -353,7 +333,7 @@ async def get_quilc_version( Args: client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration - + Raises: - ``LoadError`` If there is an issue loading the QCS Client configuration. - ``CompilationError`` If there is an issue fetching the version from the quilc compiler. @@ -389,3 +369,18 @@ async def get_quilt_calibrations( - ``QcsGetQuiltCalibrationsError`` If there was a problem fetching Quil-T calibrations. """ ... + +async def get_instruction_set_architecture( + quantum_processor_id: str, client: Optional[QcsClient] +) -> InstructionSetArchitecture: + """ + Retrieve the InstructionSetArchitecture (ISA) for the given Quantum Processor ID. + + Args: + quantum_processor_id: The ID of the quantum processor + client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration + + Raises: + - ``IsaError`` if there was a problem fetching the ISA. + """ + ... diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi index e596a2f0a..b9da7d6f1 100644 --- a/crates/python/qcs_sdk/qpu/__init__.pyi +++ b/crates/python/qcs_sdk/qpu/__init__.pyi @@ -1,2 +1,6 @@ -class QcsIsaError(RuntimeError): - ... +from .result_data import ( + QPUResultData as QPUResultData, + ReadoutValues as ReadoutValues, +) + +class QcsIsaError(RuntimeError): ... diff --git a/crates/python/qcs_sdk/qpu/client.pyi b/crates/python/qcs_sdk/qpu/client.pyi index a2966cded..87cbd762a 100644 --- a/crates/python/qcs_sdk/qpu/client.pyi +++ b/crates/python/qcs_sdk/qpu/client.pyi @@ -1,6 +1,5 @@ from typing import Optional - class QcsClient: """ Configuration for connecting and authenticating to QCS API resources. @@ -17,11 +16,10 @@ class QcsClient: ) -> "QcsClient": """ Construct a client from scratch. - + Use ``QcsClient.load`` to construct an environment-based profile. """ ... - @staticmethod async def load( profile_name: Optional[str] = None, @@ -33,67 +31,57 @@ class QcsClient: See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files """ ... - @property def api_url(self) -> str: """URL to access the QCS API.""" ... - @property def grpc_api_url(self) -> str: """URL to access the gRPC API.""" ... - @property def quilc_url(self) -> str: """URL to access the `quilc` compiler.""" ... - @property def qvm_url(self) -> str: """URL to access the QVM.""" ... - class QcsClientAuthServer: """Authentication server configuration for the QCS API.""" + def __init__(self, client_id: str, issuer: str): ... @property def client_id(self) -> str: ... @client_id.setter def client_id(self, value: str): ... - @property def issuer(self) -> str: ... @issuer.setter def issuer(self, value: str): ... - class QcsClientTokens: """Authentication tokens for the QCS API.""" + def __init__(self, bearer_access_token: str, refresh_token: str): ... @property def bearer_access_token(self) -> Optional[str]: ... @bearer_access_token.setter def bearer_access_token(self, value: Optional[str]): ... - @property def refresh_token(self) -> Optional[str]: ... @refresh_token.setter def refresh_token(self, value: Optional[str]): ... - class QcsGrpcClientError(RuntimeError): """Error encountered while loading a QCS gRPC API client.""" - class QcsGrpcEndpointError(RuntimeError): """Error when trying to resolve the QCS gRPC API endpoint.""" - class QcsGrpcError(RuntimeError): """Error during QCS gRPC API requests.""" - class QcsLoadError(RuntimeError): """Error encountered while loading the QCS API client configuration.""" diff --git a/crates/python/qcs_sdk/qpu/quilc.pyi b/crates/python/qcs_sdk/qpu/quilc.pyi index 8d36e73b5..a8141c05b 100644 --- a/crates/python/qcs_sdk/qpu/quilc.pyi +++ b/crates/python/qcs_sdk/qpu/quilc.pyi @@ -1,11 +1,8 @@ -from typing import Any, Dict, Optional +from typing import Optional DEFAULT_COMPILER_TIMEOUT: int - -class QuilcError(RuntimeError): - ... - +class QuilcError(RuntimeError): ... class CompilerOpts: """A set of options that determine the behavior of compiling programs with quilc.""" @@ -14,16 +11,10 @@ class CompilerOpts: def timeout(self) -> Optional[int]: """The number of seconds to wait before timing out. If `None`, there is no timeout.""" ... - def __new__( - cls, - timeout: Optional[int] = DEFAULT_COMPILER_TIMEOUT - ) -> "CompilerOpts": - ... - + cls, timeout: Optional[int] = DEFAULT_COMPILER_TIMEOUT + ) -> "CompilerOpts": ... @staticmethod def default() -> "CompilerOpts": ... - -class TargetDevice: - ... +class TargetDevice: ... diff --git a/crates/python/qcs_sdk/qpu/result_data.pyi b/crates/python/qcs_sdk/qpu/result_data.pyi new file mode 100644 index 000000000..1c0d3b432 --- /dev/null +++ b/crates/python/qcs_sdk/qpu/result_data.pyi @@ -0,0 +1,56 @@ +from typing import Dict, List, Optional + +class ReadoutValues: + """ + A row of readout values from the QPU. Each row contains all the values emitted + to a memory reference across all shots. There is a variant for each possible type + the list of readout values could be. + + Variants: + - ``integer``: Corresponds to the Quil `BIT`, `OCTET`, or `INTEGER` types. + - ``real``: Corresponds to the Quil `REAL` type. + - ``complex``: Corresponds to readout values containing complex numbers + + 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. + + """ + + def is_integer(self) -> bool: ... + def is_real(self) -> bool: ... + def is_complex(self) -> bool: ... + def as_integer(self) -> Optional[List[int]]: ... + def as_real(self) -> Optional[List[float]]: ... + def as_f64(self) -> Optional[List[complex]]: ... + def to_integer(self) -> List[int]: ... + def to_real(self) -> List[float]: ... + def to_complex(self) -> List[complex]: ... + @staticmethod + def from_integer(inner: List[int]) -> "ReadoutValues": ... + @staticmethod + def from_real(inner: List[float]) -> "ReadoutValues": ... + @staticmethod + def from_complex(inner: List[complex]) -> "ReadoutValues": ... + +class QPUResultData: + """ + Encapsulates data returned from the QPU after executing a job. + """ + + def __init__( + self, mappings: Dict[str, str], readout_values: Dict[str, ReadoutValues] + ): ... + @property + def mappings(self) -> Dict[str, str]: ... + """ + Get the mappings of a memory region (ie. "ro[0]") to it's key name in readout_values + """ + + @property + def readout_values(self) -> Dict[str, ReadoutValues]: ... + """ + Get the mappings of a readout values identifier (ie. "q0") to a set of ``ReadoutValues`` + """ diff --git a/crates/python/qcs_sdk/qvm/__init__.pyi b/crates/python/qcs_sdk/qvm/__init__.pyi new file mode 100644 index 000000000..edbea0f16 --- /dev/null +++ b/crates/python/qcs_sdk/qvm/__init__.pyi @@ -0,0 +1 @@ +from .result_data import QVMResultData as QVMResultData diff --git a/crates/python/qcs_sdk/qvm/result_data.pyi b/crates/python/qcs_sdk/qvm/result_data.pyi new file mode 100644 index 000000000..e21638868 --- /dev/null +++ b/crates/python/qcs_sdk/qvm/result_data.pyi @@ -0,0 +1,25 @@ +""" +Do not import this file, it has no exports. +It is only here to represent the structure of the rust source code 1:1 +""" + +from typing import Dict + +from .._register_data import RegisterData + +class QVMResultData: + """ + Encapsulates data returned from the QVM after executing a program. + """ + + @staticmethod + def from_memory_map(memory: Dict[str, RegisterData]) -> "QVMResultData": ... + """ + Build a ``QVMResultData`` from a mapping of register names to a ``RegisterData`` matrix. + """ + + @property + def memory(self) -> Dict[str, RegisterData]: ... + """ + Get the mapping of register names (ie. "ro") to a ``RegisterData`` matrix containing the register values. + """ diff --git a/crates/python/src/executable.rs b/crates/python/src/executable.rs index be5f43c3d..5e44f7940 100644 --- a/crates/python/src/executable.rs +++ b/crates/python/src/executable.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use pyo3::{pyclass, FromPyObject}; -use qcs::{Error, Executable, JobHandle, Qpu, Qvm, Service}; +use qcs::{Error, Executable, ExecutionData, JobHandle, Service}; use rigetti_pyo3::{ impl_as_mut_for_wrapper, py_wrap_error, py_wrap_simple_enum, py_wrap_type, pyo3::{exceptions::PyRuntimeError, pymethods, types::PyDict, Py, PyAny, PyResult, Python}, @@ -100,7 +100,7 @@ impl PyExecutable { .await .execute_on_qvm() .await - .map(Qvm::from) + .map(ExecutionData::from) .map(|qvm| Python::with_gil(|py| qvm.to_python(py))) .map_err(ExecutionError::from) .map_err(ExecutionError::to_py_err)? @@ -118,7 +118,7 @@ impl PyExecutable { .await .execute_on_qpu(quantum_processor_id) .await - .map(Qpu::from) + .map(ExecutionData::from) .map(|qpu| Python::with_gil(|py| qpu.to_python(py))) .map_err(ExecutionError::from) .map_err(ExecutionError::to_py_err)? @@ -136,7 +136,7 @@ impl PyExecutable { .await .retrieve_results(job_handle.into_inner()) .await - .map(Qpu::from) + .map(ExecutionData::from) .map(|qpu| Python::with_gil(|py| qpu.to_python(py))) .map_err(ExecutionError::from) .map_err(ExecutionError::to_py_err)? diff --git a/crates/python/src/execution_data.rs b/crates/python/src/execution_data.rs index 076881545..afe039323 100644 --- a/crates/python/src/execution_data.rs +++ b/crates/python/src/execution_data.rs @@ -1,31 +1,63 @@ -use std::{collections::HashMap, time::Duration}; +use std::time::Duration; -use pyo3::{ - pymethods, - types::{PyDelta, PyDict}, - Py, PyResult, Python, -}; -use qcs::{Qpu, Qvm, ReadoutMap, RegisterData}; +use numpy::{Complex64, PyArray2}; +use pyo3::{exceptions::PyValueError, pymethods, types::PyDelta, Py, PyResult, Python}; +use qcs::{ExecutionData, RegisterMap, RegisterMatrix, ResultData}; use qcs_api_client_grpc::models::controller::{readout_values::Values, ReadoutValues}; -use rigetti_pyo3::{py_wrap_data_struct, py_wrap_type, PyWrapper, ToPython}; +use rigetti_pyo3::{ + py_wrap_data_struct, py_wrap_error, py_wrap_type, py_wrap_union_enum, wrap_error, PyTryFrom, + PyWrapper, ToPython, ToPythonError, +}; -use crate::grpc::models::controller::PyReadoutValuesValues; -use crate::register_data::PyRegisterData; +use crate::qvm::PyQvmResultData; +use crate::{grpc::models::controller::PyReadoutValuesValues, qpu::PyQpuResultData}; -py_wrap_data_struct! { - PyQvm(Qvm) as "QVM" { - registers: HashMap => HashMap => Py, - duration: Option => Option> +py_wrap_union_enum! { + PyResultData(ResultData) as "ResultData" { + qpu: Qpu => PyQpuResultData, + qvm: Qvm => PyQvmResultData + } +} + +wrap_error!(RegisterMatrixConversionError( + qcs::RegisterMatrixConversionError +)); +py_wrap_error!( + execution_data, + RegisterMatrixConversionError, + PyRegisterMatrixConversionError, + PyValueError +); + +#[pymethods] +impl PyResultData { + fn to_register_map(&self, py: Python) -> PyResult { + self.as_inner() + .to_register_map() + .map_err(RegisterMatrixConversionError) + .map_err(ToPythonError::to_py_err)? + .to_python(py) } } py_wrap_data_struct! { - PyQpu(Qpu) as "QPU" { - readout_data: ReadoutMap => PyReadoutMap, + PyExecutionData(ExecutionData) as "ExecutionData" { + result_data: ResultData => PyResultData, duration: Option => Option> } } +#[pymethods] +impl PyExecutionData { + #[new] + fn __new__(py: Python<'_>, result_data: PyResultData, duration: Option) -> PyResult { + Ok(Self(ExecutionData { + result_data: ResultData::py_try_from(py, &result_data)?, + duration: duration.map(Duration::from_micros), + })) + } +} + // From gRPC py_wrap_data_struct! { PyReadoutValues(ReadoutValues) as "ReadoutValues" { @@ -34,28 +66,96 @@ py_wrap_data_struct! { } py_wrap_type! { - PyReadoutMap(ReadoutMap) as "ReadoutMap"; + PyRegisterMap(RegisterMap) as "RegisterMap"; +} + +py_wrap_type! { + PyRegisterMatrix(RegisterMatrix) as "RegisterMatrix" +} + +#[pymethods] +impl PyRegisterMatrix { + #[staticmethod] + fn from_integer(matrix: &PyArray2) -> PyRegisterMatrix { + Self(RegisterMatrix::Integer(matrix.to_owned_array())) + } + + fn to_integer<'a>(&self, py: Python<'a>) -> PyResult<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_integer() { + Ok(PyArray2::from_array(py, matrix)) + } else { + Err(PyValueError::new_err("not a integer register")) + } + } + + fn as_integer<'a>(&self, py: Python<'a>) -> Option<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_integer() { + Some(PyArray2::from_array(py, matrix)) + } else { + None + } + } + + fn is_integer(&self) -> bool { + matches!(self.as_inner(), RegisterMatrix::Integer(_)) + } + + #[staticmethod] + fn from_real(matrix: &PyArray2) -> PyRegisterMatrix { + Self(RegisterMatrix::Real(matrix.to_owned_array())) + } + + fn to_real<'a>(&self, py: Python<'a>) -> PyResult<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_real() { + Ok(PyArray2::from_array(py, matrix)) + } else { + Err(PyValueError::new_err("not a real numbered register")) + } + } + + fn as_real<'a>(&self, py: Python<'a>) -> Option<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_real() { + Some(PyArray2::from_array(py, matrix)) + } else { + None + } + } + + fn is_real(&self) -> bool { + matches!(self.as_inner(), RegisterMatrix::Real(_)) + } + + #[staticmethod] + fn from_complex(matrix: &PyArray2) -> PyRegisterMatrix { + Self(RegisterMatrix::Complex(matrix.to_owned_array())) + } + + fn to_complex<'a>(&self, py: Python<'a>) -> PyResult<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_complex() { + Ok(PyArray2::from_array(py, matrix)) + } else { + Err(PyValueError::new_err("not a complex numbered register")) + } + } + + fn as_complex<'a>(&self, py: Python<'a>) -> Option<&'a PyArray2> { + if let Some(matrix) = self.as_inner().as_complex() { + Some(PyArray2::from_array(py, matrix)) + } else { + None + } + } + + fn is_complex(&self) -> bool { + matches!(self.as_inner(), RegisterMatrix::Complex(_)) + } } #[pymethods] -impl PyReadoutMap { - pub fn get_readout_values(&self, field: String, index: u64) -> Option { +impl PyRegisterMap { + pub fn get_register_matrix(&self, register_name: String) -> Option { self.as_inner() - .get_readout_values(field, index) - .map(PyReadoutValues::from) - } - - pub fn get_readout_values_for_field( - &self, - py: Python, - field: &str, - ) -> PyResult>>> { - let op = self.as_inner().get_readout_values_for_field(field)?; - op.map(|list| { - list.into_iter() - .map(|op| op.to_python(py)) - .collect::>() - }) - .transpose() + .get_register_matrix(®ister_name) + .map(PyRegisterMatrix::from) } } diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index fd35e7b56..5805aceec 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -8,13 +8,15 @@ pub mod executable; pub mod execution_data; pub mod grpc; pub mod qpu; +pub mod qvm; pub mod register_data; create_init_submodule! { classes: [ - execution_data::PyQpu, - execution_data::PyQvm, - execution_data::PyReadoutMap, + execution_data::PyExecutionData, + execution_data::PyResultData, + execution_data::PyRegisterMap, + execution_data::PyRegisterMatrix, executable::PyExecutable, executable::PyParameter, executable::PyJobHandle, @@ -23,7 +25,8 @@ create_init_submodule! { qpu::client::PyQcsClient ], errors: [ - QcsExecutionError + QcsExecutionError, + execution_data::PyRegisterMatrixConversionError ], funcs: [ api::compile, @@ -38,7 +41,8 @@ create_init_submodule! { ], submodules: [ "api": api::init_submodule, - "qpu": qpu::init_submodule + "qpu": qpu::init_submodule, + "qvm": qvm::init_submodule ], } diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs index 859d6df0c..5c2928b62 100644 --- a/crates/python/src/qpu/mod.rs +++ b/crates/python/src/qpu/mod.rs @@ -1,11 +1,15 @@ use pyo3::exceptions::PyRuntimeError; use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error}; +pub use result_data::{PyQpuResultData, PyReadoutValues}; + pub mod client; pub mod isa; pub mod quilc; +mod result_data; create_init_submodule! { + classes: [PyQpuResultData, PyReadoutValues], errors: [QcsIsaError], submodules: [ "client": client::init_submodule, diff --git a/crates/python/src/qpu/result_data.rs b/crates/python/src/qpu/result_data.rs new file mode 100644 index 000000000..c95e434d5 --- /dev/null +++ b/crates/python/src/qpu/result_data.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use pyo3::{ + pymethods, + types::{PyComplex, PyFloat, PyInt}, + Py, PyResult, Python, +}; +use qcs::qpu::{QpuResultData, ReadoutValues}; +use rigetti_pyo3::{py_wrap_type, py_wrap_union_enum, PyTryFrom, PyWrapper, ToPython}; + +py_wrap_union_enum! { + PyReadoutValues(ReadoutValues) as "ReadoutValues" { + integer: Integer => Vec>, + real: Real => Vec>, + complex: Complex => Vec> + } +} + +py_wrap_type! { + PyQpuResultData(QpuResultData) as "QPUResultData" +} + +#[pymethods] +impl PyQpuResultData { + #[new] + fn __new__( + py: Python<'_>, + mappings: HashMap, + readout_values: HashMap, + ) -> PyResult { + Ok(Self(QpuResultData::from_mappings_and_values( + mappings, + HashMap::::py_try_from(py, &readout_values)?, + ))) + } + + #[getter] + fn mappings(&self, py: Python<'_>) -> PyResult> { + self.as_inner().mappings().to_python(py) + } + + #[getter] + fn readout_values(&self, py: Python<'_>) -> PyResult> { + self.as_inner().readout_values().to_python(py) + } +} diff --git a/crates/python/src/qvm/mod.rs b/crates/python/src/qvm/mod.rs new file mode 100644 index 000000000..7161d7801 --- /dev/null +++ b/crates/python/src/qvm/mod.rs @@ -0,0 +1,35 @@ +use qcs::{qvm::QvmResultData, RegisterData}; +use rigetti_pyo3::{ + create_init_submodule, py_wrap_type, + pyo3::{prelude::*, Python}, + PyTryFrom, PyWrapper, ToPython, +}; +use std::collections::HashMap; + +use crate::register_data::PyRegisterData; + +py_wrap_type! { + PyQvmResultData(QvmResultData) as "QVMResultData" +} + +#[pymethods] +impl PyQvmResultData { + #[staticmethod] + fn from_memory_map(py: Python<'_>, memory: HashMap) -> PyResult { + Ok(Self(QvmResultData::from_memory_map(HashMap::< + String, + RegisterData, + >::py_try_from( + py, &memory + )?))) + } + + #[getter] + fn memory(&self, py: Python<'_>) -> PyResult> { + self.as_inner().memory().to_python(py) + } +} + +create_init_submodule! { + classes: [PyQvmResultData], +} diff --git a/crates/python/tests/execution_data/test_execution_data.py b/crates/python/tests/execution_data/test_execution_data.py new file mode 100644 index 000000000..08f73251a --- /dev/null +++ b/crates/python/tests/execution_data/test_execution_data.py @@ -0,0 +1,91 @@ +from qcs_sdk.qpu import ReadoutValues, QPUResultData +from qcs_sdk.qvm import QVMResultData +from qcs_sdk import ResultData, RegisterData, RegisterMatrix +import numpy as np +from numpy.testing import assert_array_equal +import pytest + + +class TestResultData: + def test_to_register_map_from_qpu_result_data(self): + mappings = { + "ro[0]": "qA", + "ro[1]": "qB", + "ro[2]": "qC", + } + values = { + "qA": ReadoutValues.from_integer([0, 1]), + "qB": ReadoutValues.from_integer([1, 2]), + "qC": ReadoutValues.from_integer([2, 3]), + } + result_data = ResultData.from_qpu(QPUResultData(mappings, values)) + register_map = result_data.to_register_map() + ro = register_map.get_register_matrix("ro") + assert ro is not None, "'ro' should exist in the register map" + ro = ro.as_integer() + assert ro is not None, "'ro' should be an integer register matrix" + expected = np.array([[0, 1, 2], [1, 2, 3]]) + + assert_array_equal(ro, expected) + + def test_to_register_map_from_jagged_qpu_result_data(self): + mappings = { + "ro[0]": "qA", + "ro[1]": "qB", + "ro[2]": "qC", + } + values = { + "qA": ReadoutValues.from_integer([0, 1]), + "qB": ReadoutValues.from_integer([1]), + "qC": ReadoutValues.from_integer([2, 3]), + } + result_data = ResultData.from_qpu(QPUResultData(mappings, values)) + + with pytest.raises(ValueError): + result_data.to_register_map() + + def test_to_register_map_from_qvm_result_data(self): + qvm_memory_map = {"ro": RegisterData.from_i16([[0, 1, 2], [1, 2, 3]])} + qvm_result_data = QVMResultData.from_memory_map(qvm_memory_map) + result_data = ResultData.from_qvm(qvm_result_data) + register_map = result_data.to_register_map() + ro = register_map.get_register_matrix("ro") + assert ro is not None, "'ro' should exist in the register map" + ro = ro.as_integer() + assert ro is not None, "'ro' should be an integer register matrix" + expected = np.array([[0, 1, 2], [1, 2, 3]]) + + assert_array_equal(ro, expected) + + +class TestRegisterMatrix: + def test_integer(self): + m = np.array([[0, 1, 2], [1, 2, 3]]) + register_matrix = RegisterMatrix.from_integer(m) + assert register_matrix.is_integer() + register_matrix = register_matrix.as_integer() + assert ( + register_matrix is not None + ), "register_matrix should be an integer matrix" + assert_array_equal(register_matrix, m) + + def test_real(self): + m = np.array([[0.0, 1.1, 2.2], [1.1, 2.2, 3.3]]) + register_matrix = RegisterMatrix.from_real(m) + assert register_matrix.is_real() + register_matrix = register_matrix.as_real() + assert register_matrix is not None, "register_matrix should be a real matrix" + assert_array_equal(register_matrix, m) + + def test_complex(self): + m = np.array( + [ + [complex(0, 1), complex(1, 2), complex(2, 3)], + [complex(1, 2), complex(2, 3), complex(3, 4)], + ] + ) + register_matrix = RegisterMatrix.from_complex(m) + assert register_matrix.is_complex() + register_matrix = register_matrix.as_complex() + assert register_matrix is not None, "register_matrix should be a complex matrix" + assert_array_equal(register_matrix, m) diff --git a/crates/python/tests/test_api.py b/crates/python/tests/test_api.py index eb0993991..f8104b0f0 100644 --- a/crates/python/tests/test_api.py +++ b/crates/python/tests/test_api.py @@ -23,7 +23,7 @@ async def test_execute(bell_program: str, device_2q): compiled_program = await qcs_sdk.compile(bell_program, device_2q) translation_result = await qcs_sdk.translate(compiled_program, 1, "Aspen-11") job_id = await qcs_sdk.submit(translation_result["program"], {}, "Aspen-11") - await qcs_sdk.retrieve_results(job_id, "Aspen-11") + await qcs_sdk.retrieve_results(job_id, "Aspen-M-3") def test_rewrite_arithmetic(): diff --git a/crates/python/tests/test_isa.py b/crates/python/tests/test_isa.py index 9b2039646..d7e3b6be9 100644 --- a/crates/python/tests/test_isa.py +++ b/crates/python/tests/test_isa.py @@ -7,24 +7,19 @@ from qcs_sdk.qpu.isa import InstructionSetArchitecture, Family - def ignore_nones(value): """Recursively ignore `None` values, useful for comparing json serializations.""" if isinstance(value, list): return [ignore_nones(x) for x in value if x is not None] elif isinstance(value, dict): - return { - key: ignore_nones(val) - for key, val in value.items() - if val is not None - } + return {key: ignore_nones(val) for key, val in value.items() if val is not None} else: return value @pytest.fixture def aspen_m_3_json() -> str: - filepath = path.join(path.dirname(__file__), "fixtures/aspen-m-3.json" ) + filepath = path.join(path.dirname(__file__), "fixtures/aspen-m-3.json") with open(filepath) as f: contents = f.read() return contents diff --git a/deny.toml b/deny.toml index 2879cef42..b426e96c4 100644 --- a/deny.toml +++ b/deny.toml @@ -36,6 +36,7 @@ allow = [ "ISC", "MIT", "OpenSSL", + "BSD-2-Clause", "BSD-3-Clause", "Unicode-DFS-2016", ]