From b92e8f557d016d41c4cd5f3a031fd6d48819d985 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Tue, 19 Nov 2024 16:53:39 -0800 Subject: [PATCH 01/17] feat: support randomized measurements --- Cargo.lock | 269 +------- crates/lib/Cargo.toml | 12 +- crates/lib/src/qpu/extern_call.rs | 225 +++++++ crates/lib/src/qpu/mod.rs | 2 + crates/lib/src/qpu/randomized_measurements.rs | 580 ++++++++++++++++++ crates/lib/tests/prng_test_cases.json | 339 ++++++++++ crates/python/Cargo.toml | 2 +- 7 files changed, 1171 insertions(+), 258 deletions(-) create mode 100644 crates/lib/src/qpu/extern_call.rs create mode 100644 crates/lib/src/qpu/randomized_measurements.rs create mode 100644 crates/lib/tests/prng_test_cases.json diff --git a/Cargo.lock b/Cargo.lock index 9d03ab06d..5888ce240 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,15 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.86" @@ -139,17 +130,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -225,7 +205,7 @@ checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -244,30 +224,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "cfg-if 0.1.10", - "clang-sys", - "clap", - "env_logger", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -387,15 +343,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "cexpr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" -dependencies = [ - "nom 5.1.3", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -406,12 +353,6 @@ dependencies = [ "target-lexicon", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -427,32 +368,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "clang-sys" -version = "0.29.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" -dependencies = [ - "glob", - "libc", - "libloading 0.5.2", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - [[package]] name = "console" version = "0.15.8" @@ -798,19 +713,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -997,7 +899,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "libc", "wasi", @@ -1146,15 +1048,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -1244,15 +1137,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] - [[package]] name = "hyper" version = "0.14.29" @@ -1590,7 +1474,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1611,7 +1495,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.52.0", ] @@ -1698,12 +1582,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lexical" version = "7.0.1" @@ -1783,49 +1661,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libloading" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" -dependencies = [ - "cc", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" -dependencies = [ - "cfg-if 1.0.0", - "windows-targets 0.52.6", -] - [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" -[[package]] -name = "libquil-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c6dd6ae79389c6811ea65beac8ce9b43cccc61ebc457a13ef16c500a65ab47" -dependencies = [ - "bindgen", - "cc", - "libc", - "libloading 0.8.3", - "num-complex", - "paste", - "pkg-config", - "serde_json", - "thiserror", -] - [[package]] name = "libredox" version = "0.1.3" @@ -2014,16 +1855,6 @@ dependencies = [ "serde", ] -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -2042,7 +1873,7 @@ checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", - "nom 7.1.3", + "nom", ] [[package]] @@ -2144,7 +1975,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -2361,7 +2192,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall", "smallvec", @@ -2434,12 +2265,6 @@ dependencies = [ "syn 2.0.66", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem" version = "3.0.4" @@ -2658,7 +2483,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "indoc", "inventory", "libc", @@ -2816,7 +2641,6 @@ dependencies = [ "insta", "itertools 0.11.0", "lazy_static", - "libquil-sys", "maplit", "ndarray", "num", @@ -2967,12 +2791,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quil-rs" version = "0.29.2" @@ -2983,7 +2801,7 @@ dependencies = [ "itertools 0.12.1", "lexical", "ndarray", - "nom 7.1.3", + "nom", "nom_locate", "num-complex", "once_cell", @@ -3293,7 +3111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "cfg-if 1.0.0", + "cfg-if", "getrandom", "libc", "spin", @@ -3341,7 +3159,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "proc-macro2", "quote", "rustc_version", @@ -3613,7 +3431,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest", ] @@ -3624,7 +3442,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest", ] @@ -3647,12 +3465,6 @@ dependencies = [ "dirs", ] -[[package]] -name = "shlex" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" - [[package]] name = "simba" version = "0.6.0" @@ -3750,12 +3562,6 @@ dependencies = [ "rand", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -3863,21 +3669,12 @@ version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "test-case" version = "3.3.1" @@ -3893,7 +3690,7 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "proc-macro2", "quote", "syn 2.0.66", @@ -3911,15 +3708,6 @@ dependencies = [ "test-case-core", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.61" @@ -3946,7 +3734,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "once_cell", ] @@ -4480,12 +4268,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "unindent" version = "0.2.3" @@ -4549,12 +4331,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version-compare" version = "0.2.0" @@ -4625,7 +4401,7 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] @@ -4650,7 +4426,7 @@ version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -4724,15 +4500,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" -dependencies = [ - "libc", -] - [[package]] name = "wide" version = "0.7.26" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 880699e83..1cc291297 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -14,7 +14,7 @@ manual-tests = [] tracing = ["dep:tracing", "qcs-api-client-common/tracing", "qcs-api-client-grpc/tracing", "qcs-api-client-openapi/tracing"] tracing-config = ["tracing", "qcs-api-client-common/tracing-config", "qcs-api-client-grpc/tracing-config", "qcs-api-client-openapi/tracing-config"] otel-tracing = ["tracing-config", "qcs-api-client-grpc/otel-tracing", "qcs-api-client-openapi/otel-tracing"] -libquil = ["dep:libquil-sys"] +# libquil = ["dep:libquil-sys"] grpc-web = ["qcs-api-client-grpc/grpc-web"] tracing-opentelemetry = ["tracing-config", "qcs-api-client-grpc/tracing-opentelemetry", "qcs-api-client-openapi/tracing-opentelemetry"] @@ -46,7 +46,7 @@ zmq = { version = "0.10.0" } itertools = "0.11.0" derive_builder = "0.12.0" async-trait = "0.1.73" -libquil-sys = { version = "0.4.0", optional = true } +# libquil-sys = { version = "0.4.0", optional = true } [dev-dependencies] erased-serde = "0.3.23" @@ -68,7 +68,7 @@ assert2 = "0.3.11" [build-dependencies] built = "0.6.1" -[[example]] -name = "compilation-and-simulation-with-libquil" -path = "examples/libquil.rs" -required-features = ["libquil"] +# [[example]] +# name = "compilation-and-simulation-with-libquil" +# path = "examples/libquil.rs" +# required-features = ["libquil"] diff --git a/crates/lib/src/qpu/extern_call.rs b/crates/lib/src/qpu/extern_call.rs new file mode 100644 index 000000000..c03096753 --- /dev/null +++ b/crates/lib/src/qpu/extern_call.rs @@ -0,0 +1,225 @@ +use std::ops::BitXor; + +use num::ToPrimitive; +use quil_rs::instruction::ExternError; + +/// An error that may occur when simulating control system extern +/// function calls. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error( + "seed values must be in range [0, {MAX_SEQUENCER_VALUE}) and losslessly convertible to f64, found {0}" + )] + InvalidSeed(u64), + #[error("error converting to Quil: {0}")] + ToQuilError(String), + #[error("error constructing extern signature: {0}")] + ExternSignatureError(#[from] ExternError), +} + +/// A specialized `Result` type for hardware extern function calls. +pub type Result = std::result::Result; + +/// A trait for supporting `PRAGMA EXTERN` and [`quil_rs::instruction::Call`] instructions. +pub trait ExternedCall { + /// The name of the externed function. + const NAME: &'static str; + + /// Build the signature for the externed function. The Magneto service + /// may use this function to check whether user submitted signatures match + /// the expected signature. + fn build_signature() -> Result; + + /// instruction in tests. + fn pragma_extern() -> Result { + use quil_rs::quil::Quil; + + Ok(quil_rs::instruction::Pragma::new( + quil_rs::instruction::RESERVED_PRAGMA_EXTERN.to_string(), + vec![quil_rs::instruction::PragmaArgument::Identifier( + Self::NAME.to_string(), + )], + Some( + Self::build_signature()? + .to_quil() + .map_err(|e| Error::ToQuilError(e.to_string()))?, + ), + )) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ChooseRandomRealSubRegions; + +impl ExternedCall for ChooseRandomRealSubRegions { + const NAME: &str = "choose_random_real_sub_regions"; + + #[allow(clippy::doc_markdown)] + /// Build the signature for the `PRAGMA EXTERN choose_random_real_sub_regions` instruction. + /// The signature is: + /// + /// rust ignore + /// "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" + fn build_signature() -> Result { + use quil_rs::instruction::{ExternParameter, ExternParameterType}; + + let parameters = vec![ + ExternParameter::try_new( + "destination".to_string(), + true, + ExternParameterType::VariableLengthVector(quil_rs::instruction::ScalarType::Real), + ), + ExternParameter::try_new( + "source".to_string(), + false, + ExternParameterType::VariableLengthVector(quil_rs::instruction::ScalarType::Real), + ), + ExternParameter::try_new( + "sub_region_size".to_string(), + false, + ExternParameterType::Scalar(quil_rs::instruction::ScalarType::Integer), + ), + ExternParameter::try_new( + "seed".to_string(), + true, + ExternParameterType::Scalar(quil_rs::instruction::ScalarType::Integer), + ), + ] + .into_iter() + .map(|r| r.map_err(Error::from)) + .collect::>>()?; + Ok(quil_rs::instruction::ExternSignature::new(None, parameters)) + } +} + +/// Hardware values are 48 bits long. +const MAX_SEQUENCER_VALUE: u64 = 0xFFFF_FFFF_FFFF; + +/// Hardware multiplication currently uses the lower 16 bits of +/// the PRNG value. +const MAX_UNSIGNED_MULTIPLIER: u64 = 0x0000_0000_FFFF; + +#[derive(Debug, Clone, Copy)] +pub struct PrngSeedValue { + as_u64: u64, + pub(super) as_f64: f64, +} + +impl PrngSeedValue { + pub fn try_new(value: u64) -> Result { + if !(1..=MAX_SEQUENCER_VALUE).contains(&value) { + return Err(Error::InvalidSeed(value)); + } + if let Some(f64_value) = value.to_f64() { + Ok(Self { + as_u64: value, + as_f64: f64_value, + }) + } else { + Err(Error::InvalidSeed(value)) + } + } +} + +fn lfsr_next(seed: u64, taps: &[u32]) -> u64 { + let feedback_value = taps.iter().fold(0, |acc, tap| { + let base = 2u64.pow(*tap - 1); + let bit = u64::from((seed & base) != 0); + acc.bitxor(bit) + }); + (seed << 1) & MAX_SEQUENCER_VALUE | feedback_value +} + +/// This represents the LFSR currently implemented on Rigetti control systems. Specifically, +/// it implements a 48-bit LFSR with taps at indices 48, 47, 21, and 20. +#[must_use] +pub fn lfsr_v1_next(seed: PrngSeedValue) -> u64 { + lfsr_next(seed.as_u64, &[48, 47, 21, 20]) +} + +fn generate_lfsr_v1_sequence(seed: u64, start_index: u32, series_length: u32) -> Vec { + let mut lfsr = seed & MAX_SEQUENCER_VALUE; + + let range = start_index..(start_index + series_length); + let mut collection = vec![]; + for i in 0..(start_index + series_length) { + lfsr = lfsr_next(lfsr, &[48, 47, 21, 20]); + if range.contains(&i) { + collection.push(lfsr); + } + } + collection +} + +fn prng_value_to_sub_region_index(value: u64, sub_region_count: u8) -> u8 { + ((value & MAX_UNSIGNED_MULTIPLIER) % u64::from(sub_region_count)) + .to_u8() + .expect("modulo u8 should always produce a valid value") +} + +/// Given a seed, start index, series length, and sub-region count, this function +/// will generate the sequence of pseudo-randomly chosen indices Rigetti control +/// systems. +/// +/// For instance, if the following Quil program is run for 100 shots: +/// +/// ```quil +/// # presumed sub-region size is 3. +/// DECLARE destination REAL[6] # prng invocations per shot = (6 / sub_region_size) = 2 +/// DELCARE source REAL[12] # implicit sub-region count = (12 / sub_region_size) = 4 +/// DECLARE seed INTEGER[1] +/// DECLARE ro BIT[1] +/// +/// DELAY 0 1e-6 +/// +// PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" +// CALL choose_random_real_sub_regions destination source 3 seed +/// ``` +/// +/// with a seed of 639523, you could backout the randomly chosen sub-regions with the following: +/// +/// ```rust +/// let seed = 639523; +/// let start_index = 0; +/// let prng_invocations_per_shot = 2; +/// let shot_count = 100; +/// let series_length = prng_invocations_per_shot * shot_count; +/// let sub_region_count = 4; +/// let _random_indices = choose_random_real_sub_region_indices(seed, start_index, series_length, sub_region_count).unwrap(); +/// ``` +#[must_use] +pub fn choose_random_real_sub_region_indices( + seed: PrngSeedValue, + start_index: u32, + series_length: u32, + sub_region_count: u8, +) -> Vec { + generate_lfsr_v1_sequence(seed.as_u64, start_index, series_length) + .iter() + .map(|&value| prng_value_to_sub_region_index(value, sub_region_count)) + .collect() +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, fs::File}; + + fn prng_sequences() -> HashMap> { + serde_json::de::from_reader(File::open("tests/prng_test_cases.json").unwrap()).unwrap() + } + + #[test] + fn test_lfsr_v1_next() { + for (num_shots, sequences) in prng_sequences() { + for (seed, expected) in sequences { + let sequence = super::generate_lfsr_v1_sequence(seed, num_shots - 1, 1); + assert_eq!(sequence.len(), 1); + let end_of_sequence = sequence[0]; + assert_eq!( + end_of_sequence, expected, + "seed={seed}, num_shots={num_shots}", + ); + } + } + } +} diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs index c7204f5b8..5af28fcdb 100644 --- a/crates/lib/src/qpu/mod.rs +++ b/crates/lib/src/qpu/mod.rs @@ -16,6 +16,8 @@ use tokio::time::error::Elapsed; pub mod api; mod execution; +pub mod extern_call; +pub mod randomized_measurements; pub mod result_data; pub mod translation; diff --git a/crates/lib/src/qpu/randomized_measurements.rs b/crates/lib/src/qpu/randomized_measurements.rs new file mode 100644 index 000000000..2e7f22a4d --- /dev/null +++ b/crates/lib/src/qpu/randomized_measurements.rs @@ -0,0 +1,580 @@ +use std::collections::HashMap; + +use itertools::Itertools; +use ndarray::{Array2, Order}; +use num::{complex::Complex64, ToPrimitive}; +use quil_rs::{ + expression::Expression, + instruction::{ + Call, CallError, Declaration, Delay, Fence, Gate, Instruction, Measurement, + MemoryReference, Qubit, ScalarType, UnresolvedCallArgument, Vector, + }, + quil::Quil, + Program, +}; + +use crate::executable::Parameters; + +use super::extern_call::{ExternedCall, PrngSeedValue}; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("only measurements on fixed qubits are supported, found {0:?}")] + UnsupportedMeasurementQubit(Qubit), + #[error("error declaring external function: {0}")] + Pragma(#[from] super::extern_call::Error), + #[error("error initializing call instruction: {0}")] + Call(#[from] CallError), + #[error("seed not provided for qubit {}", .0.to_quil_or_debug())] + MissingSeed(Qubit), + #[error("shape error occurred during parameter conversion: {0}")] + UnitariesShape(#[from] ndarray::ShapeError), + #[error("invalid unitary set; expected {expected} unitaries, found {found}")] + InvalidUnitarySet { expected: usize, found: usize }, + #[error("the number of parameters per unitary must be convertible to f64, found {0}")] + ParametersPerUnitaryF64Conversion(usize), + #[error("the seed value must be convertible to f64, found {0}")] + SeedValueF64Conversion(usize), + #[error("the unitary count must be convertible to u8, found {0}")] + UnitaryCountU8Conversion(usize), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +struct QubitRandomization { + seed_declaration: Declaration, + destination_declaration: Declaration, + measurement: Measurement, +} + +impl QubitRandomization { + fn destination_reference(&self, index: u64) -> MemoryReference { + MemoryReference::new(self.destination_declaration.name.clone(), index) + } +} + +const RANDOMIZED_MEASUREMENT_SOURCE: &str = "randomized_measurement_source"; +const RANDOMIZED_MEASUREMENT_DESTINATION: &str = "randomized_measurement_destination"; +const RANDOMIZED_MEASUREMENT_SEED: &str = "randomized_measurement_seed"; + +#[derive(Debug, Clone)] +pub struct RandomizedMeasurements { + leading_delay: Expression, + unitary_set: UnitarySet, + unitary_count_as_u8: u8, + qubit_randomizations: Vec, +} + +impl RandomizedMeasurements { + pub fn try_new( + measurements: Vec, + unitary_set: UnitarySet, + leading_delay: Expression, + ) -> Result { + let qubit_randomizations: Vec = measurements + .into_iter() + .map(|measurement| { + if let Qubit::Fixed(qubit) = measurement.qubit { + Ok((measurement, format!("q{qubit}"))) + } else { + Err(Error::UnsupportedMeasurementQubit(measurement.qubit)) + } + }) + .map_ok(|(measurement, qubit_name)| QubitRandomization { + seed_declaration: Declaration { + name: format!("{RANDOMIZED_MEASUREMENT_SEED}_{}", qubit_name.clone()), + size: Vector::new(ScalarType::Integer, 1), + sharing: None, + }, + destination_declaration: Declaration { + name: format!( + "{RANDOMIZED_MEASUREMENT_DESTINATION}_{}", + qubit_name.clone() + ), + size: Vector::new( + ScalarType::Real, + unitary_set.parameters_per_unitary() as u64, + ), + sharing: None, + }, + measurement, + }) + .collect::>()?; + let unitary_count_as_u8 = unitary_set + .unitary_count() + .to_u8() + .ok_or_else(|| Error::UnitaryCountU8Conversion(unitary_set.unitary_count()))?; + Ok(Self { + leading_delay, + unitary_set, + unitary_count_as_u8, + qubit_randomizations, + }) + } +} + +impl RandomizedMeasurements { + pub fn append_to_program(&self, target_program: Program) -> Result { + let mut program = target_program.clone_without_body_instructions(); + + program.add_instruction(Instruction::Declaration(Declaration { + name: RANDOMIZED_MEASUREMENT_SOURCE.to_string(), + size: Vector::new( + ScalarType::Real, + (self.unitary_set.parameters_per_unitary() * self.unitary_set.unitary_count()) + as u64, + ), + sharing: None, + })); + + for qubit_randomization in &self.qubit_randomizations { + program.add_instruction(Instruction::Declaration( + qubit_randomization.destination_declaration.clone(), + )); + program.add_instruction(Instruction::Declaration( + qubit_randomization.seed_declaration.clone(), + )); + } + + // prepend delay + program.add_instruction(Instruction::Delay(Delay::new( + self.leading_delay.clone(), + Vec::new(), + self.qubit_randomizations + .iter() + .map(|randomization| randomization.measurement.qubit.clone()) + .collect(), + ))); + + // declare "choose_random_real_sub_regions" as an external function + program.add_instruction(Instruction::Pragma( + super::extern_call::ChooseRandomRealSubRegions::pragma_extern()?, + )); + + let parameters_per_unitary = self + .unitary_set + .parameters_per_unitary() + .to_f64() + .ok_or_else(|| { + Error::ParametersPerUnitaryF64Conversion(self.unitary_set.parameters_per_unitary()) + })?; + // Before the pulse program begins, set the randomized unitary for each qubit. + let calls: Vec<_> = self + .qubit_randomizations + .iter() + .map(|qubit_randomization| { + Call::try_new( + super::extern_call::ChooseRandomRealSubRegions::NAME.to_string(), + vec![ + UnresolvedCallArgument::Identifier( + qubit_randomization.destination_declaration.name.clone(), + ), + UnresolvedCallArgument::Identifier( + RANDOMIZED_MEASUREMENT_SOURCE.to_string(), + ), + UnresolvedCallArgument::Immediate(Complex64 { + re: parameters_per_unitary, + im: 0.0, + }), + UnresolvedCallArgument::Identifier( + qubit_randomization.seed_declaration.name.clone(), + ), + ], + ) + .map_err(Error::from) + }) + .map_ok(Instruction::Call) + .collect::>>()?; + program.add_instructions(calls); + + // Include the program body that was passed in. + program.add_instructions(target_program.into_body_instructions()); + + // Play the random unitaries on each qubit. + program.add_instructions( + self.unitary_set + .to_instructions(self.qubit_randomizations.as_slice()), + ); + + program.add_instruction(Instruction::Fence(Fence { qubits: Vec::new() })); + // Measure each qubit. + for qubit_randomization in &self.qubit_randomizations { + program.add_instruction(Instruction::Measurement( + qubit_randomization.measurement.clone(), + )); + } + Ok(program) + } + + pub fn to_parameters(&self, seed_values: &HashMap) -> Result { + let mut parameters = HashMap::new(); + parameters.insert( + RANDOMIZED_MEASUREMENT_SOURCE.to_string().into_boxed_str(), + self.unitary_set.to_parameters()?, + ); + + for qubit_randomization in &self.qubit_randomizations { + let seed_value = seed_values + .get(&qubit_randomization.measurement.qubit) + .ok_or_else(|| Error::MissingSeed(qubit_randomization.measurement.qubit.clone()))?; + parameters.insert( + qubit_randomization + .seed_declaration + .name + .clone() + .into_boxed_str(), + vec![seed_value.as_f64], + ); + parameters.insert( + qubit_randomization + .destination_declaration + .name + .clone() + .into_boxed_str(), + std::iter::repeat(0.0) + .take(self.unitary_set.parameters_per_unitary()) + .collect(), + ); + } + + Ok(parameters) + } + + #[must_use] + pub fn get_random_indices( + &self, + seed_values: &HashMap, + shot_count: u32, + ) -> HashMap> { + seed_values + .iter() + .map(|(qubit, seed_value)| { + ( + qubit.clone(), + super::extern_call::choose_random_real_sub_region_indices( + *seed_value, + 0, + shot_count, + self.unitary_count_as_u8, + ), + ) + }) + .collect() + } +} + +#[derive(Debug, Clone)] +pub enum UnitarySet { + Zxzxz(Array2), +} + +impl UnitarySet { + pub fn try_new_zxzxz(unitaries: Array2) -> Result { + if unitaries.ncols() != 3 { + return Err(Error::InvalidUnitarySet { + expected: 3, + found: unitaries.ncols(), + }); + } + Ok(UnitarySet::Zxzxz(unitaries)) + } + + fn unitary_count(&self) -> usize { + match self { + UnitarySet::Zxzxz(unitaries) => unitaries.nrows(), + } + } + + const fn parameters_per_unitary(&self) -> usize { + match self { + UnitarySet::Zxzxz(_) => 3, + } + } + + fn to_parameters(&self) -> Result> { + match self { + UnitarySet::Zxzxz(unitaries) => Ok(unitaries + .to_shape((unitaries.len(), Order::RowMajor))? + .iter() + .copied() + .collect()), + } + } + + fn to_instructions(&self, qubit_randomizations: &[QubitRandomization]) -> Vec { + match self { + Self::Zxzxz(_) => Self::to_zxzxz_instructions(qubit_randomizations), + } + } + + fn to_zxzxz_instructions(qubit_randomizations: &[QubitRandomization]) -> Vec { + let mut instructions = vec![Instruction::Fence(Fence { qubits: Vec::new() })]; + for qubit_randomization in qubit_randomizations { + instructions.extend(vec![ + rz( + qubit_randomization.measurement.qubit.clone(), + qubit_randomization.destination_reference(0), + ), + rx_pi_on_2(qubit_randomization.measurement.qubit.clone()), + rz( + qubit_randomization.measurement.qubit.clone(), + qubit_randomization.destination_reference(1), + ), + ]); + } + instructions.push(Instruction::Fence(Fence { qubits: Vec::new() })); + for qubit_randomization in qubit_randomizations { + instructions.extend(vec![ + rx_pi_on_2(qubit_randomization.measurement.qubit.clone()), + rz( + qubit_randomization.measurement.qubit.clone(), + qubit_randomization.destination_reference(2), + ), + ]); + } + instructions + } +} + +fn rx_pi_on_2(qubit: Qubit) -> Instruction { + Instruction::Gate(Gate { + name: "RX".to_string(), + parameters: vec![ + Expression::PiConstant / Expression::Number(Complex64 { re: 2.0, im: 0.0 }), + ], + qubits: vec![qubit], + modifiers: vec![], + }) +} + +fn rz(qubit: Qubit, memory_reference: MemoryReference) -> Instruction { + Instruction::Gate(Gate { + name: "RZ".to_string(), + parameters: vec![ + Expression::Number(Complex64 { re: 2.0, im: 0.0 }) + * Expression::PiConstant + * Expression::Address(memory_reference), + ], + qubits: vec![qubit], + modifiers: vec![], + }) +} + +#[cfg(test)] +mod tests { + use core::f64; + use std::str::FromStr; + + use super::*; + use rstest::*; + + const BASE_QUIL_PROGRAM: &str = r" +DECLARE ro BIT[3] + +H 0 +H 1 +H 2 +"; + + const BASE_QUIL_PROGRAM_WITH_MEASUREMENTS: &str = r#" +DECLARE ro BIT[3] +DECLARE randomized_measurement_source REAL[36] +DECLARE randomized_measurement_destination_q0 REAL[3] +DECLARE randomized_measurement_seed_q0 INTEGER[1] +DECLARE randomized_measurement_destination_q1 REAL[3] +DECLARE randomized_measurement_seed_q1 INTEGER[1] +DECLARE randomized_measurement_destination_q2 REAL[3] +DECLARE randomized_measurement_seed_q2 INTEGER[1] + +DELAY 0 1 2 1e-6 + +PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" + +CALL choose_random_real_sub_regions randomized_measurement_destination_q0 randomized_measurement_source 3 randomized_measurement_seed_q0 +CALL choose_random_real_sub_regions randomized_measurement_destination_q1 randomized_measurement_source 3 randomized_measurement_seed_q1 +CALL choose_random_real_sub_regions randomized_measurement_destination_q2 randomized_measurement_source 3 randomized_measurement_seed_q2 + +H 0 +H 1 +H 2 + +FENCE + +RZ(2*pi*randomized_measurement_destination_q0[0]) 0 +RX(pi/2) 0 +RZ(2*pi*randomized_measurement_destination_q0[1]) 0 + +RZ(2*pi*randomized_measurement_destination_q1[0]) 1 +RX(pi/2) 1 +RZ(2*pi*randomized_measurement_destination_q1[1]) 1 + +RZ(2*pi*randomized_measurement_destination_q2[0]) 2 +RX(pi/2) 2 +RZ(2*pi*randomized_measurement_destination_q2[1]) 2 + +FENCE + +RX(pi/2) 0 +RZ(2*pi*randomized_measurement_destination_q0[2]) 0 + +RX(pi/2) 1 +RZ(2*pi*randomized_measurement_destination_q1[2]) 1 + +RX(pi/2) 2 +RZ(2*pi*randomized_measurement_destination_q2[2]) 2 + +FENCE + +MEASURE 0 ro[0] +MEASURE 1 ro[1] +MEASURE 2 ro[2] +"#; + + #[fixture] + fn unitary_set() -> Vec { + vec![ + 0., + f64::consts::FRAC_PI_2, + -f64::consts::FRAC_PI_2, + f64::consts::PI, + f64::consts::FRAC_PI_2, + -f64::consts::FRAC_PI_2, + 0., + f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + f64::consts::PI, + f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + -f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + f64::consts::PI, + -f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + 0., + f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + f64::consts::PI, + f64::consts::FRAC_PI_2, + f64::consts::FRAC_PI_2, + 0., + f64::consts::FRAC_PI_2, + f64::consts::PI, + -f64::consts::FRAC_PI_2, + f64::consts::PI, + f64::consts::PI, + 0., + -f64::consts::FRAC_PI_2, + 0., + -f64::consts::FRAC_PI_2, + 0., + 0., + f64::consts::PI, + ] + } + + #[fixture] + fn randomized_measurements(unitary_set: Vec) -> RandomizedMeasurements { + let measurements = (0..3) + .map(|i| { + Measurement::new( + Qubit::Fixed(i), + Some(MemoryReference::new("ro".to_string(), i)), + ) + }) + .collect(); + + let unitary_set = UnitarySet::try_new_zxzxz( + Array2::from_shape_vec((12, 3), unitary_set).expect("must be valid unitary array"), + ) + .expect("valid unitary set"); + let leading_delay = Expression::Number(Complex64 { re: 1e-6, im: 0.0 }); + + RandomizedMeasurements::try_new(measurements, unitary_set, leading_delay) + .expect("must be valid randomized measurements") + } + + #[rstest] + fn test_append_to_program(randomized_measurements: RandomizedMeasurements) { + let randomized_program = randomized_measurements + .append_to_program( + Program::from_str(BASE_QUIL_PROGRAM).expect("must be valid Quil program"), + ) + .expect("must append to program"); + + let expected_program = Program::from_str(BASE_QUIL_PROGRAM_WITH_MEASUREMENTS) + .expect("must be valid Quil program"); + + println!( + "randomized_program: {}", + randomized_program.to_quil().unwrap() + ); + + println!("{}", expected_program.to_quil().unwrap()); + assert_eq!(randomized_program, expected_program); + } + + #[fixture] + fn seeds() -> Vec { + vec![463_692_700, 733_101_278, 925_742_198] + } + + #[fixture] + fn seed_values(seeds: Vec) -> HashMap { + seeds + .iter() + .enumerate() + .map(|(i, seed)| { + ( + Qubit::Fixed(i as u64), + PrngSeedValue::try_new(*seed).expect("valid seed value"), + ) + }) + .collect() + } + + #[rstest] + fn test_to_parameters( + randomized_measurements: RandomizedMeasurements, + unitary_set: Vec, + seeds: Vec, + seed_values: HashMap, + ) { + let mut expected_parameters = HashMap::new(); + expected_parameters.insert( + "randomized_measurement_source".to_string().into_boxed_str(), + unitary_set, + ); + seeds.iter().enumerate().for_each(|(i, seed)| { + expected_parameters.insert( + format!("randomized_measurement_seed_q{i}").into_boxed_str(), + vec![seed.to_f64().expect("valid f64 seed value")], + ); + expected_parameters.insert( + format!("randomized_measurement_destination_q{i}").into_boxed_str(), + vec![0.0, 0.0, 0.0], + ); + }); + + let parameters = randomized_measurements + .to_parameters(&seed_values) + .expect("must be valid parameters"); + + assert_eq!(parameters, expected_parameters); + } + + #[rstest] + fn test_get_random_indices( + randomized_measurements: RandomizedMeasurements, + seed_values: HashMap, + ) { + let shot_count = 3; + let expected_random_indices = [vec![0, 8, 1], vec![1, 2, 1], vec![5, 10, 5]] + .iter() + .enumerate() + .map(|(i, indices)| (Qubit::Fixed(i as u64), indices.clone())) + .collect::>>(); + let random_indices = randomized_measurements.get_random_indices(&seed_values, shot_count); + assert_eq!(random_indices, expected_random_indices); + } +} diff --git a/crates/lib/tests/prng_test_cases.json b/crates/lib/tests/prng_test_cases.json new file mode 100644 index 000000000..262304e07 --- /dev/null +++ b/crates/lib/tests/prng_test_cases.json @@ -0,0 +1,339 @@ +{ + "1": [ + [ + 2, + 4 + ], + [ + 4, + 8 + ], + [ + 8, + 16 + ], + [ + 16, + 32 + ], + [ + 32, + 64 + ], + [ + 64, + 128 + ], + [ + 128, + 256 + ], + [ + 256, + 512 + ], + [ + 512, + 1024 + ], + [ + 1024, + 2048 + ], + [ + 2048, + 4096 + ], + [ + 4096, + 8192 + ], + [ + 8192, + 16384 + ], + [ + 16384, + 32768 + ], + [ + 32768, + 65536 + ], + [ + 65536, + 131072 + ], + [ + 131072, + 262144 + ], + [ + 262144, + 524288 + ], + [ + 524288, + 1048577 + ], + [ + 1048577, + 2097155 + ], + [ + 2097155, + 4194310 + ], + [ + 4194310, + 8388620 + ], + [ + 8388620, + 16777240 + ], + [ + 16777240, + 33554480 + ], + [ + 33554480, + 67108960 + ], + [ + 67108960, + 134217920 + ], + [ + 134217920, + 268435840 + ], + [ + 268435840, + 536871680 + ], + [ + 536871680, + 1073743360 + ], + [ + 1073743360, + 2147486720 + ], + [ + 2147486720, + 4294973440 + ], + [ + 4294973440, + 8589946880 + ], + [ + 8589946880, + 17179893760 + ], + [ + 17179893760, + 34359787520 + ], + [ + 34359787520, + 68719575040 + ], + [ + 68719575040, + 137439150080 + ], + [ + 137439150080, + 274878300160 + ], + [ + 274878300160, + 549756600320 + ], + [ + 549756600320, + 1099513200641 + ], + [ + 1099513200641, + 2199026401282 + ], + [ + 2199026401282, + 4398052802565 + ], + [ + 4398052802565, + 8796105605130 + ], + [ + 8796105605130, + 17592211210260 + ], + [ + 17592211210260, + 35184422420520 + ], + [ + 35184422420520, + 70368844841040 + ], + [ + 70368844841040, + 140737689682081 + ], + [ + 140737689682081, + 402653507 + ], + [ + 402653507, + 805307014 + ], + [ + 805307014, + 1610614028 + ], + [ + 1610614028, + 3221228056 + ], + [ + 3221228056, + 6442456112 + ], + [ + 6442456112, + 12884912224 + ], + [ + 12884912224, + 25769824448 + ], + [ + 25769824448, + 51539648896 + ], + [ + 51539648896, + 103079297792 + ], + [ + 103079297792, + 206158595584 + ], + [ + 206158595584, + 412317191168 + ], + [ + 412317191168, + 824634382336 + ], + [ + 824634382336, + 1649268764673 + ], + [ + 1649268764673, + 3298537529347 + ], + [ + 3298537529347, + 6597075058695 + ], + [ + 6597075058695, + 13194150117391 + ], + [ + 13194150117391, + 26388300234782 + ], + [ + 26388300234782, + 52776600469564 + ], + [ + 52776600469564, + 105553200939128 + ] + ], + "10000": [ + [ + 262233119786494, + 231523667603426 + ], + [ + 98342925703104, + 43851413449043 + ], + [ + 189861291528451, + 24142703242138 + ], + [ + 143187344962073, + 274766841870622 + ], + [ + 62527754824409, + 128786755851799 + ], + [ + 89112260344295, + 147239233983656 + ], + [ + 32200423113636, + 169316971173610 + ], + [ + 164847846973270, + 210847153311833 + ], + [ + 115798256647477, + 256540846036132 + ], + [ + 262233119786494, + 231523667603426 + ], + [ + 98342925703104, + 43851413449043 + ], + [ + 189861291528451, + 24142703242138 + ], + [ + 143187344962073, + 274766841870622 + ], + [ + 62527754824409, + 128786755851799 + ], + [ + 89112260344295, + 147239233983656 + ], + [ + 32200423113636, + 169316971173610 + ], + [ + 164847846973270, + 210847153311833 + ], + [ + 115798256647477, + 256540846036132 + ] + ] +} + diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml index 4d0abc698..83e53c18f 100644 --- a/crates/python/Cargo.toml +++ b/crates/python/Cargo.toml @@ -10,7 +10,7 @@ categories = ["api-bindings", "compilers", "science", "emulators"] readme = "./README.md" [features] -libquil = ["qcs/libquil"] +# libquil = ["qcs/libquil"] grpc-web = ["qcs/grpc-web"] [lib] From 276ea18c27a048884dc707c7a8e5d9379bbfdf74 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 25 Nov 2024 08:45:51 -0800 Subject: [PATCH 02/17] feat: support randomized measurements --- Cargo.lock | 269 ++++++++++++++++++++++++++++++++++++--- crates/lib/Cargo.toml | 12 +- crates/python/Cargo.toml | 2 +- 3 files changed, 258 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5888ce240..51acd5115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -130,6 +139,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -205,7 +225,7 @@ checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -224,6 +244,30 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -343,6 +387,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.3", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -353,6 +406,12 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -368,6 +427,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "clang-sys" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" +dependencies = [ + "glob", + "libc", + "libloading 0.5.2", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "console" version = "0.15.8" @@ -713,6 +798,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -899,7 +997,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi", @@ -1048,6 +1146,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1137,6 +1244,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + [[package]] name = "hyper" version = "0.14.29" @@ -1474,7 +1590,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1495,7 +1611,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.52.0", ] @@ -1582,6 +1698,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical" version = "7.0.1" @@ -1661,12 +1783,49 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if 1.0.0", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libquil-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c6dd6ae79389c6811ea65beac8ce9b43cccc61ebc457a13ef16c500a65ab47" +dependencies = [ + "bindgen", + "cc", + "libc", + "libloading 0.8.5", + "num-complex", + "paste", + "pkg-config", + "serde_json", + "thiserror", +] + [[package]] name = "libredox" version = "0.1.3" @@ -1855,6 +2014,16 @@ dependencies = [ "serde", ] +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -1873,7 +2042,7 @@ checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", - "nom", + "nom 7.1.3", ] [[package]] @@ -1975,7 +2144,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -2192,7 +2361,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", @@ -2265,6 +2434,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "3.0.4" @@ -2483,7 +2658,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "indoc", "inventory", "libc", @@ -2641,6 +2816,7 @@ dependencies = [ "insta", "itertools 0.11.0", "lazy_static", + "libquil-sys", "maplit", "ndarray", "num", @@ -2791,6 +2967,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quil-rs" version = "0.29.2" @@ -2801,7 +2983,7 @@ dependencies = [ "itertools 0.12.1", "lexical", "ndarray", - "nom", + "nom 7.1.3", "nom_locate", "num-complex", "once_cell", @@ -3111,7 +3293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.0", "getrandom", "libc", "spin", @@ -3159,7 +3341,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro2", "quote", "rustc_version", @@ -3431,7 +3613,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] @@ -3442,7 +3624,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] @@ -3465,6 +3647,12 @@ dependencies = [ "dirs", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "simba" version = "0.6.0" @@ -3562,6 +3750,12 @@ dependencies = [ "rand", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.10.0" @@ -3669,12 +3863,21 @@ version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "rustix", "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-case" version = "3.3.1" @@ -3690,7 +3893,7 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro2", "quote", "syn 2.0.66", @@ -3708,6 +3911,15 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.61" @@ -3734,7 +3946,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] @@ -4268,6 +4480,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unindent" version = "0.2.3" @@ -4331,6 +4549,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.0" @@ -4401,7 +4625,7 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -4426,7 +4650,7 @@ version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -4500,6 +4724,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "wide" version = "0.7.26" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 1cc291297..880699e83 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -14,7 +14,7 @@ manual-tests = [] tracing = ["dep:tracing", "qcs-api-client-common/tracing", "qcs-api-client-grpc/tracing", "qcs-api-client-openapi/tracing"] tracing-config = ["tracing", "qcs-api-client-common/tracing-config", "qcs-api-client-grpc/tracing-config", "qcs-api-client-openapi/tracing-config"] otel-tracing = ["tracing-config", "qcs-api-client-grpc/otel-tracing", "qcs-api-client-openapi/otel-tracing"] -# libquil = ["dep:libquil-sys"] +libquil = ["dep:libquil-sys"] grpc-web = ["qcs-api-client-grpc/grpc-web"] tracing-opentelemetry = ["tracing-config", "qcs-api-client-grpc/tracing-opentelemetry", "qcs-api-client-openapi/tracing-opentelemetry"] @@ -46,7 +46,7 @@ zmq = { version = "0.10.0" } itertools = "0.11.0" derive_builder = "0.12.0" async-trait = "0.1.73" -# libquil-sys = { version = "0.4.0", optional = true } +libquil-sys = { version = "0.4.0", optional = true } [dev-dependencies] erased-serde = "0.3.23" @@ -68,7 +68,7 @@ assert2 = "0.3.11" [build-dependencies] built = "0.6.1" -# [[example]] -# name = "compilation-and-simulation-with-libquil" -# path = "examples/libquil.rs" -# required-features = ["libquil"] +[[example]] +name = "compilation-and-simulation-with-libquil" +path = "examples/libquil.rs" +required-features = ["libquil"] diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml index 83e53c18f..4d0abc698 100644 --- a/crates/python/Cargo.toml +++ b/crates/python/Cargo.toml @@ -10,7 +10,7 @@ categories = ["api-bindings", "compilers", "science", "emulators"] readme = "./README.md" [features] -# libquil = ["qcs/libquil"] +libquil = ["qcs/libquil"] grpc-web = ["qcs/grpc-web"] [lib] From 9976b095bfe28e3830d81b4518ad92079fcce883 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 25 Nov 2024 09:18:52 -0800 Subject: [PATCH 03/17] refactor: move randomization implementations to qpu/experimental --- CODEOWNERS | 1 + crates/lib/src/qpu/experimental/mod.rs | 2 ++ .../{extern_call.rs => experimental/random.rs} | 0 .../randomized_measurements.rs | 2 +- crates/lib/src/qpu/mod.rs | 3 +-- .../_tracing_subscriber/common/__init__.py | 1 + .../_tracing_subscriber/common/__init__.pyi | 6 ++++-- .../_tracing_subscriber/layers/__init__.py | 1 + .../_tracing_subscriber/layers/__init__.pyi | 16 ++++++++-------- .../_tracing_subscriber/layers/file/__init__.py | 2 ++ .../_tracing_subscriber/layers/file/__init__.pyi | 1 + .../layers/otel_otlp/__init__.py | 1 + .../layers/otel_otlp/__init__.pyi | 6 ++++-- .../layers/otel_otlp_file/__init__.py | 1 + .../layers/otel_otlp_file/__init__.pyi | 10 +++------- .../_tracing_subscriber/subscriber/__init__.py | 1 + .../_tracing_subscriber/subscriber/__init__.pyi | 1 + 17 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 CODEOWNERS create mode 100644 crates/lib/src/qpu/experimental/mod.rs rename crates/lib/src/qpu/{extern_call.rs => experimental/random.rs} (100%) rename crates/lib/src/qpu/{ => experimental}/randomized_measurements.rs (99%) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..a711ec08b --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +crates/lib/src/qpu/experimental @erichulburd diff --git a/crates/lib/src/qpu/experimental/mod.rs b/crates/lib/src/qpu/experimental/mod.rs new file mode 100644 index 000000000..781616e03 --- /dev/null +++ b/crates/lib/src/qpu/experimental/mod.rs @@ -0,0 +1,2 @@ +pub mod random; +pub mod randomized_measurements; diff --git a/crates/lib/src/qpu/extern_call.rs b/crates/lib/src/qpu/experimental/random.rs similarity index 100% rename from crates/lib/src/qpu/extern_call.rs rename to crates/lib/src/qpu/experimental/random.rs diff --git a/crates/lib/src/qpu/randomized_measurements.rs b/crates/lib/src/qpu/experimental/randomized_measurements.rs similarity index 99% rename from crates/lib/src/qpu/randomized_measurements.rs rename to crates/lib/src/qpu/experimental/randomized_measurements.rs index 2e7f22a4d..82886cc67 100644 --- a/crates/lib/src/qpu/randomized_measurements.rs +++ b/crates/lib/src/qpu/experimental/randomized_measurements.rs @@ -15,7 +15,7 @@ use quil_rs::{ use crate::executable::Parameters; -use super::extern_call::{ExternedCall, PrngSeedValue}; +use super::random::{ExternedCall, PrngSeedValue}; #[derive(Debug, Clone, thiserror::Error)] pub enum Error { diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs index 5af28fcdb..42b51105a 100644 --- a/crates/lib/src/qpu/mod.rs +++ b/crates/lib/src/qpu/mod.rs @@ -16,8 +16,7 @@ use tokio::time::error::Elapsed; pub mod api; mod execution; -pub mod extern_call; -pub mod randomized_measurements; +pub mod experimental; pub mod result_data; pub mod translation; diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py index f5a875b22..16c779086 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py @@ -15,3 +15,4 @@ __doc__ = common.__doc__ __all__ = getattr(common, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi index 6395b5e35..07127164f 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi @@ -12,14 +12,15 @@ from typing import Dict, Optional, final + @final class InstrumentationLibrary: """ Information about a library or crate providing instrumentation. - + An instrumentation library should be named to follow any naming conventions of the instrumented library (e.g. 'middleware' for a web framework). - + See the `instrumentation libraries `_ spec for more information. """ @@ -41,3 +42,4 @@ class InstrumentationLibrary: :param attributes: The attributes of the instrumentation library. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py index 8c674f36e..a4123cdf1 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py @@ -15,3 +15,4 @@ __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi index 6fdd52b85..741a427e2 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi @@ -13,18 +13,18 @@ from __future__ import annotations from typing import TYPE_CHECKING -from . import file as file +from . import file as file from . import otel_otlp_file as otel_otlp_file from . import otel_otlp as otel_otlp if TYPE_CHECKING: - from typing import Union + from typing import Union - Config = Union[ - file.Config, - otel_otlp_file.Config, - otel_otlp.Config, - ] - """ + Config = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py index 80cff859b..f1ac331e1 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py @@ -15,3 +15,5 @@ __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) + + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi index 8054827fe..7caa5c2e0 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi @@ -39,3 +39,4 @@ class Config: :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py index 8a3bcdd6e..7b1f6baf6 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -15,3 +15,4 @@ __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi index 2e47373d3..f5990ffc8 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -48,6 +48,8 @@ class Resource: schema_url: Optional[str] = None, ) -> "Resource": ... + + @final class Config: """ @@ -99,14 +101,14 @@ class Config: ... if TYPE_CHECKING: - from typing import List, Union + from typing import List, Union ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue = Union[bool, int, float, str, ResourceValueArray] + ResourceValue= Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py index 4d7ea6443..7f928b757 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -15,3 +15,4 @@ __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index e55db109d..0b55c2f8a 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -13,6 +13,7 @@ from typing import Optional, final from qcs_sdk._tracing_subscriber.common import InstrumentationLibrary + @final class Config: """ @@ -20,13 +21,7 @@ class Config: layer. """ - def __new__( - cls, - *, - file_path: Optional[str] = None, - filter: Optional[str] = None, - instrumentation_library: Optional[InstrumentationLibrary] = None, - ) -> "Config": + def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None, instrumentation_library: Optional[InstrumentationLibrary] = None) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the @@ -40,3 +35,4 @@ class Config: :param instrumentation_library: Information about the library providing the tracing instrumentation. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py index 5f4924199..fb711d054 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py @@ -15,3 +15,4 @@ __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi index e0894d0a4..548de0cc5 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi @@ -22,3 +22,4 @@ class Config: """ def __new__(cls, *, layer: layers.Config) -> "Config": ... + From c40fe9b8f18b6baf268d0f2cc4d0150c77334634 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 27 Nov 2024 15:50:20 -0800 Subject: [PATCH 04/17] refactor: break extern call functionality into a separate module --- crates/lib/src/qpu/experimental/mod.rs | 9 ++ crates/lib/src/qpu/experimental/random.rs | 113 +++++++++++----- .../experimental/randomized_measurements.rs | 124 ++++++++++++------ crates/lib/src/qpu/externed_call.rs | 43 ++++++ crates/lib/src/qpu/mod.rs | 1 + crates/python/src/qpu/experimental.rs | 0 crates/python/src/qpu/mod.rs | 1 + 7 files changed, 220 insertions(+), 71 deletions(-) create mode 100644 crates/lib/src/qpu/externed_call.rs create mode 100644 crates/python/src/qpu/experimental.rs diff --git a/crates/lib/src/qpu/experimental/mod.rs b/crates/lib/src/qpu/experimental/mod.rs index 781616e03..66c5847ca 100644 --- a/crates/lib/src/qpu/experimental/mod.rs +++ b/crates/lib/src/qpu/experimental/mod.rs @@ -1,2 +1,11 @@ +//! This module supports experimental features that meet the following criteria: +//! +//! * They support recent and compelling research in quantum computing. +//! * Implementation is specific to Rigetti's QPUs; implementation may +//! not otherise be expressed through a generalized quantum computing +//! language, such as Quil or QASM. +//! +//! As such, the features contained herein should be considered unstable, possibly +//! ephemeral, and subject to specific authorization checks. pub mod random; pub mod randomized_measurements; diff --git a/crates/lib/src/qpu/experimental/random.rs b/crates/lib/src/qpu/experimental/random.rs index c03096753..957f98fca 100644 --- a/crates/lib/src/qpu/experimental/random.rs +++ b/crates/lib/src/qpu/experimental/random.rs @@ -1,62 +1,103 @@ -use std::ops::BitXor; +//! This module supports low-level primitives for randomization on Rigetti's QPUs. +use std::{convert::TryFrom, ops::BitXor}; -use num::ToPrimitive; -use quil_rs::instruction::ExternError; +use num::{complex::Complex64, ToPrimitive}; +use quil_rs::instruction::{Call, CallError, ExternError, UnresolvedCallArgument}; + +use crate::qpu::externed_call::ExternedCall; /// An error that may occur when simulating control system extern /// function calls. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { + /// An invalid seed value was provided. #[error( - "seed values must be in range [0, {MAX_SEQUENCER_VALUE}) and losslessly convertible to f64, found {0}" + "seed values must be in range [1, {MAX_SEQUENCER_VALUE}) and losslessly convertible to f64, found {0}" )] InvalidSeed(u64), + /// An error occurred while converting to Quil. #[error("error converting to Quil: {0}")] ToQuilError(String), + /// An error occurred while constructing an extern signature. #[error("error constructing extern signature: {0}")] ExternSignatureError(#[from] ExternError), } +impl From for Error { + fn from(e: quil_rs::quil::ToQuilError) -> Self { + Self::ToQuilError(e.to_string()) + } +} + /// A specialized `Result` type for hardware extern function calls. pub type Result = std::result::Result; -/// A trait for supporting `PRAGMA EXTERN` and [`quil_rs::instruction::Call`] instructions. -pub trait ExternedCall { - /// The name of the externed function. - const NAME: &'static str; - - /// Build the signature for the externed function. The Magneto service - /// may use this function to check whether user submitted signatures match - /// the expected signature. - fn build_signature() -> Result; - - /// instruction in tests. - fn pragma_extern() -> Result { - use quil_rs::quil::Quil; - - Ok(quil_rs::instruction::Pragma::new( - quil_rs::instruction::RESERVED_PRAGMA_EXTERN.to_string(), - vec![quil_rs::instruction::PragmaArgument::Identifier( - Self::NAME.to_string(), - )], - Some( - Self::build_signature()? - .to_quil() - .map_err(|e| Error::ToQuilError(e.to_string()))?, - ), - )) +/// An [`ExternedCall`] that may be used to select one or more random +/// sub-regions from a source array of real values to a destination array. +#[derive(Debug, Clone)] +pub struct ChooseRandomRealSubRegions { + destination: String, + source: String, + sub_region_size: Complex64, + seed: String, +} + +impl ChooseRandomRealSubRegions { + /// Create a new instance of [`ChooseRandomRealSubRegions`]. + /// + /// # Parameters + /// + /// * `destination` - The name of the destination array. + /// * `source` - The identifier of the source array. + /// * `sub_region_size` - The size of the sub-regions to select from + /// the source array. Note, `len(source) % sub_region_size` and + /// `len(destination)` must be zero. + /// * `seed` - The name of the seed value. + /// + /// The values provided for `destination`, `source`, and `seed` must + /// be declared within the Quil program where the call is made. + pub fn new>( + destination: String, + source: String, + sub_region_size: T, + seed: String, + ) -> Self { + Self { + destination, + source, + sub_region_size: Complex64 { + re: sub_region_size.into(), + im: 0.0, + }, + seed, + } } } -#[derive(Debug, Clone, Copy)] -pub struct ChooseRandomRealSubRegions; +impl TryFrom for Call { + type Error = CallError; + + fn try_from(value: ChooseRandomRealSubRegions) -> std::result::Result { + Self::try_new( + ChooseRandomRealSubRegions::NAME.to_string(), + vec![ + UnresolvedCallArgument::Identifier(value.destination.clone()), + UnresolvedCallArgument::Identifier(value.source.to_string()), + UnresolvedCallArgument::Immediate(value.sub_region_size), + UnresolvedCallArgument::Identifier(value.seed.clone()), + ], + ) + } +} impl ExternedCall for ChooseRandomRealSubRegions { + type Error = Error; + const NAME: &str = "choose_random_real_sub_regions"; #[allow(clippy::doc_markdown)] - /// Build the signature for the `PRAGMA EXTERN choose_random_real_sub_regions` instruction. - /// The signature is: + /// Build the signature for the `PRAGMA EXTERN choose_random_real_sub_regions` + /// instruction. The signature is: /// /// rust ignore /// "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" @@ -99,6 +140,9 @@ const MAX_SEQUENCER_VALUE: u64 = 0xFFFF_FFFF_FFFF; /// the PRNG value. const MAX_UNSIGNED_MULTIPLIER: u64 = 0x0000_0000_FFFF; +/// A valid seed value that may be used to initialize the PRNG. Such +/// values are in the range `[1, MAX_SEQUENCER_VALUE]` and are losslessly +/// convertible to `f64`. #[derive(Debug, Clone, Copy)] pub struct PrngSeedValue { as_u64: u64, @@ -106,6 +150,9 @@ pub struct PrngSeedValue { } impl PrngSeedValue { + /// Attempt to create a new instance of `PrngSeedValue` from a `u64`. + /// The value must be in the range `[1, MAX_SEQUENCER_VALUE]` and + /// losslessly convertible to `f64`. pub fn try_new(value: u64) -> Result { if !(1..=MAX_SEQUENCER_VALUE).contains(&value) { return Err(Error::InvalidSeed(value)); diff --git a/crates/lib/src/qpu/experimental/randomized_measurements.rs b/crates/lib/src/qpu/experimental/randomized_measurements.rs index 82886cc67..61c762d06 100644 --- a/crates/lib/src/qpu/experimental/randomized_measurements.rs +++ b/crates/lib/src/qpu/experimental/randomized_measurements.rs @@ -1,4 +1,23 @@ -use std::collections::HashMap; +//! This module contains generalized support for adding randomized measurements +//! to a Quil program. It builds off of the primitives defined in [`super::random`]. +//! +//! There are three critical components to correctly adding randomized measurements +//! to a program: +//! +//! 1. Program construction - adding the classical randomization calls to the +//! prologue of the program (i.e. before the pulse program begins) and referencing +//! those randomized values within a unitary decomposition prior to measurement. +//! 2. Parameter construction - building a map of [`Parameters`] with seeds for +//! each qubit. +//! 3. PRNG reconstruction - backing out the random indices that played on each +//! qubit during program execution. +//! +//! Recall this is not QIS (quantum information science) library, but rather an +//! SDK for collecting data from Rigetti QPUs. As such, defining the proper +//! unitary set and using randomized measurement data is unsupported by this +//! library. + +use std::{collections::HashMap, convert::TryFrom}; use itertools::Itertools; use ndarray::{Array2, Order}; @@ -7,38 +26,55 @@ use quil_rs::{ expression::Expression, instruction::{ Call, CallError, Declaration, Delay, Fence, Gate, Instruction, Measurement, - MemoryReference, Qubit, ScalarType, UnresolvedCallArgument, Vector, + MemoryReference, Qubit, ScalarType, Vector, }, quil::Quil, Program, }; +use super::random::PrngSeedValue; use crate::executable::Parameters; +use crate::qpu::externed_call::ExternedCall; -use super::random::{ExternedCall, PrngSeedValue}; - +/// An error that may occur when constructing randomized measurements. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { + /// Received measurement on non-fixed qubits. #[error("only measurements on fixed qubits are supported, found {0:?}")] UnsupportedMeasurementQubit(Qubit), - #[error("error declaring external function: {0}")] - Pragma(#[from] super::extern_call::Error), + /// An error occurred while constructing `PRAGMA EXTERN` instruction. + #[error("error declaring extern function: {0}")] + Pragma(#[from] super::random::Error), + /// An error occurred while constructing a call instruction. #[error("error initializing call instruction: {0}")] Call(#[from] CallError), + /// A seed value was not provided for a qubit. #[error("seed not provided for qubit {}", .0.to_quil_or_debug())] MissingSeed(Qubit), + /// An error occur while flattening an [`ndarray::Array`]. #[error("shape error occurred during parameter conversion: {0}")] UnitariesShape(#[from] ndarray::ShapeError), - #[error("invalid unitary set; expected {expected} unitaries, found {found}")] - InvalidUnitarySet { expected: usize, found: usize }, + /// A unitary set was specified with an incorrect number of columns. + #[error("invalid unitary set; expected unitaries of length {expected}, found {found}")] + InvalidUnitarySet { + /// The expected number of columns + expected: usize, + /// The number of columns + found: usize, + }, #[error("the number of parameters per unitary must be convertible to f64, found {0}")] + /// The number of parameters per unitary could not be expressed losslessly as + /// an f64. ParametersPerUnitaryF64Conversion(usize), #[error("the seed value must be convertible to f64, found {0}")] + /// A seed value could not be expressed losslessly as an f64. SeedValueF64Conversion(usize), + /// The implicit unitary count could not be expressed as a u8. #[error("the unitary count must be convertible to u8, found {0}")] UnitaryCountU8Conversion(usize), } +/// A specialized `Result` type for randomized measurements. pub type Result = std::result::Result; #[derive(Debug, Clone)] @@ -49,6 +85,16 @@ struct QubitRandomization { } impl QubitRandomization { + fn into_instruction(self, parameters_per_unitary: f64) -> Result { + let externed_call = super::random::ChooseRandomRealSubRegions::new( + self.destination_declaration.name, + RANDOMIZED_MEASUREMENT_SOURCE.to_string(), + parameters_per_unitary, + self.seed_declaration.name, + ); + Ok(Instruction::Call(Call::try_from(externed_call)?)) + } + fn destination_reference(&self, index: u64) -> MemoryReference { MemoryReference::new(self.destination_declaration.name.clone(), index) } @@ -58,6 +104,7 @@ const RANDOMIZED_MEASUREMENT_SOURCE: &str = "randomized_measurement_source"; const RANDOMIZED_MEASUREMENT_DESTINATION: &str = "randomized_measurement_destination"; const RANDOMIZED_MEASUREMENT_SEED: &str = "randomized_measurement_seed"; +/// Configuration for adding randomized measurements to a Quil program. #[derive(Debug, Clone)] pub struct RandomizedMeasurements { leading_delay: Expression, @@ -67,6 +114,16 @@ pub struct RandomizedMeasurements { } impl RandomizedMeasurements { + /// Initialize a new instance of [`RandomizedMeasurements`]. + /// + /// # Parameters + /// + /// * `measurements` - A vector of measurements to randomize. Note, these + /// measurements should not be added to a program a priori. + /// * `unitary_set` - The set of unitaries to apply to each qubit before + /// measurement. + /// * `leading_delay` - The delay to prepend to the program before the + /// randomized measurements begin. Typically, this will be 1e-6s to 1e-5s. pub fn try_new( measurements: Vec, unitary_set: UnitarySet, @@ -115,6 +172,8 @@ impl RandomizedMeasurements { } impl RandomizedMeasurements { + /// Append the randomized measurements to a Quil program. The provided program + /// should not contain any preexisting measurements. pub fn append_to_program(&self, target_program: Program) -> Result { let mut program = target_program.clone_without_body_instructions(); @@ -149,7 +208,7 @@ impl RandomizedMeasurements { // declare "choose_random_real_sub_regions" as an external function program.add_instruction(Instruction::Pragma( - super::extern_call::ChooseRandomRealSubRegions::pragma_extern()?, + super::random::ChooseRandomRealSubRegions::pragma_extern()?, )); let parameters_per_unitary = self @@ -163,28 +222,8 @@ impl RandomizedMeasurements { let calls: Vec<_> = self .qubit_randomizations .iter() - .map(|qubit_randomization| { - Call::try_new( - super::extern_call::ChooseRandomRealSubRegions::NAME.to_string(), - vec![ - UnresolvedCallArgument::Identifier( - qubit_randomization.destination_declaration.name.clone(), - ), - UnresolvedCallArgument::Identifier( - RANDOMIZED_MEASUREMENT_SOURCE.to_string(), - ), - UnresolvedCallArgument::Immediate(Complex64 { - re: parameters_per_unitary, - im: 0.0, - }), - UnresolvedCallArgument::Identifier( - qubit_randomization.seed_declaration.name.clone(), - ), - ], - ) - .map_err(Error::from) - }) - .map_ok(Instruction::Call) + .cloned() + .map(|qubit_randomization| qubit_randomization.into_instruction(parameters_per_unitary)) .collect::>>()?; program.add_instructions(calls); @@ -207,6 +246,8 @@ impl RandomizedMeasurements { Ok(program) } + /// Given a map of qubits to seed values, construct the parameters required + /// to randomize measurements accordingly. pub fn to_parameters(&self, seed_values: &HashMap) -> Result { let mut parameters = HashMap::new(); parameters.insert( @@ -241,6 +282,8 @@ impl RandomizedMeasurements { Ok(parameters) } + /// Given a map of qubits to seed values, return the random indices that + /// were played on each qubit during program execution. #[must_use] pub fn get_random_indices( &self, @@ -252,7 +295,7 @@ impl RandomizedMeasurements { .map(|(qubit, seed_value)| { ( qubit.clone(), - super::extern_call::choose_random_real_sub_region_indices( + super::random::choose_random_real_sub_region_indices( *seed_value, 0, shot_count, @@ -264,12 +307,23 @@ impl RandomizedMeasurements { } } +/// A set of unitaries, each of which may be expressed as a set of Quil +/// instructions. #[derive(Debug, Clone)] pub enum UnitarySet { + /// A set of unitaries expressed as a sequence of the following gate + /// operations: + /// + /// RZ(angle_0)-RX(pi/2)-RZ(angle_1)-RX(pi/2)-RZ(angle_2). + /// + /// The unitaries are stored in a 2D array where each row represents + /// a single unitary expressed as the three RZ angles. Zxzxz(Array2), } impl UnitarySet { + /// Attempt to create a new instance of [`UnitarySet`] from a 2D array + /// of unitaries. The array must have three columns. pub fn try_new_zxzxz(unitaries: Array2) -> Result { if unitaries.ncols() != 3 { return Err(Error::InvalidUnitarySet { @@ -505,12 +559,6 @@ MEASURE 2 ro[2] let expected_program = Program::from_str(BASE_QUIL_PROGRAM_WITH_MEASUREMENTS) .expect("must be valid Quil program"); - println!( - "randomized_program: {}", - randomized_program.to_quil().unwrap() - ); - - println!("{}", expected_program.to_quil().unwrap()); assert_eq!(randomized_program, expected_program); } diff --git a/crates/lib/src/qpu/externed_call.rs b/crates/lib/src/qpu/externed_call.rs new file mode 100644 index 000000000..7bdd105d6 --- /dev/null +++ b/crates/lib/src/qpu/externed_call.rs @@ -0,0 +1,43 @@ +//! This module contains a definition for the [`ExternedCall`] trait. +//! Implementations of this trait represent externed functions that +//! the QCS backend supports. +//! +//! Trait implementations support semantically meaningful interfaces +//! that may be converted into Quil [`quil_rs::instruction::Call`] +//! instructions as well as their associated `PRAGMA EXTERN` +//! instructions. +use std::convert::TryInto; + +use quil_rs::quil::ToQuilError; + +/// A trait for supporting `PRAGMA EXTERN` and [`quil_rs::instruction::Call`] instructions. +pub trait ExternedCall: Sized + TryInto { + /// An error that may occur when building the signature. + type Error: From; + + /// The name of the externed function. + const NAME: &'static str; + + /// Build the signature for the externed function. The Magneto service + /// may use this function to check whether user submitted signatures match + /// the expected signature. + fn build_signature( + ) -> Result::Error>; + + /// Build a `PRAGMA EXTERN` instruction for the externed function. + fn pragma_extern() -> Result::Error> { + use quil_rs::quil::Quil; + + Ok(quil_rs::instruction::Pragma::new( + quil_rs::instruction::RESERVED_PRAGMA_EXTERN.to_string(), + vec![quil_rs::instruction::PragmaArgument::Identifier( + Self::NAME.to_string(), + )], + Some( + Self::build_signature()? + .to_quil() + .map_err(::Error::from)?, + ), + )) + } +} diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs index 42b51105a..279e75ef7 100644 --- a/crates/lib/src/qpu/mod.rs +++ b/crates/lib/src/qpu/mod.rs @@ -17,6 +17,7 @@ use tokio::time::error::Elapsed; pub mod api; mod execution; pub mod experimental; +pub mod externed_call; pub mod result_data; pub mod translation; diff --git a/crates/python/src/qpu/experimental.rs b/crates/python/src/qpu/experimental.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs index bdfdf7773..770d943c5 100644 --- a/crates/python/src/qpu/mod.rs +++ b/crates/python/src/qpu/mod.rs @@ -8,6 +8,7 @@ use rigetti_pyo3::{ pub use result_data::{PyQpuResultData, PyReadoutValues, RawQpuReadoutData}; pub mod api; +pub mod experimental; pub mod isa; mod result_data; pub mod translation; From 89a8d269c577dc6d772da73118c19452b0bed6a9 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 11:44:16 -0800 Subject: [PATCH 05/17] feat: support randomized measurements over python bindings --- crates/python/qcs_sdk/qpu/__init__.pyi | 1 + crates/python/qcs_sdk/qpu/experimental.pyi | 171 +++++++++++++++ crates/python/src/qpu/experimental.rs | 241 +++++++++++++++++++++ crates/python/src/qpu/mod.rs | 5 +- 4 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 crates/python/qcs_sdk/qpu/experimental.pyi diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi index 147f92836..4948a79b4 100644 --- a/crates/python/qcs_sdk/qpu/__init__.pyi +++ b/crates/python/qcs_sdk/qpu/__init__.pyi @@ -10,6 +10,7 @@ from qcs_sdk.client import QCSClient from qcs_sdk.qpu import ( api as api, + experimental as experimental, isa as isa, translation as translation, ) diff --git a/crates/python/qcs_sdk/qpu/experimental.pyi b/crates/python/qcs_sdk/qpu/experimental.pyi new file mode 100644 index 000000000..8264e903e --- /dev/null +++ b/crates/python/qcs_sdk/qpu/experimental.pyi @@ -0,0 +1,171 @@ +from typing import Dict, List, Optional, Self, Tuple, final + +import numpy as np +from numpy.typing import NDArray + + +@final +class RandomError(ValueError): + """ + An error that may occur while initializing a seed value or + generating a QPU PRNG sequence. + """ + ... + +@final +class PrngSeedValue: + """A seed value for the Rigetti QPU PRNG.""" + + def __new__(cls, value: int) -> Self: ... + + +@final +class ChooseRandomRealSubRegions: + """ + A class that represents a QPU extern call to pseudo-randomly choose sub-regions of + a source memory declaration. The parameters of the call function are as follows: + + * `destination` - The destination memory region to copy the pseudo-randomly chosen + sub-region to. + * `source` - The source memory region to choose sub-regions from. + * `sub_region_size` - The size of each sub-region to choose. + * `seed` - A memory reference to an integer value that will be used to seed the PRNG. + This value will be mutated to the next element in the PRNG sequence, so you may + use it to generate subsequent pseudo-random values. + + Note, `len(destination) % sub_region-size` and `len(source) % sub_region_size` must be 0. + + # Example + + The following example declares a source with 12 real values, representing 4 sub-regions + each of size 3. The destination memory region is declared with 3 real values, representing + a single sub-region. The seed memory region is declared with a single integer value. + + The `Call` instruction will pseudo-randomly choose a sub-region of the source memory region + and copy it to the destination memory region according to the seed value. The seed value is + mutated to the next element in the PRNG sequence. + + >>> program = Program() + >>> destination = Declaration("destination", Vector(ScalarType.Real, 3), None) + >>> program.add_instruction(Instruction.from_declaration(destination)) + >>> source = Declaration("source", Vector(ScalarType.Real, 12), None) + >>> program.add_instruction(Instruction.from_declaration(source)) + >>> seed = Declaration("seed", Vector(ScalarType.Integer, 1), None) + >>> program.add_instruction(Instruction.from_declaration(seed)) + >>> pragma_extern = Pragma("EXTERN", [PragmaArgument.from_identifier(ChooseRandomRealSubRegions.NAME)], ChooseRandomRealSubRegions.build_signature()) + >>> program.add_instruction(Instruction.from_pragma(pragma_extern)) + >>> call = Call(ChooseRandomRealSubRegions.NAME, [ + ... CallArgument.from_identifier("destination"), + ... CallArgument.from_identifier("source"), + ... CallArgument.from_immediate(complex(3, 0)), + ... CallArgument.from_memory_reference(MemoryReference("seed", 0)), + ... ]) + >>> program.add_instruction(Instruction.from_call(call)) + >>> print(program.to_quil()) + PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" + DECLARE destination REAL[3] + DECLARE source REAL[9] + DECLARE seed INTEGER[1] + CALL choose_random_real_sub_regions destination source 3 seed[0] + + From there, you may reference the `destination` memory region in your pulse program. + """ + + NAME: str + """ + The name of the extern call function, which may be used in `PRAGMA EXTERN` and `CALL` instructions. + """ + + @classmethod + def build_signature(cls) -> str: + """Build the Quil signature of the `PRAGMA EXTERN` instruction.""" + ... + + +def lfsr_v1_next(seed_value: PrngSeedValue) -> PrngSeedValue: + """Given a seed value, return the next value in the LFSR v1 PRNG sequence.""" + ... + + +def choose_random_real_sub_region_indices( + seed: PrngSeedValue, + start_index: int, + series_length: int, + sub_region_count: int, +) -> List[int]: + """ + Given a seed value, the starting index and length of a pseudo-random series, and the number of + sub-regions from which to choose, return a list of the sub-region indices that were chosen. + + The LFSR v1 pseudo-random number generator underlies this sequence. + """ + ... + + +@final +class RandomizedMeasurementsError(ValueError): + """ + An error that can occur when adding randomized measurements to a program. + """ + ... + + +@final +class UnitarySet: + """ + A set of unitaries that may be applied to a pulse program prior to measurement. Currently, + there is a single enum variant, `ZXZXZ`, which represents a unitary as the following + sequence of instructions: + + RZ(angle_0)-RX(pi/2)-RZ(angle_1)-RX(pi/2)-RZ(angle_2) + + Each unitary is, thus, represented as the three angles which parameterize the RZ gates. + """ + + def is_zxzxz(self) -> bool: ... + def as_zxzxz(self) -> Optional[NDArray[np.float64]]: ... + def to_zxzxz(self) -> NDArray[np.float64]: ... + @staticmethod + def from_zxzxz(inner: NDArray[np.float64]) -> "UnitarySet": + """ + Initialize a `UnitarySet` as a ZXZXZ decomposition. The input array must have shape (n, 3), + where n is the number of unitaries in the set. + """ + ... + + +@final +class RandomizedMeasurement: + # TODO: should we make this signature more infallible? + def __new__(cls, qubit: int, target: Optional[Tuple[str, int]]) -> Self: ... + + +@final +class RandomizedMeasurements: + """A class that supports the addition of randomized measurements to a Quil program.""" + + def __new__(cls, measurements: List[RandomizedMeasurement], unitary_set: UnitarySet, leading_delay: float = ...) -> Self: ... + + def append_to_program(self, target_program: str) -> str: + """ + Given a target Quil program, this routine will add the necessary declarations, `PRAGMA EXTERN`, `CALL`, and + `DELAY` instructions to the beginning of the program for randomly selecting a measurement basis. + + It will then apply the pseudo-randomly selected measurement basis (see `UnitarySet`) to each measured + qubit before measuring the qubit. + """ + ... + + def to_parameters(self, seed_values: Dict[int, PrngSeedValue]) -> Dict[str, List[float]]: + """ + Given a map of fixed qubit indices to seed values, return a memory map that is necessary to + realize a pseudo-random sequence of measurements according to the specified seeds. + """ + ... + + def get_random_indices(self, seed_values: Dict[int, PrngSeedValue], shot_count: int) -> Dict[int, List[int]]: + """ + Given a map of fixed qubit indices to seed values, return the pseudo-randomly selected measurement + indices for each qubit according to the specified seeds. + """ + ... diff --git a/crates/python/src/qpu/experimental.rs b/crates/python/src/qpu/experimental.rs index e69de29bb..23115a642 100644 --- a/crates/python/src/qpu/experimental.rs +++ b/crates/python/src/qpu/experimental.rs @@ -0,0 +1,241 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use numpy::{Complex64, PyArray2}; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; +use qcs::qpu::experimental::random::{ + choose_random_real_sub_region_indices, lfsr_v1_next, ChooseRandomRealSubRegions, PrngSeedValue, +}; +use qcs::qpu::experimental::randomized_measurements::{RandomizedMeasurements, UnitarySet}; +use qcs::qpu::externed_call::ExternedCall; +use quil_rs::instruction::{Measurement, MemoryReference, Qubit}; +use quil_rs::quil::Quil; +use rigetti_pyo3::pyo3::exceptions::PyValueError; +use rigetti_pyo3::{ + create_init_submodule, py_wrap_error, py_wrap_type, wrap_error, PyWrapper, ToPythonError, +}; + +create_init_submodule! { + classes: [ + PyPrngSeedValue, + PyRandomizedMeasurement, + PyRandomizedMeasurements, + PyUnitarySet, + PyChooseRandomRealSubRegions + ], + errors: [ + RandomError, + RandomizedMeasurementsError + ], + funcs: [ + py_lfsr_v1_next, + py_choose_random_real_sub_region_indices + ], +} + +wrap_error!(RustRandomError(qcs::qpu::experimental::random::Error)); +py_wrap_error!(experimental, RustRandomError, RandomError, PyValueError); + +#[pyclass(name = "PrngSeedValue")] +#[derive(Clone)] +struct PyPrngSeedValue { + inner: PrngSeedValue, +} + +#[pymethods] +impl PyPrngSeedValue { + #[new] + fn new(seed: u64) -> PyResult { + PrngSeedValue::try_new(seed) + .map(|inner| Self { inner }) + .map_err(RustRandomError::from) + .map_err(RustRandomError::to_py_err) + } +} + +#[pyclass(name = "ChooseRandomRealSubRegions")] +struct PyChooseRandomRealSubRegions; + +#[pymethods] +impl PyChooseRandomRealSubRegions { + #[classattr] + const NAME: &'static str = ChooseRandomRealSubRegions::NAME; + + #[classmethod] + fn build_signature(_cls: &pyo3::types::PyType) -> PyResult { + ChooseRandomRealSubRegions::build_signature() + .map_err(RustRandomError::from) + .map_err(RustRandomError::to_py_err) + .and_then(|signature| { + signature.to_quil().map_err(|e| { + PyRuntimeError::new_err(format!("failed to write signature as Quil: {e}")) + .to_py_err() + }) + }) + } +} + +#[pyfunction(name = "lfsr_v1_next")] +fn py_lfsr_v1_next(seed_value: PyPrngSeedValue) -> PyResult { + PyPrngSeedValue::new(lfsr_v1_next(seed_value.inner)) +} + +#[pyfunction(name = "choose_random_real_sub_region_indices")] +fn py_choose_random_real_sub_region_indices( + seed: PyPrngSeedValue, + start_index: u32, + series_length: u32, + sub_region_count: u8, +) -> Vec { + choose_random_real_sub_region_indices(seed.inner, start_index, series_length, sub_region_count) +} + +wrap_error!(RustRandomizedMeasurementsError( + qcs::qpu::experimental::randomized_measurements::Error +)); +py_wrap_error!( + experimental, + RustRandomizedMeasurementsError, + RandomizedMeasurementsError, + PyValueError +); + +py_wrap_type! { + PyUnitarySet(UnitarySet) as "UnitarySet"; +} +rigetti_pyo3::impl_repr!(PyUnitarySet); + +#[pymethods] +impl PyUnitarySet { + #[staticmethod] + fn from_zxzxz(inner: &PyArray2) -> PyUnitarySet { + Self(UnitarySet::Zxzxz(inner.to_owned_array())) + } + + fn to_zxzxz<'a>(&self, py: Python<'a>) -> PyResult<&'a PyArray2> { + let UnitarySet::Zxzxz(matrix) = self.as_inner(); + Ok(PyArray2::from_array(py, matrix)) + } + + fn as_zxzxz<'a>(&self, py: Python<'a>) -> Option<&'a PyArray2> { + self.to_zxzxz(py).ok() + } + + fn is_zxzxz(&self) -> bool { + matches!(self.as_inner(), UnitarySet::Zxzxz(_)) + } +} + +#[pyclass(name = "RandomizedMeasurement")] +#[derive(Clone)] +struct PyRandomizedMeasurement { + inner: Measurement, +} + +#[pymethods] +impl PyRandomizedMeasurement { + #[new] + fn new(qubit: u64, target: Option<(String, u64)>) -> Self { + Self { + inner: Measurement { + qubit: Qubit::Fixed(qubit), + target: target.map(|(name, index)| MemoryReference { name, index }), + }, + } + } +} + +#[pyclass(name = "RandomizedMeasurements")] +#[derive(Clone)] +struct PyRandomizedMeasurements { + inner: RandomizedMeasurements, +} + +impl From for Measurement { + fn from(py_measurement: PyRandomizedMeasurement) -> Self { + py_measurement.inner + } +} + +#[pymethods] +impl PyRandomizedMeasurements { + #[new] + #[pyo3(signature = (measurements, unitary_set, leading_delay = 1e-5))] + fn new( + measurements: Vec, + unitary_set: PyUnitarySet, + leading_delay: f64, + ) -> PyResult { + RandomizedMeasurements::try_new( + measurements.into_iter().map(Measurement::from).collect(), + unitary_set.as_inner().clone(), + quil_rs::expression::Expression::Number(Complex64 { + re: leading_delay, + im: 0.0, + }), + ) + .map(|inner| Self { inner }) + .map_err(RustRandomizedMeasurementsError::from) + .map_err(RustRandomizedMeasurementsError::to_py_err) + } + + fn append_to_program(&self, target_program: String) -> PyResult { + let target_program = quil_rs::Program::from_str(target_program.as_str()) + .map_err(|e| PyValueError::new_err(format!("failed to parse target program: {e}")))?; + self.inner + .append_to_program(target_program) + .map_err(RustRandomizedMeasurementsError::from) + .map_err(RustRandomizedMeasurementsError::to_py_err) + .and_then(|program| { + program.to_quil().map_err(|e| { + PyRuntimeError::new_err(format!("failed to write program as Quil: {e}")) + }) + }) + } + + fn to_parameters( + &self, + seed_values: HashMap, + ) -> PyResult>> { + self.inner + .to_parameters( + &seed_values + .into_iter() + .map(|(k, v)| (Qubit::Fixed(k), v.inner)) + .collect(), + ) + .map(|parameters| { + parameters + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect() + }) + .map_err(RustRandomizedMeasurementsError::from) + .map_err(RustRandomizedMeasurementsError::to_py_err) + } + + fn get_random_indices( + &self, + seed_values: HashMap, + shot_count: u32, + ) -> PyResult>> { + self.inner + .get_random_indices( + &seed_values + .into_iter() + .map(|(k, v)| (Qubit::Fixed(k), v.inner)) + .collect(), + shot_count, + ) + .into_iter() + .map(|(qubit, sequence)| match qubit { + Qubit::Fixed(qubit) => Ok((qubit, sequence)), + _ => Err(PyRuntimeError::new_err( + "The Rust implementation erroneously produced non-fixed qubits", + ) + .to_py_err()), + }) + .collect::>, PyErr>>() + } +} diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs index 770d943c5..d100dbe83 100644 --- a/crates/python/src/qpu/mod.rs +++ b/crates/python/src/qpu/mod.rs @@ -8,7 +8,7 @@ use rigetti_pyo3::{ pub use result_data::{PyQpuResultData, PyReadoutValues, RawQpuReadoutData}; pub mod api; -pub mod experimental; +pub(crate) mod experimental; pub mod isa; mod result_data; pub mod translation; @@ -34,7 +34,8 @@ create_init_submodule! { submodules: [ "api": api::init_submodule, "isa": isa::init_submodule, - "translation": translation::init_submodule + "translation": translation::init_submodule, + "experimental": experimental::init_submodule ], } From 91aee5ed57eba2cd5c808b4665cb1008a7456e09 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 12:18:11 -0800 Subject: [PATCH 06/17] test: randomized measurements --- crates/python/Makefile.toml | 5 + crates/python/qcs_sdk/qpu/experimental.pyi | 21 ++- crates/python/tests/qpu/test_experimental.py | 129 +++++++++++++++++++ 3 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 crates/python/tests/qpu/test_experimental.py diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml index 4874a965f..5856a4d9e 100644 --- a/crates/python/Makefile.toml +++ b/crates/python/Makefile.toml @@ -60,6 +60,10 @@ script = [ command = "pytest" args = ["tests"] +[tasks.doctest] +command = "pytest" +args = ["qcs_sdk/qpu/experimental.pyi", "--doctest-modules"] + [tasks.test-session] command = "pytest" args = ["tests", "--with-qcs-session"] @@ -73,6 +77,7 @@ dependencies = [ "pre-test-docker-up", "install-python-package", "format-tracing-subscriber", + "doctest", "test", "post-test", ] diff --git a/crates/python/qcs_sdk/qpu/experimental.pyi b/crates/python/qcs_sdk/qpu/experimental.pyi index 8264e903e..23f86f48b 100644 --- a/crates/python/qcs_sdk/qpu/experimental.pyi +++ b/crates/python/qcs_sdk/qpu/experimental.pyi @@ -3,21 +3,20 @@ from typing import Dict, List, Optional, Self, Tuple, final import numpy as np from numpy.typing import NDArray - @final class RandomError(ValueError): """ An error that may occur while initializing a seed value or generating a QPU PRNG sequence. """ + ... @final class PrngSeedValue: """A seed value for the Rigetti QPU PRNG.""" - def __new__(cls, value: int) -> Self: ... - + def __new__(cls, value: int) -> Self: ... @final class ChooseRandomRealSubRegions: @@ -81,18 +80,16 @@ class ChooseRandomRealSubRegions: """Build the Quil signature of the `PRAGMA EXTERN` instruction.""" ... - def lfsr_v1_next(seed_value: PrngSeedValue) -> PrngSeedValue: """Given a seed value, return the next value in the LFSR v1 PRNG sequence.""" ... - def choose_random_real_sub_region_indices( seed: PrngSeedValue, start_index: int, series_length: int, sub_region_count: int, -) -> List[int]: +) -> List[int]: """ Given a seed value, the starting index and length of a pseudo-random series, and the number of sub-regions from which to choose, return a list of the sub-region indices that were chosen. @@ -101,14 +98,13 @@ def choose_random_real_sub_region_indices( """ ... - @final -class RandomizedMeasurementsError(ValueError): +class RandomizedMeasurementsError(ValueError): """ An error that can occur when adding randomized measurements to a program. """ - ... + ... @final class UnitarySet: @@ -133,19 +129,18 @@ class UnitarySet: """ ... - @final class RandomizedMeasurement: # TODO: should we make this signature more infallible? def __new__(cls, qubit: int, target: Optional[Tuple[str, int]]) -> Self: ... - @final class RandomizedMeasurements: """A class that supports the addition of randomized measurements to a Quil program.""" - def __new__(cls, measurements: List[RandomizedMeasurement], unitary_set: UnitarySet, leading_delay: float = ...) -> Self: ... - + def __new__( + cls, measurements: List[RandomizedMeasurement], unitary_set: UnitarySet, leading_delay: float = ... + ) -> Self: ... def append_to_program(self, target_program: str) -> str: """ Given a target Quil program, this routine will add the necessary declarations, `PRAGMA EXTERN`, `CALL`, and diff --git a/crates/python/tests/qpu/test_experimental.py b/crates/python/tests/qpu/test_experimental.py new file mode 100644 index 000000000..97177ff59 --- /dev/null +++ b/crates/python/tests/qpu/test_experimental.py @@ -0,0 +1,129 @@ +from typing import List +from qcs_sdk.qpu.experimental import PrngSeedValue, RandomizedMeasurements, RandomizedMeasurement, UnitarySet +import numpy as np +import pytest +from quil.program import Program + +BASE_QUIL_PROGRM = """ +DECLARE ro BIT[3] + +H 0 +H 1 +H 2 +""" + +BASE_QUIL_PROGRAM_WITH_MEASUREMENTS = """ +DECLARE ro BIT[3] +DECLARE randomized_measurement_source REAL[36] +DECLARE randomized_measurement_destination_q0 REAL[3] +DECLARE randomized_measurement_seed_q0 INTEGER[1] +DECLARE randomized_measurement_destination_q1 REAL[3] +DECLARE randomized_measurement_seed_q1 INTEGER[1] +DECLARE randomized_measurement_destination_q2 REAL[3] +DECLARE randomized_measurement_seed_q2 INTEGER[1] + +DELAY 0 1 2 1e-6 + +PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" + +CALL choose_random_real_sub_regions randomized_measurement_destination_q0 randomized_measurement_source 3 randomized_measurement_seed_q0 +CALL choose_random_real_sub_regions randomized_measurement_destination_q1 randomized_measurement_source 3 randomized_measurement_seed_q1 +CALL choose_random_real_sub_regions randomized_measurement_destination_q2 randomized_measurement_source 3 randomized_measurement_seed_q2 + +H 0 +H 1 +H 2 + +FENCE + +RZ(2*pi*randomized_measurement_destination_q0[0]) 0 +RX(pi/2) 0 +RZ(2*pi*randomized_measurement_destination_q0[1]) 0 + +RZ(2*pi*randomized_measurement_destination_q1[0]) 1 +RX(pi/2) 1 +RZ(2*pi*randomized_measurement_destination_q1[1]) 1 + +RZ(2*pi*randomized_measurement_destination_q2[0]) 2 +RX(pi/2) 2 +RZ(2*pi*randomized_measurement_destination_q2[1]) 2 + +FENCE + +RX(pi/2) 0 +RZ(2*pi*randomized_measurement_destination_q0[2]) 0 + +RX(pi/2) 1 +RZ(2*pi*randomized_measurement_destination_q1[2]) 1 + +RX(pi/2) 2 +RZ(2*pi*randomized_measurement_destination_q2[2]) 2 + +FENCE + +MEASURE 0 ro[0] +MEASURE 1 ro[1] +MEASURE 2 ro[2] +""" + + +@pytest.fixture +def randomized_measurements() -> RandomizedMeasurements: + """ + Returns an initialized instance of RandomizedMeasurements, where three qubits are measured + and the unitary set is of length 12 and zero-initialized. + """ + measurements = [RandomizedMeasurement(qubit, ("ro", qubit)) for qubit in range(3)] + unitary_set = UnitarySet.from_zxzxz(np.zeros((12, 3))) + return RandomizedMeasurements(measurements, unitary_set, 1e-6) + + +@pytest.fixture +def seed_values() -> List[PrngSeedValue]: + """ + Returns a list of valid seed values. These values are valid and correspond to test expectations, + but are otherwise indeed random. + """ + return [PrngSeedValue(463_692_700), PrngSeedValue(733_101_278), PrngSeedValue(925_742_198)] + + +def test_randomized_measurements_append_to_program(randomized_measurements: RandomizedMeasurements): + """Test that the randomized measurements are correctly appended to a Quil program.""" + program_with_randomized_measurements = randomized_measurements.append_to_program(BASE_QUIL_PROGRM) + assert Program.parse(program_with_randomized_measurements) == Program.parse(BASE_QUIL_PROGRAM_WITH_MEASUREMENTS) + + +def test_randomized_measurements_to_parameters( + randomized_measurements: RandomizedMeasurements, seed_values: List[PrngSeedValue] +): + """Test that the randomized measurement parameters are correctly generated.""" + parameters = randomized_measurements.to_parameters( + {qubit: seed_value for qubit, seed_value in enumerate(seed_values)} + ) + + expected_parameters = { + # The unitary set was initialized with zeros, so the angles are all zero. + "randomized_measurement_source": [0.0] * 36, + "randomized_measurement_seed_q0": [463_692_700], + "randomized_measurement_destination_q0": [0.0, 0.0, 0.0], + "randomized_measurement_seed_q1": [733_101_278], + "randomized_measurement_destination_q1": [0.0, 0.0, 0.0], + "randomized_measurement_seed_q2": [925_742_198], + "randomized_measurement_destination_q2": [0.0, 0.0, 0.0], + } + + assert parameters == expected_parameters + + +def test_randomized_measurements_get_random_indices( + randomized_measurements: RandomizedMeasurements, seed_values: List[PrngSeedValue] +): + """Test that, given a set of seed values, the correct random indices are generated.""" + shot_count = 3 + random_indices = randomized_measurements.get_random_indices( + {qubit: seed_value for qubit, seed_value in enumerate(seed_values)}, shot_count + ) + + expected_random_indices = {0: [0, 8, 1], 1: [1, 2, 1], 2: [5, 10, 5]} + + assert random_indices == expected_random_indices From 2f988fa1e1d34c5e646144cd96b418be73989228 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 12:21:35 -0800 Subject: [PATCH 07/17] fix: make randomized measurement constructor more infallible --- crates/python/Makefile.toml | 4 ---- .../_tracing_subscriber/common/__init__.py | 1 - .../_tracing_subscriber/common/__init__.pyi | 6 ++---- .../_tracing_subscriber/layers/__init__.py | 1 - .../_tracing_subscriber/layers/__init__.pyi | 16 ++++++++-------- .../_tracing_subscriber/layers/file/__init__.py | 2 -- .../_tracing_subscriber/layers/file/__init__.pyi | 1 - .../layers/otel_otlp/__init__.py | 1 - .../layers/otel_otlp/__init__.pyi | 6 ++---- .../layers/otel_otlp_file/__init__.py | 1 - .../layers/otel_otlp_file/__init__.pyi | 10 +++++++--- .../_tracing_subscriber/subscriber/__init__.py | 1 - .../_tracing_subscriber/subscriber/__init__.pyi | 1 - crates/python/qcs_sdk/qpu/experimental.pyi | 3 +-- crates/python/src/qpu/experimental.rs | 7 +++++-- 15 files changed, 25 insertions(+), 36 deletions(-) diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml index 5856a4d9e..e072fd651 100644 --- a/crates/python/Makefile.toml +++ b/crates/python/Makefile.toml @@ -60,10 +60,6 @@ script = [ command = "pytest" args = ["tests"] -[tasks.doctest] -command = "pytest" -args = ["qcs_sdk/qpu/experimental.pyi", "--doctest-modules"] - [tasks.test-session] command = "pytest" args = ["tests", "--with-qcs-session"] diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py index 16c779086..f5a875b22 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py @@ -15,4 +15,3 @@ __doc__ = common.__doc__ __all__ = getattr(common, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi index 07127164f..6395b5e35 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi @@ -12,15 +12,14 @@ from typing import Dict, Optional, final - @final class InstrumentationLibrary: """ Information about a library or crate providing instrumentation. - + An instrumentation library should be named to follow any naming conventions of the instrumented library (e.g. 'middleware' for a web framework). - + See the `instrumentation libraries `_ spec for more information. """ @@ -42,4 +41,3 @@ class InstrumentationLibrary: :param attributes: The attributes of the instrumentation library. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py index a4123cdf1..8c674f36e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py @@ -15,4 +15,3 @@ __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi index 741a427e2..6fdd52b85 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi @@ -13,18 +13,18 @@ from __future__ import annotations from typing import TYPE_CHECKING -from . import file as file +from . import file as file from . import otel_otlp_file as otel_otlp_file from . import otel_otlp as otel_otlp if TYPE_CHECKING: - from typing import Union + from typing import Union - Config = Union[ - file.Config, - otel_otlp_file.Config, - otel_otlp.Config, - ] - """ + Config = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py index f1ac331e1..80cff859b 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py @@ -15,5 +15,3 @@ __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) - - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi index 7caa5c2e0..8054827fe 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi @@ -39,4 +39,3 @@ class Config: :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py index 7b1f6baf6..8a3bcdd6e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi index f5990ffc8..2e47373d3 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -48,8 +48,6 @@ class Resource: schema_url: Optional[str] = None, ) -> "Resource": ... - - @final class Config: """ @@ -101,14 +99,14 @@ class Config: ... if TYPE_CHECKING: - from typing import List, Union + from typing import List, Union ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue= Union[bool, int, float, str, ResourceValueArray] + ResourceValue = Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py index 7f928b757..4d7ea6443 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index 0b55c2f8a..e55db109d 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -13,7 +13,6 @@ from typing import Optional, final from qcs_sdk._tracing_subscriber.common import InstrumentationLibrary - @final class Config: """ @@ -21,7 +20,13 @@ class Config: layer. """ - def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None, instrumentation_library: Optional[InstrumentationLibrary] = None) -> "Config": + def __new__( + cls, + *, + file_path: Optional[str] = None, + filter: Optional[str] = None, + instrumentation_library: Optional[InstrumentationLibrary] = None, + ) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the @@ -35,4 +40,3 @@ class Config: :param instrumentation_library: Information about the library providing the tracing instrumentation. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py index fb711d054..5f4924199 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py @@ -15,4 +15,3 @@ __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi index 548de0cc5..e0894d0a4 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi @@ -22,4 +22,3 @@ class Config: """ def __new__(cls, *, layer: layers.Config) -> "Config": ... - diff --git a/crates/python/qcs_sdk/qpu/experimental.pyi b/crates/python/qcs_sdk/qpu/experimental.pyi index 23f86f48b..0bf34c57e 100644 --- a/crates/python/qcs_sdk/qpu/experimental.pyi +++ b/crates/python/qcs_sdk/qpu/experimental.pyi @@ -131,8 +131,7 @@ class UnitarySet: @final class RandomizedMeasurement: - # TODO: should we make this signature more infallible? - def __new__(cls, qubit: int, target: Optional[Tuple[str, int]]) -> Self: ... + def __new__(cls, qubit: int, target: Tuple[str, int]) -> Self: ... @final class RandomizedMeasurements: diff --git a/crates/python/src/qpu/experimental.rs b/crates/python/src/qpu/experimental.rs index 23115a642..8c286a9da 100644 --- a/crates/python/src/qpu/experimental.rs +++ b/crates/python/src/qpu/experimental.rs @@ -136,11 +136,14 @@ struct PyRandomizedMeasurement { #[pymethods] impl PyRandomizedMeasurement { #[new] - fn new(qubit: u64, target: Option<(String, u64)>) -> Self { + fn new(qubit: u64, target: (String, u64)) -> Self { Self { inner: Measurement { qubit: Qubit::Fixed(qubit), - target: target.map(|(name, index)| MemoryReference { name, index }), + target: Some(MemoryReference { + name: target.0, + index: target.1, + }), }, } } From fb01969b48593d49c45c94630cc999f5124c6a3f Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 12:33:49 -0800 Subject: [PATCH 08/17] chore: update codeowners --- CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index a711ec08b..6aa05eb7c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,4 @@ crates/lib/src/qpu/experimental @erichulburd +crates/python/src/qpu/experimental.rs @erichulburd +crates/python/qcs_sdk/qpu/experimental.pyi @erichulburd +crates/python/tests/qpu/test_experimental.py @erichulburd From 5e7a9e83cc451c3bb6c2077a3391bc8cba005573 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 12:46:16 -0800 Subject: [PATCH 09/17] ci: drop errant doctest dependency --- crates/python/Makefile.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml index e072fd651..4874a965f 100644 --- a/crates/python/Makefile.toml +++ b/crates/python/Makefile.toml @@ -73,7 +73,6 @@ dependencies = [ "pre-test-docker-up", "install-python-package", "format-tracing-subscriber", - "doctest", "test", "post-test", ] From 63d1282bbffcfc1ded388af7b9ddd1385afc73f9 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 2 Dec 2024 12:46:24 -0800 Subject: [PATCH 10/17] fix: clippy unnecessary return --- crates/lib/src/executable.rs | 5 ++--- .../_tracing_subscriber/common/__init__.py | 1 + .../_tracing_subscriber/common/__init__.pyi | 6 ++++-- .../_tracing_subscriber/layers/__init__.py | 1 + .../_tracing_subscriber/layers/__init__.pyi | 16 ++++++++-------- .../_tracing_subscriber/layers/file/__init__.py | 2 ++ .../_tracing_subscriber/layers/file/__init__.pyi | 1 + .../layers/otel_otlp/__init__.py | 1 + .../layers/otel_otlp/__init__.pyi | 6 ++++-- .../layers/otel_otlp_file/__init__.py | 1 + .../layers/otel_otlp_file/__init__.pyi | 10 +++------- .../_tracing_subscriber/subscriber/__init__.py | 1 + .../_tracing_subscriber/subscriber/__init__.pyi | 1 + 13 files changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs index ba9c5ff24..3f3a9d2a3 100644 --- a/crates/lib/src/executable.rs +++ b/crates/lib/src/executable.rs @@ -344,10 +344,9 @@ impl Executable<'_, '_> { } fn get_readouts(&self) -> &[Cow<'_, str>] { - return self - .readout_memory_region_names + self.readout_memory_region_names .as_ref() - .map_or(&[Cow::Borrowed("ro")], Vec::as_slice); + .map_or(&[Cow::Borrowed("ro")], Vec::as_slice) } /// Execute on a QVM which must be available at the configured URL (default ). diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py index f5a875b22..16c779086 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py @@ -15,3 +15,4 @@ __doc__ = common.__doc__ __all__ = getattr(common, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi index 6395b5e35..07127164f 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi @@ -12,14 +12,15 @@ from typing import Dict, Optional, final + @final class InstrumentationLibrary: """ Information about a library or crate providing instrumentation. - + An instrumentation library should be named to follow any naming conventions of the instrumented library (e.g. 'middleware' for a web framework). - + See the `instrumentation libraries `_ spec for more information. """ @@ -41,3 +42,4 @@ class InstrumentationLibrary: :param attributes: The attributes of the instrumentation library. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py index 8c674f36e..a4123cdf1 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py @@ -15,3 +15,4 @@ __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi index 6fdd52b85..741a427e2 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi @@ -13,18 +13,18 @@ from __future__ import annotations from typing import TYPE_CHECKING -from . import file as file +from . import file as file from . import otel_otlp_file as otel_otlp_file from . import otel_otlp as otel_otlp if TYPE_CHECKING: - from typing import Union + from typing import Union - Config = Union[ - file.Config, - otel_otlp_file.Config, - otel_otlp.Config, - ] - """ + Config = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py index 80cff859b..f1ac331e1 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py @@ -15,3 +15,5 @@ __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) + + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi index 8054827fe..7caa5c2e0 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi @@ -39,3 +39,4 @@ class Config: :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py index 8a3bcdd6e..7b1f6baf6 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -15,3 +15,4 @@ __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi index 2e47373d3..f5990ffc8 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -48,6 +48,8 @@ class Resource: schema_url: Optional[str] = None, ) -> "Resource": ... + + @final class Config: """ @@ -99,14 +101,14 @@ class Config: ... if TYPE_CHECKING: - from typing import List, Union + from typing import List, Union ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue = Union[bool, int, float, str, ResourceValueArray] + ResourceValue= Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py index 4d7ea6443..7f928b757 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -15,3 +15,4 @@ __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index e55db109d..0b55c2f8a 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -13,6 +13,7 @@ from typing import Optional, final from qcs_sdk._tracing_subscriber.common import InstrumentationLibrary + @final class Config: """ @@ -20,13 +21,7 @@ class Config: layer. """ - def __new__( - cls, - *, - file_path: Optional[str] = None, - filter: Optional[str] = None, - instrumentation_library: Optional[InstrumentationLibrary] = None, - ) -> "Config": + def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None, instrumentation_library: Optional[InstrumentationLibrary] = None) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the @@ -40,3 +35,4 @@ class Config: :param instrumentation_library: Information about the library providing the tracing instrumentation. """ ... + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py index 5f4924199..fb711d054 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py @@ -15,3 +15,4 @@ __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) + diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi index e0894d0a4..548de0cc5 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi @@ -22,3 +22,4 @@ class Config: """ def __new__(cls, *, layer: layers.Config) -> "Config": ... + From ce85aa95ab0eccc6a8cd4a812cedf798adcd38e9 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 14:54:57 -0800 Subject: [PATCH 11/17] docs: improve docstrings and error messages --- crates/lib/src/qpu/experimental/random.rs | 162 +++++++++++++----- .../experimental/randomized_measurements.rs | 65 ++++--- crates/lib/src/qpu/externed_call.rs | 2 +- 3 files changed, 162 insertions(+), 67 deletions(-) diff --git a/crates/lib/src/qpu/experimental/random.rs b/crates/lib/src/qpu/experimental/random.rs index 957f98fca..13ecb6191 100644 --- a/crates/lib/src/qpu/experimental/random.rs +++ b/crates/lib/src/qpu/experimental/random.rs @@ -1,14 +1,16 @@ //! This module supports low-level primitives for randomization on Rigetti's QPUs. use std::{convert::TryFrom, ops::BitXor}; -use num::{complex::Complex64, ToPrimitive}; -use quil_rs::instruction::{Call, CallError, ExternError, UnresolvedCallArgument}; - use crate::qpu::externed_call::ExternedCall; +use num::{complex::Complex64, ToPrimitive}; +use quil_rs::{ + instruction::{Call, CallError, ExternError, UnresolvedCallArgument}, + quil::ToQuilError, +}; -/// An error that may occur when simulating control system extern -/// function calls. -#[derive(Debug, Clone, thiserror::Error)] +/// An error that may occur using the randomization primitives defined +/// in this module. +#[derive(Debug, thiserror::Error)] pub enum Error { /// An invalid seed value was provided. #[error( @@ -17,16 +19,42 @@ pub enum Error { InvalidSeed(u64), /// An error occurred while converting to Quil. #[error("error converting to Quil: {0}")] - ToQuilError(String), + ToQuilError(#[from] ToQuilError), /// An error occurred while constructing an extern signature. #[error("error constructing extern signature: {0}")] ExternSignatureError(#[from] ExternError), -} - -impl From for Error { - fn from(e: quil_rs::quil::ToQuilError) -> Self { - Self::ToQuilError(e.to_string()) - } + /// The destination must be a real array. + #[error("destination must be a real array, found {destination_type:?}")] + InvalidDestinationType { + /// The type on the destination declaration. + destination_type: quil_rs::instruction::ScalarType, + }, + /// The source must be a real array. + #[error("source must be a real array, found {source_type:?}")] + InvalidSourceType { + /// The type on the source declaration. + source_type: quil_rs::instruction::ScalarType, + }, + /// The destination length must be divisible by the sub-region size. + #[error( + "destination length must be in range [0, {}] and divisible by the sub-region size, found {destination_length} % {sub_region_size}", 2u64.pow(f64::MANTISSA_DIGITS) - 1 + )] + InvalidDestinationLength { + /// The length of the destination declaration. + destination_length: u64, + /// The size of each sub-region in source and destination memory arrays. + sub_region_size: f64, + }, + /// The source length must be divisible by the sub-region size. + #[error( + "source length must be in range [0, {}] and divisible by the sub-region size, found {source_length} % {sub_region_size}", 2u64.pow(f64::MANTISSA_DIGITS) - 1 + )] + InvalidSourceLength { + /// The length of the source declaration. + source_length: u64, + /// The size of each sub-region in source and destination memory arrays. + sub_region_size: f64, + }, } /// A specialized `Result` type for hardware extern function calls. @@ -36,10 +64,10 @@ pub type Result = std::result::Result; /// sub-regions from a source array of real values to a destination array. #[derive(Debug, Clone)] pub struct ChooseRandomRealSubRegions { - destination: String, - source: String, - sub_region_size: Complex64, - seed: String, + destination_memory_region_name: String, + source_memory_region_name: String, + sub_region_size: f64, + seed_memory_region_name: String, } impl ChooseRandomRealSubRegions { @@ -51,26 +79,62 @@ impl ChooseRandomRealSubRegions { /// * `source` - The identifier of the source array. /// * `sub_region_size` - The size of the sub-regions to select from /// the source array. Note, `len(source) % sub_region_size` and - /// `len(destination)` must be zero. + /// `len(destination) % sub_region_size` must be zero. /// * `seed` - The name of the seed value. /// /// The values provided for `destination`, `source`, and `seed` must /// be declared within the Quil program where the call is made. - pub fn new>( - destination: String, - source: String, + pub fn try_new + Copy>( + destination: &quil_rs::instruction::Declaration, + source: &quil_rs::instruction::Declaration, sub_region_size: T, - seed: String, - ) -> Self { - Self { - destination, - source, - sub_region_size: Complex64 { - re: sub_region_size.into(), - im: 0.0, - }, - seed, + seed: &quil_rs::instruction::MemoryReference, + ) -> Result { + if !matches!( + destination.size.data_type, + quil_rs::instruction::ScalarType::Real + ) { + return Err(Error::InvalidDestinationType { + destination_type: destination.size.data_type, + }); + } + if !matches!( + source.size.data_type, + quil_rs::instruction::ScalarType::Real + ) { + return Err(Error::InvalidSourceType { + source_type: source.size.data_type, + }); } + if destination + .size + .length + .to_f64() + .map_or(true, |destination_length| { + destination_length % sub_region_size.into() != 0f64 + }) + { + return Err(Error::InvalidDestinationLength { + destination_length: destination.size.length, + sub_region_size: sub_region_size.into(), + }); + } + + if source.size.length.to_f64().map_or(true, |source_length| { + source_length % sub_region_size.into() != 0f64 + }) { + return Err(Error::InvalidSourceLength { + source_length: source.size.length, + sub_region_size: sub_region_size.into(), + }); + } + + Ok(Self { + destination_memory_region_name: destination.name.clone(), + source_memory_region_name: source.name.clone(), + sub_region_size: sub_region_size.into(), + seed_memory_region_name: seed.name.clone(), + }) } } @@ -81,10 +145,13 @@ impl TryFrom for Call { Self::try_new( ChooseRandomRealSubRegions::NAME.to_string(), vec![ - UnresolvedCallArgument::Identifier(value.destination.clone()), - UnresolvedCallArgument::Identifier(value.source.to_string()), - UnresolvedCallArgument::Immediate(value.sub_region_size), - UnresolvedCallArgument::Identifier(value.seed.clone()), + UnresolvedCallArgument::Identifier(value.destination_memory_region_name), + UnresolvedCallArgument::Identifier(value.source_memory_region_name), + UnresolvedCallArgument::Immediate(Complex64 { + re: value.sub_region_size, + im: 0.0, + }), + UnresolvedCallArgument::Identifier(value.seed_memory_region_name), ], ) } @@ -99,8 +166,9 @@ impl ExternedCall for ChooseRandomRealSubRegions { /// Build the signature for the `PRAGMA EXTERN choose_random_real_sub_regions` /// instruction. The signature is: /// - /// rust ignore + /// ```text /// "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" + /// ``` fn build_signature() -> Result { use quil_rs::instruction::{ExternParameter, ExternParameterType}; @@ -205,27 +273,29 @@ fn prng_value_to_sub_region_index(value: u64, sub_region_count: u8) -> u8 { } /// Given a seed, start index, series length, and sub-region count, this function -/// will generate the sequence of pseudo-randomly chosen indices Rigetti control -/// systems. +/// will generate and return the sequence of pseudo-randomly chosen indices on +/// the Rigetti control systems. /// /// For instance, if the following Quil program is run for 100 shots: /// /// ```quil /// # presumed sub-region size is 3. /// DECLARE destination REAL[6] # prng invocations per shot = (6 / sub_region_size) = 2 -/// DELCARE source REAL[12] # implicit sub-region count = (12 / sub_region_size) = 4 +/// DECLARE source REAL[12] # implicit sub-region count = (12 / sub_region_size) = 4 /// DECLARE seed INTEGER[1] /// DECLARE ro BIT[1] /// /// DELAY 0 1e-6 /// -// PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" -// CALL choose_random_real_sub_regions destination source 3 seed +/// PRAGMA EXTERN choose_random_real_sub_regions "(destination : mut REAL[], source : REAL[], sub_region_size : INTEGER, seed : mut INTEGER)" +/// CALL choose_random_real_sub_regions destination source 3 seed /// ``` /// /// with a seed of 639523, you could backout the randomly chosen sub-regions with the following: /// /// ```rust +/// use super::choose_random_real_sub_regions; +/// /// let seed = 639523; /// let start_index = 0; /// let prng_invocations_per_shot = 2; @@ -251,8 +321,16 @@ pub fn choose_random_real_sub_region_indices( mod tests { use std::{collections::HashMap, fs::File}; + /// These are values that have been validated as final memory read off Rigetti QPUs. fn prng_sequences() -> HashMap> { - serde_json::de::from_reader(File::open("tests/prng_test_cases.json").unwrap()).unwrap() + serde_json::de::from_reader( + File::open(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/prng_test_cases.json" + )) + .unwrap(), + ) + .unwrap() } #[test] diff --git a/crates/lib/src/qpu/experimental/randomized_measurements.rs b/crates/lib/src/qpu/experimental/randomized_measurements.rs index 61c762d06..33c1177db 100644 --- a/crates/lib/src/qpu/experimental/randomized_measurements.rs +++ b/crates/lib/src/qpu/experimental/randomized_measurements.rs @@ -12,9 +12,9 @@ //! 3. PRNG reconstruction - backing out the random indices that played on each //! qubit during program execution. //! -//! Recall this is not QIS (quantum information science) library, but rather an +//! Recall this is not a QIS (quantum information science) library, but rather an //! SDK for collecting data from Rigetti QPUs. As such, defining the proper -//! unitary set and using randomized measurement data is unsupported by this +//! unitary set and using randomized measurement data is beyond the scope of this //! library. use std::{collections::HashMap, convert::TryFrom}; @@ -37,17 +37,17 @@ use crate::executable::Parameters; use crate::qpu::externed_call::ExternedCall; /// An error that may occur when constructing randomized measurements. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum Error { /// Received measurement on non-fixed qubits. #[error("only measurements on fixed qubits are supported, found {0:?}")] UnsupportedMeasurementQubit(Qubit), /// An error occurred while constructing `PRAGMA EXTERN` instruction. #[error("error declaring extern function: {0}")] - Pragma(#[from] super::random::Error), + BuildPragma(#[from] super::random::Error), /// An error occurred while constructing a call instruction. #[error("error initializing call instruction: {0}")] - Call(#[from] CallError), + BuildCall(#[from] CallError), /// A seed value was not provided for a qubit. #[error("seed not provided for qubit {}", .0.to_quil_or_debug())] MissingSeed(Qubit), @@ -62,15 +62,17 @@ pub enum Error { /// The number of columns found: usize, }, - #[error("the number of parameters per unitary must be convertible to f64, found {0}")] /// The number of parameters per unitary could not be expressed losslessly as /// an f64. + #[error( + "the number of parameters per unitary must be within range [0, {}], found {0}", 2_u64.pow(f64::MANTISSA_DIGITS) - 1 + )] ParametersPerUnitaryF64Conversion(usize), - #[error("the seed value must be convertible to f64, found {0}")] /// A seed value could not be expressed losslessly as an f64. + #[error("the seed value must be within range [0, {}], found {0}", 2_u64.pow(f64::MANTISSA_DIGITS) - 1)] SeedValueF64Conversion(usize), /// The implicit unitary count could not be expressed as a u8. - #[error("the unitary count must be convertible to u8, found {0}")] + #[error("the unitary count must be within range [0, {}], found {0}", u8::MAX)] UnitaryCountU8Conversion(usize), } @@ -85,13 +87,21 @@ struct QubitRandomization { } impl QubitRandomization { - fn into_instruction(self, parameters_per_unitary: f64) -> Result { - let externed_call = super::random::ChooseRandomRealSubRegions::new( - self.destination_declaration.name, - RANDOMIZED_MEASUREMENT_SOURCE.to_string(), + fn into_instruction( + self, + parameters_per_unitary: f64, + source_declaration: &Declaration, + ) -> Result { + let externed_call = super::random::ChooseRandomRealSubRegions::try_new( + &self.destination_declaration, + source_declaration, parameters_per_unitary, - self.seed_declaration.name, - ); + &MemoryReference { + name: self.seed_declaration.name.clone(), + index: 0, + }, + ) + .unwrap(); Ok(Instruction::Call(Call::try_from(externed_call)?)) } @@ -111,6 +121,7 @@ pub struct RandomizedMeasurements { unitary_set: UnitarySet, unitary_count_as_u8: u8, qubit_randomizations: Vec, + source_declaration: Declaration, } impl RandomizedMeasurements { @@ -129,6 +140,14 @@ impl RandomizedMeasurements { unitary_set: UnitarySet, leading_delay: Expression, ) -> Result { + let source_declaration = Declaration { + name: RANDOMIZED_MEASUREMENT_SOURCE.to_string(), + size: Vector::new( + ScalarType::Real, + (unitary_set.parameters_per_unitary() * unitary_set.unitary_count()) as u64, + ), + sharing: None, + }; let qubit_randomizations: Vec = measurements .into_iter() .map(|measurement| { @@ -167,6 +186,7 @@ impl RandomizedMeasurements { unitary_set, unitary_count_as_u8, qubit_randomizations, + source_declaration, }) } } @@ -177,15 +197,7 @@ impl RandomizedMeasurements { pub fn append_to_program(&self, target_program: Program) -> Result { let mut program = target_program.clone_without_body_instructions(); - program.add_instruction(Instruction::Declaration(Declaration { - name: RANDOMIZED_MEASUREMENT_SOURCE.to_string(), - size: Vector::new( - ScalarType::Real, - (self.unitary_set.parameters_per_unitary() * self.unitary_set.unitary_count()) - as u64, - ), - sharing: None, - })); + program.add_instruction(Instruction::Declaration(self.source_declaration.clone())); for qubit_randomization in &self.qubit_randomizations { program.add_instruction(Instruction::Declaration( @@ -223,7 +235,10 @@ impl RandomizedMeasurements { .qubit_randomizations .iter() .cloned() - .map(|qubit_randomization| qubit_randomization.into_instruction(parameters_per_unitary)) + .map(|qubit_randomization| { + qubit_randomization + .into_instruction(parameters_per_unitary, &self.source_declaration) + }) .collect::>>()?; program.add_instructions(calls); @@ -563,6 +578,8 @@ MEASURE 2 ro[2] } #[fixture] + /// Returns a list of valid seed values. These values are valid and correspond to test expectations, + /// but are otherwise indeed random. fn seeds() -> Vec { vec![463_692_700, 733_101_278, 925_742_198] } diff --git a/crates/lib/src/qpu/externed_call.rs b/crates/lib/src/qpu/externed_call.rs index 7bdd105d6..ba7ffa4d4 100644 --- a/crates/lib/src/qpu/externed_call.rs +++ b/crates/lib/src/qpu/externed_call.rs @@ -18,7 +18,7 @@ pub trait ExternedCall: Sized + TryInto { /// The name of the externed function. const NAME: &'static str; - /// Build the signature for the externed function. The Magneto service + /// Build the signature for the externed function. The translation service /// may use this function to check whether user submitted signatures match /// the expected signature. fn build_signature( From 070232f5e7a66d94056337e3ac5a1f658b6dea97 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 14:56:01 -0800 Subject: [PATCH 12/17] refactor: rename python lifetime parameter --- crates/python/src/qpu/experimental.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/python/src/qpu/experimental.rs b/crates/python/src/qpu/experimental.rs index 8c286a9da..30004024d 100644 --- a/crates/python/src/qpu/experimental.rs +++ b/crates/python/src/qpu/experimental.rs @@ -113,12 +113,12 @@ impl PyUnitarySet { Self(UnitarySet::Zxzxz(inner.to_owned_array())) } - fn to_zxzxz<'a>(&self, py: Python<'a>) -> PyResult<&'a PyArray2> { + fn to_zxzxz<'py>(&self, py: Python<'py>) -> PyResult<&'py PyArray2> { let UnitarySet::Zxzxz(matrix) = self.as_inner(); Ok(PyArray2::from_array(py, matrix)) } - fn as_zxzxz<'a>(&self, py: Python<'a>) -> Option<&'a PyArray2> { + fn as_zxzxz<'py>(&self, py: Python<'py>) -> Option<&'py PyArray2> { self.to_zxzxz(py).ok() } From 4524ff30819e5aa3abe135f5fc30e424890e5a62 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 14:56:33 -0800 Subject: [PATCH 13/17] fix: reformat tracing subscriber --- .../_tracing_subscriber/common/__init__.py | 1 - .../_tracing_subscriber/common/__init__.pyi | 6 ++---- .../_tracing_subscriber/layers/__init__.py | 1 - .../_tracing_subscriber/layers/__init__.pyi | 16 ++++++++-------- .../_tracing_subscriber/layers/file/__init__.py | 2 -- .../_tracing_subscriber/layers/file/__init__.pyi | 1 - .../layers/otel_otlp/__init__.py | 1 - .../layers/otel_otlp/__init__.pyi | 6 ++---- .../layers/otel_otlp_file/__init__.py | 1 - .../layers/otel_otlp_file/__init__.pyi | 10 +++++++--- .../_tracing_subscriber/subscriber/__init__.py | 1 - .../_tracing_subscriber/subscriber/__init__.pyi | 1 - 12 files changed, 19 insertions(+), 28 deletions(-) diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py index 16c779086..f5a875b22 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.py @@ -15,4 +15,3 @@ __doc__ = common.__doc__ __all__ = getattr(common, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi index 07127164f..6395b5e35 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/common/__init__.pyi @@ -12,15 +12,14 @@ from typing import Dict, Optional, final - @final class InstrumentationLibrary: """ Information about a library or crate providing instrumentation. - + An instrumentation library should be named to follow any naming conventions of the instrumented library (e.g. 'middleware' for a web framework). - + See the `instrumentation libraries `_ spec for more information. """ @@ -42,4 +41,3 @@ class InstrumentationLibrary: :param attributes: The attributes of the instrumentation library. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py index a4123cdf1..8c674f36e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py @@ -15,4 +15,3 @@ __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi index 741a427e2..6fdd52b85 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi @@ -13,18 +13,18 @@ from __future__ import annotations from typing import TYPE_CHECKING -from . import file as file +from . import file as file from . import otel_otlp_file as otel_otlp_file from . import otel_otlp as otel_otlp if TYPE_CHECKING: - from typing import Union + from typing import Union - Config = Union[ - file.Config, - otel_otlp_file.Config, - otel_otlp.Config, - ] - """ + Config = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py index f1ac331e1..80cff859b 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py @@ -15,5 +15,3 @@ __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) - - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi index 7caa5c2e0..8054827fe 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi @@ -39,4 +39,3 @@ class Config: :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py index 7b1f6baf6..8a3bcdd6e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi index f5990ffc8..2e47373d3 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -48,8 +48,6 @@ class Resource: schema_url: Optional[str] = None, ) -> "Resource": ... - - @final class Config: """ @@ -101,14 +99,14 @@ class Config: ... if TYPE_CHECKING: - from typing import List, Union + from typing import List, Union ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue= Union[bool, int, float, str, ResourceValueArray] + ResourceValue = Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py index 7f928b757..4d7ea6443 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index 0b55c2f8a..e55db109d 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -13,7 +13,6 @@ from typing import Optional, final from qcs_sdk._tracing_subscriber.common import InstrumentationLibrary - @final class Config: """ @@ -21,7 +20,13 @@ class Config: layer. """ - def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None, instrumentation_library: Optional[InstrumentationLibrary] = None) -> "Config": + def __new__( + cls, + *, + file_path: Optional[str] = None, + filter: Optional[str] = None, + instrumentation_library: Optional[InstrumentationLibrary] = None, + ) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the @@ -35,4 +40,3 @@ class Config: :param instrumentation_library: Information about the library providing the tracing instrumentation. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py index fb711d054..5f4924199 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py @@ -15,4 +15,3 @@ __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi index 548de0cc5..e0894d0a4 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi @@ -22,4 +22,3 @@ class Config: """ def __new__(cls, *, layer: layers.Config) -> "Config": ... - From c035ecd9829f7bf5bfcf085dc48bcbde872304fb Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 15:03:09 -0800 Subject: [PATCH 14/17] docs: fix doc test import --- crates/lib/src/qpu/experimental/random.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/src/qpu/experimental/random.rs b/crates/lib/src/qpu/experimental/random.rs index 13ecb6191..754507afe 100644 --- a/crates/lib/src/qpu/experimental/random.rs +++ b/crates/lib/src/qpu/experimental/random.rs @@ -294,7 +294,7 @@ fn prng_value_to_sub_region_index(value: u64, sub_region_count: u8) -> u8 { /// with a seed of 639523, you could backout the randomly chosen sub-regions with the following: /// /// ```rust -/// use super::choose_random_real_sub_regions; +/// use crate::qpu::experimental::random::choose_random_real_sub_regions; /// /// let seed = 639523; /// let start_index = 0; From 10d46b3d7582e29b437e1006b441576496a37a49 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 18:07:47 -0800 Subject: [PATCH 15/17] refactor: ensure privacy of unitary set --- .../experimental/randomized_measurements.rs | 116 ++++++++++++------ crates/python/src/qpu/experimental.rs | 9 +- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/crates/lib/src/qpu/experimental/randomized_measurements.rs b/crates/lib/src/qpu/experimental/randomized_measurements.rs index 33c1177db..52bb57742 100644 --- a/crates/lib/src/qpu/experimental/randomized_measurements.rs +++ b/crates/lib/src/qpu/experimental/randomized_measurements.rs @@ -17,7 +17,10 @@ //! unitary set and using randomized measurement data is beyond the scope of this //! library. -use std::{collections::HashMap, convert::TryFrom}; +use std::{ + collections::{HashMap, HashSet}, + convert::TryFrom, +}; use itertools::Itertools; use ndarray::{Array2, Order}; @@ -32,7 +35,7 @@ use quil_rs::{ Program, }; -use super::random::PrngSeedValue; +use super::random::{ChooseRandomRealSubRegions, PrngSeedValue}; use crate::executable::Parameters; use crate::qpu::externed_call::ExternedCall; @@ -68,6 +71,9 @@ pub enum Error { "the number of parameters per unitary must be within range [0, {}], found {0}", 2_u64.pow(f64::MANTISSA_DIGITS) - 1 )] ParametersPerUnitaryF64Conversion(usize), + /// The program already contains measurements. + #[error("program contains preexisting measurements on qubits: {0:?}")] + ProgramContainsPreexistingMeasurements(HashSet), /// A seed value could not be expressed losslessly as an f64. #[error("the seed value must be within range [0, {}], found {0}", 2_u64.pow(f64::MANTISSA_DIGITS) - 1)] SeedValueF64Conversion(usize), @@ -92,7 +98,7 @@ impl QubitRandomization { parameters_per_unitary: f64, source_declaration: &Declaration, ) -> Result { - let externed_call = super::random::ChooseRandomRealSubRegions::try_new( + let externed_call = ChooseRandomRealSubRegions::try_new( &self.destination_declaration, source_declaration, parameters_per_unitary, @@ -159,15 +165,12 @@ impl RandomizedMeasurements { }) .map_ok(|(measurement, qubit_name)| QubitRandomization { seed_declaration: Declaration { - name: format!("{RANDOMIZED_MEASUREMENT_SEED}_{}", qubit_name.clone()), + name: format!("{RANDOMIZED_MEASUREMENT_SEED}_{qubit_name}"), size: Vector::new(ScalarType::Integer, 1), sharing: None, }, destination_declaration: Declaration { - name: format!( - "{RANDOMIZED_MEASUREMENT_DESTINATION}_{}", - qubit_name.clone() - ), + name: format!("{RANDOMIZED_MEASUREMENT_DESTINATION}_{qubit_name}"), size: Vector::new( ScalarType::Real, unitary_set.parameters_per_unitary() as u64, @@ -193,8 +196,30 @@ impl RandomizedMeasurements { impl RandomizedMeasurements { /// Append the randomized measurements to a Quil program. The provided program - /// should not contain any preexisting measurements. + /// must not contain any preexisting measurements. pub fn append_to_program(&self, target_program: Program) -> Result { + let measured_qubits = target_program + .to_instructions() + .iter() + .filter_map(|instruction| { + if let Instruction::Measurement(measurement) = instruction { + Some(measurement.qubit.clone()) + } else { + None + } + }) + .collect::>(); + let qubits_with_redundant_measurements = self + .qubit_randomizations + .iter() + .filter(|randomization| measured_qubits.contains(&randomization.measurement.qubit)) + .map(|randomization| randomization.measurement.qubit.clone()) + .collect::>(); + if !qubits_with_redundant_measurements.is_empty() { + return Err(Error::ProgramContainsPreexistingMeasurements( + qubits_with_redundant_measurements, + )); + } let mut program = target_program.clone_without_body_instructions(); program.add_instruction(Instruction::Declaration(self.source_declaration.clone())); @@ -220,7 +245,7 @@ impl RandomizedMeasurements { // declare "choose_random_real_sub_regions" as an external function program.add_instruction(Instruction::Pragma( - super::random::ChooseRandomRealSubRegions::pragma_extern()?, + ChooseRandomRealSubRegions::pragma_extern()?, )); let parameters_per_unitary = self @@ -288,9 +313,7 @@ impl RandomizedMeasurements { .name .clone() .into_boxed_str(), - std::iter::repeat(0.0) - .take(self.unitary_set.parameters_per_unitary()) - .collect(), + vec![0.0; self.unitary_set.parameters_per_unitary()], ); } @@ -325,45 +348,47 @@ impl RandomizedMeasurements { /// A set of unitaries, each of which may be expressed as a set of Quil /// instructions. #[derive(Debug, Clone)] -pub enum UnitarySet { - /// A set of unitaries expressed as a sequence of the following gate +pub struct UnitarySet(UnitarySetInner); + +#[derive(Debug, Clone)] +enum UnitarySetInner { + Zxzxz(Array2), +} + +impl UnitarySet { + /// Initialize a set of unitaries expressed as a sequence of the following gate /// operations: /// /// RZ(angle_0)-RX(pi/2)-RZ(angle_1)-RX(pi/2)-RZ(angle_2). /// /// The unitaries are stored in a 2D array where each row represents - /// a single unitary expressed as the three RZ angles. - Zxzxz(Array2), -} - -impl UnitarySet { - /// Attempt to create a new instance of [`UnitarySet`] from a 2D array - /// of unitaries. The array must have three columns. - pub fn try_new_zxzxz(unitaries: Array2) -> Result { + /// a single unitary expressed as the three RZ angles. The array must have + /// three columns. + pub fn try_new_zxzxz(unitaries: Array2) -> Result { if unitaries.ncols() != 3 { return Err(Error::InvalidUnitarySet { expected: 3, found: unitaries.ncols(), }); } - Ok(UnitarySet::Zxzxz(unitaries)) + Ok(UnitarySet(UnitarySetInner::Zxzxz(unitaries))) } fn unitary_count(&self) -> usize { - match self { - UnitarySet::Zxzxz(unitaries) => unitaries.nrows(), + match &self.0 { + UnitarySetInner::Zxzxz(unitaries) => unitaries.nrows(), } } const fn parameters_per_unitary(&self) -> usize { - match self { - UnitarySet::Zxzxz(_) => 3, + match self.0 { + UnitarySetInner::Zxzxz(_) => 3, } } fn to_parameters(&self) -> Result> { - match self { - UnitarySet::Zxzxz(unitaries) => Ok(unitaries + match &self.0 { + UnitarySetInner::Zxzxz(unitaries) => Ok(unitaries .to_shape((unitaries.len(), Order::RowMajor))? .iter() .copied() @@ -371,9 +396,24 @@ impl UnitarySet { } } + /// Return the unitaries underlying a ZXZXZ decomposition. If the + /// [`UnitarySet`] is not a ZXZXZ decomposition, return `None`. + #[must_use] + pub fn to_zxzxz(&self) -> Option<&Array2> { + match &self.0 { + UnitarySetInner::Zxzxz(unitaries) => Some(unitaries), + } + } + + /// Indicates whether the [`UnitarySet`] is a ZXZXZ decomposition. + #[must_use] + pub fn is_zxzxz(&self) -> bool { + matches!(self.0, UnitarySetInner::Zxzxz(_)) + } + fn to_instructions(&self, qubit_randomizations: &[QubitRandomization]) -> Vec { - match self { - Self::Zxzxz(_) => Self::to_zxzxz_instructions(qubit_randomizations), + match self.0 { + UnitarySetInner::Zxzxz(_) => Self::to_zxzxz_instructions(qubit_randomizations), } } @@ -385,7 +425,7 @@ impl UnitarySet { qubit_randomization.measurement.qubit.clone(), qubit_randomization.destination_reference(0), ), - rx_pi_on_2(qubit_randomization.measurement.qubit.clone()), + rx_pi_over_2(qubit_randomization.measurement.qubit.clone()), rz( qubit_randomization.measurement.qubit.clone(), qubit_randomization.destination_reference(1), @@ -395,7 +435,7 @@ impl UnitarySet { instructions.push(Instruction::Fence(Fence { qubits: Vec::new() })); for qubit_randomization in qubit_randomizations { instructions.extend(vec![ - rx_pi_on_2(qubit_randomization.measurement.qubit.clone()), + rx_pi_over_2(qubit_randomization.measurement.qubit.clone()), rz( qubit_randomization.measurement.qubit.clone(), qubit_randomization.destination_reference(2), @@ -406,7 +446,7 @@ impl UnitarySet { } } -fn rx_pi_on_2(qubit: Qubit) -> Instruction { +fn rx_pi_over_2(qubit: Qubit) -> Instruction { Instruction::Gate(Gate { name: "RX".to_string(), parameters: vec![ @@ -446,7 +486,7 @@ H 1 H 2 "; - const BASE_QUIL_PROGRAM_WITH_MEASUREMENTS: &str = r#" + const BASE_QUIL_PROGRAM_WITH_RANDOMIZED_MEASUREMENTS: &str = r#" DECLARE ro BIT[3] DECLARE randomized_measurement_source REAL[36] DECLARE randomized_measurement_destination_q0 REAL[3] @@ -556,7 +596,7 @@ MEASURE 2 ro[2] let unitary_set = UnitarySet::try_new_zxzxz( Array2::from_shape_vec((12, 3), unitary_set).expect("must be valid unitary array"), ) - .expect("valid unitary set"); + .expect("must be a valid unitary set"); let leading_delay = Expression::Number(Complex64 { re: 1e-6, im: 0.0 }); RandomizedMeasurements::try_new(measurements, unitary_set, leading_delay) @@ -571,7 +611,7 @@ MEASURE 2 ro[2] ) .expect("must append to program"); - let expected_program = Program::from_str(BASE_QUIL_PROGRAM_WITH_MEASUREMENTS) + let expected_program = Program::from_str(BASE_QUIL_PROGRAM_WITH_RANDOMIZED_MEASUREMENTS) .expect("must be valid Quil program"); assert_eq!(randomized_program, expected_program); diff --git a/crates/python/src/qpu/experimental.rs b/crates/python/src/qpu/experimental.rs index 30004024d..50ddccfe5 100644 --- a/crates/python/src/qpu/experimental.rs +++ b/crates/python/src/qpu/experimental.rs @@ -110,11 +110,14 @@ rigetti_pyo3::impl_repr!(PyUnitarySet); impl PyUnitarySet { #[staticmethod] fn from_zxzxz(inner: &PyArray2) -> PyUnitarySet { - Self(UnitarySet::Zxzxz(inner.to_owned_array())) + Self(UnitarySet::try_new_zxzxz(inner.to_owned_array()).unwrap()) } fn to_zxzxz<'py>(&self, py: Python<'py>) -> PyResult<&'py PyArray2> { - let UnitarySet::Zxzxz(matrix) = self.as_inner(); + let matrix = self + .as_inner() + .to_zxzxz() + .ok_or_else(|| PyValueError::new_err("unitary set is not ZXZXZ"))?; Ok(PyArray2::from_array(py, matrix)) } @@ -123,7 +126,7 @@ impl PyUnitarySet { } fn is_zxzxz(&self) -> bool { - matches!(self.as_inner(), UnitarySet::Zxzxz(_)) + self.as_inner().is_zxzxz() } } From 0445366501412c65871202340cd8a1091101a8e9 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 18:10:25 -0800 Subject: [PATCH 16/17] chore: improve pyo3 tracing subscriber stub maintenance --- crates/python/Cargo.toml | 1 + crates/python/Makefile.toml | 9 +++++---- crates/python/build.rs | 18 +++++++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml index 4d0abc698..00fb23860 100644 --- a/crates/python/Cargo.toml +++ b/crates/python/Cargo.toml @@ -12,6 +12,7 @@ readme = "./README.md" [features] libquil = ["qcs/libquil"] grpc-web = ["qcs/grpc-web"] +pyo3-tracing-subscriber-build = [] [lib] name = "qcs_sdk" diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml index 4874a965f..ab3882224 100644 --- a/crates/python/Makefile.toml +++ b/crates/python/Makefile.toml @@ -32,11 +32,14 @@ script = [ ''' ] -[tasks.install-python-package] +[tasks.maturin-develop] dependencies = ["check-venv", "install-deps"] description = "Build the python package and install to the active virtual environment." command = "maturin" -args = ["develop"] +args = ["develop", "--features", "pyo3-tracing-subscriber-build"] + +[tasks.install-python-package] +dependencies = ["maturin-develop", "format-tracing-subscriber"] [tasks.pre-test-docker-up] script = [ @@ -72,7 +75,6 @@ args = ["tests", "--with-qcs-session", "--with-qcs-execution"] dependencies = [ "pre-test-docker-up", "install-python-package", - "format-tracing-subscriber", "test", "post-test", ] @@ -90,7 +92,6 @@ args = [ [tasks.stubtest-flow] dependencies = [ "install-python-package", - "format-tracing-subscriber", "stubtest", ] diff --git a/crates/python/build.rs b/crates/python/build.rs index 63b2fab0e..839a6086d 100644 --- a/crates/python/build.rs +++ b/crates/python/build.rs @@ -1,11 +1,19 @@ -use std::path::PathBuf; +/// Note, we put `pyo3-tracing-subscriber` stub build behind a feature flag, so that stubs are not +/// rewritten by default. When an actual build is needed, we use Cargo make commands so that we +/// can properly format the stub files after building them. +#[cfg(feature = "pyo3-tracing-subscriber-build")] +fn build_pyo3_tracing_subscriber_stubs() { + use pyo3_tracing_subscriber::stubs::write_stub_files; + use std::path::PathBuf; -use pyo3_tracing_subscriber::stubs::write_stub_files; - -fn main() { - pyo3_build_config::add_extension_module_link_args(); let tracing_subscriber_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("qcs_sdk/_tracing_subscriber"); write_stub_files("qcs_sdk", "_tracing_subscriber", &tracing_subscriber_path) .expect("Failed to write pyo3-tracing-subscriber stub files"); } + +fn main() { + pyo3_build_config::add_extension_module_link_args(); + #[cfg(feature = "pyo3-tracing-subscriber-build")] + build_pyo3_tracing_subscriber_stubs(); +} From 031699568aa94bd2bc7b5d0d43046f19a4e4c2d8 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 11 Dec 2024 18:31:45 -0800 Subject: [PATCH 17/17] tests: fix test import path --- crates/lib/src/qpu/experimental/random.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/src/qpu/experimental/random.rs b/crates/lib/src/qpu/experimental/random.rs index 754507afe..7c24e1df3 100644 --- a/crates/lib/src/qpu/experimental/random.rs +++ b/crates/lib/src/qpu/experimental/random.rs @@ -294,7 +294,7 @@ fn prng_value_to_sub_region_index(value: u64, sub_region_count: u8) -> u8 { /// with a seed of 639523, you could backout the randomly chosen sub-regions with the following: /// /// ```rust -/// use crate::qpu::experimental::random::choose_random_real_sub_regions; +/// use qcs::qpu::experimental::random::choose_random_real_sub_regions; /// /// let seed = 639523; /// let start_index = 0;