From 809fff38bec4b4640295004fb91050d69fad28cd Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 11 Jan 2024 13:47:23 +0000 Subject: [PATCH 01/27] Begin work on new OpenQASM 3 importer --- Cargo.lock | 606 +++++++++++++++++++++++++++- crates/qasm3/Cargo.toml | 20 + crates/qasm3/README.md | 4 + crates/qasm3/src/build.rs | 330 +++++++++++++++ crates/qasm3/src/circuit.rs | 309 ++++++++++++++ crates/qasm3/src/error.rs | 15 + crates/qasm3/src/expr.rs | 258 ++++++++++++ crates/qasm3/src/lib.rs | 129 ++++++ qiskit/qasm/libs/dummy/stdgates.inc | 75 ++++ setup.py | 6 + 10 files changed, 1739 insertions(+), 13 deletions(-) create mode 100644 crates/qasm3/Cargo.toml create mode 100644 crates/qasm3/README.md create mode 100644 crates/qasm3/src/build.rs create mode 100644 crates/qasm3/src/circuit.rs create mode 100644 crates/qasm3/src/error.rs create mode 100644 crates/qasm3/src/expr.rs create mode 100644 crates/qasm3/src/lib.rs create mode 100644 qiskit/qasm/libs/dummy/stdgates.inc diff --git a/Cargo.lock b/Cargo.lock index c51a584610e2..648024f02ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,26 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "always-assert" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4436e0292ab1bb631b42973c61205e704475fe8126af845c8d923c0996328127" +dependencies = [ + "log", +] + +[[package]] +name = "ariadne" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd" +dependencies = [ + "concolor", + "unicode-width", + "yansi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -33,12 +53,70 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "boolenum" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c8abd585d7026df20a9ae12982127ba5e81cc7a09397b957e71659da8c5de8" +dependencies = [ + "proc-macro-error", + "quote", + "syn 1.0.109", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "concolor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b946244a988c390a94667ae0e3958411fa40cc46ea496a929b263d883f5f9c3" +dependencies = [ + "bitflags 1.3.2", + "concolor-query", + "is-terminal", +] + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cov-mark" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d48d8f76bd9331f19fe2aaf3821a9f9fb32c3963e1e3d6ce82a8c09cef7444a" + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.4" @@ -70,6 +148,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + [[package]] name = "either" version = "1.9.0" @@ -82,6 +166,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -122,6 +216,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "indexmap" version = "1.9.3" @@ -149,6 +249,17 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -158,6 +269,12 @@ dependencies = [ "either", ] +[[package]] +name = "jod-thread" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" + [[package]] name = "libc" version = "0.2.151" @@ -170,6 +287,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "lock_api" version = "0.4.11" @@ -180,6 +303,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "matrixmultiply" version = "0.3.8" @@ -199,6 +328,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ffbca2f655e33c08be35d87278e5b18b89550a37dbd598c20db92f6a471123" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "ndarray" version = "0.15.6" @@ -268,12 +406,79 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oq3_lexer" +version = "0.0.1" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +dependencies = [ + "unicode-properties", + "unicode-xid", +] + +[[package]] +name = "oq3_parser" +version = "0.0.1" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +dependencies = [ + "drop_bomb", + "oq3_lexer", + "ra_ap_limit", +] + +[[package]] +name = "oq3_semantics" +version = "0.0.1" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +dependencies = [ + "boolenum", + "hashbrown 0.14.3", + "oq3_source_file", + "oq3_syntax", + "rowan", +] + +[[package]] +name = "oq3_source_file" +version = "0.0.1" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +dependencies = [ + "ariadne", + "oq3_syntax", + "source-span", +] + +[[package]] +name = "oq3_syntax" +version = "0.0.1" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +dependencies = [ + "cov-mark", + "either", + "indexmap 2.1.0", + "itertools", + "once_cell", + "oq3_lexer", + "oq3_parser", + "ra_ap_stdx", + "rowan", + "rustc-hash", + "smol_str", + "triomphe", + "xshell", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -292,9 +497,9 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -323,6 +528,30 @@ dependencies = [ "indexmap 1.9.3", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.75" @@ -382,7 +611,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -394,7 +623,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -405,6 +634,15 @@ dependencies = [ "pyo3", ] +[[package]] +name = "qiskit-qasm3" +version = "1.0.0" +dependencies = [ + "hashbrown 0.14.3", + "oq3_semantics", + "pyo3", +] + [[package]] name = "qiskit_accelerate" version = "1.0.0" @@ -435,6 +673,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ra_ap_limit" +version = "0.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d33758724f997689f84146e5401e28d875a061804f861f113696f44f5232aa" + +[[package]] +name = "ra_ap_stdx" +version = "0.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80fb2ff88b31fa35cde89ae13ea7c9ada97c7a2c778dcafef530a267658000" +dependencies = [ + "always-assert", + "crossbeam-channel", + "jod-thread", + "libc", + "miow", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -521,13 +779,41 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_termios" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "rowan" +version = "0.15.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" +dependencies = [ + "countme", + "hashbrown 0.14.3", + "memoffset", + "rustc-hash", + "text-size", ] [[package]] @@ -536,6 +822,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustworkx-core" version = "0.13.2" @@ -561,12 +860,61 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "smallvec" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e" +[[package]] +name = "smol_str" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +dependencies = [ + "serde", +] + +[[package]] +name = "source-span" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3afeb8b7a1ae2e8721f2193cc2291c9e00dd68511907fd8b807e2c8d42caa3c" +dependencies = [ + "termion", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -584,12 +932,54 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall 0.2.16", + "redox_termios", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-properties" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f91c8b21fbbaa18853c3d0801c78f4fc94cdb976699bb03e832e75f7fd22f0" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unindent" version = "0.2.3" @@ -608,63 +998,253 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xshell" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2107fe03e558353b4c71ad7626d58ed82efaf56c54134228608893c77023ad" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2c411759b501fb9501aac2b1b2d287a6e93e5bdcf13c25306b23e1b716dd0e" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.32" @@ -682,5 +1262,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml new file mode 100644 index 000000000000..9ae9014e6aba --- /dev/null +++ b/crates/qasm3/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "qiskit-qasm3" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[lib] +name = "qiskit_qasm3" +crate-type = ["cdylib"] + +[features] +# This is a test-only shim removable feature. See the root `Cargo.toml`. +default = ["extension-module"] +extension-module = ["pyo3/extension-module"] + +[dependencies] +pyo3.workspace = true +hashbrown.version = "0.14.0" +oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "57407ee" } diff --git a/crates/qasm3/README.md b/crates/qasm3/README.md new file mode 100644 index 000000000000..f8dc1623021e --- /dev/null +++ b/crates/qasm3/README.md @@ -0,0 +1,4 @@ +# `qiskit._qasm3` + +This crate is the Rust-level Qiskit interface to an OpenQASM 3 parser. The parser itself does not know +about Qiskit, and this crate interfaces with it in a Qiskit-specific manner to produce `QuantumCircuit`s. diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs new file mode 100644 index 000000000000..d1b8386f8497 --- /dev/null +++ b/crates/qasm3/src/build.rs @@ -0,0 +1,330 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::prelude::*; +use pyo3::types::{PyString, PyTuple}; + +use hashbrown::HashMap; + +use oq3_semantics::asg; +use oq3_semantics::symbols::{SymbolId, SymbolTable, SymbolType}; +use oq3_semantics::types::{ArrayDims, Type}; + +use crate::circuit::{PyCircuit, PyCircuitModule, PyClassicalRegister, PyGate, PyQuantumRegister}; +use crate::error::QASM3ImporterError; +use crate::expr; + +/// Our internal symbol table mapping base symbols to the Python-space object that represents them. +#[derive(Default)] +pub struct PySymbolTable { + /// Gate-constructor objects. + pub gates: HashMap, + /// Scalar `Qubit` objects. + pub qubits: HashMap>, + /// Scalar `Clbit` objects. + pub clbits: HashMap>, + /// `QuantumRegister` objects. + pub qregs: HashMap, + /// `ClassicalRegister` objects. + pub cregs: HashMap, +} + +struct BuilderState { + /// The base circuit under construction. + qc: PyCircuit, + /// Symbol table mapping AST symbols into typed Python / Rust objects. This is owned state; we + /// mutate it and build it up as we parse the AST. + symbols: PySymbolTable, + /// Handle to the constructor object for Python-space objects. + module: PyCircuitModule, + /// Constructors for gate objects. + pygates: HashMap, +} + +impl BuilderState { + fn declare_classical( + &mut self, + py: Python, + ast_symbols: &SymbolTable, + decl: &asg::DeclareClassical, + ) -> PyResult<()> { + let name_id = decl + .name() + .as_ref() + .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + let name_symbol = &ast_symbols[name_id]; + match name_symbol.symbol_type() { + Type::Bit(is_const) => { + if is_const.clone().into() { + Err(QASM3ImporterError::new_err("cannot handle consts")) + } else if decl.initializer().is_some() { + Err(QASM3ImporterError::new_err( + "cannot handle initialised bits", + )) + } else { + self.add_clbit(py, name_id.clone()) + } + } + Type::BitArray(dims, is_const) => { + if is_const.clone().into() { + Err(QASM3ImporterError::new_err("cannot handle consts")) + } else if decl.initializer().is_some() { + Err(QASM3ImporterError::new_err( + "cannot handle initialised registers", + )) + } else { + match dims { + ArrayDims::D1(size) => { + self.add_creg(py, name_id.clone(), name_symbol.name(), *size) + } + _ => Err(QASM3ImporterError::new_err( + "cannot handle quantum registers with more than one dimension", + )), + } + } + } + ty => Err(QASM3ImporterError::new_err(format!( + "unhandled classical type: {:?}", + ty, + ))), + } + } + + fn declare_quantum( + &mut self, + py: Python, + ast_symbols: &SymbolTable, + decl: &asg::DeclareQuantum, + ) -> PyResult<()> { + let name_id = decl + .name() + .as_ref() + .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + let name_symbol = &ast_symbols[name_id]; + match name_symbol.symbol_type() { + Type::Qubit => self.add_qubit(py, name_id.clone()), + Type::QubitArray(dims) => match dims { + ArrayDims::D1(size) => { + self.add_qreg(py, name_id.clone(), name_symbol.name(), *size) + } + _ => Err(QASM3ImporterError::new_err( + "cannot handle quantum registers with more than one dimension", + )), + }, + _ => unreachable!(), + } + } + + fn call_gate( + &mut self, + py: Python, + ast_symbols: &SymbolTable, + call: &asg::GateCall, + ) -> PyResult<()> { + if call.modifier().is_some() { + return Err(QASM3ImporterError::new_err("gate modifiers not handled")); + } + let gate_id = call + .name() + .as_ref() + .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + let gate = self.symbols.gates.get(gate_id).ok_or_else(|| { + QASM3ImporterError::new_err(format!("internal logic error: unknown gate {:?}", gate_id)) + })?; + let params = PyTuple::new( + py, + call.params() + .as_ref() + .map(|params| params as &[asg::TExpr]) + .unwrap_or_default() + .iter() + .map(|param| expr::eval_gate_param(py, &self.symbols, ast_symbols, param)) + .collect::>>()?, + ); + let qargs = call.qubits(); + if params.len() != gate.num_params() { + return Err(QASM3ImporterError::new_err(format!( + "incorrect number of params to '{}': expected {}, got {}", + gate.name(), + gate.num_params(), + params.len(), + ))); + } + if qargs.len() != gate.num_qubits() { + return Err(QASM3ImporterError::new_err(format!( + "incorrect number of quantum arguments to '{}': expected {}, got {}", + gate.name(), + gate.num_qubits(), + qargs.len(), + ))); + } + let gate_instance = gate.construct(py, params)?; + for qubits in expr::broadcast_qubits(py, &self.symbols, ast_symbols, qargs)? { + self.qc.append( + py, + self.module + .new_instruction(py, gate_instance.clone_ref(py), qubits, ())?, + )?; + } + Ok(()) + } + + fn define_gate( + &mut self, + _py: Python, + ast_symbols: &SymbolTable, + decl: &asg::GateDeclaration, + ) -> PyResult<()> { + let name_id = decl + .name() + .as_ref() + .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + let name_symbol = &ast_symbols[name_id]; + let pygate = self.pygates.get(name_symbol.name()).ok_or_else(|| { + QASM3ImporterError::new_err(format!( + "can't handle non-built-in gate: '{}'", + name_symbol.name() + )) + })?; + let defined_num_params = decl.params().as_ref().map_or(0, Vec::len); + let defined_num_qubits = decl.qubits().len(); + if pygate.num_params() != defined_num_params { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} parameters, but is defined as taking {}", + pygate.name(), + pygate.num_params(), + defined_num_params, + ))); + } + if pygate.num_qubits() != defined_num_qubits { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} qubits, but is defined as taking {}", + pygate.name(), + pygate.num_qubits(), + defined_num_qubits, + ))); + } + self.symbols.gates.insert(name_id.clone(), pygate.clone()); + Ok(()) + } + + fn add_qubit(&mut self, py: Python, ast_symbol: SymbolId) -> PyResult<()> { + let qubit = self.module.new_qubit(py)?; + if self + .symbols + .qubits + .insert(ast_symbol, qubit.clone_ref(py)) + .is_some() + { + panic!("internal logic error: attempted to add the same qubit multiple times") + } + self.qc.add_qubit(py, qubit) + } + + fn add_clbit(&mut self, py: Python, ast_symbol: SymbolId) -> PyResult<()> { + let clbit = self.module.new_clbit(py)?; + if self + .symbols + .clbits + .insert(ast_symbol, clbit.clone_ref(py)) + .is_some() + { + panic!("internal logic error: attempted to add the same clbit multiple times") + } + self.qc.add_clbit(py, clbit) + } + + fn add_qreg>>( + &mut self, + py: Python, + ast_symbol: SymbolId, + name: T, + size: usize, + ) -> PyResult<()> { + let qreg = self.module.new_qreg(py, name, size)?; + self.qc.add_qreg(py, &qreg)?; + if self.symbols.qregs.insert(ast_symbol, qreg).is_some() { + panic!("internal logic error: attempted to add the same register multiple times") + } + Ok(()) + } + + fn add_creg>>( + &mut self, + py: Python, + ast_symbol: SymbolId, + name: T, + size: usize, + ) -> PyResult<()> { + let creg = self.module.new_creg(py, name, size)?; + self.qc.add_creg(py, &creg)?; + if self.symbols.cregs.insert(ast_symbol, creg).is_some() { + panic!("internal logic error: attempted to add the same register multiple times") + } + Ok(()) + } +} + +pub fn convert_asg( + py: Python, + program: &asg::Program, + ast_symbols: &SymbolTable, + gate_constructors: HashMap, +) -> PyResult { + let module = PyCircuitModule::import(py)?; + let mut state = BuilderState { + qc: module.new_circuit(py)?, + symbols: Default::default(), + pygates: gate_constructors, + module, + }; + for statement in program.stmts().iter() { + match statement { + asg::Stmt::DeclareClassical(decl) => state.declare_classical(py, ast_symbols, decl)?, + asg::Stmt::DeclareQuantum(decl) => state.declare_quantum(py, ast_symbols, decl)?, + asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, + asg::Stmt::GateDeclaration(decl) => state.define_gate(py, ast_symbols, decl)?, + asg::Stmt::Alias + | asg::Stmt::AnnotatedStmt(_) + | asg::Stmt::Assignment(_) + | asg::Stmt::Barrier + | asg::Stmt::Block(_) + | asg::Stmt::Box + | asg::Stmt::Break + | asg::Stmt::Cal + | asg::Stmt::Continue + | asg::Stmt::Def + | asg::Stmt::DefCal + | asg::Stmt::Delay + | asg::Stmt::End + | asg::Stmt::ExprStmt(_) + | asg::Stmt::Extern + | asg::Stmt::For + | asg::Stmt::GPhaseCall(_) + | asg::Stmt::IODeclaration + | asg::Stmt::If(_) + | asg::Stmt::Include(_) + | asg::Stmt::NullStmt + | asg::Stmt::OldStyleDeclaration + | asg::Stmt::Pragma(_) + | asg::Stmt::Reset + | asg::Stmt::Return + | asg::Stmt::While(_) => { + return Err(QASM3ImporterError::new_err(format!( + "this statement is not yet handled during OpenQASM 3 import: {:?}", + statement + ))); + } + } + } + Ok(state.qc) +} diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs new file mode 100644 index 000000000000..20720233b2e9 --- /dev/null +++ b/crates/qasm3/src/circuit.rs @@ -0,0 +1,309 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::prelude::*; +use pyo3::types::{PyList, PyString, PyTuple, PyType}; + +use crate::error::QASM3ImporterError; + +macro_rules! register_type { + ($name: ident) => { + /// Rust-space wrapper around Qiskit `Register` objects. + pub struct $name { + /// The actual register instance. + object: Py, + /// A pointer to the inner list of bits. We keep a handle to this for lookup + /// efficiency; we can use direct list methods to retrieve the bit instances, rather + /// than needing to indirect through the general `__getitem__` of the register, or + /// looking up the qubit instances on the circuit. + items: Py, + } + + impl $name { + /// Get an individual bit from the register. + pub fn bit(&self, py: Python, index: usize) -> PyResult> { + // Unfortunately, `PyList::get_item_unchecked` isn't usable with the stable ABI. + self.items + .as_ref(py) + .get_item(index) + .map(|item| item.into_py(py)) + } + + pub fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator { + self.items.as_ref(py).iter() + } + } + + impl ::pyo3::IntoPy> for $name { + fn into_py(self, _py: Python) -> Py { + self.object + } + } + + impl ::pyo3::ToPyObject for $name { + fn to_object(&self, _py: Python) -> Py { + // _Technically_, allowing access this internal object can let the Rust-space + // wrapper get out-of-sync since we keep a direct handle to the list, but in + // practice, the field it's viewing is private and "inaccessible" from Python. + self.object.clone() + } + } + }; +} + +register_type!(PyQuantumRegister); +register_type!(PyClassicalRegister); + +/// Information received from Python space about how to construct a Python-space object to +/// represent a given gate that might be declared. +#[pyclass(module = "qiskit._qasm3", frozen, name = "CustomGate")] +#[derive(Clone, Debug)] +pub struct PyGate { + constructor: Py, + name: String, + num_params: usize, + num_qubits: usize, +} + +impl PyGate { + pub fn new>>( + py: Python, + constructor: T, + name: String, + num_params: usize, + num_qubits: usize, + ) -> Self { + Self { + constructor: constructor.into_py(py), + name, + num_params, + num_qubits, + } + } + + /// Construct a Python-space instance of the custom gate. + pub fn construct(&self, py: Python, args: A) -> PyResult> + where + A: IntoPy>, + { + let args = args.into_py(py); + let received_num_params = args.as_ref(py).len(); + if received_num_params == self.num_params { + self.constructor.call1(py, args.as_ref(py)) + } else { + Err(QASM3ImporterError::new_err(format!( + "internal logic error: wrong number of params for {} (got {}, expected {})", + &self.name, received_num_params, self.num_params + ))) + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn num_params(&self) -> usize { + self.num_params + } + + pub fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +#[pymethods] +impl PyGate { + #[new] + #[pyo3(signature=(/, constructor, name, num_params, num_qubits))] + fn __new__(constructor: Py, name: String, num_params: usize, num_qubits: usize) -> Self { + Self { + constructor, + name, + num_params, + num_qubits, + } + } + + fn __repr__<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + PyString::new(py, "CustomGate(name={!r}, num_params={}, num_qubits={})").call_method1( + "format", + ( + PyString::new(py, &self.name), + self.num_params, + self.num_qubits, + ), + ) + } + + fn __reduce__(&self, py: Python) -> Py { + ( + PyType::new::(py), + ( + self.constructor.clone_ref(py), + &self.name, + self.num_params, + self.num_qubits, + ), + ) + .into_py(py) + } +} + +/// Wrapper around various Python-space imports. This is just a convenience wrapper to save us +/// needing to `getattr` things off a Python-space module quite so frequently. This is +/// give-or-take just a manual lookup for a few `import` items at the top of a Python module, and +/// the attached constructor functions produce (when appropriate), Rust-space wrappers around the +/// Python objects. +pub struct PyCircuitModule { + circuit: Py, + qreg: Py, + qubit: Py, + creg: Py, + clbit: Py, + instruction: Py, +} + +impl PyCircuitModule { + /// Import the necessary components from `qiskit.circuit`. + pub fn import(py: Python) -> PyResult { + let module = PyModule::import(py, "qiskit.circuit")?; + Ok(Self { + circuit: module + .getattr("QuantumCircuit")? + .downcast::()? + .into_py(py), + qreg: module + .getattr("QuantumRegister")? + .downcast::()? + .into_py(py), + qubit: module.getattr("Qubit")?.downcast::()?.into_py(py), + creg: module + .getattr("ClassicalRegister")? + .downcast::()? + .into_py(py), + clbit: module.getattr("Clbit")?.downcast::()?.into_py(py), + instruction: module + .getattr("CircuitInstruction")? + .downcast::()? + .into_py(py), + }) + } + + pub fn new_circuit(&self, py: Python) -> PyResult { + Ok(PyCircuit { + qc: self.circuit.call0(py)?, + }) + } + + pub fn new_qreg>>( + &self, + py: Python, + name: T, + size: usize, + ) -> PyResult { + let qreg = self.qreg.call1(py, (size, name.into_py(py)))?; + Ok(PyQuantumRegister { + items: qreg + .getattr(py, "_bits")? + .downcast::(py)? + .into_py(py), + object: qreg, + }) + } + + pub fn new_qubit(&self, py: Python) -> PyResult> { + self.qubit.call0(py) + } + + pub fn new_creg>>( + &self, + py: Python, + name: T, + size: usize, + ) -> PyResult { + let creg = self.creg.call1(py, (size, name.into_py(py)))?; + Ok(PyClassicalRegister { + items: creg + .getattr(py, "_bits")? + .downcast::(py)? + .into_py(py), + object: creg, + }) + } + + pub fn new_clbit(&self, py: Python) -> PyResult> { + self.clbit.call0(py) + } + + pub fn new_instruction( + &self, + py: Python, + operation: O, + qubits: Q, + clbits: C, + ) -> PyResult> + where + O: IntoPy>, + Q: IntoPy>, + C: IntoPy>, + { + self.instruction + .call1(py, (operation, qubits.into_py(py), clbits.into_py(py))) + } +} + +/// Circuit construction context object to provide an easier Rust-space interface for us to +/// construct the Python :class:`.QuantumCircuit`. The idea of doing this from Rust space like +/// this is that we might steadily be able to move more and more of it into being native Rust as +/// the Rust-space APIs around the internal circuit data stabilise. +pub struct PyCircuit { + /// The actual circuit object that's under construction. + qc: Py, +} + +impl PyCircuit { + pub fn add_qreg(&mut self, py: Python, qreg: &PyQuantumRegister) -> PyResult<()> { + self.qc + .call_method1(py, "add_register", (qreg.to_object(py),)) + .map(|_| ()) + } + + pub fn add_qubit(&mut self, py: Python, qubit: Py) -> PyResult<()> { + self.qc + .call_method1(py, "add_bits", ((qubit,),)) + .map(|_| ()) + } + + pub fn add_creg(&mut self, py: Python, creg: &PyClassicalRegister) -> PyResult<()> { + self.qc + .call_method1(py, "add_register", (creg.to_object(py),)) + .map(|_| ()) + } + + pub fn add_clbit>>(&mut self, py: Python, clbit: T) -> PyResult<()> { + self.qc + .call_method1(py, "add_bits", ((clbit,),)) + .map(|_| ()) + } + + pub fn append>>(&mut self, py: Python, instruction: T) -> PyResult<()> { + self.qc + .call_method1(py, "_append", (instruction.into_py(py),)) + .map(|_| ()) + } +} + +impl ::pyo3::IntoPy> for PyCircuit { + fn into_py(self, py: Python) -> Py { + self.qc.clone_ref(py) + } +} diff --git a/crates/qasm3/src/error.rs b/crates/qasm3/src/error.rs new file mode 100644 index 000000000000..1bde455f3a36 --- /dev/null +++ b/crates/qasm3/src/error.rs @@ -0,0 +1,15 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::import_exception; + +import_exception!(qiskit.qasm3.exceptions, QASM3ImporterError); diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs new file mode 100644 index 000000000000..7f587cb6bd22 --- /dev/null +++ b/crates/qasm3/src/expr.rs @@ -0,0 +1,258 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +use oq3_semantics::asg; +use oq3_semantics::symbols::{SymbolId, SymbolTable}; +use oq3_semantics::types::Type; + +use crate::build::PySymbolTable; +use crate::error::QASM3ImporterError; + +pub fn eval_gate_param( + _py: Python, + _our_symbols: &PySymbolTable, + _ast_symbols: &SymbolTable, + param: &asg::TExpr, +) -> PyResult { + // Only handling float parameters in this first pass of the importer. + match param.get_type() { + Type::Float(_, is_const) => { + if is_const.clone().into() { + match param.expression() { + asg::Expr::Literal(asg::Literal::Float(lit)) => { + lit.value().parse().map_err(|_| { + QASM3ImporterError::new_err(format!( + "invalid float literal: '{}'", + lit.value() + )) + }) + } + expr => Err(QASM3ImporterError::new_err(format!( + "unhandled expression for floating-point constant: {:?}", + expr + ))), + } + } else { + Err(QASM3ImporterError::new_err(format!( + "expected a constant float, but found a runtime value: {:?}", + param + ))) + } + } + ty => Err(QASM3ImporterError::new_err(format!( + "expected an angle-like type, but saw {:?}", + ty + ))), + } +} + +fn eval_const_int(_py: Python, _ast_symbols: &SymbolTable, expr: &asg::TExpr) -> PyResult { + match expr.get_type() { + Type::Int(_, is_const) | Type::UInt(_, is_const) => { + if is_const.clone().into() { + match expr.expression() { + asg::Expr::Literal(asg::Literal::Int(lit)) => Ok(*lit.value() as isize), + expr => Err(QASM3ImporterError::new_err(format!( + "unhandled expression type for constant-integer evaluatation: {:?}", + expr + ))), + } + } else { + Err(QASM3ImporterError::new_err(format!( + "expected a constant integer, but found a runtime value: {:?}", + expr + ))) + } + } + ty => Err(QASM3ImporterError::new_err(format!( + "expected a constant integer, but found a value of type: {:?}", + ty + ))), + } +} + +fn eval_const_uint(py: Python, ast_symbols: &SymbolTable, expr: &asg::TExpr) -> PyResult { + eval_const_int(py, ast_symbols, expr).and_then(|val| { + val.try_into().map_err(|_| { + QASM3ImporterError::new_err(format!("expected an unsigned integer but found '{}'", val)) + }) + }) +} + +enum BroadcastItem { + Bit(Py), + Register(Vec>), +} + +struct BroadcastQubitsIter<'py> { + py: Python<'py>, + len: usize, + offset: usize, + items: Vec, +} + +impl<'py> Iterator for BroadcastQubitsIter<'py> { + type Item = &'py PyTuple; + + fn next(&mut self) -> Option { + if self.offset >= self.len { + return None; + } + let offset = self.offset; + let to_scalar = |item: &BroadcastItem| match item { + BroadcastItem::Bit(bit) => bit.clone(), + BroadcastItem::Register(bits) => bits[offset].clone(), + }; + self.offset += 1; + Some(PyTuple::new(self.py, self.items.iter().map(to_scalar))) + } + + fn size_hint(&self) -> (usize, Option) { + (self.len - self.offset, Some(self.len - self.offset)) + } +} + +fn broadcast_bits_for_identifier( + py: Python, + our_symbols: &PySymbolTable, + iden_symbol: &SymbolId, +) -> PyResult { + if let Some(bit) = our_symbols.qubits.get(iden_symbol) { + Ok(BroadcastItem::Bit(bit.clone())) + } else if let Some(reg) = our_symbols.qregs.get(iden_symbol) { + Ok(BroadcastItem::Register( + reg.iter(py).map(|obj| obj.into_py(py)).collect(), + )) + } else { + Err(QASM3ImporterError::new_err(format!( + "unknown symbol: {:?}", + iden_symbol + ))) + } +} + +fn broadcast_apply_index( + py: Python, + ast_symbols: &SymbolTable, + broadcasted: BroadcastItem, + index: &asg::IndexOperator, +) -> PyResult { + let bits = match broadcasted { + BroadcastItem::Register(bits) => Ok(bits), + BroadcastItem::Bit(_) => Err(QASM3ImporterError::new_err( + "cannot index into a scalar value", + )), + }?; + let eval_single_index = |expr: &asg::TExpr| -> PyResult> { + let index = eval_const_uint(py, ast_symbols, expr)?; + match bits.get(index) { + Some(bit) => Ok(bit.clone_ref(py)), + None => Err(QASM3ImporterError::new_err(format!( + "index {} out of range for register of length {}", + index, + bits.len() + ))), + } + }; + match index { + asg::IndexOperator::SetExpression(exprs) => exprs + .expressions() + .iter() + .map(eval_single_index) + .collect::>>() + .map(BroadcastItem::Register), + asg::IndexOperator::ExpressionList(exprs) => { + let expr = match &exprs.expressions[..] { + [expr] => Ok(expr), + _ => Err(QASM3ImporterError::new_err( + "registers can only be one-dimensional", + )), + }?; + match expr.get_type() { + Type::UInt(_, _) | Type::Int(_, _) => { + Ok(BroadcastItem::Bit(eval_single_index(expr)?)) + } + ty => Err(QASM3ImporterError::new_err(format!( + "unhandled index type: {:?}", + ty + ))), + } + } + } +} + +pub fn broadcast_qubits<'a, 'py, T>( + py: Python<'py>, + our_symbols: &PySymbolTable, + ast_symbols: &SymbolTable, + qargs: T, +) -> PyResult> +where + T: IntoIterator + 'a, +{ + let items = qargs + .into_iter() + .map(|item| -> PyResult { + match item.expression() { + asg::Expr::GateOperand(operand) => match operand { + asg::GateOperand::Identifier(iden) => broadcast_bits_for_identifier( + py, + our_symbols, + iden.symbol().as_ref().unwrap(), + ), + asg::GateOperand::IndexedIdentifier(indexed) => { + let iden_symbol = indexed.identifier().as_ref().unwrap(); + indexed.indexes().iter().fold( + broadcast_bits_for_identifier(py, our_symbols, iden_symbol), + |item, index| { + item.and_then(|item| { + broadcast_apply_index(py, ast_symbols, item, index) + }) + }, + ) + } + asg::GateOperand::HardwareQubit(_) => { + Err(QASM3ImporterError::new_err("cannot handle hardware qubits")) + } + }, + ty => Err(QASM3ImporterError::new_err(format!( + "unhandled gate operand expression type: {:?}", + ty + ))), + } + }) + .collect::>>()?; + + let mut broadcast_len = None; + for item in items.iter() { + match (item, broadcast_len) { + (BroadcastItem::Bit(_), _) => (), + (BroadcastItem::Register(reg), Some(len)) => { + if reg.len() != len { + return Err(QASM3ImporterError::new_err("invalid broadcast")); + } + } + (BroadcastItem::Register(reg), None) => { + broadcast_len = Some(reg.len()); + } + } + } + Ok(BroadcastQubitsIter { + py, + len: broadcast_len.unwrap_or(if items.is_empty() { 0 } else { 1 }), + offset: 0, + items, + }) +} diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs new file mode 100644 index 000000000000..8efb5ac41169 --- /dev/null +++ b/crates/qasm3/src/lib.rs @@ -0,0 +1,129 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +mod build; +mod circuit; +mod error; +mod expr; + +use hashbrown::HashMap; + +use pyo3::prelude::*; +use pyo3::types::{PyModule, PyTuple}; + +use oq3_semantics::syntax_to_semantics::parse_source_string; + +use crate::error::QASM3ImporterError; + +/// Load an OpenQASM 3 program from a string into a :class:`.QuantumCircuit`. +#[pyfunction] +#[pyo3(pass_module)] +pub fn loads( + module: &PyModule, + py: Python, + source: String, + custom_gates: Option>, +) -> PyResult { + let result = parse_source_string(source, None); + if result.any_errors() { + result.print_errors(); + return Err(QASM3ImporterError::new_err( + "errors during parsing; see printed errors", + )); + } + let gates = match custom_gates { + Some(gates) => gates + .into_iter() + .map(|gate| (gate.name().to_owned(), gate)) + .collect(), + None => module + .getattr("_STDGATES_INC_GATES")? + .iter()? + .map(|obj| { + let gate = obj?.extract::()?; + Ok((gate.name().to_owned(), gate)) + }) + .collect::>>()?, + }; + crate::build::convert_asg(py, result.program(), result.symbol_table(), gates) +} + +fn stdgates_inc_gates(py: Python) -> PyResult<&PyTuple> { + let library = PyModule::import(py, "qiskit.circuit.library")?; + Ok(PyTuple::new( + py, + vec![ + circuit::PyGate::new(py, library.getattr("PhaseGate")?, "p".to_owned(), 1, 1) + .into_py(py), + circuit::PyGate::new(py, library.getattr("XGate")?, "x".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("YGate")?, "y".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("ZGate")?, "z".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("HGate")?, "h".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("SGate")?, "s".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("SdgGate")?, "sdg".to_owned(), 0, 1) + .into_py(py), + circuit::PyGate::new(py, library.getattr("TGate")?, "t".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("TdgGate")?, "tdg".to_owned(), 0, 1) + .into_py(py), + circuit::PyGate::new(py, library.getattr("SXGate")?, "sx".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("RXGate")?, "rx".to_owned(), 1, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("RYGate")?, "ry".to_owned(), 1, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("RZGate")?, "rz".to_owned(), 1, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("CXGate")?, "cx".to_owned(), 0, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("CYGate")?, "cy".to_owned(), 0, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("CZGate")?, "cz".to_owned(), 0, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("CPhaseGate")?, "cp".to_owned(), 1, 2) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CRXGate")?, "crx".to_owned(), 1, 2) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CRYGate")?, "cry".to_owned(), 1, 2) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CRZGate")?, "crz".to_owned(), 1, 2) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CHGate")?, "ch".to_owned(), 0, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("SwapGate")?, "swap".to_owned(), 0, 2) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CCXGate")?, "ccx".to_owned(), 0, 3) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CSwapGate")?, "cswap".to_owned(), 0, 3) + .into_py(py), + circuit::PyGate::new(py, library.getattr("CUGate")?, "cu".to_owned(), 4, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("CXGate")?, "CX".to_owned(), 0, 2).into_py(py), + circuit::PyGate::new(py, library.getattr("PhaseGate")?, "phase".to_owned(), 1, 1) + .into_py(py), + circuit::PyGate::new( + py, + library.getattr("CPhaseGate")?, + "cphase".to_owned(), + 1, + 2, + ) + .into_py(py), + circuit::PyGate::new(py, library.getattr("IGate")?, "id".to_owned(), 0, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("U1Gate")?, "u1".to_owned(), 1, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("U2Gate")?, "u2".to_owned(), 2, 1).into_py(py), + circuit::PyGate::new(py, library.getattr("U3Gate")?, "u3".to_owned(), 3, 1).into_py(py), + ], + )) +} + +#[pymodule] +fn _qasm3(py: Python<'_>, module: &PyModule) -> PyResult<()> { + module.add_function(wrap_pyfunction!(loads, module)?)?; + module.add_class::()?; + module.add( + "QASM3ImporterError", + py.get_type::(), + )?; + module.add("_STDGATES_INC_GATES", stdgates_inc_gates(py)?)?; + Ok(()) +} diff --git a/qiskit/qasm/libs/dummy/stdgates.inc b/qiskit/qasm/libs/dummy/stdgates.inc new file mode 100644 index 000000000000..d9deaf261606 --- /dev/null +++ b/qiskit/qasm/libs/dummy/stdgates.inc @@ -0,0 +1,75 @@ +// OpenQASM 3 standard gate library + +// phase gate +gate p(lambda) a {} + +// Pauli gate: bit-flip or NOT gate +gate x a {} +// Pauli gate: bit and phase flip +gate y a {} +// Pauli gate: phase flip +gate z a {} + +// Clifford gate: Hadamard +gate h a {} +// Clifford gate: sqrt(Z) or S gate +gate s a {} +// Clifford gate: inverse of sqrt(Z) +gate sdg a {} + +// sqrt(S) or T gate +gate t a {} +// inverse of sqrt(S) +gate tdg a {} + +// sqrt(NOT) gate +gate sx a {} + +// Rotation around X-axis +gate rx(theta) a {} +// rotation around Y-axis +gate ry(theta) a {} +// rotation around Z axis +gate rz(lambda) a {} + +// controlled-NOT +gate cx c, t {} +// controlled-Y +gate cy a, b {} +// controlled-Z +gate cz a, b {} +// controlled-phase +gate cp(lambda) a, b {} +// controlled-rx +gate crx(theta) a, b {} +// controlled-ry +gate cry(theta) a, b {} +// controlled-rz +gate crz(theta) a, b {} +// controlled-H +gate ch a, b {} + +// swap +gate swap a, b {} + +// Toffoli +gate ccx a, b, c {} +// controlled-swap +gate cswap a, b, c {} + +// four parameter controlled-U gate with relative phase +gate cu(theta, phi, lambda, gamma) c, t {} + +// Gates for OpenQASM 2 backwards compatibility +// CNOT +gate CX c, t {} +// phase gate +gate phase(lambda) q {} +// controlled-phase +gate cphase(lambda) a, b {} +// identity or idle gate +gate id a {} +// IBM Quantum experience gates +gate u1(lambda) q {} +gate u2(phi, lambda) q {} +gate u3(theta, phi, lambda) q {} diff --git a/setup.py b/setup.py index 91fb20c83cf2..4bf16f2fc9c7 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,12 @@ binding=Binding.PyO3, debug=rust_debug, ), + RustExtension( + "qiskit._qasm3", + "crates/qasm3/Cargo.toml", + binding=Binding.PyO3, + debug=rust_debug, + ), ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, ) From cb36c3051d51e72df3cd3d8f075363b3f1ce4559 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 17 Jan 2024 16:53:47 +0000 Subject: [PATCH 02/27] Tidy up public interface construction --- crates/qasm3/src/circuit.rs | 6 +- crates/qasm3/src/lib.rs | 180 ++++++++++++++++++++++++------------ 2 files changed, 124 insertions(+), 62 deletions(-) diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 20720233b2e9..444481254e5e 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -75,16 +75,16 @@ pub struct PyGate { } impl PyGate { - pub fn new>>( + pub fn new>, S: AsRef>( py: Python, constructor: T, - name: String, + name: S, num_params: usize, num_qubits: usize, ) -> Self { Self { constructor: constructor.into_py(py), - name, + name: name.as_ref().to_owned(), num_params, num_qubits, } diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index 8efb5ac41169..c1a5a2df1de7 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -24,29 +24,98 @@ use oq3_semantics::syntax_to_semantics::parse_source_string; use crate::error::QASM3ImporterError; +/// The inner parser at the moment can only read the include path from this environment variable in +/// the `PATH` conventions of the current operating system. +const INCLUDE_PATH_ENV_VAR: &str = "QASM3_PATH"; +/// The name of a Python attribute to define on the given module where the default implementation +/// of the ``stdgates.inc`` custom instructions is located. +const STDGATES_INC_CUSTOM_GATES_ATTR: &str = "STDGATES_INC_GATES"; + +/// Set the include path in the magic environment variable to the given structured include path. +/// Returns the previously value of the environment variable, if any, so it can be restored by +/// [restore_include_path]. +/// +/// The `module` should be the [PyModule] representing the `qiskit._qasm3` module itself. +fn set_include_path( + module: &PyModule, + include_path: Option<&[OsString]>, +) -> PyResult> { + let old_path = std::env::var_os(INCLUDE_PATH_ENV_VAR); + if let Some(includes) = include_path { + let path = std::env::join_paths(includes).map_err(|err| { + QASM3ImporterError::new_err(format!("failed to create an include path: {:?}", err)) + })?; + std::env::set_var(INCLUDE_PATH_ENV_VAR, path); + } else if old_path.is_none() { + // Our module should be in the root of the Qiskit package. + let lib_path = Path::new(module.filename()?) + .parent() + .unwrap() + .join(["qasm", "libs", "dummy"].iter().collect::()); + std::env::set_var(INCLUDE_PATH_ENV_VAR, lib_path); + } + Ok(old_path) +} + +/// Restore the state of the environment to the given value. If no value is given, the key is +/// removed from the environment (an explicitly empty path is different to a missing path). +fn restore_include_path(include_path: Option<&OsStr>) { + if let Some(old_path) = include_path { + std::env::set_var(INCLUDE_PATH_ENV_VAR, old_path); + } else { + std::env::remove_var(INCLUDE_PATH_ENV_VAR); + } +} + /// Load an OpenQASM 3 program from a string into a :class:`.QuantumCircuit`. +/// +/// Args: +/// source (str): the program source in a Python string. +/// custom_instructions (Iterable[CustomGate]): Python constructors to use for particular named +/// gates. If not supplied, Qiskit will use its own standard-library constructors for +/// gates defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. +/// include_path (Iterable[str]): the path to search when resolving ``include`` statements. +/// If not given, Qiskit will arrange for this to point to a location containing +/// ``stdgates.inc`` only. Paths are tried in the sequence order. +/// +/// As an implementation detail of the internal parser, if this argument is not given but +/// the environment variable ``QASM3_PATH`` is set, it will be used instead, and is +/// formatted using the ``PATH`` conventions of your operating system (e.g. earlier +/// directories are tried first, paths are separated by ``:`` on Unix-likes and ``;`` on +/// Windows, and so forth). +/// +/// Returns: +/// :class:`.QuantumCircuit`: the constructed circuit object. +/// +/// Raises: +/// :class:`.QASM3ImporterError`: if an error occurred during parsing or semantic analysis. +/// In the case of a parsing error, most of the error messages are printed to the terminal +/// and formatted, for better legibility. #[pyfunction] -#[pyo3(pass_module)] +#[pyo3(pass_module, signature = (source, /, *, custom_instructions=None, include_path=None))] pub fn loads( module: &PyModule, py: Python, source: String, - custom_gates: Option>, + custom_instructions: Option>, + include_path: Option>, ) -> PyResult { + let old_path = set_include_path(module, include_path.as_deref())?; let result = parse_source_string(source, None); + restore_include_path(old_path.as_deref()); if result.any_errors() { result.print_errors(); return Err(QASM3ImporterError::new_err( "errors during parsing; see printed errors", )); } - let gates = match custom_gates { + let gates = match custom_instructions { Some(gates) => gates .into_iter() .map(|gate| (gate.name().to_owned(), gate)) .collect(), None => module - .getattr("_STDGATES_INC_GATES")? + .getattr(STDGATES_INC_CUSTOM_GATES_ATTR)? .iter()? .map(|obj| { let gate = obj?.extract::()?; @@ -57,73 +126,66 @@ pub fn loads( crate::build::convert_asg(py, result.program(), result.symbol_table(), gates) } +/// Create a suitable sequence for use with the ``custom_gates`` of :func:`load` and :func:`loads`, +/// as a Python object on the Python heap, so we can re-use it, and potentially expose it has a +/// data attribute to users. fn stdgates_inc_gates(py: Python) -> PyResult<&PyTuple> { let library = PyModule::import(py, "qiskit.circuit.library")?; + let stdlib_gate = |qiskit_class, name, num_params, num_qubits| -> PyResult> { + Ok(circuit::PyGate::new( + py, + library.getattr(qiskit_class)?, + name, + num_params, + num_qubits, + ) + .into_py(py)) + }; Ok(PyTuple::new( py, vec![ - circuit::PyGate::new(py, library.getattr("PhaseGate")?, "p".to_owned(), 1, 1) - .into_py(py), - circuit::PyGate::new(py, library.getattr("XGate")?, "x".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("YGate")?, "y".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("ZGate")?, "z".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("HGate")?, "h".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("SGate")?, "s".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("SdgGate")?, "sdg".to_owned(), 0, 1) - .into_py(py), - circuit::PyGate::new(py, library.getattr("TGate")?, "t".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("TdgGate")?, "tdg".to_owned(), 0, 1) - .into_py(py), - circuit::PyGate::new(py, library.getattr("SXGate")?, "sx".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("RXGate")?, "rx".to_owned(), 1, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("RYGate")?, "ry".to_owned(), 1, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("RZGate")?, "rz".to_owned(), 1, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("CXGate")?, "cx".to_owned(), 0, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("CYGate")?, "cy".to_owned(), 0, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("CZGate")?, "cz".to_owned(), 0, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("CPhaseGate")?, "cp".to_owned(), 1, 2) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CRXGate")?, "crx".to_owned(), 1, 2) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CRYGate")?, "cry".to_owned(), 1, 2) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CRZGate")?, "crz".to_owned(), 1, 2) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CHGate")?, "ch".to_owned(), 0, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("SwapGate")?, "swap".to_owned(), 0, 2) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CCXGate")?, "ccx".to_owned(), 0, 3) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CSwapGate")?, "cswap".to_owned(), 0, 3) - .into_py(py), - circuit::PyGate::new(py, library.getattr("CUGate")?, "cu".to_owned(), 4, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("CXGate")?, "CX".to_owned(), 0, 2).into_py(py), - circuit::PyGate::new(py, library.getattr("PhaseGate")?, "phase".to_owned(), 1, 1) - .into_py(py), - circuit::PyGate::new( - py, - library.getattr("CPhaseGate")?, - "cphase".to_owned(), - 1, - 2, - ) - .into_py(py), - circuit::PyGate::new(py, library.getattr("IGate")?, "id".to_owned(), 0, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("U1Gate")?, "u1".to_owned(), 1, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("U2Gate")?, "u2".to_owned(), 2, 1).into_py(py), - circuit::PyGate::new(py, library.getattr("U3Gate")?, "u3".to_owned(), 3, 1).into_py(py), + stdlib_gate("PhaseGate", "p", 1, 1)?, + stdlib_gate("XGate", "x", 0, 1)?, + stdlib_gate("YGate", "y", 0, 1)?, + stdlib_gate("ZGate", "z", 0, 1)?, + stdlib_gate("HGate", "h", 0, 1)?, + stdlib_gate("SGate", "s", 0, 1)?, + stdlib_gate("SdgGate", "sdg", 0, 1)?, + stdlib_gate("TGate", "t", 0, 1)?, + stdlib_gate("TdgGate", "tdg", 0, 1)?, + stdlib_gate("SXGate", "sx", 0, 1)?, + stdlib_gate("RXGate", "rx", 1, 1)?, + stdlib_gate("RYGate", "ry", 1, 1)?, + stdlib_gate("RZGate", "rz", 1, 1)?, + stdlib_gate("CXGate", "cx", 0, 2)?, + stdlib_gate("CYGate", "cy", 0, 2)?, + stdlib_gate("CZGate", "cz", 0, 2)?, + stdlib_gate("CPhaseGate", "cp", 1, 2)?, + stdlib_gate("CRXGate", "crx", 1, 2)?, + stdlib_gate("CRYGate", "cry", 1, 2)?, + stdlib_gate("CRZGate", "crz", 1, 2)?, + stdlib_gate("CHGate", "ch", 0, 2)?, + stdlib_gate("SwapGate", "swap", 0, 2)?, + stdlib_gate("CCXGate", "ccx", 0, 3)?, + stdlib_gate("CSwapGate", "cswap", 0, 3)?, + stdlib_gate("CUGate", "cu", 4, 2)?, + stdlib_gate("CXGate", "CX", 0, 2)?, + stdlib_gate("PhaseGate", "phase", 1, 1)?, + stdlib_gate("CPhaseGate", "cphase", 1, 2)?, + stdlib_gate("IGate", "id", 0, 1)?, + stdlib_gate("U1Gate", "u1", 1, 1)?, + stdlib_gate("U2Gate", "u2", 2, 1)?, + stdlib_gate("U3Gate", "u3", 3, 1)?, ], )) } +/// Internal module supplying the OpenQASM 3 import capabilities. The entries in it should largely +/// be re-exposed directly to public Python space. #[pymodule] fn _qasm3(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(loads, module)?)?; module.add_class::()?; - module.add( - "QASM3ImporterError", - py.get_type::(), - )?; - module.add("_STDGATES_INC_GATES", stdgates_inc_gates(py)?)?; + module.add(STDGATES_INC_CUSTOM_GATES_ATTR, stdgates_inc_gates(py)?)?; Ok(()) } From 6cfcd81769f37d987981dbc156b19a06b2b6b03b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 17 Jan 2024 16:54:30 +0000 Subject: [PATCH 03/27] Add load-from-file entry point --- crates/qasm3/src/lib.rs | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index c1a5a2df1de7..3c53093f49f4 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -15,6 +15,9 @@ mod circuit; mod error; mod expr; +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; + use hashbrown::HashMap; use pyo3::prelude::*; @@ -126,6 +129,63 @@ pub fn loads( crate::build::convert_asg(py, result.program(), result.symbol_table(), gates) } +/// Load an OpenQASM 3 program from a source file into a :class:`.QuantumCircuit`. +/// +/// Args: +/// pathlike_or_filelike (str | os.PathLike | io.TextIOBase): the program source. This can +/// either be given as a filepath, or an open text stream object. If the stream is already +/// opened it is consumed in Python space, whereas filenames are opened and consumed in +/// Rust space; there might be slightly different performance characteristics, depending on +/// your system and how the streams are buffered by default. +/// custom_instructions (Iterable[CustomGate]): Python constructors to use for particular named +/// gates. If not supplied, Qiskit will use its own standard-library constructors for +/// gates defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. +/// include_path (Iterable[str]): the path to search when resolving ``include`` statements. +/// If not given, Qiskit will arrange for this to point to a location containing +/// ``stdgates.inc`` only. Paths are tried in the sequence order. +/// +/// As an implementation detail of the internal parser, if this argument is not given but +/// the environment variable ``QASM3_PATH`` is set, it will be used instead, and is +/// formatted using the ``PATH`` conventions of your operating system (e.g. earlier +/// directories are tried first, paths are separated by ``:`` on Unix-likes and ``;`` on +/// Windows, and so forth). +/// +/// Returns: +/// :class:`.QuantumCircuit`: the constructed circuit object. +/// +/// Raises: +/// :class:`.QASM3ImporterError`: if an error occurred during parsing or semantic analysis. +/// In the case of a parsing error, most of the error messages are printed to the terminal +/// and formatted, for better legibility. +#[pyfunction] +#[pyo3( + pass_module, + signature = (pathlike_or_filelike, /, *, custom_instructions=None, include_path=None), +)] +pub fn load( + module: &PyModule, + py: Python, + pathlike_or_filelike: &PyAny, + custom_instructions: Option>, + include_path: Option>, +) -> PyResult { + let source = + if pathlike_or_filelike.is_instance(PyModule::import(py, "io")?.getattr("TextIOBase")?)? { + pathlike_or_filelike + .call_method0("read")? + .extract::()? + } else { + let path = PyModule::import(py, "os")? + .getattr("fspath")? + .call1((pathlike_or_filelike,))? + .extract::()?; + std::fs::read_to_string(&path).map_err(|err| { + QASM3ImporterError::new_err(format!("failed to read file '{:?}': {:?}", &path, err)) + })? + }; + loads(module, py, source, custom_instructions, include_path) +} + /// Create a suitable sequence for use with the ``custom_gates`` of :func:`load` and :func:`loads`, /// as a Python object on the Python heap, so we can re-use it, and potentially expose it has a /// data attribute to users. @@ -185,6 +245,7 @@ fn stdgates_inc_gates(py: Python) -> PyResult<&PyTuple> { #[pymodule] fn _qasm3(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(loads, module)?)?; + module.add_function(wrap_pyfunction!(load, module)?)?; module.add_class::()?; module.add(STDGATES_INC_CUSTOM_GATES_ATTR, stdgates_inc_gates(py)?)?; Ok(()) From 3fed881f863bae8a48418c1d063d7c2374cca79d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 17 Jan 2024 17:34:24 +0000 Subject: [PATCH 04/27] Create public experimental interface --- crates/qasm3/src/lib.rs | 24 +++++++------- pyproject.toml | 2 ++ qiskit/exceptions.py | 23 ++++++++++++++ qiskit/qasm3/__init__.py | 69 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index 3c53093f49f4..552394e38a88 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -74,9 +74,9 @@ fn restore_include_path(include_path: Option<&OsStr>) { /// /// Args: /// source (str): the program source in a Python string. -/// custom_instructions (Iterable[CustomGate]): Python constructors to use for particular named -/// gates. If not supplied, Qiskit will use its own standard-library constructors for -/// gates defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. +/// custom_gates (Iterable[CustomGate]): Python constructors to use for particular named gates. +/// If not supplied, Qiskit will use its own standard-library constructors for gates +/// defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. /// include_path (Iterable[str]): the path to search when resolving ``include`` statements. /// If not given, Qiskit will arrange for this to point to a location containing /// ``stdgates.inc`` only. Paths are tried in the sequence order. @@ -95,12 +95,12 @@ fn restore_include_path(include_path: Option<&OsStr>) { /// In the case of a parsing error, most of the error messages are printed to the terminal /// and formatted, for better legibility. #[pyfunction] -#[pyo3(pass_module, signature = (source, /, *, custom_instructions=None, include_path=None))] +#[pyo3(pass_module, signature = (source, /, *, custom_gates=None, include_path=None))] pub fn loads( module: &PyModule, py: Python, source: String, - custom_instructions: Option>, + custom_gates: Option>, include_path: Option>, ) -> PyResult { let old_path = set_include_path(module, include_path.as_deref())?; @@ -112,7 +112,7 @@ pub fn loads( "errors during parsing; see printed errors", )); } - let gates = match custom_instructions { + let gates = match custom_gates { Some(gates) => gates .into_iter() .map(|gate| (gate.name().to_owned(), gate)) @@ -137,9 +137,9 @@ pub fn loads( /// opened it is consumed in Python space, whereas filenames are opened and consumed in /// Rust space; there might be slightly different performance characteristics, depending on /// your system and how the streams are buffered by default. -/// custom_instructions (Iterable[CustomGate]): Python constructors to use for particular named -/// gates. If not supplied, Qiskit will use its own standard-library constructors for -/// gates defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. +/// custom_gates (Iterable[CustomGate]): Python constructors to use for particular named gates. +/// If not supplied, Qiskit will use its own standard-library constructors for gates +/// defined in the OpenQASM 3.0 standard-library file ``stdgates.inc``. /// include_path (Iterable[str]): the path to search when resolving ``include`` statements. /// If not given, Qiskit will arrange for this to point to a location containing /// ``stdgates.inc`` only. Paths are tried in the sequence order. @@ -160,13 +160,13 @@ pub fn loads( #[pyfunction] #[pyo3( pass_module, - signature = (pathlike_or_filelike, /, *, custom_instructions=None, include_path=None), + signature = (pathlike_or_filelike, /, *, custom_gates=None, include_path=None), )] pub fn load( module: &PyModule, py: Python, pathlike_or_filelike: &PyAny, - custom_instructions: Option>, + custom_gates: Option>, include_path: Option>, ) -> PyResult { let source = @@ -183,7 +183,7 @@ pub fn load( QASM3ImporterError::new_err(format!("failed to read file '{:?}': {:?}", &path, err)) })? }; - loads(module, py, source, custom_instructions, include_path) + loads(module, py, source, custom_gates, include_path) } /// Create a suitable sequence for use with the ``custom_gates`` of :func:`load` and :func:`loads`, diff --git a/pyproject.toml b/pyproject.toml index d42b249a1058..60511c2b8197 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,6 +173,8 @@ select = [ extension-pkg-allow-list = [ "numpy", "qiskit._accelerate", + "qiskit._qasm2", + "qiskit._qasm3", # We can't allow pylint to load qiskit._qasm2 because it's not able to # statically resolve the cyclical load of the exception and it bugs out. "retworkx", diff --git a/qiskit/exceptions.py b/qiskit/exceptions.py index e26d4097fa90..796bc16d30b5 100644 --- a/qiskit/exceptions.py +++ b/qiskit/exceptions.py @@ -61,6 +61,25 @@ is not present, but will raise :exc:`OptionalDependencyImportWarning` to let you know about it. .. autoexception:: OptionalDependencyImportWarning + +When experimental features are being used, Qiskit will raise :exc:`ExperimentalWarning`. + +.. autoexception:: ExperimentalWarning + + +Filtering warnings +------------------ + +Python has built-in mechanisms to filter warnings, described in the documentation of the +:mod:`warnings` module. You can use these subclasses in your warning filters from within Python to +silence warnings you are not interested in. For example, if you are knowingly using experimental +features and are comfortable that they make break in later versions, you can silence +:exc:`ExperimentalWarning` like this: + + import warnings + from qiskit.exceptions import ExperimentalWarning + + warnings.filterwarnings("ignore", category=ExperimentalWarning) """ from typing import Optional @@ -124,3 +143,7 @@ class OptionalDependencyImportWarning(QiskitWarning): """Raised when an optional library raises errors during its import.""" # Not a subclass of `ImportWarning` because those are hidden by default. + + +class ExperimentalWarning(QiskitWarning): + """Raised when an experimental feature is being used.""" diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 8e3de893ea3c..e2159c946fff 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -172,13 +172,60 @@ \"\"\" circuit = qiskit.qasm3.loads(program) circuit.draw("mpl") + + +Experimental import interface +----------------------------- + +The import functions given above rely on the ANTLR-based reference parser from the OpenQASM project +itself, which is more intended as a language reference than a performant parser. You need to have +the extension ``qiskit-qasm3-import`` installed to use it. + +Qiskit is developing a native parser, written in Rust, which is available as part of the core Qiskit +package. This parser is still in its early experimental stages, so is missing features and its +interface is changing and expanding, but it is typically orders of magnitude more performant for the +subset of OpenQASM 3 it currently supports. + +You can use the experimental interface immediately, with similar functions to the main interface +above: + +.. autofunction:: load_experimental +.. autofunction:: loads_experimental + +These two functions both raise :exc:`.ExperimentalWarning`; you can disable this warning by doing:: + + import warnings + from qiskit.exceptions import ExperimentalWarning + + warnings.filterwarnings("ignore", category=ExperimentalWarning, module="qiskit.qasm3") + +These two functions allow for specifying include paths as an iterable of paths, and for specifying +custom Python constructors to use for particular gates. These custom constructors are specified by +using the :class:`CustomGate` object: + +.. autoclass:: CustomGate + +In ``custom_gates`` is not given, Qiskit will attempt to use its standard-library gate objects for +the gates defined in OpenQASM 3 standard library file ``stdgates.inc``. This sequence of gates is +available on this module, if you wish to build on top of it: + +.. py:data:: STDGATES_INC_GATES + + A tuple of :class:`CustomGate` objects specifying the Qiskit constructors to use for the + ``stdgates.inc`` include file. """ +import functools +import warnings + +from qiskit import _qasm3 +from qiskit.exceptions import ExperimentalWarning from qiskit.utils import optionals as _optionals from .experimental import ExperimentalFeatures from .exporter import Exporter from .exceptions import QASM3Error, QASM3ImporterError, QASM3ExporterError +from qiskit._qasm3 import CustomGate, STDGATES_INC_GATES def dumps(circuit, **kwargs) -> str: @@ -253,3 +300,25 @@ def loads(program: str): return qiskit_qasm3_import.parse(program) except qiskit_qasm3_import.ConversionError as exc: raise QASM3ImporterError(str(exc)) from exc + + +@functools.wraps(_qasm3.loads) +def loads_experimental(source, /, *, custom_gates=None, include_path=None): + """""" + warnings.warn( + "This is an experimental native version of the OpenQASM 3 importer." + " Beware that its interface might change, and it might be missing features.", + category=ExperimentalWarning, + ) + return _qasm3.loads(source, custom_gates=custom_gates, include_path=include_path) + + +@functools.wraps(_qasm3.load) +def load_experimental(pathlike_or_filelike, /, *, custom_gates=None, include_path=None): + """""" + warnings.warn( + "This is an experimental native version of the OpenQASM 3 importer." + " Beware that its interface might change, and it might be missing features.", + category=ExperimentalWarning, + ) + return _qasm3.load(pathlike_or_filelike, custom_gates=custom_gates, include_path=include_path) From 1994717047cf1909cfa931fd1fd7570e67c2d5fe Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 17 Jan 2024 17:36:05 +0000 Subject: [PATCH 05/27] Credit John While I've been the author of the Qiskit side of this, John wrote the separate Rust crates that this depends on, so in principle this contribution to Qiskit is from both of us. Co-authored-by: John Lapeyre From f84e86a27170168768c4ab4c552ce01c208f0087 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 18 Jan 2024 13:13:38 +0000 Subject: [PATCH 06/27] Fix lint --- qiskit/qasm3/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index e2159c946fff..63520af6ccf6 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -225,7 +225,7 @@ from .experimental import ExperimentalFeatures from .exporter import Exporter from .exceptions import QASM3Error, QASM3ImporterError, QASM3ExporterError -from qiskit._qasm3 import CustomGate, STDGATES_INC_GATES +from .._qasm3 import CustomGate, STDGATES_INC_GATES def dumps(circuit, **kwargs) -> str: From 9c622cfc895c0dc03c5ad0f818c0590bb81df45f Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Thu, 18 Jan 2024 14:48:12 -0500 Subject: [PATCH 07/27] Bump version (git hash) of dependency on openqasm3_parser The qasm3 crate fails to build on Windows. This commit should fix this. --- crates/qasm3/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 9ae9014e6aba..43cd2e5d8c72 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -17,4 +17,5 @@ extension-module = ["pyo3/extension-module"] [dependencies] pyo3.workspace = true hashbrown.version = "0.14.0" -oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "57407ee" } +oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "c9d9ed9" } + From 58cb87b6f4d6fe23ede8ded6891178abace1c6c7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 19 Jan 2024 14:34:57 +0000 Subject: [PATCH 08/27] Add support for barrier --- Cargo.lock | 56 ++++------------------------ crates/qasm3/Cargo.toml | 4 +- crates/qasm3/src/build.rs | 49 +++++++++++++++++++++++- crates/qasm3/src/circuit.rs | 58 ++++++++++++++++------------- crates/qasm3/src/expr.rs | 74 +++++++++++++++++++++++-------------- 5 files changed, 134 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 648024f02ce3..f231995fe2a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,12 +406,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "numtoa" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" - [[package]] name = "once_cell" version = "1.19.0" @@ -421,7 +415,7 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" dependencies = [ "unicode-properties", "unicode-xid", @@ -430,7 +424,7 @@ dependencies = [ [[package]] name = "oq3_parser" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" dependencies = [ "drop_bomb", "oq3_lexer", @@ -440,7 +434,7 @@ dependencies = [ [[package]] name = "oq3_semantics" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" dependencies = [ "boolenum", "hashbrown 0.14.3", @@ -452,17 +446,16 @@ dependencies = [ [[package]] name = "oq3_source_file" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" dependencies = [ "ariadne", "oq3_syntax", - "source-span", ] [[package]] name = "oq3_syntax" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=57407ee#57407ee9504a8e251eb476c054e990e0f686822a" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" dependencies = [ "cov-mark", "either", @@ -497,7 +490,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -639,6 +632,7 @@ name = "qiskit-qasm3" version = "1.0.0" dependencies = [ "hashbrown 0.14.3", + "indexmap 2.1.0", "oq3_semantics", "pyo3", ] @@ -779,15 +773,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -797,12 +782,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_termios" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" - [[package]] name = "rowan" version = "0.15.15" @@ -895,15 +874,6 @@ dependencies = [ "serde", ] -[[package]] -name = "source-span" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3afeb8b7a1ae2e8721f2193cc2291c9e00dd68511907fd8b807e2c8d42caa3c" -dependencies = [ - "termion", -] - [[package]] name = "syn" version = "1.0.109" @@ -932,18 +902,6 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" -[[package]] -name = "termion" -version = "1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" -dependencies = [ - "libc", - "numtoa", - "redox_syscall 0.2.16", - "redox_termios", -] - [[package]] name = "text-size" version = "1.1.1" diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 43cd2e5d8c72..2b820f9738b6 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -16,6 +16,6 @@ extension-module = ["pyo3/extension-module"] [dependencies] pyo3.workspace = true +indexmap.version = "2.1.0" hashbrown.version = "0.14.0" -oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "c9d9ed9" } - +oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "45556a9" } diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index d1b8386f8497..1916791fa231 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -11,9 +11,10 @@ // that they have been altered from the originals. use pyo3::prelude::*; -use pyo3::types::{PyString, PyTuple}; +use pyo3::types::{PySequence, PyString, PyTuple}; use hashbrown::HashMap; +use indexmap::IndexMap; use oq3_semantics::asg; use oq3_semantics::symbols::{SymbolId, SymbolTable, SymbolType}; @@ -178,6 +179,50 @@ impl BuilderState { Ok(()) } + fn apply_barrier( + &mut self, + py: Python, + ast_symbols: &SymbolTable, + barrier: &asg::Barrier, + ) -> PyResult<()> { + let qubits = if barrier.qubits().is_empty() { + // If there's no qargs, it's a barrier over all in-scope qubits (which is all qubits, + // unless we're in a gate/subroutine body). + self.qc + .inner(py) + .getattr("qubits")? + .downcast::()? + .to_tuple()? + } else { + // We want any deterministic order for easier circuit reproducibility in Python space, + // and to include each seen qubit once. This simply maintains insertion order. + let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py>::with_capacity( + barrier.qubits().len(), + ); + for qarg in barrier.qubits().iter() { + let qarg = expr::expect_gate_operand(qarg)?; + match expr::eval_gate_qarg(py, &self.symbols, ast_symbols, qarg)? { + expr::BroadcastItem::Bit(bit) => { + let _ = qubits.insert(bit.as_ptr(), bit); + } + expr::BroadcastItem::Register(register) => { + register.into_iter().for_each(|bit| { + let _ = qubits.insert(bit.as_ptr(), bit); + }) + } + } + } + PyTuple::new(py, qubits.values()) + }; + let instruction = self.module.new_instruction( + py, + self.module.new_barrier(py, qubits.len())?, + qubits, + (), + )?; + self.qc.append(py, instruction).map(|_| ()) + } + fn define_gate( &mut self, _py: Python, @@ -293,10 +338,10 @@ pub fn convert_asg( asg::Stmt::DeclareQuantum(decl) => state.declare_quantum(py, ast_symbols, decl)?, asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, asg::Stmt::GateDeclaration(decl) => state.define_gate(py, ast_symbols, decl)?, + asg::Stmt::Barrier(barrier) => state.apply_barrier(py, ast_symbols, barrier)?, asg::Stmt::Alias | asg::Stmt::AnnotatedStmt(_) | asg::Stmt::Assignment(_) - | asg::Stmt::Barrier | asg::Stmt::Block(_) | asg::Stmt::Box | asg::Stmt::Break diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 444481254e5e..afcbdd9363f6 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -169,7 +169,8 @@ pub struct PyCircuitModule { qubit: Py, creg: Py, clbit: Py, - instruction: Py, + circuit_instruction: Py, + barrier: Py, } impl PyCircuitModule { @@ -191,17 +192,16 @@ impl PyCircuitModule { .downcast::()? .into_py(py), clbit: module.getattr("Clbit")?.downcast::()?.into_py(py), - instruction: module + circuit_instruction: module .getattr("CircuitInstruction")? .downcast::()? .into_py(py), + barrier: module.getattr("Barrier")?.downcast::()?.into_py(py), }) } pub fn new_circuit(&self, py: Python) -> PyResult { - Ok(PyCircuit { - qc: self.circuit.call0(py)?, - }) + self.circuit.call0(py).map(PyCircuit) } pub fn new_qreg>>( @@ -256,54 +256,60 @@ impl PyCircuitModule { Q: IntoPy>, C: IntoPy>, { - self.instruction + self.circuit_instruction .call1(py, (operation, qubits.into_py(py), clbits.into_py(py))) } + + pub fn new_barrier(&self, py: Python, num_qubits: usize) -> PyResult> { + self.barrier.call1(py, (num_qubits,)).map(|x| x.into_py(py)) + } } /// Circuit construction context object to provide an easier Rust-space interface for us to /// construct the Python :class:`.QuantumCircuit`. The idea of doing this from Rust space like /// this is that we might steadily be able to move more and more of it into being native Rust as /// the Rust-space APIs around the internal circuit data stabilise. -pub struct PyCircuit { - /// The actual circuit object that's under construction. - qc: Py, -} +pub struct PyCircuit(Py); impl PyCircuit { - pub fn add_qreg(&mut self, py: Python, qreg: &PyQuantumRegister) -> PyResult<()> { - self.qc - .call_method1(py, "add_register", (qreg.to_object(py),)) + /// Untyped access to the inner Python object. + pub fn inner<'a>(&'a self, py: Python<'a>) -> &'a PyAny { + self.0.as_ref(py) + } + + pub fn add_qreg(&self, py: Python, qreg: &PyQuantumRegister) -> PyResult<()> { + self.inner(py) + .call_method1("add_register", (qreg.to_object(py),)) .map(|_| ()) } - pub fn add_qubit(&mut self, py: Python, qubit: Py) -> PyResult<()> { - self.qc - .call_method1(py, "add_bits", ((qubit,),)) + pub fn add_qubit(&self, py: Python, qubit: Py) -> PyResult<()> { + self.inner(py) + .call_method1("add_bits", ((qubit,),)) .map(|_| ()) } - pub fn add_creg(&mut self, py: Python, creg: &PyClassicalRegister) -> PyResult<()> { - self.qc - .call_method1(py, "add_register", (creg.to_object(py),)) + pub fn add_creg(&self, py: Python, creg: &PyClassicalRegister) -> PyResult<()> { + self.inner(py) + .call_method1("add_register", (creg.to_object(py),)) .map(|_| ()) } - pub fn add_clbit>>(&mut self, py: Python, clbit: T) -> PyResult<()> { - self.qc - .call_method1(py, "add_bits", ((clbit,),)) + pub fn add_clbit>>(&self, py: Python, clbit: T) -> PyResult<()> { + self.inner(py) + .call_method1("add_bits", ((clbit,),)) .map(|_| ()) } - pub fn append>>(&mut self, py: Python, instruction: T) -> PyResult<()> { - self.qc - .call_method1(py, "_append", (instruction.into_py(py),)) + pub fn append>>(&self, py: Python, instruction: T) -> PyResult<()> { + self.inner(py) + .call_method1("_append", (instruction.into_py(py),)) .map(|_| ()) } } impl ::pyo3::IntoPy> for PyCircuit { fn into_py(self, py: Python) -> Py { - self.qc.clone_ref(py) + self.0.clone_ref(py) } } diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 7f587cb6bd22..46fd0594e04f 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -91,7 +91,7 @@ fn eval_const_uint(py: Python, ast_symbols: &SymbolTable, expr: &asg::TExpr) -> }) } -enum BroadcastItem { +pub enum BroadcastItem { Bit(Py), Register(Vec>), } @@ -193,6 +193,50 @@ fn broadcast_apply_index( } } +pub fn eval_gate_qarg( + py: Python, + our_symbols: &PySymbolTable, + ast_symbols: &SymbolTable, + qarg: &asg::GateOperand, +) -> PyResult { + match qarg { + asg::GateOperand::Identifier(iden) => { + broadcast_bits_for_identifier(py, our_symbols, iden.symbol().as_ref().unwrap()) + } + asg::GateOperand::IndexedIdentifier(indexed) => { + let iden_symbol = indexed.identifier().as_ref().unwrap(); + indexed.indexes().iter().fold( + broadcast_bits_for_identifier(py, our_symbols, iden_symbol), + |item, index| { + item.and_then(|item| broadcast_apply_index(py, ast_symbols, item, index)) + }, + ) + } + asg::GateOperand::HardwareQubit(_) => { + Err(QASM3ImporterError::new_err("cannot handle hardware qubits")) + } + } +} + +pub fn expect_gate_operand(expr: &asg::TExpr) -> PyResult<&asg::GateOperand> { + match expr.get_type() { + Type::Qubit | Type::QubitArray(_) | Type::HardwareQubit => (), + ty => { + return Err(QASM3ImporterError::new_err(format!( + "unhandled gate operand expression type: {:?}", + ty + ))); + } + } + match expr.expression() { + asg::Expr::GateOperand(operand) => Ok(operand), + expr => Err(QASM3ImporterError::new_err(format!( + "internal logic error: not a gate operand {:?}", + expr + ))), + } +} + pub fn broadcast_qubits<'a, 'py, T>( py: Python<'py>, our_symbols: &PySymbolTable, @@ -205,33 +249,7 @@ where let items = qargs .into_iter() .map(|item| -> PyResult { - match item.expression() { - asg::Expr::GateOperand(operand) => match operand { - asg::GateOperand::Identifier(iden) => broadcast_bits_for_identifier( - py, - our_symbols, - iden.symbol().as_ref().unwrap(), - ), - asg::GateOperand::IndexedIdentifier(indexed) => { - let iden_symbol = indexed.identifier().as_ref().unwrap(); - indexed.indexes().iter().fold( - broadcast_bits_for_identifier(py, our_symbols, iden_symbol), - |item, index| { - item.and_then(|item| { - broadcast_apply_index(py, ast_symbols, item, index) - }) - }, - ) - } - asg::GateOperand::HardwareQubit(_) => { - Err(QASM3ImporterError::new_err("cannot handle hardware qubits")) - } - }, - ty => Err(QASM3ImporterError::new_err(format!( - "unhandled gate operand expression type: {:?}", - ty - ))), - } + eval_gate_qarg(py, our_symbols, ast_symbols, expect_gate_operand(item)?) }) .collect::>>()?; From 2ad78800f8d8495e7b3a7712c23a71cfa3fa7fb7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 22 Jan 2024 17:15:49 +0000 Subject: [PATCH 09/27] Handle measurement with new parser version Measures, including broadcasted measures, are now supported following increased support on the parser side. The updated version also includes changes to a few APIs, and the include-path settings are now configurable at the entry point, so we switch to those. --- Cargo.lock | 10 +-- crates/qasm3/Cargo.toml | 2 +- crates/qasm3/src/build.rs | 59 ++++++++++++---- crates/qasm3/src/circuit.rs | 19 ++++- crates/qasm3/src/expr.rs | 135 ++++++++++++++++++++++++++++++++---- crates/qasm3/src/lib.rs | 67 +++--------------- 6 files changed, 201 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f231995fe2a6..88f3fd44c284 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,7 +415,7 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" dependencies = [ "unicode-properties", "unicode-xid", @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "oq3_parser" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" dependencies = [ "drop_bomb", "oq3_lexer", @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "oq3_semantics" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" dependencies = [ "boolenum", "hashbrown 0.14.3", @@ -446,7 +446,7 @@ dependencies = [ [[package]] name = "oq3_source_file" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" dependencies = [ "ariadne", "oq3_syntax", @@ -455,7 +455,7 @@ dependencies = [ [[package]] name = "oq3_syntax" version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=45556a9#45556a9d2cfb0dd7a805006af3821e8454480a75" +source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" dependencies = [ "cov-mark", "either", diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 2b820f9738b6..deab924fcc84 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -18,4 +18,4 @@ extension-module = ["pyo3/extension-module"] pyo3.workspace = true indexmap.version = "2.1.0" hashbrown.version = "0.14.0" -oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "45556a9" } +oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "4b5110d" } diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 1916791fa231..4840149cc698 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -185,23 +185,15 @@ impl BuilderState { ast_symbols: &SymbolTable, barrier: &asg::Barrier, ) -> PyResult<()> { - let qubits = if barrier.qubits().is_empty() { - // If there's no qargs, it's a barrier over all in-scope qubits (which is all qubits, - // unless we're in a gate/subroutine body). - self.qc - .inner(py) - .getattr("qubits")? - .downcast::()? - .to_tuple()? - } else { + let qubits = if let Some(asg_qubits) = barrier.qubits().as_ref() { // We want any deterministic order for easier circuit reproducibility in Python space, // and to include each seen qubit once. This simply maintains insertion order. let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py>::with_capacity( - barrier.qubits().len(), + asg_qubits.len() ); - for qarg in barrier.qubits().iter() { + for qarg in asg_qubits.iter() { let qarg = expr::expect_gate_operand(qarg)?; - match expr::eval_gate_qarg(py, &self.symbols, ast_symbols, qarg)? { + match expr::eval_qarg(py, &self.symbols, ast_symbols, qarg)? { expr::BroadcastItem::Bit(bit) => { let _ = qubits.insert(bit.as_ptr(), bit); } @@ -213,6 +205,15 @@ impl BuilderState { } } PyTuple::new(py, qubits.values()) + } else { + // If there's no qargs (represented in the ASG with a `None` rather than an empty + // vector), it's a barrier over all in-scope qubits, which is all qubits, unless we're + // in a gate/subroutine body. + self.qc + .inner(py) + .getattr("qubits")? + .downcast::()? + .to_tuple()? }; let instruction = self.module.new_instruction( py, @@ -262,6 +263,36 @@ impl BuilderState { Ok(()) } + fn assign( + &mut self, + py: Python, + ast_symbols: &SymbolTable, + assignment: &asg::Assignment, + ) -> PyResult<()> { + // Only handling measurements in this first pass. + let qarg = match assignment.rvalue().expression() { + asg::Expr::MeasureExpression(target) => expr::eval_qarg( + py, + &self.symbols, + ast_symbols, + expr::expect_gate_operand(target.operand())?, + ), + expr => Err(QASM3ImporterError::new_err(format!( + "only measurement assignments are currently supported, not {:?}", + expr, + ))), + }?; + let carg = expr::eval_measure_carg(py, &self.symbols, ast_symbols, assignment.lvalue())?; + for (qubits, clbits) in expr::broadcast_measure(py, &qarg, &carg)? { + self.qc.append( + py, + self.module + .new_instruction(py, self.module.measure(py), qubits, clbits)?, + )? + } + Ok(()) + } + fn add_qubit(&mut self, py: Python, ast_symbol: SymbolId) -> PyResult<()> { let qubit = self.module.new_qubit(py)?; if self @@ -334,14 +365,14 @@ pub fn convert_asg( }; for statement in program.stmts().iter() { match statement { + asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, asg::Stmt::DeclareClassical(decl) => state.declare_classical(py, ast_symbols, decl)?, asg::Stmt::DeclareQuantum(decl) => state.declare_quantum(py, ast_symbols, decl)?, - asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, asg::Stmt::GateDeclaration(decl) => state.define_gate(py, ast_symbols, decl)?, asg::Stmt::Barrier(barrier) => state.apply_barrier(py, ast_symbols, barrier)?, + asg::Stmt::Assignment(assignment) => state.assign(py, ast_symbols, assignment)?, asg::Stmt::Alias | asg::Stmt::AnnotatedStmt(_) - | asg::Stmt::Assignment(_) | asg::Stmt::Block(_) | asg::Stmt::Box | asg::Stmt::Break diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index afcbdd9363f6..d1e99c637cfd 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -15,6 +15,11 @@ use pyo3::types::{PyList, PyString, PyTuple, PyType}; use crate::error::QASM3ImporterError; +pub trait PyRegister { + fn bit(&self, py: Python, index: usize) -> PyResult>; + fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; +} + macro_rules! register_type { ($name: ident) => { /// Rust-space wrapper around Qiskit `Register` objects. @@ -28,9 +33,9 @@ macro_rules! register_type { items: Py, } - impl $name { + impl PyRegister for $name { /// Get an individual bit from the register. - pub fn bit(&self, py: Python, index: usize) -> PyResult> { + fn bit(&self, py: Python, index: usize) -> PyResult> { // Unfortunately, `PyList::get_item_unchecked` isn't usable with the stable ABI. self.items .as_ref(py) @@ -38,7 +43,7 @@ macro_rules! register_type { .map(|item| item.into_py(py)) } - pub fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator { + fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator { self.items.as_ref(py).iter() } } @@ -171,6 +176,8 @@ pub struct PyCircuitModule { clbit: Py, circuit_instruction: Py, barrier: Py, + // The singleton object. + measure: Py, } impl PyCircuitModule { @@ -197,6 +204,8 @@ impl PyCircuitModule { .downcast::()? .into_py(py), barrier: module.getattr("Barrier")?.downcast::()?.into_py(py), + // Measure is a singleton, so just store the object. + measure: module.getattr("Measure")?.call0()?.into_py(py), }) } @@ -263,6 +272,10 @@ impl PyCircuitModule { pub fn new_barrier(&self, py: Python, num_qubits: usize) -> PyResult> { self.barrier.call1(py, (num_qubits,)).map(|x| x.into_py(py)) } + + pub fn measure(&self, py: Python) -> Py { + self.measure.clone_ref(py) + } } /// Circuit construction context object to provide an easier Rust-space interface for us to diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 46fd0594e04f..5ad2fc34e0ec 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -13,11 +13,14 @@ use pyo3::prelude::*; use pyo3::types::PyTuple; +use hashbrown::HashMap; + use oq3_semantics::asg; use oq3_semantics::symbols::{SymbolId, SymbolTable}; use oq3_semantics::types::Type; use crate::build::PySymbolTable; +use crate::circuit::PyRegister; use crate::error::QASM3ImporterError; pub fn eval_gate_param( @@ -112,8 +115,8 @@ impl<'py> Iterator for BroadcastQubitsIter<'py> { } let offset = self.offset; let to_scalar = |item: &BroadcastItem| match item { - BroadcastItem::Bit(bit) => bit.clone(), - BroadcastItem::Register(bits) => bits[offset].clone(), + BroadcastItem::Bit(bit) => bit.clone_ref(self.py), + BroadcastItem::Register(bits) => bits[offset].clone_ref(self.py), }; self.offset += 1; Some(PyTuple::new(self.py, self.items.iter().map(to_scalar))) @@ -123,15 +126,50 @@ impl<'py> Iterator for BroadcastQubitsIter<'py> { (self.len - self.offset, Some(self.len - self.offset)) } } +impl<'py> ExactSizeIterator for BroadcastQubitsIter<'py> {} + +struct BroadcastMeasureIter<'a, 'py> { + py: Python<'py>, + len: usize, + offset: usize, + qarg: &'a BroadcastItem, + carg: &'a BroadcastItem, +} + +impl<'a, 'py> Iterator for BroadcastMeasureIter<'a, 'py> { + type Item = (&'py PyTuple, &'py PyTuple); + + fn next(&mut self) -> Option { + if self.offset >= self.len { + return None; + } + let offset = self.offset; + let to_scalar = |item: &BroadcastItem| match item { + BroadcastItem::Bit(bit) => bit.clone_ref(self.py), + BroadcastItem::Register(bits) => bits[offset].clone_ref(self.py), + }; + self.offset += 1; + Some(( + PyTuple::new(self.py, &[to_scalar(self.qarg)]), + PyTuple::new(self.py, &[to_scalar(self.carg)]), + )) + } + + fn size_hint(&self) -> (usize, Option) { + (self.len - self.offset, Some(self.len - self.offset)) + } +} +impl<'a, 'py> ExactSizeIterator for BroadcastMeasureIter<'a, 'py> {} -fn broadcast_bits_for_identifier( +fn broadcast_bits_for_identifier( py: Python, - our_symbols: &PySymbolTable, + bits: &HashMap>, + registers: &HashMap, iden_symbol: &SymbolId, ) -> PyResult { - if let Some(bit) = our_symbols.qubits.get(iden_symbol) { + if let Some(bit) = bits.get(iden_symbol) { Ok(BroadcastItem::Bit(bit.clone())) - } else if let Some(reg) = our_symbols.qregs.get(iden_symbol) { + } else if let Some(reg) = registers.get(iden_symbol) { Ok(BroadcastItem::Register( reg.iter(py).map(|obj| obj.into_py(py)).collect(), )) @@ -193,20 +231,28 @@ fn broadcast_apply_index( } } -pub fn eval_gate_qarg( +pub fn eval_qarg( py: Python, our_symbols: &PySymbolTable, ast_symbols: &SymbolTable, qarg: &asg::GateOperand, ) -> PyResult { match qarg { - asg::GateOperand::Identifier(iden) => { - broadcast_bits_for_identifier(py, our_symbols, iden.symbol().as_ref().unwrap()) - } + asg::GateOperand::Identifier(iden) => broadcast_bits_for_identifier( + py, + &our_symbols.qubits, + &our_symbols.qregs, + iden.symbol().as_ref().unwrap(), + ), asg::GateOperand::IndexedIdentifier(indexed) => { let iden_symbol = indexed.identifier().as_ref().unwrap(); indexed.indexes().iter().fold( - broadcast_bits_for_identifier(py, our_symbols, iden_symbol), + broadcast_bits_for_identifier( + py, + &our_symbols.qubits, + &our_symbols.qregs, + iden_symbol, + ), |item, index| { item.and_then(|item| broadcast_apply_index(py, ast_symbols, item, index)) }, @@ -218,6 +264,36 @@ pub fn eval_gate_qarg( } } +pub fn eval_measure_carg( + py: Python, + our_symbols: &PySymbolTable, + ast_symbols: &SymbolTable, + carg: &asg::LValue, +) -> PyResult { + match carg { + asg::LValue::Identifier(iden) => { + let symbol_id = iden.as_ref().map_err(|err| { + QASM3ImporterError::new_err(format!("internal logic error: {:?}", err)) + })?; + broadcast_bits_for_identifier(py, &our_symbols.clbits, &our_symbols.cregs, symbol_id) + } + asg::LValue::IndexedIdentifier(indexed) => { + let iden_symbol = indexed.identifier().as_ref().unwrap(); + indexed.indexes().iter().fold( + broadcast_bits_for_identifier( + py, + &our_symbols.clbits, + &our_symbols.cregs, + iden_symbol, + ), + |item, index| { + item.and_then(|item| broadcast_apply_index(py, ast_symbols, item, index)) + }, + ) + } + } +} + pub fn expect_gate_operand(expr: &asg::TExpr) -> PyResult<&asg::GateOperand> { match expr.get_type() { Type::Qubit | Type::QubitArray(_) | Type::HardwareQubit => (), @@ -249,7 +325,7 @@ where let items = qargs .into_iter() .map(|item| -> PyResult { - eval_gate_qarg(py, our_symbols, ast_symbols, expect_gate_operand(item)?) + eval_qarg(py, our_symbols, ast_symbols, expect_gate_operand(item)?) }) .collect::>>()?; @@ -274,3 +350,38 @@ where items, }) } + +pub fn broadcast_measure<'a, 'py>( + py: Python<'py>, + qarg: &'a BroadcastItem, + carg: &'a BroadcastItem, +) -> PyResult + 'a> +where + 'py: 'a, +{ + let len = match (qarg, carg) { + (BroadcastItem::Bit(_), BroadcastItem::Bit(_)) => Ok(1), + (BroadcastItem::Bit(_), BroadcastItem::Register(_)) + | (BroadcastItem::Register(_), BroadcastItem::Bit(_)) => Err(QASM3ImporterError::new_err( + "invalid measurement broadcast: cannot broadcast a bit against a register", + )), + (BroadcastItem::Register(qreg), BroadcastItem::Register(creg)) => { + if qreg.len() == creg.len() { + Ok(qreg.len()) + } else { + Err(QASM3ImporterError::new_err(format!( + "invalid measurement broadcast: qarg has length {}, carg has length {}", + qreg.len(), + creg.len() + ))) + } + } + }?; + Ok(BroadcastMeasureIter { + py, + len, + offset: 0, + qarg, + carg, + }) +} diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index 552394e38a88..064159906f1b 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -15,7 +15,7 @@ mod circuit; mod error; mod expr; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::path::{Path, PathBuf}; use hashbrown::HashMap; @@ -27,49 +27,10 @@ use oq3_semantics::syntax_to_semantics::parse_source_string; use crate::error::QASM3ImporterError; -/// The inner parser at the moment can only read the include path from this environment variable in -/// the `PATH` conventions of the current operating system. -const INCLUDE_PATH_ENV_VAR: &str = "QASM3_PATH"; /// The name of a Python attribute to define on the given module where the default implementation /// of the ``stdgates.inc`` custom instructions is located. const STDGATES_INC_CUSTOM_GATES_ATTR: &str = "STDGATES_INC_GATES"; -/// Set the include path in the magic environment variable to the given structured include path. -/// Returns the previously value of the environment variable, if any, so it can be restored by -/// [restore_include_path]. -/// -/// The `module` should be the [PyModule] representing the `qiskit._qasm3` module itself. -fn set_include_path( - module: &PyModule, - include_path: Option<&[OsString]>, -) -> PyResult> { - let old_path = std::env::var_os(INCLUDE_PATH_ENV_VAR); - if let Some(includes) = include_path { - let path = std::env::join_paths(includes).map_err(|err| { - QASM3ImporterError::new_err(format!("failed to create an include path: {:?}", err)) - })?; - std::env::set_var(INCLUDE_PATH_ENV_VAR, path); - } else if old_path.is_none() { - // Our module should be in the root of the Qiskit package. - let lib_path = Path::new(module.filename()?) - .parent() - .unwrap() - .join(["qasm", "libs", "dummy"].iter().collect::()); - std::env::set_var(INCLUDE_PATH_ENV_VAR, lib_path); - } - Ok(old_path) -} - -/// Restore the state of the environment to the given value. If no value is given, the key is -/// removed from the environment (an explicitly empty path is different to a missing path). -fn restore_include_path(include_path: Option<&OsStr>) { - if let Some(old_path) = include_path { - std::env::set_var(INCLUDE_PATH_ENV_VAR, old_path); - } else { - std::env::remove_var(INCLUDE_PATH_ENV_VAR); - } -} - /// Load an OpenQASM 3 program from a string into a :class:`.QuantumCircuit`. /// /// Args: @@ -81,12 +42,6 @@ fn restore_include_path(include_path: Option<&OsStr>) { /// If not given, Qiskit will arrange for this to point to a location containing /// ``stdgates.inc`` only. Paths are tried in the sequence order. /// -/// As an implementation detail of the internal parser, if this argument is not given but -/// the environment variable ``QASM3_PATH`` is set, it will be used instead, and is -/// formatted using the ``PATH`` conventions of your operating system (e.g. earlier -/// directories are tried first, paths are separated by ``:`` on Unix-likes and ``;`` on -/// Windows, and so forth). -/// /// Returns: /// :class:`.QuantumCircuit`: the constructed circuit object. /// @@ -103,9 +58,15 @@ pub fn loads( custom_gates: Option>, include_path: Option>, ) -> PyResult { - let old_path = set_include_path(module, include_path.as_deref())?; - let result = parse_source_string(source, None); - restore_include_path(old_path.as_deref()); + let default_include_path = || -> PyResult> { + Ok(vec![Path::new(module.filename()?) + .parent() + .unwrap() + .join(["qasm", "libs", "dummy"].iter().collect::()) + .into_os_string()]) + }; + let include_path = include_path.map(Ok).unwrap_or_else(default_include_path)?; + let result = parse_source_string(source, None, Some(&include_path)); if result.any_errors() { result.print_errors(); return Err(QASM3ImporterError::new_err( @@ -144,12 +105,6 @@ pub fn loads( /// If not given, Qiskit will arrange for this to point to a location containing /// ``stdgates.inc`` only. Paths are tried in the sequence order. /// -/// As an implementation detail of the internal parser, if this argument is not given but -/// the environment variable ``QASM3_PATH`` is set, it will be used instead, and is -/// formatted using the ``PATH`` conventions of your operating system (e.g. earlier -/// directories are tried first, paths are separated by ``:`` on Unix-likes and ``;`` on -/// Windows, and so forth). -/// /// Returns: /// :class:`.QuantumCircuit`: the constructed circuit object. /// @@ -179,7 +134,7 @@ pub fn load( .getattr("fspath")? .call1((pathlike_or_filelike,))? .extract::()?; - std::fs::read_to_string(&path).map_err(|err| { + ::std::fs::read_to_string(&path).map_err(|err| { QASM3ImporterError::new_err(format!("failed to read file '{:?}': {:?}", &path, err)) })? }; From 3cc0e3ed5cc8c39d469529b33a263481839758da Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 24 Jan 2024 18:21:45 +0000 Subject: [PATCH 10/27] Fix Rust 1.75 code --- crates/qasm3/src/build.rs | 2 +- crates/qasm3/src/circuit.rs | 11 ++++++++--- crates/qasm3/src/expr.rs | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 4840149cc698..cb265372069a 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -189,7 +189,7 @@ impl BuilderState { // We want any deterministic order for easier circuit reproducibility in Python space, // and to include each seen qubit once. This simply maintains insertion order. let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py>::with_capacity( - asg_qubits.len() + asg_qubits.len(), ); for qarg in asg_qubits.iter() { let qarg = expr::expect_gate_operand(qarg)?; diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index d1e99c637cfd..1fd95b06c617 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -17,7 +17,12 @@ use crate::error::QASM3ImporterError; pub trait PyRegister { fn bit(&self, py: Python, index: usize) -> PyResult>; - fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; + // This really should be + // fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; + // or at a minimum + // fn iter<'a>(&'a self, py: Python<'a>) -> ::pyo3::types::iter::PyListIterator<'a>; + // but we can't use the former before Rust 1.75 and the latter before PyO3 0.21. + fn bit_list<'a>(&'a self, py: Python<'a>) -> &'a PyList; } macro_rules! register_type { @@ -43,8 +48,8 @@ macro_rules! register_type { .map(|item| item.into_py(py)) } - fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator { - self.items.as_ref(py).iter() + fn bit_list<'a>(&'a self, py: Python<'a>) -> &'a PyList { + self.items.as_ref(py) } } diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 5ad2fc34e0ec..6493065a113f 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -171,7 +171,7 @@ fn broadcast_bits_for_identifier( Ok(BroadcastItem::Bit(bit.clone())) } else if let Some(reg) = registers.get(iden_symbol) { Ok(BroadcastItem::Register( - reg.iter(py).map(|obj| obj.into_py(py)).collect(), + reg.bit_list(py).iter().map(|obj| obj.into_py(py)).collect(), )) } else { Err(QASM3ImporterError::new_err(format!( From b6648c0ae9eb3b06571ba514f90a18a4b3df99d9 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Sun, 28 Jan 2024 19:30:12 -0500 Subject: [PATCH 11/27] Depend on released version of oq3_seamntics Change crates/qasm3/Cargo.toml to depend on a released version of the parser. that is the crate oq3_semantics, rather than a commit of the github repo. All of the commits added since the commit previously specified in the dependency are either updating the README or tweaking the github actions. --- Cargo.lock | 25 +++++++++++++++---------- crates/qasm3/Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88f3fd44c284..35b1a0aa463b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,8 +414,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" -version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c1e1b6015360f999d2f44310df253bbe4bb757f8e4212976f6ccac32594829" dependencies = [ "unicode-properties", "unicode-xid", @@ -423,8 +424,9 @@ dependencies = [ [[package]] name = "oq3_parser" -version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000b3f22a52bb46f7c7a27c3aad0602a337bf21c6e51ff18c4c932ec50e62d61" dependencies = [ "drop_bomb", "oq3_lexer", @@ -433,8 +435,9 @@ dependencies = [ [[package]] name = "oq3_semantics" -version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7615d2eac8cca6e75ae7e647ebadf2fee5372f300453a9c789b67f5fa1d91495" dependencies = [ "boolenum", "hashbrown 0.14.3", @@ -445,8 +448,9 @@ dependencies = [ [[package]] name = "oq3_source_file" -version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffda2ed90e88699e2178f2495141a97cc84f6c9fd0769ca43acb8cceb79022b" dependencies = [ "ariadne", "oq3_syntax", @@ -454,8 +458,9 @@ dependencies = [ [[package]] name = "oq3_syntax" -version = "0.0.1" -source = "git+https://github.com/Qiskit/openqasm3_parser.git?rev=4b5110d#4b5110db0232d3330aa5a9c1a39c95feba41b18e" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db850066b59357bc2355afd6644aa7d64d6f7e22a90ea59e5a4e5560b410d25e" dependencies = [ "cov-mark", "either", diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index deab924fcc84..6deaf75838fa 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -18,4 +18,4 @@ extension-module = ["pyo3/extension-module"] pyo3.workspace = true indexmap.version = "2.1.0" hashbrown.version = "0.14.0" -oq3_semantics = { git = "https://github.com/Qiskit/openqasm3_parser.git", rev = "4b5110d" } +oq3_semantics = "0.0.5" From 66e7d6084bc27aa7e7e9ec95d266fe1d428c6403 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 Jan 2024 18:28:17 +0000 Subject: [PATCH 12/27] Add tests of interface successes --- test/python/qasm3/test_import.py | 259 +++++++++++++++++++++++++++++-- 1 file changed, 248 insertions(+), 11 deletions(-) diff --git a/test/python/qasm3/test_import.py b/test/python/qasm3/test_import.py index dcc0ba3f823e..2dae71e2567e 100644 --- a/test/python/qasm3/test_import.py +++ b/test/python/qasm3/test_import.py @@ -20,24 +20,24 @@ import os import tempfile import unittest +import warnings from qiskit import qasm3 -from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.exceptions import ExperimentalWarning +from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Qubit, Clbit +from qiskit.circuit import library as lib from qiskit.test import QiskitTestCase from qiskit.utils import optionals -class TestQASM3Import(QiskitTestCase): - @unittest.skipUnless( - optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" - ) +@unittest.skipUnless(optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports") +class TestOldQASM3Import(QiskitTestCase): + # These tests are for the `qiskit-qasm3-import` hooks, not the native one. + def test_import_errors_converted(self): with self.assertRaises(qasm3.QASM3ImporterError): qasm3.loads("OPENQASM 3.0; qubit[2.5] q;") - @unittest.skipUnless( - optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" - ) def test_loads_can_succeed(self): program = """ OPENQASM 3.0; @@ -57,9 +57,6 @@ def test_loads_can_succeed(self): expected.measure(1, 1) self.assertEqual(parsed, expected) - @unittest.skipUnless( - optionals.HAS_QASM3_IMPORT, "need qiskit-qasm3-import for OpenQASM 3 imports" - ) def test_load_can_succeed(self): program = """ OPENQASM 3.0; @@ -82,3 +79,243 @@ def test_load_can_succeed(self): expected.measure(0, 0) expected.measure(1, 1) self.assertEqual(parsed, expected) + + +class TestQASM3Import(QiskitTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._cm = warnings.catch_warnings() + cls._cm.__enter__() + # We're knowingly testing the experimental code. + warnings.filterwarnings("ignore", category=ExperimentalWarning, module="qiskit.qasm3") + + @classmethod + def tearDownClass(cls): + cls._cm.__exit__(None, None, None) + super().tearDownClass() + + def test_load_can_succeed(self): + """Basic test of `load` - everything else we'll do via `loads` because it's easier.""" + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] qr; + bit[2] cr; + h qr[0]; + cx qr[0], qr[1]; + cr[0] = measure qr[0]; + cr[1] = measure qr[1]; + """ + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = os.path.join(tmp_dir, "bell.qasm") + with open(tmp_path, "w") as fptr: + fptr.write(program) + parsed = qasm3.load_experimental(tmp_path) + expected = QuantumCircuit(QuantumRegister(2, "qr"), ClassicalRegister(2, "cr")) + expected.h(0) + expected.cx(0, 1) + expected.measure(0, 0) + expected.measure(1, 1) + self.assertEqual(parsed, expected) + + def test_simple_loose_bits(self): + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit q0; + qubit q1; + bit c0; + bit c1; + + h q0; + cx q0, q1; + c0 = measure q0; + c1 = measure q1; + """ + parsed = qasm3.loads_experimental(program) + expected = QuantumCircuit([Qubit(), Qubit(), Clbit(), Clbit()]) + expected.h(0) + expected.cx(0, 1) + expected.measure(0, 0) + expected.measure(1, 1) + self.assertEqual(parsed, expected) + + def test_all_stdlib_gates(self): + # Notably this doesn't include negative floating-point values yet. Can be fixed after: + # https://github.com/Qiskit/openqasm3_parser/issues/81 + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + p(0.5) q[0]; + x q[0]; + y q[0]; + z q[0]; + h q[0]; + s q[0]; + sdg q[0]; + t q[0]; + tdg q[0]; + sx q[0]; + rx(0.5) q[0]; + ry(0.5) q[0]; + rz(0.5) q[0]; + cx q[0], q[1]; + cy q[0], q[1]; + cz q[0], q[1]; + cp(1.5) q[0], q[1]; + crx(0.25) q[0], q[1]; + cry(0.75) q[0], q[1]; + crz(0.5) q[0], q[1]; + ch q[0], q[1]; + swap q[0], q[1]; + ccx q[0], q[1], q[2]; + cswap q[0], q[1], q[2]; + cu(0.25, 0.5, 0.75, 1.0) q[0], q[1]; + CX q[0], q[1]; + phase(0.5) q[0]; + cphase(0.5) q[0], q[1]; + id q[0]; + u1(0.5) q[0]; + u2(0.25, 0.5) q[0]; + u3(0.25, 0.5, 0.75) q[0]; + """ + parsed = qasm3.loads_experimental(program) + expected = QuantumCircuit(QuantumRegister(3, "q")) + expected.p(0.5, 0) + expected.x(0) + expected.y(0) + expected.z(0) + expected.h(0) + expected.s(0) + expected.sdg(0) + expected.t(0) + expected.tdg(0) + expected.sx(0) + expected.rx(0.5, 0) + expected.ry(0.5, 0) + expected.rz(0.5, 0) + expected.cx(0, 1) + expected.cy(0, 1) + expected.cz(0, 1) + expected.cp(1.5, 0, 1) + expected.crx(0.25, 0, 1) + expected.cry(0.75, 0, 1) + expected.crz(0.5, 0, 1) + expected.ch(0, 1) + expected.swap(0, 1) + expected.ccx(0, 1, 2) + expected.cswap(0, 1, 2) + expected.cu(0.25, 0.5, 0.75, 1, 0, 1) + expected.cx(0, 1) + expected.p(0.5, 0) + expected.cp(0.5, 0, 1) + expected.id(0) + expected.append(lib.U1Gate(0.5), [0], []) + expected.append(lib.U2Gate(0.25, 0.5), [0], []) + expected.append(lib.U3Gate(0.25, 0.5, 0.75), [0], []) + self.assertEqual(parsed, expected) + + def test_barrier(self): + program = """ + OPENQASM 3.0; + qubit[2] a; + qubit b; + qubit[5] c; + barrier b; + barrier a; + barrier a, b; + barrier a[0], c[2]; + barrier c[{3, 1, 4}][{2, 0, 1}][0], b; + """ + parsed = qasm3.loads_experimental(program) + a, b, c = QuantumRegister(2, "a"), Qubit(), QuantumRegister(5, "c") + expected = QuantumCircuit(a, [b], c) + expected.barrier(b) + expected.barrier(a) + expected.barrier(a, b) + expected.barrier(a[0], c[2]) + expected.barrier(c[4], b) + self.assertEqual(parsed, expected) + + def test_measure(self): + program = """ + OPENQASM 3.0; + qubit[2] q0; + qubit q1; + qubit[5] q2; + bit[2] c0; + bit c1; + bit[5] c2; + c0 = measure q0; + c0[0] = measure q0[0]; + c1 = measure q1; + c2[{3, 1, 4}] = measure q2[{4, 2, 3}]; + """ + parsed = qasm3.loads_experimental(program) + q0, q1, q2 = QuantumRegister(2, "q0"), Qubit(), QuantumRegister(5, "q2") + c0, c1, c2 = ClassicalRegister(2, "c0"), Clbit(), ClassicalRegister(5, "c2") + expected = QuantumCircuit(q0, [q1], q2, c0, [c1], c2) + expected.measure(q0, c0) + expected.measure(q0[0], c0[0]) + expected.measure(q1, c1) + expected.measure(q2[[4, 2, 3]], c2[[3, 1, 4]]) + self.assertEqual(parsed, expected) + + def test_override_custom_gate_parameterless(self): + program = """ + OPENQASM 3.0; + gate my_gate a, b {} + qubit[2] q; + my_gate q[0], q[1]; + my_gate q[1], q[0]; + """ + parsed = qasm3.loads_experimental( + program, custom_gates=[qasm3.CustomGate(lib.CXGate, "my_gate", 0, 2)] + ) + expected = QuantumCircuit(QuantumRegister(2, "q")) + expected.cx(0, 1) + expected.cx(1, 0) + self.assertEqual(parsed, expected) + + def test_override_custom_gate_parametric(self): + program = """ + OPENQASM 3.0; + gate my_crx(ang) a, b {} + qubit[2] q; + my_crx(0.5) q[0], q[1]; + my_crx(1.5) q[1], q[0]; + """ + parsed = qasm3.loads_experimental( + program, custom_gates=[qasm3.CustomGate(lib.CRXGate, "my_crx", 1, 2)] + ) + expected = QuantumCircuit(QuantumRegister(2, "q")) + expected.crx(0.5, 0, 1) + expected.crx(1.5, 1, 0) + self.assertEqual(parsed, expected) + + def test_set_include_path(self): + include = """ + gate my_gate a {} + """ + program = """ + OPENQASM 3.0; + include "my_include.qasm"; + qubit q0; + my_gate q0; + """ + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = os.path.join(tmp_dir, "my_include.qasm") + with open(tmp_path, "w") as fptr: + fptr.write(include) + # Can't test for failed import yet due to: + # https://github.com/Qiskit/openqasm3_parser/issues/74 + parsed = qasm3.loads_experimental( + program, + custom_gates=[qasm3.CustomGate(lib.XGate, "my_gate", 0, 1)], + include_path=[tmp_dir], + ) + expected = QuantumCircuit([Qubit()]) + expected.x(0) + self.assertEqual(parsed, expected) From 4e47a2cc8c9c9c88d2ec5ca9b8d05d6c1211690d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 00:59:25 +0000 Subject: [PATCH 13/27] Add dummy library to manifest --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 3e6aa7001c87..eaf3e6576b91 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include LICENSE.txt include requirements.txt -include qiskit/qasm/libs/*.inc +recursive-include qiskit/qasm/libs *.inc include qiskit/VERSION.txt include qiskit/visualization/circuit/styles/*.json recursive-include qiskit/providers/fake_provider/backends *.json From 7ea26007890c313b520b82446feac64aebb9395d Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Wed, 31 Jan 2024 12:00:23 -0500 Subject: [PATCH 14/27] Depend on lastest version, 0.0.7, of openqasm3_parser crates This commit does two things * Change the dependency on oq3_semantcis from 0.0.5 to 0.0.7 * Change in build.rs to reflect chang in API for gate modifiers from `gatecall.modifier()` returning `Option` to `gatecall.modifiers()` returning `&[GateModifier]` --- Cargo.lock | 20 ++++++++++---------- crates/qasm3/Cargo.toml | 2 +- crates/qasm3/src/build.rs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 629fff88665c..94e417acbd7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,9 +861,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c1e1b6015360f999d2f44310df253bbe4bb757f8e4212976f6ccac32594829" +checksum = "c4e867d2797100b8068715e26566a5567c598424d7eddf7118c6b38bc3b15633" dependencies = [ "unicode-properties", "unicode-xid", @@ -871,9 +871,9 @@ dependencies = [ [[package]] name = "oq3_parser" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000b3f22a52bb46f7c7a27c3aad0602a337bf21c6e51ff18c4c932ec50e62d61" +checksum = "cf260dea71b56b405d091d476748c1f9b0a4d22b4ec9af416e002e2df25613f9" dependencies = [ "drop_bomb", "oq3_lexer", @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "oq3_semantics" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7615d2eac8cca6e75ae7e647ebadf2fee5372f300453a9c789b67f5fa1d91495" +checksum = "d5ba220b91ff849190d53b296711774f761b7e06744b16a9c8f19fc2fb37de47" dependencies = [ "boolenum", "hashbrown 0.14.3", @@ -895,9 +895,9 @@ dependencies = [ [[package]] name = "oq3_source_file" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffda2ed90e88699e2178f2495141a97cc84f6c9fd0769ca43acb8cceb79022b" +checksum = "83a81fd0c1c100ad8d7a23711c897791d693c3f5b1f3d044cb8c5770766f819c" dependencies = [ "ariadne", "oq3_syntax", @@ -905,9 +905,9 @@ dependencies = [ [[package]] name = "oq3_syntax" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db850066b59357bc2355afd6644aa7d64d6f7e22a90ea59e5a4e5560b410d25e" +checksum = "c7da2ef9a591d77eee43e972e79fc95c218545e5e79b93738d20479d8d7627ec" dependencies = [ "cov-mark", "either", diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 6deaf75838fa..89a25bb01591 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -18,4 +18,4 @@ extension-module = ["pyo3/extension-module"] pyo3.workspace = true indexmap.version = "2.1.0" hashbrown.version = "0.14.0" -oq3_semantics = "0.0.5" +oq3_semantics = "0.0.7" diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index cb265372069a..6453fcf85a72 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -131,7 +131,7 @@ impl BuilderState { ast_symbols: &SymbolTable, call: &asg::GateCall, ) -> PyResult<()> { - if call.modifier().is_some() { + if call.modifiers().len() > 0 { return Err(QASM3ImporterError::new_err("gate modifiers not handled")); } let gate_id = call From acb84d5ef320e863c7b9e9d7df09e93e965706f4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 17:39:53 +0000 Subject: [PATCH 15/27] Expand on readme --- crates/qasm3/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/qasm3/README.md b/crates/qasm3/README.md index f8dc1623021e..071b5aa89b03 100644 --- a/crates/qasm3/README.md +++ b/crates/qasm3/README.md @@ -1,4 +1,7 @@ # `qiskit._qasm3` -This crate is the Rust-level Qiskit interface to an OpenQASM 3 parser. The parser itself does not know -about Qiskit, and this crate interfaces with it in a Qiskit-specific manner to produce `QuantumCircuit`s. +This crate is the Rust-level Qiskit interface to [a separately managed OpenQASM 3 +parser](https://github.com/Qiskit/openqasm3_parser). In order to maintain a sensible separation of +concerns, and because we hope to expand the use of that parser outside Qiskit, the parsing side does +not depend on Qiskit, and this crate interfaces with it in a Qiskit-specific manner to produce +`QuantumCircuit`s. From 9f4e34241612a47cfb36e8a5ebf4be6a4765845e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 17:45:07 +0000 Subject: [PATCH 16/27] Centralise dependencies --- Cargo.lock | 4 ++-- Cargo.toml | 7 +++++++ crates/accelerate/Cargo.toml | 4 ++-- crates/qasm2/Cargo.toml | 2 +- crates/qasm3/Cargo.toml | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94e417acbd7e..4485a1da9e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "getrandom", diff --git a/Cargo.toml b/Cargo.toml index 2693c5447bbf..3200617ea109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,14 @@ edition = "2021" rust-version = "1.70" # Keep in sync with README.md and rust-toolchain.toml. license = "Apache-2.0" +# Shared dependencies that can be inherited. This just helps a little with +# making sure our crates don't directly depend on different versions of things, +# although we can't help it if our transitive dependencies pull in more. +# +# Each crate can add on specific features freely as it inherits. [workspace.dependencies] +indexmap.version = "2.2.1" +hashbrown.version = "0.14.0" # This doesn't set `extension-module` as a shared feature because we need to be able to disable it # during Rust-only testing (see # https://github.com/PyO3/pyo3/issues/340). pyo3 = { version = "0.20.2", features = ["abi3-py38"] } diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 82231dd5f0a8..de3d4d143108 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -39,11 +39,11 @@ version = "^0.15.6" features = ["rayon"] [dependencies.hashbrown] -version = "0.14.0" +workspace = true features = ["rayon"] [dependencies.indexmap] -version = "2.2.1" +workspace = true features = ["rayon"] [dependencies.faer] diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 197f70bcf5c8..bbeb057f20a4 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -15,5 +15,5 @@ default = ["extension-module"] extension-module = ["pyo3/extension-module"] [dependencies] -hashbrown = "0.14.0" +hashbrown.workspace = true pyo3.workspace = true diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 89a25bb01591..c48d0e9fa378 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -16,6 +16,6 @@ extension-module = ["pyo3/extension-module"] [dependencies] pyo3.workspace = true -indexmap.version = "2.1.0" -hashbrown.version = "0.14.0" +indexmap.workspace = true +hashbrown.workspace = true oq3_semantics = "0.0.7" From a2d2c4f3ec6da135319a803e78c736c8f5c5ba91 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 18:19:24 +0000 Subject: [PATCH 17/27] Fix typing and error typing --- crates/qasm3/src/build.rs | 40 ++++++++++++++++++++++++------------- crates/qasm3/src/circuit.rs | 2 +- crates/qasm3/src/expr.rs | 4 ++-- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 6453fcf85a72..67c6b36583e7 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -61,7 +61,7 @@ impl BuilderState { let name_id = decl .name() .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; let name_symbol = &ast_symbols[name_id]; match name_symbol.symbol_type() { Type::Bit(is_const) => { @@ -88,7 +88,7 @@ impl BuilderState { self.add_creg(py, name_id.clone(), name_symbol.name(), *size) } _ => Err(QASM3ImporterError::new_err( - "cannot handle quantum registers with more than one dimension", + "cannot handle classical registers with more than one dimension", )), } } @@ -109,7 +109,7 @@ impl BuilderState { let name_id = decl .name() .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; let name_symbol = &ast_symbols[name_id]; match name_symbol.symbol_type() { Type::Qubit => self.add_qubit(py, name_id.clone()), @@ -137,9 +137,9 @@ impl BuilderState { let gate_id = call .name() .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; let gate = self.symbols.gates.get(gate_id).ok_or_else(|| { - QASM3ImporterError::new_err(format!("internal logic error: unknown gate {:?}", gate_id)) + QASM3ImporterError::new_err(format!("internal error: unknown gate {:?}", gate_id)) })?; let params = PyTuple::new( py, @@ -233,7 +233,7 @@ impl BuilderState { let name_id = decl .name() .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error {:?}", err)))?; + .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; let name_symbol = &ast_symbols[name_id]; let pygate = self.pygates.get(name_symbol.name()).ok_or_else(|| { QASM3ImporterError::new_err(format!( @@ -301,9 +301,12 @@ impl BuilderState { .insert(ast_symbol, qubit.clone_ref(py)) .is_some() { - panic!("internal logic error: attempted to add the same qubit multiple times") + Err(QASM3ImporterError::new_err( + "internal error: attempted to add the same qubit multiple times", + )) + } else { + self.qc.add_qubit(py, qubit) } - self.qc.add_qubit(py, qubit) } fn add_clbit(&mut self, py: Python, ast_symbol: SymbolId) -> PyResult<()> { @@ -314,9 +317,12 @@ impl BuilderState { .insert(ast_symbol, clbit.clone_ref(py)) .is_some() { - panic!("internal logic error: attempted to add the same clbit multiple times") + Err(QASM3ImporterError::new_err( + "internal error: attempted to add the same clbit multiple times", + )) + } else { + self.qc.add_clbit(py, clbit) } - self.qc.add_clbit(py, clbit) } fn add_qreg>>( @@ -329,9 +335,12 @@ impl BuilderState { let qreg = self.module.new_qreg(py, name, size)?; self.qc.add_qreg(py, &qreg)?; if self.symbols.qregs.insert(ast_symbol, qreg).is_some() { - panic!("internal logic error: attempted to add the same register multiple times") + Err(QASM3ImporterError::new_err( + "internal error: attempted to add the same register multiple times", + )) + } else { + Ok(()) } - Ok(()) } fn add_creg>>( @@ -344,9 +353,12 @@ impl BuilderState { let creg = self.module.new_creg(py, name, size)?; self.qc.add_creg(py, &creg)?; if self.symbols.cregs.insert(ast_symbol, creg).is_some() { - panic!("internal logic error: attempted to add the same register multiple times") + Err(QASM3ImporterError::new_err( + "internal error: attempted to add the same register multiple times", + )) + } else { + Ok(()) } - Ok(()) } } diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 1fd95b06c617..e00b35c1ad1d 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -111,7 +111,7 @@ impl PyGate { self.constructor.call1(py, args.as_ref(py)) } else { Err(QASM3ImporterError::new_err(format!( - "internal logic error: wrong number of params for {} (got {}, expected {})", + "internal error: wrong number of params for {} (got {}, expected {})", &self.name, received_num_params, self.num_params ))) } diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 6493065a113f..482563f839f0 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -273,7 +273,7 @@ pub fn eval_measure_carg( match carg { asg::LValue::Identifier(iden) => { let symbol_id = iden.as_ref().map_err(|err| { - QASM3ImporterError::new_err(format!("internal logic error: {:?}", err)) + QASM3ImporterError::new_err(format!("internal error: {:?}", err)) })?; broadcast_bits_for_identifier(py, &our_symbols.clbits, &our_symbols.cregs, symbol_id) } @@ -307,7 +307,7 @@ pub fn expect_gate_operand(expr: &asg::TExpr) -> PyResult<&asg::GateOperand> { match expr.expression() { asg::Expr::GateOperand(operand) => Ok(operand), expr => Err(QASM3ImporterError::new_err(format!( - "internal logic error: not a gate operand {:?}", + "internal error: not a gate operand {:?}", expr ))), } From eb474fad2f2db5e2a112579a0f23c9b939f6cab5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 18:19:52 +0000 Subject: [PATCH 18/27] Credit Matt Co-authored-by: Matthew Treinish From 5b8398ab131f041dcf5b3a2c28c93ecc699bc721 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 18:21:11 +0000 Subject: [PATCH 19/27] Fix clippy --- crates/qasm3/src/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 67c6b36583e7..b3829ceb0b88 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -131,8 +131,8 @@ impl BuilderState { ast_symbols: &SymbolTable, call: &asg::GateCall, ) -> PyResult<()> { - if call.modifiers().len() > 0 { - return Err(QASM3ImporterError::new_err("gate modifiers not handled")); + if !call.modifiers().is_empty() { + return Err(QASM3ImporterError::new_err("gate modifiers not currently handled")); } let gate_id = call .name() From 3345c34f22c9da02cc7494dad44ab7c291d6c557 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 18:21:48 +0000 Subject: [PATCH 20/27] Clone using GIL --- crates/qasm3/src/circuit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index e00b35c1ad1d..a2c39e754023 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -60,11 +60,11 @@ macro_rules! register_type { } impl ::pyo3::ToPyObject for $name { - fn to_object(&self, _py: Python) -> Py { + fn to_object(&self, py: Python) -> Py { // _Technically_, allowing access this internal object can let the Rust-space // wrapper get out-of-sync since we keep a direct handle to the list, but in // practice, the field it's viewing is private and "inaccessible" from Python. - self.object.clone() + self.object.clone_ref(py) } } }; From 9bd6476f331ea8a8f737d121e8ba64fafd591cf4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 19:56:01 +0000 Subject: [PATCH 21/27] Format --- crates/qasm3/src/build.rs | 4 +++- crates/qasm3/src/expr.rs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index b3829ceb0b88..cc80aa4f762d 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -132,7 +132,9 @@ impl BuilderState { call: &asg::GateCall, ) -> PyResult<()> { if !call.modifiers().is_empty() { - return Err(QASM3ImporterError::new_err("gate modifiers not currently handled")); + return Err(QASM3ImporterError::new_err( + "gate modifiers not currently handled", + )); } let gate_id = call .name() diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 482563f839f0..aec392a4b783 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -272,9 +272,9 @@ pub fn eval_measure_carg( ) -> PyResult { match carg { asg::LValue::Identifier(iden) => { - let symbol_id = iden.as_ref().map_err(|err| { - QASM3ImporterError::new_err(format!("internal error: {:?}", err)) - })?; + let symbol_id = iden + .as_ref() + .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; broadcast_bits_for_identifier(py, &our_symbols.clbits, &our_symbols.cregs, symbol_id) } asg::LValue::IndexedIdentifier(indexed) => { From fcc79dfea2829d5ad9d6c618f49d99e6576896b3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 21:25:14 +0000 Subject: [PATCH 22/27] Improve error for unexpected angle --- crates/qasm3/src/expr.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index aec392a4b783..8fc4c8a8a534 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -54,6 +54,9 @@ pub fn eval_gate_param( ))) } } + Type::Angle(_, _) => Err(QASM3ImporterError::new_err( + "the OpenQASM 3 'angle' type is not yet supported", + )), ty => Err(QASM3ImporterError::new_err(format!( "expected an angle-like type, but saw {:?}", ty From 6712183a8330e650d016de41558fe08a9c9a3404 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 21:32:04 +0000 Subject: [PATCH 23/27] Add test of gate broadcasting --- test/python/qasm3/test_import.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/python/qasm3/test_import.py b/test/python/qasm3/test_import.py index 134e030760ba..85da3f41933e 100644 --- a/test/python/qasm3/test_import.py +++ b/test/python/qasm3/test_import.py @@ -319,3 +319,35 @@ def test_set_include_path(self): expected = QuantumCircuit([Qubit()]) expected.x(0) self.assertEqual(parsed, expected) + + def test_gate_broadcast(self): + program = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q0; + qubit q1; + qubit[2] q2; + h q0; + cx q0, q1; + cx q0[0], q2; + cx q0, q2; + ccx q0[{1, 0}], q1, q2; + """ + parsed = qasm3.loads_experimental(program) + q0, q1, q2 = QuantumRegister(2, "q0"), Qubit(), QuantumRegister(2, "q2") + expected = QuantumCircuit(q0, [q1], q2) + expected.h(q0[0]) + expected.h(q0[1]) + # + expected.cx(q0[0], q1) + expected.cx(q0[1], q1) + # + expected.cx(q0[0], q2[0]) + expected.cx(q0[0], q2[1]) + # + expected.cx(q0[0], q2[0]) + expected.cx(q0[1], q2[1]) + # + expected.ccx(q0[1], q1, q2[0]) + expected.ccx(q0[0], q1, q2[1]) + self.assertEqual(parsed, expected) From 6e729bfdd77937ddee89076aae0649594e3fdd7a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 21:36:26 +0000 Subject: [PATCH 24/27] Add additional warnings about experimental interface --- crates/qasm3/src/lib.rs | 12 ++++++++++++ qiskit/qasm3/__init__.py | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index 064159906f1b..0db8317ba6a1 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -33,6 +33,12 @@ const STDGATES_INC_CUSTOM_GATES_ATTR: &str = "STDGATES_INC_GATES"; /// Load an OpenQASM 3 program from a string into a :class:`.QuantumCircuit`. /// +/// .. warning:: +/// +/// This native version of the OpenQASM 3 importer is currently experimental. It is typically +/// much faster than :func:`~qiskit.qasm3.loads`, but has a reduced supported feature set, +/// which will expand over time. +/// /// Args: /// source (str): the program source in a Python string. /// custom_gates (Iterable[CustomGate]): Python constructors to use for particular named gates. @@ -92,6 +98,12 @@ pub fn loads( /// Load an OpenQASM 3 program from a source file into a :class:`.QuantumCircuit`. /// +/// .. warning:: +/// +/// This native version of the OpenQASM 3 importer is currently experimental. It is typically +/// much faster than :func:`~qiskit.qasm3.load`, but has a reduced supported feature set, which +/// will expand over time. +/// /// Args: /// pathlike_or_filelike (str | os.PathLike | io.TextIOBase): the program source. This can /// either be given as a filepath, or an open text stream object. If the stream is already diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 63520af6ccf6..d66977821a4d 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -192,7 +192,11 @@ .. autofunction:: load_experimental .. autofunction:: loads_experimental -These two functions both raise :exc:`.ExperimentalWarning`; you can disable this warning by doing:: +These two functions are both experimental, meaning they issue an :exc:`.ExperimentalWarning` on +usage, and their interfaces may be subject to change within the Qiskit 1.0. In particular, the +native parser may be promoted to be the default version of :func:`load` and :func:`loads`. If you +are happy to accept the risk of using the experimental interface, you can disable the warning by +doing:: import warnings from qiskit.exceptions import ExperimentalWarning From c64c8c11ea4be405e7b67de477d63ac577004d68 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 22:13:05 +0000 Subject: [PATCH 25/27] Add release note --- .../qasm3-importer-0ba0691bcdeba72f.yaml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 releasenotes/notes/qasm3-importer-0ba0691bcdeba72f.yaml diff --git a/releasenotes/notes/qasm3-importer-0ba0691bcdeba72f.yaml b/releasenotes/notes/qasm3-importer-0ba0691bcdeba72f.yaml new file mode 100644 index 000000000000..5cb9ab2c8c03 --- /dev/null +++ b/releasenotes/notes/qasm3-importer-0ba0691bcdeba72f.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + The :mod:`qiskit.qasm3` package now contains a built-in, Rust-based parser for reading OpenQASM + 3 programs into :class:`.QuantumCircuit`\ s, found at :func:`qiskit.qasm3.load_experimental` and + :func:`~.qasm3.loads_experimental`. These are typically several times faster than the existing, + pure Python :func:`~.qasm3.load` and :func:`~.qasm3.loads` functions, which additionally require + ``qiskit-qasm3-import`` to be installed. + + For example, we can create a 20,000-instruction entangling :class:`.QuantumCircuit`:: + + import numpy as np + import qiskit.qasm3 + from qiskit.circuit.library import RealAmplitudes + + qc = RealAmplitudes(100, reps=100, flatten=True) + qc = qc.assign_parameters(np.random.rand(qc.num_parameters)) + oq3 = qiskit.qasm3.dumps(qc) + + The old :func:`.qasm3.loads` took about 7.3s to load the resulting OpenQASM 3 program, whereas + :func:`.qasm3.loads_experimental` took under 300ms on a consumer Macbook Pro (i7, 2020)–a speedup + of 25x! + + The supported feature set of the experimental parser is very limited in this preview version, + but this will expand as both the Qiskit side and `the native Rust-based parser + `__ improve. + + One of our main goals with this new parser, alongside the huge speed improvements, is to provide + top-quality error diagnostics. As with other parts of the parser, these are a work in progress, + but you'll start to see much higher quality error messages displayed when parsing invalid + OpenQASM 3 programs with the experimental parser. From fd80ea1b6c8c62ea3a8f68c2ce66486e5265ef48 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 31 Jan 2024 22:15:45 +0000 Subject: [PATCH 26/27] Add comment about diagnostics in docs --- qiskit/qasm3/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index d66977821a4d..453f08dda9af 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -184,7 +184,8 @@ Qiskit is developing a native parser, written in Rust, which is available as part of the core Qiskit package. This parser is still in its early experimental stages, so is missing features and its interface is changing and expanding, but it is typically orders of magnitude more performant for the -subset of OpenQASM 3 it currently supports. +subset of OpenQASM 3 it currently supports, and its internals produce better error diagnostics on +parsing failures. You can use the experimental interface immediately, with similar functions to the main interface above: From f37f279e0ad88e5fb81c5febbbd1b46470d30b64 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 1 Feb 2024 00:30:12 +0000 Subject: [PATCH 27/27] Correct release series comment --- qiskit/qasm3/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 453f08dda9af..b50dd21a49ae 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -194,10 +194,10 @@ .. autofunction:: loads_experimental These two functions are both experimental, meaning they issue an :exc:`.ExperimentalWarning` on -usage, and their interfaces may be subject to change within the Qiskit 1.0. In particular, the -native parser may be promoted to be the default version of :func:`load` and :func:`loads`. If you -are happy to accept the risk of using the experimental interface, you can disable the warning by -doing:: +usage, and their interfaces may be subject to change within the Qiskit 1.x release series. In +particular, the native parser may be promoted to be the default version of :func:`load` and +:func:`loads`. If you are happy to accept the risk of using the experimental interface, you can +disable the warning by doing:: import warnings from qiskit.exceptions import ExperimentalWarning