diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index a641ae215efd..2be97d1d30ad 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -45,17 +45,6 @@ jobs: path: .stestr displayName: "Cache stestr" - - ${{ if eq(parameters.testRust, true) }}: - # We need to avoid linking our crates into full Python extension libraries during Rust-only - # testing because Rust/PyO3 can't handle finding a static CPython interpreter. - - bash: cargo test --no-default-features - env: - # On Linux we link against `libpython` dynamically, but it isn't written into the rpath - # of the test executable (I'm not 100% sure why ---Jake). It's easiest just to forcibly - # include the correct place in the `dlopen` search path. - LD_LIBRARY_PATH: '$(usePython.pythonLocation)/lib:$LD_LIBRARY_PATH' - displayName: "Run Rust tests" - - bash: | set -e python -m pip install --upgrade pip setuptools wheel virtualenv @@ -107,6 +96,22 @@ jobs: sudo apt-get install -y graphviz displayName: 'Install optional non-Python dependencies' + # Note that we explicitly use the virtual env with Qiskit installed to run the Rust + # tests since some of them still depend on Qiskit's Python API via PyO3. + - ${{ if eq(parameters.testRust, true) }}: + # We need to avoid linking our crates into full Python extension libraries during Rust-only + # testing because Rust/PyO3 can't handle finding a static CPython interpreter. + - bash: | + source test-job/bin/activate + python tools/report_numpy_state.py + PYTHONUSERBASE="$VIRTUAL_ENV" cargo test --no-default-features + env: + # On Linux we link against `libpython` dynamically, but it isn't written into the rpath + # of the test executable (I'm not 100% sure why ---Jake). It's easiest just to forcibly + # include the correct place in the `dlopen` search path. + LD_LIBRARY_PATH: '$(usePython.pythonLocation)/lib:$LD_LIBRARY_PATH' + displayName: "Run Rust tests" + - bash: | set -e source test-job/bin/activate diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a9c1839487a3..4935a2739145 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -71,7 +71,7 @@ jobs: lcov --add-tracefile python.info --add-tracefile rust.info --output-file coveralls.info - name: Coveralls - uses: coverallsapp/github-action@v2.3.4 + uses: coverallsapp/github-action@v2.3.6 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coveralls.info diff --git a/.github/workflows/wheels-build.yml b/.github/workflows/wheels-build.yml index a7453eb05568..cd9616fbbbf0 100644 --- a/.github/workflows/wheels-build.yml +++ b/.github/workflows/wheels-build.yml @@ -27,15 +27,6 @@ on: default: "default" required: false - wheels-32bit: - description: >- - The action to take for Tier 1 wheels. - Choose from 'default', 'build' or 'skip'. - This builds multiple artifacts, which all match 'wheels-32bit-*'. - type: string - default: "default" - required: false - wheels-linux-s390x: description: >- The action to take for Linux s390x wheels. @@ -69,7 +60,7 @@ on: python-version: description: "The Python version to use to host the build runner." type: string - default: "3.10" + default: "3.13" required: false pgo: @@ -90,7 +81,7 @@ jobs: os: - ubuntu-latest # Used for the x86_64 builds. - - macos-12 + - macos-13 # Used for the ARM builds. - macos-14 - windows-latest @@ -121,44 +112,18 @@ jobs: cat >>"$GITHUB_ENV" < [!TIP] +> If you've already built your changes (e.g. `python setup.py build_rust --release --inplace`), +> you can pass `--skip-pkg-install` when invoking `tox` to avoid a rebuild. This works because +> Python will instead find and use Qiskit from the current working directory (since we skipped +> its installation). + +#### Using a custom venv instead of `tox` + +If you're not using `tox`, you can also execute Cargo tests directly in your own virtual environment. +If you haven't done so already, [create a Python virtual environment](#set-up-a-python-venv) and +**_activate it_**. + +Then, run the following commands: ```bash -cargo test --no-default-features +python setup.py build_rust --inplace +PYTHONUSERBASE="$VIRTUAL_ENV" cargo test --no-default-features +``` + +> [!IMPORTANT] +> On Linux, you may need to first set your `LD_LIBRARY_PATH` env var to include the +> path to your Python installation's shared lib, e.g.: +> ```bash +> export LD_LIBRARY_PATH="$(python -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))'):$LD_LIBRARY_PATH" +> ``` + +The first command builds Qiskit in editable mode, +which ensures that Rust tests that interact with Qiskit's Python code actually +use the latest Python code from your working directory. + +The second command actually invokes the tests via Cargo. The `PYTHONUSERBASE` +environment variable tells the embedded Python interpreter to look for packages +in your active virtual environment. The `--no-default-features` +flag is used to compile an isolated test runner without building a linked CPython +extension module (which would otherwise cause linker failures). + +#### Calling Python from Rust tests +By default, our Cargo project configuration allows Rust tests to interact with the +Python interpreter by calling `Python::with_gil` to obtain a `Python` (`py`) token. +This is particularly helpful when testing Rust code that (still) requires interaction +with Python. + +To execute code that needs the GIL in your tests, define the `tests` module as +follows: + +```rust +#[cfg(all(test, not(miri)))] // disable for Miri! +mod tests { + use pyo3::prelude::*; + + #[test] + fn my_first_test() { + Python::with_gil(|py| { + todo!() // do something that needs a `py` token. + }) + } +} ``` -Our Rust-space components are configured such that setting the -``-no-default-features`` flag will compile the test runner, but not attempt to -build a linked CPython extension module, which would cause linker failures. +> [!IMPORTANT] +> Note that we explicitly disable compilation of such tests when running with Miri, i.e. +`#[cfg(not(miri))]`. This is necessary because Miri doesn't support the FFI +> code used internally by PyO3. +> +> If not all of your tests will use the `Python` token, you can disable Miri on a per-test +basis within the same module by decorating *the specific test* with `#[cfg_attr(miri, ignore)]` +instead of disabling Miri for the entire module. + ### Unsafe code and Miri diff --git a/Cargo.lock b/Cargo.lock index 2170ee3a9945..b4157bc1ce61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "approx" @@ -85,9 +85,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "block-buffer" @@ -111,22 +111,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -181,18 +181,18 @@ checksum = "0570650661aa447e7335f1d5e4f499d8e58796e617bedc9267d971e51c8b49d4" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -209,9 +209,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -261,6 +261,15 @@ dependencies = [ "reborrow", ] +[[package]] +name = "dyn-stack" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd" +dependencies = [ + "bytemuck", +] + [[package]] name = "either" version = "1.13.0" @@ -276,7 +285,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -290,11 +299,11 @@ dependencies = [ [[package]] name = "equator" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5099e7b6f0b7431c7a1c49f75929e2777693da192784f167066977a2965767af" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" dependencies = [ - "equator-macro 0.4.1", + "equator-macro 0.4.2", ] [[package]] @@ -305,18 +314,18 @@ checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] name = "equator-macro" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5322a90066ddae2b705096eb9e10c465c0498ae93bf9bdd6437415327c88e3bb" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -334,8 +343,8 @@ dependencies = [ "bytemuck", "coe-rs", "dbgf", - "dyn-stack", - "equator 0.4.1", + "dyn-stack 0.10.0", + "equator 0.4.2", "faer-entity", "gemm", "libm", @@ -364,7 +373,7 @@ dependencies = [ "libm", "num-complex", "num-traits", - "pulp", + "pulp 0.18.22", "reborrow", ] @@ -387,11 +396,11 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "gemm" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400f2ffd14e7548356236c35dc39cad6666d833a852cb8a8f3f28029359bb03" +checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-c32", "gemm-c64", "gemm-common", @@ -407,11 +416,11 @@ dependencies = [ [[package]] name = "gemm-c32" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10dc4a6176c8452d60eac1a155b454c91c668f794151a303bf3c75ea2874812d" +checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-common", "num-complex", "num-traits", @@ -422,11 +431,11 @@ dependencies = [ [[package]] name = "gemm-c64" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2032ce2c0bb150da0256338759a6fb01ca056f6dfe28c4d14af32d7f878f6f" +checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-common", "num-complex", "num-traits", @@ -437,18 +446,19 @@ dependencies = [ [[package]] name = "gemm-common" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fd234fc525939654f47b39325fd5f55e552ceceea9135f3aa8bdba61eabef6" +checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" dependencies = [ "bytemuck", - "dyn-stack", + "dyn-stack 0.13.0", "half", + "libm", "num-complex", "num-traits", "once_cell", "paste", - "pulp", + "pulp 0.21.4", "raw-cpuid", "rayon", "seq-macro", @@ -457,11 +467,11 @@ dependencies = [ [[package]] name = "gemm-f16" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc3652651f96a711d46b8833e1fac27a864be4bdfa81a374055f33ddd25c0c6" +checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-common", "gemm-f32", "half", @@ -475,11 +485,11 @@ dependencies = [ [[package]] name = "gemm-f32" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbc51c44ae3defd207e6d9416afccb3c4af1e7cef5e4960e4c720ac4d6f998e" +checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-common", "num-complex", "num-traits", @@ -490,11 +500,11 @@ dependencies = [ [[package]] name = "gemm-f64" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f37fc86e325c2415a4d0cab8324a0c5371ec06fc7d2f9cb1636fcfc9536a8d8" +checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" dependencies = [ - "dyn-stack", + "dyn-stack 0.13.0", "gemm-common", "num-complex", "num-traits", @@ -558,9 +568,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -576,12 +586,12 @@ checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "rayon", ] @@ -637,15 +647,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "matrixcompare" @@ -839,9 +849,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e" +checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" dependencies = [ "libc", "ndarray", @@ -849,7 +859,7 @@ dependencies = [ "num-integer", "num-traits", "pyo3", - "rustc-hash", + "rustc-hash 2.1.0", ] [[package]] @@ -916,7 +926,7 @@ dependencies = [ "oq3_lexer", "oq3_parser", "rowan", - "rustc-hash", + "rustc-hash 1.1.0", "rustversion", "smol_str", "triomphe", @@ -931,20 +941,20 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pest" -version = "2.7.13" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.11", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.13" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", @@ -952,22 +962,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.13" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] name = "pest_meta" -version = "2.7.13" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -986,9 +996,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "ppv-lite86" @@ -1036,9 +1046,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1052,8 +1062,22 @@ dependencies = [ "bytemuck", "libm", "num-complex", + "reborrow", +] + +[[package]] +name = "pulp" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fb7a99b37aaef4c7dd2fd15a819eb8010bfc7a2c2155230d51f497316cad6d" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", "pulp-macro", "reborrow", + "version_check", ] [[package]] @@ -1064,7 +1088,7 @@ checksum = "d315b3197b780e4873bc0e11251cb56a33f65a6032a3d39b8d1405c255513766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -1082,9 +1106,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.22.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" dependencies = [ "cfg-if", "hashbrown 0.14.5", @@ -1105,9 +1129,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.22.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" dependencies = [ "once_cell", "target-lexicon", @@ -1115,9 +1139,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.22.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" dependencies = [ "libc", "pyo3-build-config", @@ -1125,27 +1149,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] name = "pyo3-macros-backend" -version = "0.22.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -1167,7 +1191,7 @@ dependencies = [ "num-traits", "numpy", "once_cell", - "pulp", + "pulp 0.21.4", "pyo3", "qiskit-circuit", "rand", @@ -1177,7 +1201,7 @@ dependencies = [ "rustiq-core", "rustworkx-core", "smallvec", - "thiserror", + "thiserror 2.0.11", ] [[package]] @@ -1197,7 +1221,7 @@ dependencies = [ "rayon", "rustworkx-core", "smallvec", - "thiserror", + "thiserror 2.0.11", ] [[package]] @@ -1234,9 +1258,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1298,11 +1322,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "10.7.0" +version = "11.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +checksum = "c6928fa44c097620b706542d428957635951bade7143269085389d42c8a4927e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", ] [[package]] @@ -1350,9 +1374,9 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1362,9 +1386,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1386,7 +1410,7 @@ dependencies = [ "countme", "hashbrown 0.14.5", "memoffset", - "rustc-hash", + "rustc-hash 1.1.0", "text-size", ] @@ -1396,6 +1420,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustiq-core" version = "0.0.10" @@ -1409,9 +1439,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustworkx-core" @@ -1450,22 +1480,22 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] [[package]] @@ -1507,9 +1537,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -1518,15 +1548,15 @@ dependencies = [ [[package]] name = "sysctl" -version = "0.5.5" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "byteorder", "enum-as-inner", "libc", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -1544,22 +1574,42 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -1582,9 +1632,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-properties" @@ -1791,18 +1841,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "xshell" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db0ab86eae739efd1b054a8d3d16041914030ac4e01cd1dca0cf252fd8b6437" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" dependencies = [ "xshell-macros", ] [[package]] name = "xshell-macros" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" [[package]] name = "yansi" @@ -1828,5 +1878,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.96", ] diff --git a/Cargo.toml b/Cargo.toml index 14b2504000a4..f8ce108e4b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "2.0.0" edition = "2021" -rust-version = "1.70" # Keep in sync with README.md and rust-toolchain.toml. +rust-version = "1.79" # 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 @@ -14,15 +14,15 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] -bytemuck = "1.20" -indexmap.version = "2.7.0" +bytemuck = "1.21" +indexmap.version = "2.7.1" hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" ndarray = "0.15" -numpy = "0.22.1" +numpy = "0.23" smallvec = "1.13" -thiserror = "1.0" +thiserror = "2.0" rustworkx-core = "0.15" approx = "0.5" itertools = "0.13.0" @@ -34,7 +34,7 @@ rayon = "1.10" # distributions). We only activate that feature when building the C extension module; we still need # it disabled for Rust-only tests to avoid linker errors with it not being loaded. See # https://pyo3.rs/main/features#extension-module for more. -pyo3 = { version = "0.22.6", features = ["abi3-py39"] } +pyo3 = { version = "0.23", features = ["abi3-py39"] } # These are our own crates. qiskit-accelerate = { path = "crates/accelerate" } @@ -42,6 +42,19 @@ qiskit-circuit = { path = "crates/circuit" } qiskit-qasm2 = { path = "crates/qasm2" } qiskit-qasm3 = { path = "crates/qasm3" } +[workspace.lints.clippy] +# The lint forbids things like `if a < b {} else if a == b {}`, and suggests matching on `a.cmp(&b)` +# which uses the `::std::cmp::Ordering` enum as a return. Both styles are acceptable, and the `if` +# chain can be more legible to people. +comparison-chain = "allow" + +[workspace.lints.rust] +# In Rust 2021, the bodies of `unsafe fn` may use `unsafe` functions themselves without marking +# them. This is an overload of the word: `unsafe fn` is documenting something for the caller, but +# that doesn't mean the entire function body is unsafe. Denying this lint (which becomes +# warn-by-default in Rust 2024) means `unsafe fn` bodies still must use `unsafe {}` like normal. +unsafe_op_in_unsafe_fn = "deny" + [profile.release] lto = 'fat' codegen-units = 1 diff --git a/DEPRECATION.md b/DEPRECATION.md index d9e2f16f6473..4d1a5de0e6a5 100644 --- a/DEPRECATION.md +++ b/DEPRECATION.md @@ -1,6 +1,6 @@ # Deprecation Policy -Starting from the 1.0.0 release, Qiskit follows semantic versioning, with a yearly release cycle for major releases. +Starting from the 1.0 release, Qiskit follows semantic versioning, with a yearly release cycle for major releases. [Full details of the scheduling are hosted with the external public documentation](https://docs.quantum.ibm.com/open-source/qiskit-sdk-version-strategy). This document is primarily intended for developers of Qiskit themselves. @@ -150,11 +150,11 @@ and add the deprecation to that function's docstring so that it shows up in the ```python from qiskit.utils.deprecation import deprecate_arg, deprecate_func -@deprecate_func(since="0.24.0", additional_msg="No replacement is provided.") +@deprecate_func(since="1.2", additional_msg="No replacement is provided.") def deprecated_func(): pass -@deprecate_arg("bad_arg", new_alias="new_name", since="0.24.0") +@deprecate_arg("bad_arg", new_alias="new_name", since="1.2") def another_func(bad_arg: str, new_name: str): pass ``` @@ -178,7 +178,7 @@ import warnings def deprecated_function(): warnings.warn( "The function qiskit.deprecated_function() is deprecated since " - "Qiskit 0.44.0, and will be removed 3 months or more later. " + "Qiskit 1.2, and will be removed in 2.0 or a later major release." "Instead, you should use qiskit.other_function().", category=DeprecationWarning, stacklevel=2, @@ -235,9 +235,9 @@ def deprecated_function(): """ Short description of the deprecated function. - .. deprecated:: 0.44.0 + .. deprecated:: 1.2 The function qiskit.deprecated_function() is deprecated since - Qiskit 0.44.0, and will be removed 3 months or more later. + Qiskit 1.2, and will be removed in 2.0 or a later major release. Instead, you should use qiskit.other_function(). diff --git a/README.md b/README.md index e3ab8aac94ff..6932935f5264 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![License](https://img.shields.io/github/license/Qiskit/qiskit.svg?)](https://opensource.org/licenses/Apache-2.0) [![Current Release](https://img.shields.io/github/release/Qiskit/qiskit.svg?logo=Qiskit)](https://github.com/Qiskit/qiskit/releases) -[![Extended Support Release](https://img.shields.io/github/v/release/Qiskit/qiskit?sort=semver&filter=0.*&logo=Qiskit&label=extended%20support)](https://github.com/Qiskit/qiskit/releases?q=tag%3A0) + [![Downloads](https://img.shields.io/pypi/dm/qiskit.svg)](https://pypi.org/project/qiskit/) [![Coverage Status](https://coveralls.io/repos/github/Qiskit/qiskit/badge.svg?branch=main)](https://coveralls.io/github/Qiskit/qiskit?branch=main) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qiskit) -[![Minimum rustc 1.70](https://img.shields.io/badge/rustc-1.70+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![Minimum rustc 1.79](https://img.shields.io/badge/rustc-1.79+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) [![Downloads](https://static.pepy.tech/badge/qiskit)](https://pepy.tech/project/qiskit) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2583252.svg)](https://doi.org/10.5281/zenodo.2583252) @@ -22,9 +22,6 @@ For more details on how to use Qiskit, refer to the documentation located here: ## Installation -> [!WARNING] -> Do not try to upgrade an existing Qiskit 0.* environment to Qiskit 1.0 in-place. [Read more](https://docs.quantum.ibm.com/migration-guides/qiskit-1.0-installation). - We encourage installing Qiskit via ``pip``: ```bash @@ -40,7 +37,7 @@ To install from source, follow the instructions in the [documentation](https://d Now that Qiskit is installed, it's time to begin working with Qiskit. The essential parts of a quantum program are: 1. Define and build a quantum circuit that represents the quantum state 2. Define the classical output by measurements or a set of observable operators -3. Depending on the output, use the primitive function `sampler` to sample outcomes or the `estimator` to estimate values. +3. Depending on the output, use the Sampler primitive to sample outcomes or the Estimator primitive to estimate expectation values. Create an example quantum circuit using the `QuantumCircuit` class: @@ -48,22 +45,22 @@ Create an example quantum circuit using the `QuantumCircuit` class: import numpy as np from qiskit import QuantumCircuit -# 1. A quantum circuit for preparing the quantum state |000> + i |111> -qc_example = QuantumCircuit(3) -qc_example.h(0) # generate superpostion -qc_example.p(np.pi/2,0) # add quantum phase -qc_example.cx(0,1) # 0th-qubit-Controlled-NOT gate on 1st qubit -qc_example.cx(0,2) # 0th-qubit-Controlled-NOT gate on 2nd qubit +# 1. A quantum circuit for preparing the quantum state |000> + i |111> / √2 +qc = QuantumCircuit(3) +qc.h(0) # generate superposition +qc.p(np.pi / 2, 0) # add quantum phase +qc.cx(0, 1) # 0th-qubit-Controlled-NOT gate on 1st qubit +qc.cx(0, 2) # 0th-qubit-Controlled-NOT gate on 2nd qubit ``` -This simple example makes an entangled state known as a [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state) $(|000\rangle + i|111\rangle)/\sqrt{2}$. It uses the standard quantum gates: Hadamard gate (`h`), Phase gate (`p`), and CNOT gate (`cx`). +This simple example creates an entangled state known as a [GHZ state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state) $(|000\rangle + i|111\rangle)/\sqrt{2}$. It uses the standard quantum gates: Hadamard gate (`h`), Phase gate (`p`), and CNOT gate (`cx`). -Once you've made your first quantum circuit, choose which primitive function you will use. Starting with `sampler`, +Once you've made your first quantum circuit, choose which primitive you will use. Starting with the Sampler, we use `measure_all(inplace=False)` to get a copy of the circuit in which all the qubits are measured: ```python # 2. Add the classical output in the form of measurement of all qubits -qc_measured = qc_example.measure_all(inplace=False) +qc_measured = qc.measure_all(inplace=False) # 3. Execute using the Sampler primitive from qiskit.primitives import StatevectorSampler @@ -73,7 +70,7 @@ result = job.result() print(f" > Counts: {result[0].data["meas"].get_counts()}") ``` Running this will give an outcome similar to `{'000': 497, '111': 503}` which is `000` 50% of the time and `111` 50% of the time up to statistical fluctuations. -To illustrate the power of Estimator, we now use the quantum information toolbox to create the operator $XXY+XYX+YXX-YYY$ and pass it to the `run()` function, along with our quantum circuit. Note the Estimator requires a circuit _**without**_ measurement, so we use the `qc_example` circuit we created earlier. +To illustrate the power of the Estimator, we now use the quantum information toolbox to create the operator $XXY+XYX+YXX-YYY$ and pass it to the `run()` function, along with our quantum circuit. Note that the Estimator requires a circuit _**without**_ measurements, so we use the `qc` circuit we created earlier. ```python # 2. Define the observable to be measured @@ -83,7 +80,7 @@ operator = SparsePauliOp.from_list([("XXY", 1), ("XYX", 1), ("YXX", 1), ("YYY", # 3. Execute using the Estimator primitive from qiskit.primitives import StatevectorEstimator estimator = StatevectorEstimator() -job = estimator.run([(qc_example, operator)], precision=1e-3) +job = estimator.run([(qc, operator)], precision=1e-3) result = job.result() print(f" > Expectation values: {result[0].data.evs}") ``` @@ -96,17 +93,17 @@ The power of quantum computing cannot be simulated on classical computers and yo However, running a quantum circuit on hardware requires rewriting to the basis gates and connectivity of the quantum hardware. The tool that does this is the [transpiler](https://docs.quantum.ibm.com/api/qiskit/transpiler), and Qiskit includes transpiler passes for synthesis, optimization, mapping, and scheduling. However, it also includes a default compiler, which works very well in most examples. -The following code will map the example circuit to the `basis_gates = ['cz', 'sx', 'rz']` and a linear chain of qubits $0 \rightarrow 1 \rightarrow 2$ with the `coupling_map =[[0, 1], [1, 2]]`. +The following code will map the example circuit to the `basis_gates = ["cz", "sx", "rz"]` and a linear chain of qubits $0 \rightarrow 1 \rightarrow 2$ with the `coupling_map = [[0, 1], [1, 2]]`. ```python from qiskit import transpile -qc_transpiled = transpile(qc_example, basis_gates = ['cz', 'sx', 'rz'], coupling_map =[[0, 1], [1, 2]] , optimization_level=3) +qc_transpiled = transpile(qc, basis_gates=["cz", "sx", "rz"], coupling_map=[[0, 1], [1, 2]], optimization_level=3) ``` ### Executing your code on real quantum hardware Qiskit provides an abstraction layer that lets users run quantum circuits on hardware from any vendor that provides a compatible interface. -The best way to use Qiskit is with a runtime environment that provides optimized implementations of `sampler` and `estimator` for a given hardware platform. This runtime may involve using pre- and post-processing, such as optimized transpiler passes with error suppression, error mitigation, and, eventually, error correction built in. A runtime implements `qiskit.primitives.BaseSamplerV2` and `qiskit.primitives.BaseEstimatorV2` interfaces. For example, +The best way to use Qiskit is with a runtime environment that provides optimized implementations of Sampler and Estimator for a given hardware platform. This runtime may involve using pre- and post-processing, such as optimized transpiler passes with error suppression, error mitigation, and, eventually, error correction built in. A runtime implements `qiskit.primitives.BaseSamplerV2` and `qiskit.primitives.BaseEstimatorV2` interfaces. For example, some packages that provide implementations of a runtime primitive implementation are: * https://github.com/Qiskit/qiskit-ibm-runtime @@ -146,9 +143,9 @@ to the project at different levels. If you use Qiskit, please cite as per the in The changelog for a particular release is dynamically generated and gets written to the release page on Github for each release. For example, you can -find the page for the `0.46.0` release here: +find the page for the `1.2.0` release here: - + The changelog for the current release can be found in the releases tab: [![Releases](https://img.shields.io/github/release/Qiskit/qiskit.svg?style=flat&label=)](https://github.com/Qiskit/qiskit/releases) diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index cc624e7750d6..b17098930b5c 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -9,6 +9,9 @@ license.workspace = true name = "qiskit_accelerate" doctest = false +[lints] +workspace = true + [dependencies] rayon.workspace = true numpy.workspace = true @@ -58,8 +61,11 @@ version = "0.2.0" features = ["ndarray"] [dependencies.pulp] -version = "0.18.22" +version = "0.21.4" features = ["macro"] +[dev-dependencies] +pyo3 = { workspace = true, features = ["auto-initialize"] } + [features] cache_pygates = ["qiskit-circuit/cache_pygates"] diff --git a/crates/accelerate/src/barrier_before_final_measurement.rs b/crates/accelerate/src/barrier_before_final_measurement.rs index 36e3fd59b691..d76af228acde 100644 --- a/crates/accelerate/src/barrier_before_final_measurement.rs +++ b/crates/accelerate/src/barrier_before_final_measurement.rs @@ -30,24 +30,21 @@ pub fn barrier_before_final_measurements( dag: &mut DAGCircuit, label: Option, ) -> PyResult<()> { + let is_exactly_final = |inst: &PackedInstruction| FINAL_OP_NAMES.contains(&inst.op().name()); let final_ops: HashSet = dag .op_nodes(true) - .filter(|node| { - let NodeType::Operation(ref inst) = dag.dag()[*node] else { - unreachable!(); - }; - if !FINAL_OP_NAMES.contains(&inst.op().name()) { - return false; + .filter_map(|(node, inst)| { + if !is_exactly_final(inst) { + return None; } - let is_final_op = dag.bfs_successors(*node).all(|(_, child_successors)| { - !child_successors.iter().any(|suc| match dag.dag()[*suc] { - NodeType::Operation(ref suc_inst) => { - !FINAL_OP_NAMES.contains(&suc_inst.op().name()) - } - _ => false, + dag.bfs_successors(node) + .all(|(_, child_successors)| { + child_successors.iter().all(|suc| match dag[*suc] { + NodeType::Operation(ref suc_inst) => is_exactly_final(suc_inst), + _ => true, + }) }) - }); - is_final_op + .then_some(node) }) .collect(); if final_ops.is_empty() { @@ -60,7 +57,7 @@ pub fn barrier_before_final_measurements( let final_packed_ops: Vec = ordered_node_indices .into_iter() .map(|node| { - let NodeType::Operation(ref inst) = dag.dag()[node] else { + let NodeType::Operation(ref inst) = dag[node] else { unreachable!() }; let res = inst.clone(); diff --git a/crates/accelerate/src/basis/basis_translator/basis_search.rs b/crates/accelerate/src/basis/basis_translator/basis_search.rs index 4a5a608fe1e2..c92f926e2b2a 100644 --- a/crates/accelerate/src/basis/basis_translator/basis_search.rs +++ b/crates/accelerate/src/basis/basis_translator/basis_search.rs @@ -115,65 +115,63 @@ pub(crate) fn basis_search( )]) }; - let basis_transforms = match dijkstra_search( - &equiv_lib.graph(), - [dummy], - edge_weight, - |event: DijkstraEvent, u32>| { - match event { - DijkstraEvent::Discover(n, score) => { - let gate_key = &equiv_lib.graph()[n].key; - let gate = (gate_key.name.to_string(), gate_key.num_qubits); - source_basis_remain.remove(gate_key); - let mut borrowed_cost_map = opt_cost_map.borrow_mut(); - if let Some(entry) = borrowed_cost_map.get_mut(&gate) { - *entry = score; - } else { - borrowed_cost_map.insert(gate.clone(), score); - } - if let Some(rule) = predecessors.borrow().get(&gate) { - basis_transforms.push(( - (gate_key.name.to_string(), gate_key.num_qubits), - (rule.params.clone(), rule.circuit.clone()), - )); - } - - if source_basis_remain.is_empty() { - basis_transforms.reverse(); - return Control::Break(()); - } + let event_matcher = |event: DijkstraEvent, u32>| { + match event { + DijkstraEvent::Discover(n, score) => { + let gate_key = &equiv_lib.graph()[n].key; + let gate = (gate_key.name.to_string(), gate_key.num_qubits); + source_basis_remain.remove(gate_key); + let mut borrowed_cost_map = opt_cost_map.borrow_mut(); + if let Some(entry) = borrowed_cost_map.get_mut(&gate) { + *entry = score; + } else { + borrowed_cost_map.insert(gate.clone(), score); } - DijkstraEvent::EdgeRelaxed(_, target, Some(edata)) => { - let gate = &equiv_lib.graph()[target].key; - predecessors - .borrow_mut() - .entry((gate.name.to_string(), gate.num_qubits)) - .and_modify(|value| *value = edata.rule.clone()) - .or_insert(edata.rule.clone()); + if let Some(rule) = predecessors.borrow().get(&gate) { + basis_transforms.push(( + (gate_key.name.to_string(), gate_key.num_qubits), + (rule.params.clone(), rule.circuit.clone()), + )); } - DijkstraEvent::ExamineEdge(_, target, Some(edata)) => { - num_gates_remaining_for_rule - .entry(edata.index) - .and_modify(|val| *val -= 1) - .or_insert(0); - let target = &equiv_lib.graph()[target].key; - - // If there are gates in this `rule` that we have not yet generated, we can't apply - // this `rule`. if `target` is already in basis, it's not beneficial to use this rule. - if num_gates_remaining_for_rule[&edata.index] > 0 - || target_basis_keys.contains(target) - { - return Control::Prune; - } + + if source_basis_remain.is_empty() { + basis_transforms.reverse(); + return Control::Break(()); } - _ => {} - }; - Control::Continue - }, - ) { - Ok(Control::Break(_)) => Some(basis_transforms), - _ => None, + } + DijkstraEvent::EdgeRelaxed(_, target, Some(edata)) => { + let gate = &equiv_lib.graph()[target].key; + predecessors + .borrow_mut() + .entry((gate.name.to_string(), gate.num_qubits)) + .and_modify(|value| *value = edata.rule.clone()) + .or_insert(edata.rule.clone()); + } + DijkstraEvent::ExamineEdge(_, target, Some(edata)) => { + num_gates_remaining_for_rule + .entry(edata.index) + .and_modify(|val| *val -= 1) + .or_insert(0); + let target = &equiv_lib.graph()[target].key; + + // If there are gates in this `rule` that we have not yet generated, we can't apply + // this `rule`. if `target` is already in basis, it's not beneficial to use this rule. + if num_gates_remaining_for_rule[&edata.index] > 0 + || target_basis_keys.contains(target) + { + return Control::Prune; + } + } + _ => {} + }; + Control::Continue }; + + let basis_transforms = + match dijkstra_search(&equiv_lib.graph(), [dummy], edge_weight, event_matcher) { + Ok(Control::Break(_)) => Some(basis_transforms), + _ => None, + }; equiv_lib.graph_mut().remove_node(dummy); basis_transforms } diff --git a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs index 79efdac90f6c..3b28e8b63d9d 100644 --- a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs +++ b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs @@ -18,7 +18,7 @@ use qiskit_circuit::parameter_table::ParameterUuid; use qiskit_circuit::Qubit; use qiskit_circuit::{ circuit_data::CircuitData, - dag_circuit::{DAGCircuit, NodeType}, + dag_circuit::DAGCircuit, operations::{Operation, Param}, }; use smallvec::SmallVec; @@ -81,30 +81,24 @@ pub(super) fn compose_transforms<'a>( for (_, dag) in &mut mapped_instructions.values_mut() { let nodes_to_replace = dag .op_nodes(true) - .filter_map(|node| { - if let Some(NodeType::Operation(op)) = dag.dag().node_weight(node) { - if (gate_name.as_str(), *gate_num_qubits) - == (op.op().name(), op.op().num_qubits()) - { - Some(( - node, - op.params_view() - .iter() - .map(|x| x.clone_ref(py)) - .collect::>(), - )) - } else { - None - } - } else { - None - } + .filter(|(_, op)| { + (op.op().num_qubits() == *gate_num_qubits) + && (op.op().name() == gate_name.as_str()) + }) + .map(|(node, op)| { + ( + node, + op.params_view() + .iter() + .map(|x| x.clone_ref(py)) + .collect::>(), + ) }) .collect::>(); for (node, params) in nodes_to_replace { let param_mapping: HashMap = equiv_params .iter() - .map(|x| ParameterUuid::from_parameter(x.to_object(py).bind(py))) + .map(|x| ParameterUuid::from_parameter(&x.into_pyobject(py).unwrap())) .zip(params) .map(|(uuid, param)| -> PyResult<(ParameterUuid, Param)> { Ok((uuid?, param.clone_ref(py))) @@ -139,17 +133,15 @@ fn get_gates_num_params( dag: &DAGCircuit, example_gates: &mut HashMap, ) -> PyResult<()> { - for node in dag.op_nodes(true) { - if let Some(NodeType::Operation(op)) = dag.dag().node_weight(node) { - example_gates.insert( - (op.op().name().to_string(), op.op().num_qubits()), - op.params_view().len(), - ); - if op.op().control_flow() { - let blocks = op.op().blocks(); - for block in blocks { - get_gates_num_params_circuit(&block, example_gates)?; - } + for (_, inst) in dag.op_nodes(true) { + example_gates.insert( + (inst.op().name().to_string(), inst.op().num_qubits()), + inst.params_view().len(), + ); + if inst.op().control_flow() { + let blocks = inst.op().blocks(); + for block in blocks { + get_gates_num_params_circuit(&block, example_gates)?; } } } diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index c36aa73a7486..96d0a29f2efc 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -212,8 +212,7 @@ fn extract_basis( basis: &mut HashSet, min_qubits: usize, ) -> PyResult<()> { - for node in circuit.op_nodes(true) { - let operation: &PackedInstruction = circuit.dag()[node].unwrap_operation(); + for (node, operation) in circuit.op_nodes(true) { if !circuit.has_calibration_for_index(py, node)? && circuit.get_qargs(operation.qubits()).len() >= min_qubits { @@ -227,7 +226,7 @@ fn extract_basis( unreachable!("Control flow operation is not an instance of PyInstruction.") }; let inst_bound = inst.instruction.bind(py); - for block in inst_bound.getattr("blocks")?.iter()? { + for block in inst_bound.getattr("blocks")?.try_iter()? { recurse_circuit(py, block?, basis, min_qubits)?; } } @@ -258,7 +257,7 @@ fn extract_basis( if inst.op().control_flow() { let operation_ob = instruction_object.getattr(intern!(py, "operation"))?; let blocks = operation_ob.getattr("blocks")?; - for block in blocks.iter()? { + for block in blocks.try_iter()? { recurse_circuit(py, block?, basis, min_qubits)?; } } @@ -282,8 +281,7 @@ fn extract_basis_target( min_qubits: usize, qargs_with_non_global_operation: &HashMap, HashSet>, ) -> PyResult<()> { - for node in dag.op_nodes(true) { - let node_obj: &PackedInstruction = dag.dag()[node].unwrap_operation(); + for (node, node_obj) in dag.op_nodes(true) { let qargs: &[Qubit] = dag.get_qargs(node_obj.qubits()); if dag.has_calibration_for_index(py, node)? || qargs.len() < min_qubits { continue; @@ -329,7 +327,7 @@ fn extract_basis_target( let bound_inst = op.instruction.bind(py); // Use python side extraction instead of the Rust method `op.blocks` due to // required usage of a python-space method `QuantumCircuit.has_calibration_for`. - let blocks = bound_inst.getattr("blocks")?.iter()?; + let blocks = bound_inst.getattr("blocks")?.try_iter()?; for block in blocks { extract_basis_target_circ( &block?, @@ -406,7 +404,7 @@ fn extract_basis_target_circ( unreachable!("Control flow op is not a control flow op. But control_flow is `true`") }; let bound_inst = op.instruction.bind(py); - let blocks = bound_inst.getattr("blocks")?.iter()?; + let blocks = bound_inst.getattr("blocks")?.try_iter()?; for block in blocks { extract_basis_target_circ( &block?, @@ -433,7 +431,7 @@ fn apply_translation( let mut is_updated = false; let mut out_dag = dag.copy_empty_like(py, "alike")?; for node in dag.topological_op_nodes()? { - let node_obj = dag.dag()[node].unwrap_operation(); + let node_obj = dag[node].unwrap_operation(); let node_qarg = dag.get_qargs(node_obj.qubits()); let node_carg = dag.get_cargs(node_obj.clbits()); let qubit_set: HashSet = HashSet::from_iter(node_qarg.iter().copied()); @@ -446,7 +444,7 @@ fn apply_translation( let mut flow_blocks = vec![]; let bound_obj = control_op.instruction.bind(py); let blocks = bound_obj.getattr("blocks")?; - for block in blocks.iter()? { + for block in blocks.try_iter()? { let block = block?; let dag_block: DAGCircuit = circuit_to_dag(py, block.extract()?, true, None, None)?; @@ -588,7 +586,7 @@ fn replace_node( } if node.params_view().is_empty() { for inner_index in target_dag.topological_op_nodes()? { - let inner_node = &target_dag.dag()[inner_index].unwrap_operation(); + let inner_node = &target_dag[inner_index].unwrap_operation(); let old_qargs = dag.get_qargs(node.qubits()); let old_cargs = dag.get_cargs(node.clbits()); let new_qubits: Vec = target_dag @@ -641,9 +639,9 @@ fn replace_node( let parameter_map = target_params .iter() .zip(node.params_view()) - .into_py_dict_bound(py); + .into_py_dict(py)?; for inner_index in target_dag.topological_op_nodes()? { - let inner_node = &target_dag.dag()[inner_index].unwrap_operation(); + let inner_node = &target_dag[inner_index].unwrap_operation(); let old_qargs = dag.get_qargs(node.qubits()); let old_cargs = dag.get_cargs(node.clbits()); let new_qubits: Vec = target_dag @@ -676,7 +674,7 @@ fn replace_node( if let Param::ParameterExpression(param_obj) = param { let bound_param = param_obj.bind(py); let exp_params = param.iter_parameters(py)?; - let bind_dict = PyDict::new_bound(py); + let bind_dict = PyDict::new(py); for key in exp_params { let key = key?; bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; @@ -737,7 +735,7 @@ fn replace_node( if let Param::ParameterExpression(old_phase) = target_dag.global_phase() { let bound_old_phase = old_phase.bind(py); - let bind_dict = PyDict::new_bound(py); + let bind_dict = PyDict::new(py); for key in target_dag.global_phase().iter_parameters(py)? { let key = key?; bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; @@ -758,7 +756,7 @@ fn replace_node( } if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? { new_phase = new_phase.call_method0(intern!(py, "numeric"))?; - if new_phase.is_instance(&PyComplex::type_object_bound(py))? { + if new_phase.is_instance(&PyComplex::type_object(py))? { return Err(TranspilerError::new_err(format!( "Global phase must be real, but got {}", new_phase.repr()? diff --git a/crates/accelerate/src/check_map.rs b/crates/accelerate/src/check_map.rs index 80c8aaa5c24e..04a2cdcb0013 100644 --- a/crates/accelerate/src/check_map.rs +++ b/crates/accelerate/src/check_map.rs @@ -16,7 +16,7 @@ use pyo3::prelude::*; use pyo3::wrap_pyfunction; use qiskit_circuit::circuit_data::CircuitData; -use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::imports::CIRCUIT_TO_DAG; use qiskit_circuit::operations::{Operation, OperationRef}; use qiskit_circuit::Qubit; @@ -36,45 +36,43 @@ fn recurse<'py>( None => edge_set.contains(&[qubits[0].into(), qubits[1].into()]), } }; - for node in dag.op_nodes(false) { - if let NodeType::Operation(inst) = &dag.dag()[node] { - let qubits = dag.get_qargs(inst.qubits()); - if inst.op().control_flow() { - if let OperationRef::Instruction(py_inst) = inst.op().view() { - let raw_blocks = py_inst.instruction.getattr(py, "blocks")?; - let circuit_to_dag = CIRCUIT_TO_DAG.get_bound(py); - for raw_block in raw_blocks.bind(py).iter().unwrap() { - let block_obj = raw_block?; - let block = block_obj - .getattr(intern!(py, "_data"))? - .downcast::()? - .borrow(); - let new_dag: DAGCircuit = - circuit_to_dag.call1((block_obj.clone(),))?.extract()?; - let wire_map = (0..block.num_qubits()) - .map(|inner| { - let outer = qubits[inner]; - match wire_map { - Some(wire_map) => wire_map[outer.index()], - None => outer, - } - }) - .collect::>(); - let res = recurse(py, &new_dag, edge_set, Some(&wire_map))?; - if res.is_some() { - return Ok(res); - } + for (node, inst) in dag.op_nodes(false) { + let qubits = dag.get_qargs(inst.qubits()); + if inst.op().control_flow() { + if let OperationRef::Instruction(py_inst) = inst.op().view() { + let raw_blocks = py_inst.instruction.getattr(py, "blocks")?; + let circuit_to_dag = CIRCUIT_TO_DAG.get_bound(py); + for raw_block in raw_blocks.bind(py).try_iter()? { + let block_obj = raw_block?; + let block = block_obj + .getattr(intern!(py, "_data"))? + .downcast::()? + .borrow(); + let new_dag: DAGCircuit = + circuit_to_dag.call1((block_obj.clone(),))?.extract()?; + let wire_map = (0..block.num_qubits()) + .map(|inner| { + let outer = qubits[inner]; + match wire_map { + Some(wire_map) => wire_map[outer.index()], + None => outer, + } + }) + .collect::>(); + let res = recurse(py, &new_dag, edge_set, Some(&wire_map))?; + if res.is_some() { + return Ok(res); } } - } else if qubits.len() == 2 - && (dag.calibrations_empty() || !dag.has_calibration_for_index(py, node)?) - && !check_qubits(qubits) - { - return Ok(Some(( - inst.op().name().to_string(), - [qubits[0].0, qubits[1].0], - ))); } + } else if qubits.len() == 2 + && (dag.calibrations_empty() || !dag.has_calibration_for_index(py, node)?) + && !check_qubits(qubits) + { + return Ok(Some(( + inst.op().name().to_string(), + [qubits[0].0, qubits[1].0], + ))); } } Ok(None) diff --git a/crates/accelerate/src/circuit_library/blocks.rs b/crates/accelerate/src/circuit_library/blocks.rs index 80add611abb1..0e6083dadf57 100644 --- a/crates/accelerate/src/circuit_library/blocks.rs +++ b/crates/accelerate/src/circuit_library/blocks.rs @@ -42,8 +42,7 @@ impl BlockOperation { )), Self::PyCustom { builder } => { // the builder returns a Python operation plus the bound parameters - let py_params = - PyList::new_bound(py, params.iter().map(|&p| p.clone().into_py(py))).into_any(); + let py_params = PyList::new(py, params.iter().map(|&p| p.clone()))?.into_any(); let job = builder.call1(py, (py_params,))?; let result = job.downcast_bound::(py)?; @@ -51,7 +50,7 @@ impl BlockOperation { let operation: OperationFromPython = result.get_item(0)?.extract()?; let bound_params = result .get_item(1)? - .iter()? + .try_iter()? .map(|ob| Param::extract_no_coerce(&ob?)) .collect::>>()?; @@ -84,7 +83,6 @@ impl Block { #[staticmethod] #[pyo3(signature = (num_qubits, num_parameters, builder,))] pub fn from_callable( - py: Python, num_qubits: i64, num_parameters: i64, builder: &Bound, @@ -96,7 +94,7 @@ impl Block { } let block = Block { operation: BlockOperation::PyCustom { - builder: builder.to_object(py), + builder: builder.clone().unbind(), }, num_qubits: num_qubits as u32, num_parameters: num_parameters as usize, diff --git a/crates/accelerate/src/circuit_library/entanglement.rs b/crates/accelerate/src/circuit_library/entanglement.rs index 2168414cc4b0..0a879e5849c6 100644 --- a/crates/accelerate/src/circuit_library/entanglement.rs +++ b/crates/accelerate/src/circuit_library/entanglement.rs @@ -247,7 +247,7 @@ pub fn get_entangler_map<'py>( Ok(entanglement) => entanglement .into_iter() .map(|vec| match vec { - Ok(vec) => Ok(PyTuple::new_bound(py, vec)), + Ok(vec) => PyTuple::new(py, vec), Err(e) => Err(e), }) .collect::, _>>(), diff --git a/crates/accelerate/src/circuit_library/parameter_ledger.rs b/crates/accelerate/src/circuit_library/parameter_ledger.rs index 457034850196..feb00ac22d3f 100644 --- a/crates/accelerate/src/circuit_library/parameter_ledger.rs +++ b/crates/accelerate/src/circuit_library/parameter_ledger.rs @@ -29,7 +29,7 @@ pub(super) type LayerParameters<'a> = Vec>; // parameter in /// Internally, the parameters are stored in a 1-D vector and the ledger keeps track of /// which indices belong to which layer. For example, a 2-qubit circuit where both the /// rotation and entanglement layer have 1 block with 2 parameters each, we would store -/// +/// /// [x0 x1 x2 x3 x4 x5 x6 x7 ....] /// ----- ----- ----- ----- /// rep0 rep0 rep1 rep2 @@ -105,7 +105,7 @@ impl ParameterLedger { let parameter_vector: Vec = imports::PARAMETER_VECTOR .get_bound(py) .call1((parameter_prefix, num_parameters))? // get the Python ParameterVector - .iter()? // iterate over the elements and cast them to Rust Params + .try_iter()? // iterate over the elements and cast them to Rust Params .map(|ob| Param::extract_no_coerce(&ob?)) .collect::>()?; diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs index 3c5164314c08..b5aa71716341 100644 --- a/crates/accelerate/src/circuit_library/pauli_evolution.rs +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -192,22 +192,22 @@ fn multi_qubit_evolution( /// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain /// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the /// Pauli string XIYZ we have the circuit -/// ┌───┐┌───────┐┌───┐ -/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├─────────── -/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐ -/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├ -/// └──────┘└─┬─┘ └─┬─┘└────┘ -/// 2: ──────────┼───────────────────────┼──────── -/// ┌───┐ │ │ ┌───┐ -/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─ -/// └───┘ └───┘ +/// +/// ┌───┐ ┌───┐┌───────┐┌───┐┌───┐ +/// 0: ┤ H ├──────┤ X ├┤ Rz(2) ├┤ X ├┤ H ├──────── +/// └───┘ └─┬─┘└───────┘└─┬─┘└───┘ +/// 1: ─────────────┼─────────────┼─────────────── +/// ┌────┐┌───┐ │ │ ┌───┐┌──────┐ +/// 2: ┤ √X ├┤ X ├──■─────────────■──┤ X ├┤ √Xdg ├ +/// └────┘└─┬─┘ └─┬─┘└──────┘ +/// 3: ────────■───────────────────────■────────── /// /// Args: /// num_qubits: The number of qubits in the Hamiltonian. /// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements -/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form -/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as -/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4. +/// ``(pauli_string, qubit_indices, rz_rotation_angle)``. An element of the form +/// ``("XIYZ", [0,1,2,3], 2)``, for example, is interpreted in terms of qubit indices as +/// X_q0 I_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 2. /// insert_barriers: If ``true``, insert a barrier in between the evolution of individual /// Pauli terms. /// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each @@ -244,7 +244,7 @@ pub fn py_pauli_evolution( } paulis.push(pauli); - times.push(time); // note we do not multiply by 2 here, this is done Python side! + times.push(time); // note we do not multiply by 2 here, this is already done Python side! indices.push(tuple.get_item(1)?.extract::>()?) } @@ -266,12 +266,12 @@ pub fn py_pauli_evolution( }, ); - // When handling all-identity Paulis above, we added the time as global phase. - // However, the all-identity Paulis should add a negative phase, as they implement - // exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication, - // instead of doing it every time we find an all-identity Pauli. + // When handling all-identity Paulis above, we added the RZ rotation angle as global phase, + // meaning that we have implemented of exp(i 2t I). However, what we want it to implement + // exp(-i t I). To only use a single multiplication, we apply a factor of -0.5 here. + // This is faster, in particular as long as the parameter expressions are in Python. if modified_phase { - global_phase = multiply_param(&global_phase, -1.0, py); + global_phase = multiply_param(&global_phase, -0.5, py); } CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase) diff --git a/crates/accelerate/src/circuit_library/pauli_feature_map.rs b/crates/accelerate/src/circuit_library/pauli_feature_map.rs index bb9f8c25eb24..66bea22a7231 100644 --- a/crates/accelerate/src/circuit_library/pauli_feature_map.rs +++ b/crates/accelerate/src/circuit_library/pauli_feature_map.rs @@ -67,12 +67,12 @@ pub fn pauli_feature_map( let pauli_strings = _get_paulis(feature_dimension, paulis)?; // set the default value for entanglement - let default = PyString::new_bound(py, "full"); + let default = PyString::new(py, "full"); let entanglement = entanglement.unwrap_or(&default); // extract the parameters from the input variable ``parameters`` let parameter_vector = parameters - .iter()? + .try_iter()? .map(|el| Param::extract_no_coerce(&el?)) .collect::>>()?; diff --git a/crates/accelerate/src/circuit_library/quantum_volume.rs b/crates/accelerate/src/circuit_library/quantum_volume.rs index a6c5a3839d90..463c123eabb7 100644 --- a/crates/accelerate/src/circuit_library/quantum_volume.rs +++ b/crates/accelerate/src/circuit_library/quantum_volume.rs @@ -113,7 +113,7 @@ pub fn quantum_volume( let num_unitaries = width * depth; let mut permutation: Vec = (0..num_qubits).map(Qubit).collect(); - let kwargs = PyDict::new_bound(py); + let kwargs = PyDict::new(py); kwargs.set_item(intern!(py, "num_qubits"), 2)?; let mut build_instruction = |(unitary_index, unitary_array): (usize, Array2), rng: &mut Pcg64Mcg| @@ -122,7 +122,7 @@ pub fn quantum_volume( if layer_index == 0 { permutation.shuffle(rng); } - let unitary = unitary_array.into_pyarray_bound(py); + let unitary = unitary_array.into_pyarray(py); let unitary_gate = UNITARY_GATE .get_bound(py) @@ -137,7 +137,7 @@ pub fn quantum_volume( let qubit = layer_index * 2; Ok(( PackedOperation::from_gate(Box::new(instruction)), - smallvec![Param::Obj(unitary.unbind().into())], + smallvec![Param::Obj(unitary.into_any().unbind())], vec![permutation[qubit], permutation[qubit + 1]], vec![], )) diff --git a/crates/accelerate/src/commutation_analysis.rs b/crates/accelerate/src/commutation_analysis.rs index 30079beb008c..59c284913da4 100644 --- a/crates/accelerate/src/commutation_analysis.rs +++ b/crates/accelerate/src/commutation_analysis.rs @@ -80,7 +80,7 @@ pub(crate) fn analyze_commutations_inner( // if the node is an input/output node, they do not commute, so we only // continue if the nodes are operation nodes if let (NodeType::Operation(packed_inst0), NodeType::Operation(packed_inst1)) = - (&dag.dag()[current_gate_idx], &dag.dag()[*prev_gate_idx]) + (&dag[current_gate_idx], &dag[*prev_gate_idx]) { let op1 = packed_inst0.op().view(); let op2 = packed_inst1.op().view(); @@ -146,39 +146,40 @@ pub(crate) fn analyze_commutations( // The Python dict will store both of these dictionaries in one. let (commutation_set, node_indices) = analyze_commutations_inner(py, dag, commutation_checker)?; - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); // First set the {wire: [commuting_nodes_1, ...]} bit for (wire, commutations) in commutation_set { // we know all wires are of type Wire::Qubit, since in analyze_commutations_inner // we only iterater over the qubits let py_wire = match wire { - Wire::Qubit(q) => dag.qubits().get(q).unwrap().to_object(py), + Wire::Qubit(q) => dag.qubits().get(q).unwrap().into_pyobject(py), _ => return Err(PyValueError::new_err("Unexpected wire type.")), - }; + }?; out_dict.set_item( py_wire, - PyList::new_bound( + PyList::new( py, commutations.iter().map(|inner| { - PyList::new_bound( + PyList::new( py, inner .iter() .map(|node_index| dag.get_node(py, *node_index).unwrap()), ) + .unwrap() }), - ), + )?, )?; } // Then we add the {(node, wire): index} dictionary for ((node_index, wire), index) in node_indices { let py_wire = match wire { - Wire::Qubit(q) => dag.qubits().get(q).unwrap().to_object(py), + Wire::Qubit(q) => dag.qubits().get(q).unwrap().into_pyobject(py), _ => return Err(PyValueError::new_err("Unexpected wire type.")), - }; + }?; out_dict.set_item((dag.get_node(py, node_index)?, py_wire), index)?; } diff --git a/crates/accelerate/src/commutation_cancellation.rs b/crates/accelerate/src/commutation_cancellation.rs index 5264cb89805f..7c5c16c07dac 100644 --- a/crates/accelerate/src/commutation_cancellation.rs +++ b/crates/accelerate/src/commutation_cancellation.rs @@ -105,14 +105,14 @@ pub(crate) fn cancel_commutations( if let Some(wire_commutation_set) = commutation_set.get(&Wire::Qubit(wire)) { for (com_set_idx, com_set) in wire_commutation_set.iter().enumerate() { if let Some(&nd) = com_set.first() { - if !matches!(dag.dag()[nd], NodeType::Operation(_)) { + if !matches!(dag[nd], NodeType::Operation(_)) { continue; } } else { continue; } for node in com_set.iter() { - let instr = match &dag.dag()[*node] { + let instr = match &dag[*node] { NodeType::Operation(instr) => instr, _ => panic!("Unexpected type in commutation set."), }; @@ -198,7 +198,7 @@ pub(crate) fn cancel_commutations( let mut total_angle: f64 = 0.0; let mut total_phase: f64 = 0.0; for current_node in cancel_set { - let node_op = match &dag.dag()[*current_node] { + let node_op = match &dag[*current_node] { NodeType::Operation(instr) => instr, _ => panic!("Unexpected type in commutation set run."), }; diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs index fe242c73422f..89826578615f 100644 --- a/crates/accelerate/src/commutation_checker.rs +++ b/crates/accelerate/src/commutation_checker.rs @@ -21,21 +21,27 @@ use numpy::PyReadonlyArray2; use pyo3::exceptions::PyRuntimeError; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyBool, PyDict, PySequence, PyTuple}; +use pyo3::types::{PyBool, PyDict, PySequence, PyTuple}; +use pyo3::BoundObject; use qiskit_circuit::bit_data::BitData; use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::imports::QI_OPERATOR; use qiskit_circuit::operations::OperationRef::{Gate as PyGateType, Operation as PyOperationType}; -use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::operations::{ + get_standard_gate_names, Operation, OperationRef, Param, StandardGate, +}; use qiskit_circuit::{BitType, Clbit, Qubit}; use crate::unitary_compose; use crate::QiskitError; +// These gates do not commute with other gates, we do not check them. static SKIPPED_NAMES: [&str; 4] = ["measure", "reset", "delay", "initialize"]; -static NO_CACHE_NAMES: [&str; 2] = ["annotated", "linear_function"]; + +// We keep a hash-set of operations eligible for commutation checking. This is because checking +// eligibility is not for free. static SUPPORTED_OP: Lazy> = Lazy::new(|| { HashSet::from([ "rxx", "ryy", "rzz", "rzx", "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx", @@ -43,25 +49,38 @@ static SUPPORTED_OP: Lazy> = Lazy::new(|| { ]) }); -const TWOPI: f64 = 2.0 * std::f64::consts::PI; - -// map rotation gates to their generators, or to ``None`` if we cannot currently efficiently -// represent the generator in Rust and store the commutation relation in the commutation dictionary -static SUPPORTED_ROTATIONS: Lazy>> = Lazy::new(|| { +// Map rotation gates to their generators (or to ``None`` if we cannot currently efficiently +// represent the generator in Rust and store the commutation relation in the commutation dictionary) +// and their pi-periodicity. Here we mean a gate is n-pi periodic, if for angles that are +// multiples of n*pi, the gate is equal to the identity up to a global phase. +// E.g. RX is generated by X and 2-pi periodic, while CRX is generated by CX and 4-pi periodic. +static SUPPORTED_ROTATIONS: Lazy)>> = Lazy::new(|| { HashMap::from([ - ("rx", Some(OperationRef::Standard(StandardGate::XGate))), - ("ry", Some(OperationRef::Standard(StandardGate::YGate))), - ("rz", Some(OperationRef::Standard(StandardGate::ZGate))), - ("p", Some(OperationRef::Standard(StandardGate::ZGate))), - ("u1", Some(OperationRef::Standard(StandardGate::ZGate))), - ("crx", Some(OperationRef::Standard(StandardGate::CXGate))), - ("cry", Some(OperationRef::Standard(StandardGate::CYGate))), - ("crz", Some(OperationRef::Standard(StandardGate::CZGate))), - ("cp", Some(OperationRef::Standard(StandardGate::CZGate))), - ("rxx", None), // None means the gate is in the commutation dictionary - ("ryy", None), - ("rzx", None), - ("rzz", None), + ("rx", (2, Some(OperationRef::Standard(StandardGate::XGate)))), + ("ry", (2, Some(OperationRef::Standard(StandardGate::YGate)))), + ("rz", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), + ("p", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), + ("u1", (2, Some(OperationRef::Standard(StandardGate::ZGate)))), + ("rxx", (2, None)), // None means the gate is in the commutation dictionary + ("ryy", (2, None)), + ("rzx", (2, None)), + ("rzz", (2, None)), + ( + "crx", + (4, Some(OperationRef::Standard(StandardGate::CXGate))), + ), + ( + "cry", + (4, Some(OperationRef::Standard(StandardGate::CYGate))), + ), + ( + "crz", + (4, Some(OperationRef::Standard(StandardGate::CZGate))), + ), + ( + "cp", + (2, Some(OperationRef::Standard(StandardGate::CZGate))), + ), ]) }); @@ -169,14 +188,10 @@ impl CommutationChecker { cargs2: Option<&Bound>, max_num_qubits: u32, ) -> PyResult { - let qargs1 = - qargs1.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let cargs1 = - cargs1.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let qargs2 = - qargs2.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let cargs2 = - cargs2.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; + let qargs1 = qargs1.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; + let cargs1 = cargs1.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; + let qargs2 = qargs2.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; + let cargs2 = cargs2.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; let (qargs1, qargs2) = get_bits::(py, &qargs1, &qargs2)?; let (cargs1, cargs2) = get_bits::(py, &cargs1, &cargs2)?; @@ -208,15 +223,15 @@ impl CommutationChecker { } fn __getstate__(&self, py: Python) -> PyResult> { - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); out_dict.set_item("cache_max_entries", self.cache_max_entries)?; out_dict.set_item("current_cache_entries", self.current_cache_entries)?; - let cache_dict = PyDict::new_bound(py); + let cache_dict = PyDict::new(py); for (key, value) in &self.cache { cache_dict.set_item(key, commutation_entry_to_pydict(py, value)?)?; } out_dict.set_item("cache", cache_dict)?; - out_dict.set_item("library", self.library.library.to_object(py))?; + out_dict.set_item("library", self.library.library.clone().into_pyobject(py)?)?; out_dict.set_item("gates", self.gates.clone())?; Ok(out_dict.unbind()) } @@ -322,15 +337,17 @@ impl CommutationChecker { (qargs1, qargs2) }; - let skip_cache: bool = NO_CACHE_NAMES.contains(&first_op.name()) || - NO_CACHE_NAMES.contains(&second_op.name()) || - // Skip params that do not evaluate to floats for caching and commutation library - first_params.iter().any(|p| !matches!(p, Param::Float(_))) || - second_params.iter().any(|p| !matches!(p, Param::Float(_))) - && !SUPPORTED_OP.contains(op1.name()) - && !SUPPORTED_OP.contains(op2.name()); - - if skip_cache { + // For our cache to work correctly, we require the gate's definition to only depend on the + // ``params`` attribute. This cannot be guaranteed for custom gates, so we only check + // the cache for our standard gates, which we know are defined by the ``params`` AND + // that the ``params`` are float-only at this point. + let whitelist = get_standard_gate_names(); + let check_cache = whitelist.contains(&first_op.name()) + && whitelist.contains(&second_op.name()) + && first_params.iter().all(|p| matches!(p, Param::Float(_))) + && second_params.iter().all(|p| matches!(p, Param::Float(_))); + + if !check_cache { return self.commute_matmul( py, first_op, @@ -629,22 +646,27 @@ fn map_rotation<'a>( tol: f64, ) -> (&'a OperationRef<'a>, &'a [Param], bool) { let name = op.name(); - if let Some(generator) = SUPPORTED_ROTATIONS.get(name) { - // if the rotation angle is below the tolerance, the gate is assumed to + + if let Some((pi_multiple, generator)) = SUPPORTED_ROTATIONS.get(name) { + // If the rotation angle is below the tolerance, the gate is assumed to // commute with everything, and we simply return the operation with the flag that - // it commutes trivially + // it commutes trivially. if let Param::Float(angle) = params[0] { - if (angle % TWOPI).abs() < tol { + let periodicity = (*pi_multiple as f64) * ::std::f64::consts::PI; + if (angle % periodicity).abs() < tol { return (op, params, true); }; }; - // otherwise, we check if a generator is given -- if not, we'll just return the operation - // itself (e.g. RXX does not have a generator and is just stored in the commutations - // dictionary) + // Otherwise we need to cover two cases -- either a generator is given, in which case + // we return it, or we don't have a generator yet, but we know we have the operation + // stored in the commutation library. For example, RXX does not have a generator in Rust + // yet (PauliGate is not in Rust currently), but it is stored in the library, so we + // can strip the parameters and just return the gate. if let Some(gate) = generator { return (gate, &[], false); }; + return (op, &[], false); } (op, params, false) } @@ -715,6 +737,28 @@ pub enum CommutationLibraryEntry { QubitMapping(HashMap; 2]>, bool>), } +impl<'py> IntoPyObject<'py> for CommutationLibraryEntry { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let py_out = match self { + CommutationLibraryEntry::Commutes(b) => b.into_pyobject(py)?.into_bound().into_any(), + CommutationLibraryEntry::QubitMapping(qm) => { + let out = PyDict::new(py); + for (k, v) in qm { + let key = PyTuple::new(py, k.iter().map(|q| q.map(|t| t.0)))?; + let value = PyBool::new(py, v); + out.set_item(key, value)?; + } + out.into_any() + } + }; + Ok(py_out) + } +} + impl<'py> FromPyObject<'py> for CommutationLibraryEntry { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { if let Ok(b) = b.extract::() { @@ -732,25 +776,6 @@ impl<'py> FromPyObject<'py> for CommutationLibraryEntry { } } -impl ToPyObject for CommutationLibraryEntry { - fn to_object(&self, py: Python) -> PyObject { - match self { - CommutationLibraryEntry::Commutes(b) => b.into_py(py), - CommutationLibraryEntry::QubitMapping(qm) => qm - .iter() - .map(|(k, v)| { - ( - PyTuple::new_bound(py, k.iter().map(|q| q.map(|t| t.0))), - PyBool::new_bound(py, *v), - ) - }) - .into_py_dict_bound(py) - .unbind() - .into(), - } - } -} - type CacheKey = ( SmallVec<[Option; 2]>, (SmallVec<[ParameterKey; 3]>, SmallVec<[ParameterKey; 3]>), @@ -759,14 +784,14 @@ type CacheKey = ( type CommutationCacheEntry = HashMap; fn commutation_entry_to_pydict(py: Python, entry: &CommutationCacheEntry) -> PyResult> { - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); for (k, v) in entry.iter() { - let qubits = PyTuple::new_bound(py, k.0.iter().map(|q| q.map(|t| t.0))); - let params0 = PyTuple::new_bound(py, k.1 .0.iter().map(|pk| pk.0)); - let params1 = PyTuple::new_bound(py, k.1 .1.iter().map(|pk| pk.0)); + let qubits = PyTuple::new(py, k.0.iter().map(|q| q.map(|t| t.0)))?; + let params0 = PyTuple::new(py, k.1 .0.iter().map(|pk| pk.0))?; + let params1 = PyTuple::new(py, k.1 .1.iter().map(|pk| pk.0))?; out_dict.set_item( - PyTuple::new_bound(py, [qubits, PyTuple::new_bound(py, [params0, params1])]), - PyBool::new_bound(py, *v), + PyTuple::new(py, [qubits, PyTuple::new(py, [params0, params1])?])?, + PyBool::new(py, *v), )?; } Ok(out_dict.unbind()) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index dacefbf0bf0a..cdc27b739ef1 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -100,7 +100,7 @@ pub(crate) fn consolidate_blocks( block_qargs.clear(); if block.len() == 1 { let inst_node = block[0]; - let inst = dag.dag()[inst_node].unwrap_operation(); + let inst = dag[inst_node].unwrap_operation(); if !is_supported( target, basis_gates.as_ref(), @@ -112,7 +112,7 @@ pub(crate) fn consolidate_blocks( Ok(mat) => mat, Err(_) => continue, }; - let array = matrix.into_pyarray_bound(py); + let array = matrix.into_pyarray(py); let unitary_gate = UNITARY_GATE .get_bound(py) .call1((array, py.None(), false))?; @@ -123,7 +123,7 @@ pub(crate) fn consolidate_blocks( let mut basis_count: usize = 0; let mut outside_basis = false; for node in &block { - let inst = dag.dag()[*node].unwrap_operation(); + let inst = dag[*node].unwrap_operation(); block_qargs.extend(dag.get_qargs(inst.qubits())); all_block_gates.insert(*node); if inst.op().name() == basis_gate_name { @@ -151,7 +151,7 @@ pub(crate) fn consolidate_blocks( block_qargs.len() as u32, 0, block.iter().map(|node| { - let inst = dag.dag()[*node].unwrap_operation(); + let inst = dag[*node].unwrap_operation(); Ok(( inst.op().clone(), @@ -183,10 +183,11 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let unitary_gate = - UNITARY_GATE - .get_bound(py) - .call1((array.to_object(py), py.None(), false))?; + let unitary_gate = UNITARY_GATE.get_bound(py).call1(( + array.as_ref().into_pyobject(py)?, + py.None(), + false, + ))?; let clbit_pos_map = HashMap::new(); dag.replace_block_with_py_op( py, @@ -215,7 +216,7 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let array = matrix.into_pyarray_bound(py); + let array = matrix.into_pyarray(py); let unitary_gate = UNITARY_GATE .get_bound(py) @@ -245,7 +246,7 @@ pub(crate) fn consolidate_blocks( continue; } let first_inst_node = run[0]; - let first_inst = dag.dag()[first_inst_node].unwrap_operation(); + let first_inst = dag[first_inst_node].unwrap_operation(); let first_qubits = dag.get_qargs(first_inst.qubits()); if run.len() == 1 @@ -260,7 +261,7 @@ pub(crate) fn consolidate_blocks( Ok(mat) => mat, Err(_) => continue, }; - let array = matrix.into_pyarray_bound(py); + let array = matrix.into_pyarray(py); let unitary_gate = UNITARY_GATE .get_bound(py) .call1((array, py.None(), false))?; @@ -275,7 +276,7 @@ pub(crate) fn consolidate_blocks( if all_block_gates.contains(node) { already_in_block = true; } - let gate = dag.dag()[*node].unwrap_operation(); + let gate = dag[*node].unwrap_operation(); let operator = match get_matrix_from_inst(py, gate) { Ok(mat) => mat, Err(_) => { @@ -295,7 +296,7 @@ pub(crate) fn consolidate_blocks( dag.remove_op_node(node); } } else { - let array = aview2(&matrix).to_owned().into_pyarray_bound(py); + let array = aview2(&matrix).to_owned().into_pyarray(py); let unitary_gate = UNITARY_GATE .get_bound(py) .call1((array, py.None(), false))?; diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index 135e2b22eb56..5da4f7d6bcd1 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -72,7 +72,7 @@ pub fn blocks_to_matrix( let mut one_qubit_components_modified = false; let mut output_matrix: Option> = None; for node in op_list { - let inst = dag.dag()[*node].unwrap_operation(); + let inst = dag[*node].unwrap_operation(); let op_matrix = get_matrix_from_inst(py, inst)?; match dag .get_qargs(inst.qubits()) diff --git a/crates/accelerate/src/dense_layout.rs b/crates/accelerate/src/dense_layout.rs index cbe8b9ff8cc7..398a2d2cd70c 100644 --- a/crates/accelerate/src/dense_layout.rs +++ b/crates/accelerate/src/dense_layout.rs @@ -122,9 +122,9 @@ pub fn best_subset( err, ); ( - rows.into_pyarray_bound(py).into(), - cols.into_pyarray_bound(py).into(), - best_map.into_pyarray_bound(py).into(), + rows.into_pyarray(py).into_any().unbind(), + cols.into_pyarray(py).into_any().unbind(), + best_map.into_pyarray(py).into_any().unbind(), ) } diff --git a/crates/accelerate/src/edge_collections.rs b/crates/accelerate/src/edge_collections.rs index b64cbc42c356..825016229c6c 100644 --- a/crates/accelerate/src/edge_collections.rs +++ b/crates/accelerate/src/edge_collections.rs @@ -55,7 +55,7 @@ impl EdgeCollection { /// output array here would be ``[0, 1, 1, 2, 2, 3]``. #[pyo3(text_signature = "(self, /)")] pub fn edges(&self, py: Python) -> PyObject { - self.edges.clone().into_pyarray_bound(py).into() + self.edges.clone().into_pyarray(py).into_any().unbind() } fn __getstate__(&self) -> Vec { diff --git a/crates/accelerate/src/elide_permutations.rs b/crates/accelerate/src/elide_permutations.rs index f74857d02db5..d583b23726f9 100644 --- a/crates/accelerate/src/elide_permutations.rs +++ b/crates/accelerate/src/elide_permutations.rs @@ -39,7 +39,7 @@ fn run(py: Python, dag: &mut DAGCircuit) -> PyResult { let qargs = dag.get_qargs(inst.qubits()); diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index 1cc1a14eba1e..9f037c8d0cec 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -78,10 +78,7 @@ impl Key { } fn __getnewargs__(slf: PyRef) -> (Bound, u32) { - ( - PyString::new_bound(slf.py(), slf.name.as_str()), - slf.num_qubits, - ) + (PyString::new(slf.py(), slf.name.as_str()), slf.num_qubits) } // Ord methods for Python @@ -307,6 +304,19 @@ impl<'py> FromPyObject<'py> for GateOper { #[derive(Debug, Clone)] pub struct CircuitFromPython(pub CircuitData); +impl<'py> IntoPyObject<'py> for CircuitFromPython { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(QUANTUM_CIRCUIT + .get_bound(py) + .call_method1("_from_circuit_data", (self.0,))? + .clone()) + } +} + impl FromPyObject<'_> for CircuitFromPython { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { if ob.is_instance(QUANTUM_CIRCUIT.get_bound(ob.py()))? { @@ -322,22 +332,6 @@ impl FromPyObject<'_> for CircuitFromPython { } } -impl IntoPy for CircuitFromPython { - fn into_py(self, py: Python<'_>) -> PyObject { - QUANTUM_CIRCUIT - .get_bound(py) - .call_method1("_from_circuit_data", (self.0,)) - .unwrap() - .unbind() - } -} - -impl ToPyObject for CircuitFromPython { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.clone().into_py(py) - } -} - // Custom Types type GraphType = StableDiGraph>; type KTIType = IndexMap; @@ -465,7 +459,7 @@ impl EquivalenceLibrary { ._get_equivalences(&key) .into_iter() .filter_map(|equivalence| rebind_equiv(py, equivalence, &query_params).ok()); - let return_list = PyList::empty_bound(py); + let return_list = PyList::empty(py); for equiv in bound_equivalencies { return_list.append(equiv)?; } @@ -512,9 +506,9 @@ impl EquivalenceLibrary { /// List: Keys to the key to node index map. #[pyo3(name = "keys")] fn py_keys(slf: PyRef) -> PyResult { - let py_dict = PyDict::new_bound(slf.py()); + let py_dict = PyDict::new(slf.py()); for key in slf.keys() { - py_dict.set_item(key.clone().into_py(slf.py()), slf.py().None())?; + py_dict.set_item(key.clone(), slf.py().None())?; } Ok(py_dict.as_any().call_method0("keys")?.into()) } @@ -532,26 +526,26 @@ impl EquivalenceLibrary { } fn __getstate__(slf: PyRef) -> PyResult> { - let ret = PyDict::new_bound(slf.py()); + let ret = PyDict::new(slf.py()); ret.set_item("rule_id", slf.rule_id)?; - let key_to_usize_node: Bound = PyDict::new_bound(slf.py()); + let key_to_usize_node: Bound = PyDict::new(slf.py()); for (key, val) in slf.key_to_node_index.iter() { - key_to_usize_node.set_item(key.clone().into_py(slf.py()), val.index())?; + key_to_usize_node.set_item(key.clone(), val.index())?; } ret.set_item("key_to_node_index", key_to_usize_node)?; - let graph_nodes: Bound = PyList::empty_bound(slf.py()); + let graph_nodes: Bound = PyList::empty(slf.py()); for weight in slf.graph.node_weights() { - graph_nodes.append(weight.clone().into_py(slf.py()))?; + graph_nodes.append(weight.clone())?; } ret.set_item("graph_nodes", graph_nodes.unbind())?; let edges = slf.graph.edge_references().map(|edge| { ( edge.source().index(), edge.target().index(), - edge.weight().clone().into_py(slf.py()), + edge.weight().clone().into_pyobject(slf.py()).unwrap(), ) }); - let graph_edges = PyList::empty_bound(slf.py()); + let graph_edges = PyList::empty(slf.py()); for edge in edges { graph_edges.append(edge)?; } @@ -720,7 +714,7 @@ fn raise_if_param_mismatch( gate_params: &[Param], circuit_parameters: Bound, ) -> PyResult<()> { - let gate_params_obj = PySet::new_bound( + let gate_params_obj = PySet::new( py, gate_params .iter() @@ -801,10 +795,10 @@ impl Display for EquivalenceError { // Conversion helpers -fn to_pygraph(py: Python, pet_graph: &StableDiGraph) -> PyResult +fn to_pygraph<'py, N, E>(py: Python<'py>, pet_graph: &'py StableDiGraph) -> PyResult where - N: IntoPy + Clone, - E: IntoPy + Clone, + N: IntoPyObject<'py> + Clone, + E: IntoPyObject<'py> + Clone, { let graph = PYDIGRAPH.get_bound(py).call0()?; let node_weights: Vec = pet_graph.node_weights().cloned().collect(); diff --git a/crates/accelerate/src/error_map.rs b/crates/accelerate/src/error_map.rs index 1fe3cb254914..398d504d7840 100644 --- a/crates/accelerate/src/error_map.rs +++ b/crates/accelerate/src/error_map.rs @@ -12,6 +12,7 @@ use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; +use pyo3::IntoPyObjectExt; use crate::nlayout::PhysicalQubit; @@ -101,14 +102,19 @@ impl ErrorMap { } #[pyo3(signature=(key, default=None))] - fn get(&self, py: Python, key: [PhysicalQubit; 2], default: Option) -> PyObject { - match self.error_map.get(&key).copied() { - Some(val) => val.to_object(py), + fn get( + &self, + py: Python, + key: [PhysicalQubit; 2], + default: Option, + ) -> PyResult { + Ok(match self.error_map.get(&key).copied() { + Some(val) => val.into_py_any(py)?, None => match default { Some(val) => val, None => py.None(), }, - } + }) } } diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index e66b69be576a..7e3c20285e46 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -24,6 +24,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{PyList, PyString}; use pyo3::wrap_pyfunction; +use pyo3::IntoPyObjectExt; use pyo3::Python; use ndarray::prelude::*; @@ -37,7 +38,7 @@ use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::c64; -use qiskit_circuit::Qubit; +use qiskit_circuit::{impl_intopyobject_for_copy_pyclass, Qubit}; use crate::nlayout::PhysicalQubit; use crate::target_transpiler::Target; @@ -109,11 +110,13 @@ impl OneQubitGateSequence { fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { match idx.with_len(self.gates.len())? { - SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), - indices => Ok(PyList::new_bound( + SequenceIndex::Int(idx) => Ok((&self.gates[idx]).into_py_any(py)?), + indices => Ok(PyList::new( py, - indices.iter().map(|pos| self.gates[pos].to_object(py)), - ) + indices + .iter() + .map(|pos| (&self.gates[pos]).into_pyobject(py).unwrap()), + )? .into_any() .unbind()), } @@ -690,6 +693,7 @@ pub enum EulerBasis { ZSXX = 10, ZSX = 11, } +impl_intopyobject_for_copy_pyclass!(EulerBasis); impl EulerBasis { pub fn as_str(&self) -> &'static str { @@ -712,12 +716,8 @@ impl EulerBasis { #[pymethods] impl EulerBasis { - fn __reduce__(&self, py: Python) -> Py { - ( - py.get_type_bound::(), - (PyString::new_bound(py, self.as_str()),), - ) - .into_py(py) + fn __reduce__(&self, py: Python) -> PyResult> { + (py.get_type::(), (PyString::new(py, self.as_str()),)).into_py_any(py) } #[new] @@ -1089,7 +1089,7 @@ pub(crate) fn optimize_1q_gates_decomposition( Some(_) => 1., None => raw_run.len() as f64, }; - let qubit: PhysicalQubit = if let NodeType::Operation(inst) = &dag.dag()[raw_run[0]] { + let qubit: PhysicalQubit = if let NodeType::Operation(inst) = &dag[raw_run[0]] { PhysicalQubit::new(dag.get_qargs(inst.qubits())[0].0) } else { unreachable!("nodes in runs will always be op nodes") @@ -1175,7 +1175,7 @@ pub(crate) fn optimize_1q_gates_decomposition( let operator = raw_run .iter() .map(|node_index| { - let node = &dag.dag()[*node_index]; + let node = &dag[*node_index]; if let NodeType::Operation(inst) = node { if let Some(target) = target { error *= compute_error_term_from_target(inst.op().name(), target, qubit); @@ -1218,7 +1218,7 @@ pub(crate) fn optimize_1q_gates_decomposition( let mut outside_basis = false; if let Some(basis) = basis_gates { for node in &raw_run { - if let NodeType::Operation(inst) = &dag.dag()[*node] { + if let NodeType::Operation(inst) = &dag[*node] { if !basis.contains(inst.op().name()) { outside_basis = true; break; diff --git a/crates/accelerate/src/filter_op_nodes.rs b/crates/accelerate/src/filter_op_nodes.rs index 7c41391f3788..eb93e5b3d9bf 100644 --- a/crates/accelerate/src/filter_op_nodes.rs +++ b/crates/accelerate/src/filter_op_nodes.rs @@ -29,7 +29,7 @@ pub fn py_filter_op_nodes( predicate.call1((dag_op_node,))?.extract() }; let mut remove_nodes: Vec = Vec::new(); - for node in dag.op_nodes(true) { + for node in dag.op_node_indices(true) { if !callable(node)? { remove_nodes.push(node); } diff --git a/crates/accelerate/src/gate_direction.rs b/crates/accelerate/src/gate_direction.rs index 4dcb5241a088..ba9a9a268a31 100755 --- a/crates/accelerate/src/gate_direction.rs +++ b/crates/accelerate/src/gate_direction.rs @@ -23,7 +23,7 @@ use qiskit_circuit::{ circuit_instruction::CircuitInstruction, circuit_instruction::ExtraInstructionAttributes, converters::{circuit_to_dag, QuantumCircuitData}, - dag_circuit::{DAGCircuit, NodeType}, + dag_circuit::DAGCircuit, dag_node::{DAGNode, DAGOpNode}, imports, imports::get_std_gate_class, @@ -105,11 +105,7 @@ fn check_gate_direction( where T: Fn(&PackedInstruction, &[Qubit]) -> bool, { - for node in dag.op_nodes(false) { - let NodeType::Operation(packed_inst) = &dag.dag()[node] else { - panic!("PackedInstruction is expected"); - }; - + for (_, packed_inst) in dag.op_nodes(false) { let inst_qargs = dag.get_qargs(packed_inst.qubits()); if let OperationRef::Instruction(py_inst) = packed_inst.op().view() { @@ -117,7 +113,7 @@ where let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); let py_inst = py_inst.instruction.bind(py); - for block in py_inst.getattr("blocks")?.iter()? { + for block in py_inst.getattr("blocks")?.try_iter()? { let inner_dag: DAGCircuit = circuit_to_dag.call1((block?,))?.extract()?; let block_ok = if let Some(mapping) = qubit_mapping { @@ -254,9 +250,7 @@ where let mut nodes_to_replace: Vec<(NodeIndex, DAGCircuit)> = Vec::new(); let mut ops_to_replace: Vec<(NodeIndex, Vec>)> = Vec::new(); - for node in dag.op_nodes(false) { - let packed_inst = dag.dag()[node].unwrap_operation(); - + for (node, packed_inst) in dag.op_nodes(false) { let op_args = dag.get_qargs(packed_inst.qubits()); if let OperationRef::Instruction(py_inst) = packed_inst.op().view() { @@ -357,7 +351,7 @@ where } for (node, op_blocks) in ops_to_replace { - let packed_inst = dag.dag()[node].unwrap_operation(); + let packed_inst = dag[node].unwrap_operation(); let OperationRef::Instruction(py_inst) = packed_inst.op().view() else { panic!("PyInstruction is expected"); }; @@ -389,7 +383,7 @@ fn has_calibration_for_op_node( packed_inst: &PackedInstruction, qargs: &[Qubit], ) -> PyResult { - let py_args = PyTuple::new_bound(py, dag.qubits().map_indices(qargs)); + let py_args = PyTuple::new(py, dag.qubits().map_indices(qargs))?; let dag_op_node = Py::new( py, @@ -398,17 +392,12 @@ fn has_calibration_for_op_node( instruction: CircuitInstruction { operation: packed_inst.op().clone(), qubits: py_args.unbind(), - clbits: PyTuple::empty_bound(py).unbind(), - params: packed_inst - .params_view() - .iter() - .map(|param| param.clone_ref(py)) - .collect(), + clbits: PyTuple::empty(py).unbind(), + params: packed_inst.params_view().iter().cloned().collect(), extra_attrs: packed_inst.extra_attrs().clone(), #[cfg(feature = "cache_pygates")] py_op: packed_inst.py_op().clone(), }, - sort_key: "".into_py(py), }, DAGNode { node: None }, ), diff --git a/crates/accelerate/src/gates_in_basis.rs b/crates/accelerate/src/gates_in_basis.rs index f5980afca27d..937e7b1c6fcc 100644 --- a/crates/accelerate/src/gates_in_basis.rs +++ b/crates/accelerate/src/gates_in_basis.rs @@ -79,8 +79,7 @@ fn any_gate_missing_from_target(dag: &DAGCircuit, target: &Target) -> PyResult = Vec::new(); let max_index = gate_cancel_run.len() - 1; for (i, cancel_gate) in gate_cancel_run.iter().enumerate() { - let node = &dag.dag()[*cancel_gate]; + let node = &dag[*cancel_gate]; if let NodeType::Operation(inst) = node { if gate_eq(py, inst, &gate)? { chunk.push(*cancel_gate); @@ -78,13 +78,12 @@ fn run_on_self_inverse( if i == max_index { partitions.push(std::mem::take(&mut chunk)); } else { - let next_qargs = if let NodeType::Operation(next_inst) = - &dag.dag()[gate_cancel_run[i + 1]] - { - next_inst.qubits() - } else { - panic!("Not an op node") - }; + let next_qargs = + if let NodeType::Operation(next_inst) = &dag[gate_cancel_run[i + 1]] { + next_inst.qubits() + } else { + panic!("Not an op node") + }; if inst.qubits() != next_qargs { partitions.push(std::mem::take(&mut chunk)); } @@ -132,8 +131,8 @@ fn run_on_inverse_pairs( for nodes in runs { let mut i = 0; while i < nodes.len() - 1 { - if let NodeType::Operation(inst) = &dag.dag()[nodes[i]] { - if let NodeType::Operation(next_inst) = &dag.dag()[nodes[i + 1]] { + if let NodeType::Operation(inst) = &dag[nodes[i]] { + if let NodeType::Operation(next_inst) = &dag[nodes[i + 1]] { if inst.qubits() == next_inst.qubits() && ((gate_eq(py, inst, &gate_0)? && gate_eq(py, next_inst, &gate_1)?) || (gate_eq(py, inst, &gate_1)? diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index a54116b2b2f9..38e49084d840 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -36,8 +36,9 @@ pub fn reverse_qubit_state( epsilon: f64, ) -> PyObject { reverse_qubit_state_inner(&state, basis_state, epsilon) - .into_pyarray_bound(py) - .into() + .into_pyarray(py) + .into_any() + .unbind() } #[inline(always)] @@ -105,7 +106,7 @@ pub fn find_squs_for_disentangling( output.append(&mut squs); output .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect() } @@ -132,7 +133,7 @@ pub fn apply_ucg( m[[i + spacing, col]] = gate[[1, 0]] * a + gate[[1, 1]] * b; } } - m.into_pyarray_bound(py).into() + m.into_pyarray(py).into_any().unbind() } #[inline(always)] @@ -165,7 +166,7 @@ pub fn apply_diagonal_gate( m[[i, j]] = diag[diag_index] * m[[i, j]] } } - Ok(m.into_pyarray_bound(py).into()) + Ok(m.into_pyarray(py).into_any().unbind()) } #[pyfunction] @@ -247,7 +248,7 @@ pub fn apply_multi_controlled_gate( m[[e1, i]] = temp[0]; m[[e2, i]] = temp[1]; } - return m.into_pyarray_bound(py).into(); + return m.into_pyarray(py).into_any().unbind(); } for state_free in std::iter::repeat([0_u8, 1_u8]) .take(free_qubits) @@ -264,7 +265,7 @@ pub fn apply_multi_controlled_gate( m[[e2, i]] = temp[1]; } } - m.into_pyarray_bound(py).into() + m.into_pyarray(py).into_any().unbind() } #[pyfunction] @@ -317,7 +318,7 @@ pub fn merge_ucgate_and_diag( .map(|(i, raw_gate)| { let gate = raw_gate.as_array(); let res = aview2(&[[diag[2 * i], C_ZERO], [C_ZERO, diag[2 * i + 1]]]).dot(&gate); - res.into_pyarray_bound(py).into() + res.into_pyarray(py).into_any().unbind() }) .collect() } diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 45cf047a6808..e26689f4f0c3 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -10,10 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -// This stylistic lint suppression should be in `Cargo.toml`, but we can't do that until we're at an -// MSRV of 1.74 or greater. -#![allow(clippy::comparison_chain)] - use std::env; use pyo3::import_exception; @@ -58,7 +54,6 @@ pub mod twirling; pub mod two_qubit_decompose; pub mod uc_gate; pub mod unitary_synthesis; -pub mod utils; pub mod vf2_layout; mod rayon_ext; diff --git a/crates/accelerate/src/nlayout.rs b/crates/accelerate/src/nlayout.rs index dcd6e71fafa8..82f461fa0d61 100644 --- a/crates/accelerate/src/nlayout.rs +++ b/crates/accelerate/src/nlayout.rs @@ -12,6 +12,7 @@ use pyo3::prelude::*; use pyo3::types::PyList; +use pyo3::IntoPyObjectExt; use hashbrown::HashMap; @@ -23,7 +24,9 @@ use hashbrown::HashMap; /// overhead, so we just allow conversion to and from any valid `PyLong`. macro_rules! qubit_newtype { ($id: ident) => { - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntoPyObject, IntoPyObjectRef, + )] pub struct $id(pub u32); impl $id { @@ -37,17 +40,6 @@ macro_rules! qubit_newtype { } } - impl pyo3::IntoPy for $id { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } - } - impl pyo3::ToPyObject for $id { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.0.to_object(py) - } - } - impl pyo3::FromPyObject<'_> for $id { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { Ok(Self(ob.extract()?)) @@ -57,8 +49,8 @@ macro_rules! qubit_newtype { unsafe impl numpy::Element for $id { const IS_COPY: bool = true; - fn get_dtype_bound(py: Python<'_>) -> Bound<'_, numpy::PyArrayDescr> { - u32::get_dtype_bound(py) + fn get_dtype(py: Python<'_>) -> Bound<'_, numpy::PyArrayDescr> { + u32::get_dtype(py) } fn clone_ref(&self, _py: Python<'_>) -> Self { @@ -123,12 +115,11 @@ impl NLayout { } fn __reduce__(&self, py: Python) -> PyResult> { - Ok(( - py.get_type_bound::() - .getattr("from_virtual_to_physical")?, - (self.virt_to_phys.to_object(py),), + ( + py.get_type::().getattr("from_virtual_to_physical")?, + (self.virt_to_phys.clone().into_pyobject(py)?,), ) - .into_py(py)) + .into_py_any(py) } /// Return the layout mapping. @@ -143,8 +134,8 @@ impl NLayout { /// where the virtual qubit is the index in the qubit index in the circuit. /// #[pyo3(text_signature = "(self, /)")] - fn layout_mapping(&self, py: Python<'_>) -> Py { - PyList::new_bound(py, self.iter_virtual()).into() + fn layout_mapping(&self, py: Python<'_>) -> PyResult> { + PyList::new(py, self.iter_virtual()).map(|x| x.unbind()) } /// Get physical bit from virtual bit diff --git a/crates/accelerate/src/pauli_exp_val.rs b/crates/accelerate/src/pauli_exp_val.rs index bf9569b485e1..d7ca85523020 100644 --- a/crates/accelerate/src/pauli_exp_val.rs +++ b/crates/accelerate/src/pauli_exp_val.rs @@ -26,10 +26,10 @@ const PARALLEL_THRESHOLD: usize = 19; #[pulp::with_simd(fast_sum = pulp::Arch::new())] #[inline(always)] pub fn fast_sum_with_simd(simd: S, values: &[f64]) -> f64 { - let (head, tail) = S::f64s_as_simd(values); + let (head, tail) = S::as_simd_f64s(values); let sum: f64 = head .iter() - .fold(0., |acc, chunk| acc + simd.f64s_reduce_sum(*chunk)); + .fold(0., |acc, chunk| acc + simd.reduce_sum_f64s(*chunk)); sum + tail.iter().sum::() } diff --git a/crates/accelerate/src/remove_diagonal_gates_before_measure.rs b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs index d7713fc4f516..1704edf0f946 100644 --- a/crates/accelerate/src/remove_diagonal_gates_before_measure.rs +++ b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs @@ -47,18 +47,13 @@ fn run_remove_diagonal_before_measure(dag: &mut DAGCircuit) -> PyResult<()> { static DIAGONAL_3Q_GATES: [StandardGate; 1] = [StandardGate::CCZGate]; let mut nodes_to_remove = Vec::new(); - for index in dag.op_nodes(true) { - let node = &dag.dag()[index]; - let NodeType::Operation(inst) = node else { - panic!() - }; - + for (index, inst) in dag.op_nodes(true) { if inst.op().name() == "measure" { let predecessor = (dag.quantum_predecessors(index)) .next() .expect("index is an operation node, so it must have a predecessor."); - match &dag.dag()[predecessor] { + match &dag[predecessor] { NodeType::Operation(pred_inst) => match pred_inst.standard_gate() { Some(gate) => { if DIAGONAL_1Q_GATES.contains(&gate) { @@ -66,18 +61,15 @@ fn run_remove_diagonal_before_measure(dag: &mut DAGCircuit) -> PyResult<()> { } else if DIAGONAL_2Q_GATES.contains(&gate) || DIAGONAL_3Q_GATES.contains(&gate) { - let successors = dag.quantum_successors(predecessor); - let remove_s = successors - .map(|s| { - let node_s = &dag.dag()[s]; - if let NodeType::Operation(inst_s) = node_s { - inst_s.op().name() == "measure" - } else { - false - } - }) - .all(|ok_to_remove| ok_to_remove); - if remove_s { + let mut successors = dag.quantum_successors(predecessor); + if successors.all(|s| { + let node_s = &dag.dag()[s]; + if let NodeType::Operation(inst_s) = node_s { + inst_s.op().name() == "measure" + } else { + false + } + }) { nodes_to_remove.push(predecessor); } } diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index ac13af9cc532..49e1001f1323 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -74,8 +74,7 @@ fn remove_identity_equiv( } }; - for op_node in dag.op_nodes(false) { - let inst = dag.dag()[op_node].unwrap_operation(); + for (op_node, inst) in dag.op_nodes(false) { match inst.op().view() { OperationRef::Standard(gate) => { let (dim, trace) = match gate { diff --git a/crates/accelerate/src/results/marginalization.rs b/crates/accelerate/src/results/marginalization.rs index 83fb5097ac36..85c2e77fd9d4 100644 --- a/crates/accelerate/src/results/marginalization.rs +++ b/crates/accelerate/src/results/marginalization.rs @@ -19,6 +19,7 @@ use num_complex::Complex64; use numpy::IntoPyArray; use numpy::{PyReadonlyArray1, PyReadonlyArray2, PyReadonlyArray3}; use pyo3::prelude::*; +use pyo3::IntoPyObjectExt; use rayon::prelude::*; fn marginalize( @@ -132,7 +133,7 @@ pub fn marginal_memory( let first_elem = memory.first(); if first_elem.is_none() { let res: Vec = Vec::new(); - return Ok(res.to_object(py)); + return res.into_py_any(py); } let clbit_size = hex_to_bin(first_elem.unwrap()).len(); @@ -154,16 +155,16 @@ pub fn marginal_memory( .iter() .map(|x| BigUint::parse_bytes(x.as_bytes(), 2).unwrap()) .collect::>() - .to_object(py)) + .into_py_any(py)?) } else { Ok(out_mem .par_iter() .map(|x| BigUint::parse_bytes(x.as_bytes(), 2).unwrap()) .collect::>() - .to_object(py)) + .into_py_any(py)?) } } else { - Ok(out_mem.to_object(py)) + out_mem.into_py_any(py) } } @@ -178,7 +179,7 @@ pub fn marginal_measure_level_0( let new_shape = [input_shape[0], indices.len(), input_shape[2]]; let out_arr: Array3 = Array3::from_shape_fn(new_shape, |(i, j, k)| mem_arr[[i, indices[j], k]]); - out_arr.into_pyarray_bound(py).into() + out_arr.into_pyarray(py).into_any().unbind() } #[pyfunction] @@ -192,7 +193,7 @@ pub fn marginal_measure_level_0_avg( let new_shape = [indices.len(), input_shape[1]]; let out_arr: Array2 = Array2::from_shape_fn(new_shape, |(i, j)| mem_arr[[indices[i], j]]); - out_arr.into_pyarray_bound(py).into() + out_arr.into_pyarray(py).into_any().unbind() } #[pyfunction] @@ -206,7 +207,7 @@ pub fn marginal_measure_level_1( let new_shape = [input_shape[0], indices.len()]; let out_arr: Array2 = Array2::from_shape_fn(new_shape, |(i, j)| mem_arr[[i, indices[j]]]); - out_arr.into_pyarray_bound(py).into() + out_arr.into_pyarray(py).into_any().unbind() } #[pyfunction] @@ -217,5 +218,5 @@ pub fn marginal_measure_level_1_avg( ) -> PyResult { let mem_arr: &[Complex64] = memory.as_slice()?; let out_arr: Vec = indices.into_iter().map(|idx| mem_arr[idx]).collect(); - Ok(out_arr.into_pyarray_bound(py).into()) + Ok(out_arr.into_pyarray(py).into_any().unbind()) } diff --git a/crates/accelerate/src/sabre/heuristic.rs b/crates/accelerate/src/sabre/heuristic.rs index ea3b73265c77..49272a0b9260 100644 --- a/crates/accelerate/src/sabre/heuristic.rs +++ b/crates/accelerate/src/sabre/heuristic.rs @@ -13,8 +13,11 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyString; +use pyo3::IntoPyObjectExt; use pyo3::Python; +use qiskit_circuit::impl_intopyobject_for_copy_pyclass; + /// Affect the dynamic scaling of the weight of node-set-based heuristics (basic and lookahead). #[pyclass] #[pyo3(module = "qiskit._accelerate.sabre", frozen, eq)] @@ -26,6 +29,7 @@ pub enum SetScaling { /// the weight will be multiplied by ``0.2``). Size, } +impl_intopyobject_for_copy_pyclass!(SetScaling); #[pymethods] impl SetScaling { pub fn __reduce__(&self, py: Python) -> PyResult> { @@ -33,11 +37,11 @@ impl SetScaling { SetScaling::Constant => "Constant", SetScaling::Size => "Size", }; - Ok(( - py.import_bound("builtins")?.getattr("getattr")?, - (py.get_type_bound::(), name), + ( + py.import("builtins")?.getattr("getattr")?, + (py.get_type::(), name), ) - .into_py(py)) + .into_py_any(py) } } @@ -53,6 +57,7 @@ pub struct BasicHeuristic { /// Set the dynamic scaling of the weight based on the layer it is applying to. pub scale: SetScaling, } +impl_intopyobject_for_copy_pyclass!(BasicHeuristic); #[pymethods] impl BasicHeuristic { #[new] @@ -60,8 +65,8 @@ impl BasicHeuristic { Self { weight, scale } } - pub fn __getnewargs__(&self, py: Python) -> Py { - (self.weight, self.scale).into_py(py) + pub fn __getnewargs__(&self, py: Python) -> PyResult> { + (self.weight, self.scale).into_py_any(py) } pub fn __eq__(&self, py: Python, other: Py) -> bool { @@ -74,9 +79,9 @@ impl BasicHeuristic { pub fn __repr__(&self, py: Python) -> PyResult> { let fmt = "BasicHeuristic(weight={!r}, scale={!r})"; - Ok(PyString::new_bound(py, fmt) + PyString::new(py, fmt) .call_method1("format", (self.weight, self.scale))? - .into_py(py)) + .into_py_any(py) } } @@ -94,6 +99,7 @@ pub struct LookaheadHeuristic { /// Dynamic scaling of the heuristic weight depending on the lookahead set. pub scale: SetScaling, } +impl_intopyobject_for_copy_pyclass!(LookaheadHeuristic); #[pymethods] impl LookaheadHeuristic { #[new] @@ -105,8 +111,8 @@ impl LookaheadHeuristic { } } - pub fn __getnewargs__(&self, py: Python) -> Py { - (self.weight, self.size, self.scale).into_py(py) + pub fn __getnewargs__(&self, py: Python) -> PyResult> { + (self.weight, self.size, self.scale).into_py_any(py) } pub fn __eq__(&self, py: Python, other: Py) -> bool { @@ -119,9 +125,9 @@ impl LookaheadHeuristic { pub fn __repr__(&self, py: Python) -> PyResult> { let fmt = "LookaheadHeuristic(weight={!r}, size={!r}, scale={!r})"; - Ok(PyString::new_bound(py, fmt) + PyString::new(py, fmt) .call_method1("format", (self.weight, self.size, self.scale))? - .into_py(py)) + .into_py_any(py) } } @@ -138,6 +144,7 @@ pub struct DecayHeuristic { /// How frequently (in terms of swaps in the layer) to reset all qubit multipliers back to 1.0. pub reset: usize, } +impl_intopyobject_for_copy_pyclass!(DecayHeuristic); #[pymethods] impl DecayHeuristic { #[new] @@ -145,8 +152,8 @@ impl DecayHeuristic { Self { increment, reset } } - pub fn __getnewargs__(&self, py: Python) -> Py { - (self.increment, self.reset).into_py(py) + pub fn __getnewargs__(&self, py: Python) -> PyResult> { + (self.increment, self.reset).into_py_any(py) } pub fn __eq__(&self, py: Python, other: Py) -> bool { @@ -159,9 +166,9 @@ impl DecayHeuristic { pub fn __repr__(&self, py: Python) -> PyResult> { let fmt = "DecayHeuristic(increment={!r}, reset={!r})"; - Ok(PyString::new_bound(py, fmt) + PyString::new(py, fmt) .call_method1("format", (self.increment, self.reset))? - .into_py(py)) + .into_py_any(py) } } @@ -211,7 +218,7 @@ impl Heuristic { } } - pub fn __getnewargs__(&self, py: Python) -> Py { + pub fn __getnewargs__(&self, py: Python) -> PyResult> { ( self.basic, self.lookahead, @@ -219,7 +226,7 @@ impl Heuristic { self.attempt_limit, self.best_epsilon, ) - .into_py(py) + .into_py_any(py) } /// Set the weight of the ``basic`` heuristic (the sum of distances of gates in the front @@ -268,7 +275,7 @@ impl Heuristic { pub fn __repr__(&self, py: Python) -> PyResult> { let fmt = "Heuristic(basic={!r}, lookahead={!r}, decay={!r}, attempt_limit={!r}, best_epsilon={!r})"; - Ok(PyString::new_bound(py, fmt) + PyString::new(py, fmt) .call_method1( "format", ( @@ -279,6 +286,6 @@ impl Heuristic { self.best_epsilon, ), )? - .into_py(py)) + .into_py_any(py) } } diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index 9ab67c3fcfbc..cc4ab7ff19f7 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -182,10 +182,10 @@ pub fn sabre_layout_and_routing( }; ( res.0, - PyArray::from_vec_bound(py, res.1).into(), + PyArray::from_vec(py, res.1).into_any().unbind(), ( res.2.map, - res.2.node_order.into_pyarray_bound(py).into(), + res.2.node_order.into_pyarray(py).into_any().unbind(), res.2.node_block_results, ), ) diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 77057b69c272..9e386ccb431b 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -23,6 +23,7 @@ use numpy::{IntoPyArray, ToPyArray}; use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; use pyo3::wrap_pyfunction; +use pyo3::IntoPyObjectExt; use pyo3::Python; use crate::nlayout::PhysicalQubit; @@ -45,7 +46,7 @@ pub struct SabreResult { impl SabreResult { #[getter] fn node_order(&self, py: Python) -> PyObject { - self.node_order.to_pyarray_bound(py).into() + self.node_order.to_pyarray(py).into_any().unbind() } } @@ -70,10 +71,11 @@ impl NodeBlockResults { match self.results.get(&object) { Some(val) => Ok(val .iter() - .map(|x| x.clone().into_py(py)) - .collect::>() - .into_pyarray_bound(py) - .into()), + .map(|x| x.clone().into_py_any(py)) + .collect::>>()? + .into_pyarray(py) + .into_any() + .unbind()), None => Err(PyIndexError::new_err(format!( "Node index {object} has no block results", ))), @@ -96,13 +98,15 @@ pub struct BlockResult { #[pymethods] impl BlockResult { #[getter] - fn swap_epilogue(&self, py: Python) -> PyObject { - self.swap_epilogue + fn swap_epilogue(&self, py: Python) -> PyResult { + Ok(self + .swap_epilogue .iter() - .map(|x| x.into_py(py)) - .collect::>() - .into_pyarray_bound(py) - .into() + .map(|x| x.into_py_any(py)) + .collect::>>()? + .into_pyarray(py) + .into_any() + .unbind()) } } diff --git a/crates/accelerate/src/sabre/neighbor_table.rs b/crates/accelerate/src/sabre/neighbor_table.rs index 8ab80dd81a2a..e9af0945cdc7 100644 --- a/crates/accelerate/src/sabre/neighbor_table.rs +++ b/crates/accelerate/src/sabre/neighbor_table.rs @@ -107,14 +107,17 @@ impl NeighborTable { Ok(NeighborTable { neighbors }) } - fn __getstate__(&self, py: Python<'_>) -> Py { - PyList::new_bound( + fn __getstate__(&self, py: Python<'_>) -> PyResult> { + PyList::new( py, - self.neighbors - .iter() - .map(|v| PyList::new_bound(py, v.iter()).to_object(py)), + self.neighbors.iter().map(|v| { + PyList::new(py, v.iter()) + .unwrap() + .into_pyobject(py) + .unwrap() + }), ) - .into() + .map(|x| x.unbind()) } fn __setstate__(&mut self, state: &Bound) -> PyResult<()> { diff --git a/crates/accelerate/src/sabre/route.rs b/crates/accelerate/src/sabre/route.rs index 82d83d607a6e..a1ec511f1aad 100644 --- a/crates/accelerate/src/sabre/route.rs +++ b/crates/accelerate/src/sabre/route.rs @@ -470,9 +470,9 @@ pub fn sabre_routing( ); ( res.map, - res.node_order.into_pyarray_bound(py).into(), + res.node_order.into_pyarray(py).into_any().unbind(), res.node_block_results, - PyArray::from_iter_bound( + PyArray::from_iter( py, (0u32..neighbor_table.num_qubits().try_into().unwrap()).map(|phys| { PhysicalQubit::new(phys) @@ -480,7 +480,8 @@ pub fn sabre_routing( .to_phys(&final_layout) }), ) - .into(), + .into_any() + .unbind(), ) } diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index e1d2f2689d28..bce6c0572e82 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -10,31 +10,40 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::collections::btree_map; - use hashbrown::HashSet; use ndarray::Array2; use num_complex::Complex64; use num_traits::Zero; -use thiserror::Error; - use numpy::{ PyArray1, PyArray2, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyArrayMethods, PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods, }; -use pyo3::exceptions::{PyTypeError, PyValueError, PyZeroDivisionError}; -use pyo3::intern; -use pyo3::prelude::*; -use pyo3::sync::GILOnceCell; -use pyo3::types::{IntoPyDict, PyList, PyType}; +use pyo3::{ + exceptions::{PyRuntimeError, PyTypeError, PyValueError, PyZeroDivisionError}, + intern, + prelude::*, + sync::GILOnceCell, + types::{IntoPyDict, PyList, PyTuple, PyType}, + IntoPyObjectExt, PyErr, +}; +use std::{ + collections::btree_map, + ops::{AddAssign, DivAssign, MulAssign, SubAssign}, + sync::{Arc, RwLock}, +}; +use thiserror::Error; -use qiskit_circuit::imports::{ImportOnceCell, NUMPY_COPY_ONLY_IF_NEEDED}; -use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; +use qiskit_circuit::{ + imports::{ImportOnceCell, NUMPY_COPY_ONLY_IF_NEEDED}, + slice::{PySequenceIndex, SequenceIndex}, +}; static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); static PAULI_LIST_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "PauliList"); static SPARSE_PAULI_OP_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp"); +static BIT_TERM_PY_ENUM: GILOnceCell> = GILOnceCell::new(); +static BIT_TERM_INTO_PY: GILOnceCell<[Option>; 16]> = GILOnceCell::new(); /// Named handle to the alphabet of single-qubit terms. /// @@ -177,116 +186,10 @@ impl BitTerm { } } -static BIT_TERM_PY_ENUM: GILOnceCell> = GILOnceCell::new(); -static BIT_TERM_INTO_PY: GILOnceCell<[Option>; 16]> = GILOnceCell::new(); - -/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`. -/// -/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which -/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe -/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in -/// Numpy arrays for efficiency. -/// -/// The resulting class is attached to `SparseObservable` as a class attribute, and its -/// `__qualname__` is set to reflect this. -fn make_py_bit_term(py: Python) -> PyResult> { - let terms = [ - BitTerm::X, - BitTerm::Plus, - BitTerm::Minus, - BitTerm::Y, - BitTerm::Right, - BitTerm::Left, - BitTerm::Z, - BitTerm::Zero, - BitTerm::One, - ] - .into_iter() - .flat_map(|term| { - let mut out = vec![(term.py_name(), term as u8)]; - if term.py_name() != term.py_label() { - // Also ensure that the labels are created as aliases. These can't be (easily) accessed - // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) - // syntax, or programmatically with `getattr`. - out.push((term.py_label(), term as u8)); - } - out - }) - .collect::>(); - let obj = py.import_bound("enum")?.getattr("IntEnum")?.call( - ("BitTerm", terms), - Some( - &[ - ("module", "qiskit.quantum_info"), - ("qualname", "SparseObservable.BitTerm"), - ] - .into_py_dict_bound(py), - ), - )?; - Ok(obj.downcast_into::()?.unbind()) -} - -// Return the relevant value from the Python-space sister enumeration. These are Python-space -// singletons and subclasses of Python `int`. We only use this for interaction with "high level" -// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it -// efficiently. -impl IntoPy> for BitTerm { - fn into_py(self, py: Python) -> Py { - let terms = BIT_TERM_INTO_PY.get_or_init(py, || { - let py_enum = BIT_TERM_PY_ENUM - .get_or_try_init(py, || make_py_bit_term(py)) - .expect("creating a simple Python enum class should be infallible") - .bind(py); - ::std::array::from_fn(|val| { - ::bytemuck::checked::try_cast(val as u8) - .ok() - .map(|term: BitTerm| { - py_enum - .getattr(term.py_name()) - .expect("the created `BitTerm` enum should have matching attribute names to the terms") - .unbind() - }) - }) - }); - terms[self as usize] - .as_ref() - .expect("the lookup table initializer populated a 'Some' in all valid locations") - .clone_ref(py) - } -} -impl ToPyObject for BitTerm { - fn to_object(&self, py: Python) -> Py { - self.into_py(py) - } -} -impl<'py> FromPyObject<'py> for BitTerm { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let value = ob - .extract::() - .map_err(|_| match ob.get_type().repr() { - Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {}", repr)), - Err(err) => err, - })?; - let value_error = || { - PyValueError::new_err(format!( - "value {} is not a valid letter of the single-qubit alphabet for 'BitTerm'", - value - )) - }; - let value: u8 = value.try_into().map_err(|_| value_error())?; - value.try_into().map_err(|_| value_error()) - } -} - /// The error type for a failed conversion into `BitTerm`. #[derive(Error, Debug)] #[error("{0} is not a valid letter of the single-qubit alphabet")] pub struct BitTermFromU8Error(u8); -impl From for PyErr { - fn from(value: BitTermFromU8Error) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} // `BitTerm` allows safe `as` casting into `u8`. This is the reverse, which is fallible, because // `BitTerm` is a value-wise subtype of `u8`. @@ -327,11 +230,8 @@ pub enum CoherenceError { DuplicateIndices, #[error("the provided qubit mapping does not account for all contained qubits")] IndexMapTooSmall, -} -impl From for PyErr { - fn from(value: CoherenceError) -> PyErr { - PyValueError::new_err(value.to_string()) - } + #[error("cannot shrink the qubit count in an observable from {current} to {target}")] + NotEnoughQubits { current: usize, target: usize }, } /// An error related to processing of a string label (both dense and sparse). @@ -348,755 +248,1641 @@ pub enum LabelError { #[error("labels must only contain letters from the alphabet 'IXYZ+-rl01'")] OutsideAlphabet, } -impl From for PyErr { - fn from(value: LabelError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} #[derive(Error, Debug)] pub enum ArithmeticError { #[error("mismatched numbers of qubits: {left}, {right}")] MismatchedQubits { left: u32, right: u32 }, } -impl From for PyErr { - fn from(value: ArithmeticError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} /// An observable over Pauli bases that stores its data in a qubit-sparse format. /// -/// Mathematics -/// =========== -/// -/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate -/// projectors, with each term weighted by some complex number. That is, the full observable is -/// -/// .. math:: -/// -/// \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i -/// -/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a -/// restricted alphabet :math:`A^{(n)}_i`. The sum over :math:`i` is the sum of the individual -/// terms, and the tensor product produces the operator strings. -/// -/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the -/// Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are: -/// -/// .. _sparse-observable-alphabet: -/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable` -/// -/// ======= ======================================= =============== =========================== -/// Label Operator Numeric value :class:`.BitTerm` attribute -/// ======= ======================================= =============== =========================== -/// ``"I"`` :math:`I` (identity) Not stored. Not stored. -/// -/// ``"X"`` :math:`X` (Pauli X) ``0b0010`` (2) :attr:`~.BitTerm.X` -/// -/// ``"Y"`` :math:`Y` (Pauli Y) ``0b0011`` (3) :attr:`~.BitTerm.Y` -/// -/// ``"Z"`` :math:`Z` (Pauli Z) ``0b0001`` (1) :attr:`~.BitTerm.Z` -/// -/// ``"+"`` :math:`\lvert+\rangle\langle+\rvert` ``0b1010`` (10) :attr:`~.BitTerm.PLUS` -/// (projector to positive eigenstate of X) -/// -/// ``"-"`` :math:`\lvert-\rangle\langle-\rvert` ``0b0110`` (6) :attr:`~.BitTerm.MINUS` -/// (projector to negative eigenstate of X) -/// -/// ``"r"`` :math:`\lvert r\rangle\langle r\rvert` ``0b1011`` (11) :attr:`~.BitTerm.RIGHT` -/// (projector to positive eigenstate of Y) -/// -/// ``"l"`` :math:`\lvert l\rangle\langle l\rvert` ``0b0111`` (7) :attr:`~.BitTerm.LEFT` -/// (projector to negative eigenstate of Y) -/// -/// ``"0"`` :math:`\lvert0\rangle\langle0\rvert` ``0b1001`` (9) :attr:`~.BitTerm.ZERO` -/// (projector to positive eigenstate of Z) -/// -/// ``"1"`` :math:`\lvert1\rangle\langle1\rvert` ``0b0101`` (5) :attr:`~.BitTerm.ONE` -/// (projector to negative eigenstate of Z) -/// ======= ======================================= =============== =========================== -/// -/// The allowed alphabet forms an overcomplete basis of the operator space. This means that there -/// is not a unique summation to represent a given observable. By comparison, -/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of -/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order -/// `) there is only one representation of any operator. -/// -/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making -/// "efficiency of measurement" equivalent to "efficiency of representation". For example, the -/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on -/// hardware with simple :math:`Z` measurements, but can only be represented by -/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored -/// terms. :class:`SparseObservable` requires only a single term to store this. -/// -/// The downside to this is that it is impractical to take an arbitrary matrix or -/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation. You -/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to -/// decompose into one. -/// -/// -/// Representation -/// ============== -/// -/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit -/// operators. This makes it significantly more efficient to represent observables such as -/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of -/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically. -/// -/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse -/// matrices. In this analogy, the terms of the sum are the "rows", and the qubit terms are the -/// "columns", where an absent entry represents the identity rather than a zero. More explicitly, -/// the representation is made up of four contiguous arrays: -/// -/// .. _sparse-observable-arrays: -/// .. table:: Data arrays used to represent :class:`.SparseObservable` -/// -/// ================== =========== ============================================================= -/// Attribute Length Description -/// ================== =========== ============================================================= -/// :attr:`coeffs` :math:`t` The complex scalar multiplier for each term. -/// -/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit terms for all of the -/// operators, in order. These correspond to the non-identity -/// :math:`A^{(n)}_i` in the sum description, where the entries -/// are stored in order of increasing :math:`i` first, and in -/// order of increasing :math:`n` within each term. -/// -/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators -/// in :attr:`bit_terms`. :class:`SparseObservable` requires -/// that this list is term-wise sorted, and algorithms can rely -/// on this invariant being upheld. -/// -/// :attr:`boundaries` :math:`t+1` The indices that partition :attr:`bit_terms` and -/// :attr:`indices` into complete terms. For term number -/// :math:`i`, its complex coefficient is ``coeffs[i]``, and its -/// non-identity single-qubit operators and their corresponding -/// qubits are the slice ``boundaries[i] : boundaries[i+1]`` into -/// :attr:`bit_terms` and :attr:`indices` respectively. -/// :attr:`boundaries` always has an explicit 0 as its first -/// element. -/// ================== =========== ============================================================= -/// -/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is -/// the total number of non-identity single-qubit terms. -/// -/// As illustrative examples: -/// -/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other -/// vectors are empty. -/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``, -/// :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty. -/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``, -/// :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y, -/// BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``. The operator might act on more than -/// four qubits, depending on the :attr:`num_qubits` parameter. The :attr:`bit_terms` are integer -/// values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class. Note -/// that the single-bit terms and indices are sorted into termwise sorted order. This is a -/// requirement of the class. -/// -/// These cases are not special, they're fully consistent with the rules and should not need special -/// handling. -/// -/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte. The numeric values -/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are -/// accessible with named access by an enumeration: -/// -/// .. -/// This is documented manually here because the Python-space `Enum` is generated -/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere -/// else in this source file. The use of `autoattribute` is because it pulls in the numeric -/// value. -/// -/// .. py:class:: SparseObservable.BitTerm -/// -/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to -/// represent each of the single-qubit alphabet terms enumerated in -/// :ref:`sparse-observable-alphabet`. -/// -/// This class is attached to :class:`.SparseObservable`. Access it as -/// :class:`.SparseObservable.BitTerm`. If this is too much typing, and you are solely dealing -/// with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you -/// might want to shorten it as:: -/// -/// >>> ops = SparseObservable.BitTerm -/// >>> assert ops.X is SparseObservable.BitTerm.X -/// -/// You can access all the values of the enumeration by either their full all-capitals name, or -/// by their single-letter label. The single-letter labels are not generally valid Python -/// identifiers, so you must use indexing notation to access them:: -/// -/// >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"] -/// -/// The numeric structure of these is that they are all four-bit values of which the low two -/// bits are the (phase-less) symplectic representation of the Pauli operator related to the -/// object, where the low bit denotes a contribution by :math:`Z` and the second lowest a -/// contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01`` -/// for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X -/// -/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS -/// -/// The projector to the positive eigenstate of the :math:`X` operator: -/// :math:`\lvert+\rangle\langle+\rvert`. Uses the single-letter label ``"+"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS -/// -/// The projector to the negative eigenstate of the :math:`X` operator: -/// :math:`\lvert-\rangle\langle-\rvert`. Uses the single-letter label ``"-"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y -/// -/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT -/// -/// The projector to the positive eigenstate of the :math:`Y` operator: -/// :math:`\lvert r\rangle\langle r\rvert`. Uses the single-letter label ``"r"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT -/// -/// The projector to the negative eigenstate of the :math:`Y` operator: -/// :math:`\lvert l\rangle\langle l\rvert`. Uses the single-letter label ``"l"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z -/// -/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO -/// -/// The projector to the positive eigenstate of the :math:`Z` operator: -/// :math:`\lvert0\rangle\langle0\rvert`. Uses the single-letter label ``"0"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE -/// -/// The projector to the negative eigenstate of the :math:`Z` operator: -/// :math:`\lvert1\rangle\langle1\rvert`. Uses the single-letter label ``"1"``. -/// -/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these -/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy -/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the -/// field; this lets you easily do mathematics on the results, like bitwise operations on -/// :attr:`bit_terms`. You can assign to indices or slices of each of the attributes, but beware -/// that you must uphold :ref:`the data coherence rules ` while doing -/// this. For example:: -/// -/// >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) -/// >>> assert isinstance(obs.coeffs[:], np.ndarray) -/// >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. -/// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 -/// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) -/// -/// .. note:: -/// -/// The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`. -/// -/// .. _sparse-observable-canonical-order: -/// -/// Canonical ordering -/// ------------------ -/// -/// For any given mathematical observable, there are several ways of representing it with -/// :class:`SparseObservable`. For example, the same set of single-bit terms and their -/// corresponding indices might appear multiple times in the observable. Mathematically, this is -/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms -/// of the sum in a :class:`SparseObservable` can be in any order while representing the same -/// observable, since addition is commutative (although while floating-point addition is not -/// associative, :class:`SparseObservable` makes no guarantees about the summation order). -/// -/// These two categories of representation degeneracy can cause the ``==`` operator to claim that -/// two observables are not equal, despite representating the same object. In these cases, it can -/// be convenient to define some *canonical form*, which allows observables to be compared -/// structurally. -/// -/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. -/// The precise ordering of terms in canonical ordering is not specified, and may change between -/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables -/// structurally by comparing their simplified forms. -/// -/// .. note:: -/// -/// If you wish to account for floating-point tolerance in the comparison, it is safest to use -/// a recipe such as:: -/// -/// def equivalent(left, right, tol): -/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) -/// -/// .. note:: -/// -/// The canonical form produced by :meth:`simplify` will still not universally detect all -/// observables that are equivalent due to the over-complete basis alphabet; it is not -/// computationally feasible to do this at scale. For example, on observable built from ``+`` -/// and ``-`` components will not canonicalize to a single ``X`` term. -/// -/// Indexing -/// -------- -/// -/// :class:`SparseObservable` behaves as `a Python sequence -/// `__ (the standard form, not the expanded -/// :class:`collections.abc.Sequence`). The observable can be indexed by integers, and iterated -/// through to yield individual terms. -/// -/// Each term appears as an instance a self-contained class. The individual terms are copied out of -/// the base observable; mutations to them will not affect the observable. -/// -/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term -/// :members: -/// -/// Construction -/// ============ -/// -/// :class:`SparseObservable` defines several constructors. The default constructor will attempt to -/// delegate to one of the more specific constructors, based on the type of the input. You can -/// always use the specific constructors to have more control over the construction. -/// -/// .. _sparse-observable-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`from_label` Convert a dense string label into a single-term -/// :class:`.SparseObservable`. -/// -/// :meth:`from_list` Sum a list of tuples of dense string labels and the associated -/// coefficients into an observable. -/// -/// :meth:`from_sparse_list` Sum a list of tuples of sparse string labels, the qubits they -/// apply to, and their coefficients into an observable. -/// -/// :meth:`from_pauli` Raise a single :class:`.Pauli` into a single-term -/// :class:`.SparseObservable`. -/// -/// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a :class:`SparseObservable`. -/// -/// :meth:`from_terms` Sum explicit single :class:`Term` instances. -/// -/// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays -/// `. -/// ============================ ================================================================ -/// -/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`SparseObservable`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` argument. If -/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a -/// number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`SparseObservable`, in -/// which case the input is copied, a :class:`.Pauli` or :class:`.SparsePauliOp`, in which -/// case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it -/// can be a list in a valid format for either :meth:`from_list` or -/// :meth:`from_sparse_list`. -/// :param int|None num_qubits: Optional number of qubits for the operator. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for empty -/// lists or the sparse-list format. If given unnecessarily, it must match the data input. -/// -/// In addition to the conversion-based constructors, there are also helper methods that construct -/// special forms of observables. -/// -/// .. table:: Construction of special observables -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`zero` The zero operator on a given number of qubits. -/// -/// :meth:`identity` The identity operator on a given number of qubits. -/// ============================ ================================================================ -/// -/// -/// Mathematical manipulation -/// ========================= -/// -/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other -/// :mod:`~qiskit.quantum_info` operators. -/// -/// In basic arithmetic, you can: -/// -/// * add two observables using ``+`` -/// * subtract two observables using ``-`` -/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` -/// * negate all the coefficients in an observable with unary ``-`` -/// -/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, -/// which mutates the left-hand side in-place. Using these is typically more efficient than the -/// infix operators, especially for building an observable in a loop. -/// -/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of -/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be -/// equivalent to :meth:`tensor`. -/// -/// .. note:: +/// See [PySparseObservable] for detailed docs. +#[derive(Clone, Debug, PartialEq)] +pub struct SparseObservable { + /// The number of qubits the operator acts on. This is not inferable from any other shape or + /// values, since identities are not stored explicitly. + num_qubits: u32, + /// The coefficients of each abstract term in in the sum. This has as many elements as terms in + /// the sum. + coeffs: Vec, + /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored + /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` + bit_terms: Vec, + /// A flat list of the qubit indices that the corresponding entries in `bit_terms` act on. This + /// list must always be term-wise sorted, where a term is a sublist as denoted by `boundaries`. + indices: Vec, + /// Indices that partition `bit_terms` and `indices` into sublists for each individual term in + /// the sum. `boundaries[0]..boundaries[1]` is the range of indices into `bit_terms` and + /// `indices` that correspond to the first term of the sum. All unspecified qubit indices are + /// implicitly the identity. This is one item longer than `coeffs`, since `boundaries[0]` is + /// always an explicit zero (for algorithmic ease). + boundaries: Vec, +} + +impl SparseObservable { + /// Create a new observable from the raw components that make it up. + /// + /// This checks the input values for data coherence on entry. If you are certain you have the + /// correct values, you can call `new_unchecked` instead. + pub fn new( + num_qubits: u32, + coeffs: Vec, + bit_terms: Vec, + indices: Vec, + boundaries: Vec, + ) -> Result { + if coeffs.len() + 1 != boundaries.len() { + return Err(CoherenceError::MismatchedTermCount { + coeffs: coeffs.len(), + boundaries: boundaries.len(), + }); + } + if bit_terms.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + bit_terms: bit_terms.len(), + indices: indices.len(), + }); + } + // We already checked that `boundaries` is at least length 1. + if *boundaries.first().unwrap() != 0 { + return Err(CoherenceError::BadInitialBoundary(boundaries[0])); + } + if *boundaries.last().unwrap() != indices.len() { + return Err(CoherenceError::BadFinalBoundary { + last: *boundaries.last().unwrap(), + items: indices.len(), + }); + } + for (&left, &right) in boundaries[..].iter().zip(&boundaries[1..]) { + if right < left { + return Err(CoherenceError::DecreasingBoundaries); + } + let indices = &indices[left..right]; + if !indices.is_empty() { + for (index_left, index_right) in indices[..].iter().zip(&indices[1..]) { + if index_left == index_right { + return Err(CoherenceError::DuplicateIndices); + } else if index_left > index_right { + return Err(CoherenceError::UnsortedIndices); + } + } + } + if indices.last().map(|&ix| ix >= num_qubits).unwrap_or(false) { + return Err(CoherenceError::BitIndexTooHigh); + } + } + // SAFETY: we've just done the coherence checks. + Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) + } + + /// Create a new observable from the raw components without checking data coherence. + /// + /// # Safety + /// + /// It is up to the caller to ensure that the data-coherence requirements, as enumerated in the + /// struct-level documentation, have been upheld. + #[inline(always)] + pub unsafe fn new_unchecked( + num_qubits: u32, + coeffs: Vec, + bit_terms: Vec, + indices: Vec, + boundaries: Vec, + ) -> Self { + Self { + num_qubits, + coeffs, + bit_terms, + indices, + boundaries, + } + } + + /// Create a zero operator with pre-allocated space for the given number of summands and + /// single-qubit bit terms. + #[inline] + pub fn with_capacity(num_qubits: u32, num_terms: usize, num_bit_terms: usize) -> Self { + Self { + num_qubits, + coeffs: Vec::with_capacity(num_terms), + bit_terms: Vec::with_capacity(num_bit_terms), + indices: Vec::with_capacity(num_bit_terms), + boundaries: { + let mut boundaries = Vec::with_capacity(num_terms + 1); + boundaries.push(0); + boundaries + }, + } + } + + /// Get an iterator over the individual terms of the operator. + /// + /// Recall that two [SparseObservable]s that have different term orders can still represent the + /// same object. Use [canonicalize] to apply a canonical ordering to the terms. + pub fn iter(&'_ self) -> impl ExactSizeIterator> + '_ { + self.coeffs.iter().enumerate().map(|(i, coeff)| { + let start = self.boundaries[i]; + let end = self.boundaries[i + 1]; + SparseTermView { + num_qubits: self.num_qubits, + coeff: *coeff, + bit_terms: &self.bit_terms[start..end], + indices: &self.indices[start..end], + } + }) + } + + /// Get an iterator over the individual terms of the operator that allows in-place mutation. + /// + /// The length and indices of these views cannot be mutated, since both would allow breaking + /// data coherence. + pub fn iter_mut(&mut self) -> IterMut<'_> { + self.into() + } + + /// Get the number of qubits the observable is defined on. + #[inline] + pub fn num_qubits(&self) -> u32 { + self.num_qubits + } + + /// Get the number of terms in the observable. + #[inline] + pub fn num_terms(&self) -> usize { + self.coeffs.len() + } + + /// Get the coefficients of the terms. + #[inline] + pub fn coeffs(&self) -> &[Complex64] { + &self.coeffs + } + + /// Get a mutable slice of the coefficients. + #[inline] + pub fn coeffs_mut(&mut self) -> &mut [Complex64] { + &mut self.coeffs + } + + /// Get the indices of each [BitTerm]. + #[inline] + pub fn indices(&self) -> &[u32] { + &self.indices + } + + /// Get a mutable slice of the indices. + /// + /// # Safety + /// + /// Modifying the indices can cause an incoherent state of the [SparseObservable]. + /// It should be ensured that the indices are consistent with the coeffs, bit_terms, and + /// boundaries. + #[inline] + pub unsafe fn indices_mut(&mut self) -> &mut [u32] { + &mut self.indices + } + + /// Get the boundaries of each term. + #[inline] + pub fn boundaries(&self) -> &[usize] { + &self.boundaries + } + + /// Get a mutable slice of the boundaries. + /// + /// # Safety + /// + /// Modifying the boundaries can cause an incoherent state of the [SparseObservable]. + /// It should be ensured that the boundaries are sorted and the length/elements are consistent + /// with the coeffs, bit_terms, and indices. + #[inline] + pub unsafe fn boundaries_mut(&mut self) -> &mut [usize] { + &mut self.boundaries + } + + /// Get the [BitTerm]s in the observable. + #[inline] + pub fn bit_terms(&self) -> &[BitTerm] { + &self.bit_terms + } + + /// Get a muitable slice of the bit terms. + #[inline] + pub fn bit_terms_mut(&mut self) -> &mut [BitTerm] { + &mut self.bit_terms + } + + /// Create a zero operator on ``num_qubits`` qubits. + pub fn zero(num_qubits: u32) -> Self { + Self::with_capacity(num_qubits, 0, 0) + } + + /// Create an identity operator on ``num_qubits`` qubits. + pub fn identity(num_qubits: u32) -> Self { + Self { + num_qubits, + coeffs: vec![Complex64::new(1.0, 0.0)], + bit_terms: vec![], + indices: vec![], + boundaries: vec![0, 0], + } + } + + /// Clear all the terms from this operator, making it equal to the zero operator again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// substraction operations may not need to reallocate. + pub fn clear(&mut self) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.truncate(1); + } + + /// Reduce the observable to its canonical form. + /// + /// This sums like terms, removing them if the final complex coefficient's absolute value is + /// less than or equal to the tolerance. The terms are reordered to some canonical ordering. + /// + /// This function is idempotent. + pub fn canonicalize(&self, tol: f64) -> SparseObservable { + let mut terms = btree_map::BTreeMap::new(); + for term in self.iter() { + terms + .entry((term.indices, term.bit_terms)) + .and_modify(|c| *c += term.coeff) + .or_insert(term.coeff); + } + let mut out = SparseObservable::zero(self.num_qubits); + for ((indices, bit_terms), coeff) in terms { + if coeff.norm_sqr() <= tol * tol { + continue; + } + out.coeffs.push(coeff); + out.bit_terms.extend_from_slice(bit_terms); + out.indices.extend_from_slice(indices); + out.boundaries.push(out.indices.len()); + } + out + } + + /// Tensor product of `self` with `other`. + /// + /// The bit ordering is defined such that the qubit indices of `other` will remain the same, and + /// the indices of `self` will be offset by the number of qubits in `other`. This is the same + /// convention as used by the rest of Qiskit's `quantum_info` operators. + /// + /// Put another way, in the simplest case of two observables formed of dense labels: + /// + /// ``` + /// let mut left = SparseObservable::zero(5); + /// left.add_dense_label("IXY+Z", Complex64::new(1.0, 0.0)); + /// let mut right = SparseObservable::zero(6); + /// right.add_dense_label("IIrl01", Complex64::new(1.0, 0.0)); + /// + /// // The result is the concatenation of the two labels. + /// let mut out = SparseObservable::zero(11); + /// out.add_dense_label("IXY+ZIIrl01", Complex64::new(1.0, 0.0)); + /// + /// assert_eq!(left.tensor(right), out); + /// ``` + pub fn tensor(&self, other: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits + other.num_qubits, + self.coeffs.len() * other.coeffs.len(), + other.coeffs.len() * self.bit_terms.len() + self.coeffs.len() * other.bit_terms.len(), + ); + let mut self_indices = Vec::new(); + for self_term in self.iter() { + self_indices.clear(); + self_indices.extend(self_term.indices.iter().map(|i| i + other.num_qubits)); + for other_term in other.iter() { + out.coeffs.push(self_term.coeff * other_term.coeff); + out.indices.extend_from_slice(other_term.indices); + out.indices.extend_from_slice(&self_indices); + out.bit_terms.extend_from_slice(other_term.bit_terms); + out.bit_terms.extend_from_slice(self_term.bit_terms); + out.boundaries.push(out.bit_terms.len()); + } + } + out + } + + /// Calculate the adjoint of this observable. + /// + /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit + /// alphabet are self-adjoint, so the result of this operation is the same observable, except + /// its coefficients are all their complex conjugates. + pub fn adjoint(&self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c.conj()).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + + /// Calculate the transpose. + /// + /// This operation transposes the individual bit terms but does directly act + /// on the coefficients. + pub fn transpose(&self) -> SparseObservable { + let mut out = self.clone(); + for term in out.iter_mut() { + for bit_term in term.bit_terms { + match bit_term { + BitTerm::Y => { + *term.coeff = -*term.coeff; + } + BitTerm::Right => { + *bit_term = BitTerm::Left; + } + BitTerm::Left => { + *bit_term = BitTerm::Right; + } + _ => (), + } + } + } + out + } + + /// Calculate the complex conjugate. + /// + /// This operation equals transposing the observable and complex conjugating the coefficients. + pub fn conjugate(&self) -> SparseObservable { + let mut out = self.transpose(); + for coeff in out.coeffs.iter_mut() { + *coeff = coeff.conj(); + } + out + } + + /// Get a view onto a representation of a single sparse term. + /// + /// This is effectively an indexing operation into the [SparseObservable]. Recall that two + /// [SparseObservable]s that have different term orders can still represent the same object. + /// Use [canonicalize] to apply a canonical ordering to the terms. + /// + /// # Panics + /// + /// If the index is out of bounds. + pub fn term(&self, index: usize) -> SparseTermView { + debug_assert!(index < self.num_terms(), "index {index} out of bounds"); + let start = self.boundaries[index]; + let end = self.boundaries[index + 1]; + SparseTermView { + num_qubits: self.num_qubits, + coeff: self.coeffs[index], + bit_terms: &self.bit_terms[start..end], + indices: &self.indices[start..end], + } + } + + /// Add the term implied by a dense string label onto this observable. + pub fn add_dense_label>( + &mut self, + label: L, + coeff: Complex64, + ) -> Result<(), LabelError> { + let label = label.as_ref(); + if label.len() != self.num_qubits() as usize { + return Err(LabelError::WrongLengthDense { + num_qubits: self.num_qubits(), + label: label.len(), + }); + } + // The only valid characters in the alphabet are ASCII, so if we see something other than + // ASCII, we're already in the failure path. + for (i, letter) in label.iter().rev().enumerate() { + match BitTerm::try_from_u8(*letter) { + Ok(Some(term)) => { + self.bit_terms.push(term); + self.indices.push(i as u32); + } + Ok(None) => (), + Err(_) => { + // Undo any modifications to ourselves so we stay in a consistent state. + let num_single_terms = self.boundaries[self.boundaries.len() - 1]; + self.bit_terms.truncate(num_single_terms); + self.indices.truncate(num_single_terms); + return Err(LabelError::OutsideAlphabet); + } + } + } + self.coeffs.push(coeff); + self.boundaries.push(self.bit_terms.len()); + Ok(()) + } + + /// Relabel the `indices` in the operator to new values. + /// + /// This fails if any of the new indices are too large, or if any mapping would cause a term to + /// contain duplicates of the same index. It may not detect if multiple qubits are mapped to + /// the same index, if those qubits never appear together in the same term. Such a mapping + /// would not cause data-coherence problems (the output observable will be valid), but is + /// unlikely to be what you intended. + /// + /// *Panics* if `new_qubits` is not long enough to map every index used in the operator. + pub fn relabel_qubits_from_slice(&mut self, new_qubits: &[u32]) -> Result<(), CoherenceError> { + for qubit in new_qubits { + if *qubit >= self.num_qubits { + return Err(CoherenceError::BitIndexTooHigh); + } + } + let mut order = btree_map::BTreeMap::new(); + for i in 0..self.num_terms() { + let start = self.boundaries[i]; + let end = self.boundaries[i + 1]; + for j in start..end { + order.insert(new_qubits[self.indices[j] as usize], self.bit_terms[j]); + } + if order.len() != end - start { + return Err(CoherenceError::DuplicateIndices); + } + for (index, dest) in order.keys().zip(&mut self.indices[start..end]) { + *dest = *index; + } + for (bit_term, dest) in order.values().zip(&mut self.bit_terms[start..end]) { + *dest = *bit_term; + } + order.clear(); + } + Ok(()) + } + + /// Apply a transpiler layout. + pub fn apply_layout( + &self, + layout: Option<&[u32]>, + num_qubits: u32, + ) -> Result { + match layout { + None => { + let mut out = self.clone(); + if num_qubits < self.num_qubits { + // return Err(CoherenceError::BitIndexTooHigh); + return Err(CoherenceError::NotEnoughQubits { + current: self.num_qubits as usize, + target: num_qubits as usize, + }); + } + out.num_qubits = num_qubits; + Ok(out) + } + Some(layout) => { + if layout.len() < self.num_qubits as usize { + return Err(CoherenceError::IndexMapTooSmall); + } + if layout.iter().any(|qubit| *qubit >= num_qubits) { + return Err(CoherenceError::BitIndexTooHigh); + } + if layout.iter().collect::>().len() != layout.len() { + return Err(CoherenceError::DuplicateIndices); + } + let mut out = self.clone(); + out.num_qubits = num_qubits; + out.relabel_qubits_from_slice(layout)?; + Ok(out) + } + } + } + + /// Add a single term to this operator. + pub fn add_term(&mut self, term: SparseTermView) -> Result<(), ArithmeticError> { + if self.num_qubits != term.num_qubits { + return Err(ArithmeticError::MismatchedQubits { + left: self.num_qubits, + right: term.num_qubits, + }); + } + self.coeffs.push(term.coeff); + self.bit_terms.extend_from_slice(term.bit_terms); + self.indices.extend_from_slice(term.indices); + self.boundaries.push(self.bit_terms.len()); + Ok(()) + } + + /// Return a suitable Python error if two observables do not have equal numbers of qubits. + pub fn check_equal_qubits(&self, other: &SparseObservable) -> Result<(), ArithmeticError> { + if self.num_qubits != other.num_qubits { + Err(ArithmeticError::MismatchedQubits { + left: self.num_qubits, + right: other.num_qubits, + }) + } else { + Ok(()) + } + } +} + +impl ::std::ops::Add<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn add(mut self, rhs: &SparseObservable) -> SparseObservable { + self += rhs; + self + } +} +impl ::std::ops::Add for &SparseObservable { + type Output = SparseObservable; + + fn add(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out += rhs; + out + } +} +impl ::std::ops::AddAssign<&SparseObservable> for SparseObservable { + fn add_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to add two operators with incompatible qubit counts"); + } + self.coeffs.extend_from_slice(&rhs.coeffs); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Sub<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn sub(mut self, rhs: &SparseObservable) -> SparseObservable { + self -= rhs; + self + } +} +impl ::std::ops::Sub for &SparseObservable { + type Output = SparseObservable; + + fn sub(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out -= rhs; + out + } +} +impl ::std::ops::SubAssign<&SparseObservable> for SparseObservable { + fn sub_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to subtract two operators with incompatible qubit counts"); + } + self.coeffs.extend(rhs.coeffs.iter().map(|coeff| -coeff)); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Mul for SparseObservable { + type Output = SparseObservable; + + fn mul(mut self, rhs: Complex64) -> SparseObservable { + self *= rhs; + self + } +} +impl ::std::ops::Mul for &SparseObservable { + type Output = SparseObservable; + + fn mul(self, rhs: Complex64) -> SparseObservable { + if rhs == Complex64::new(0.0, 0.0) { + SparseObservable::zero(self.num_qubits) + } else { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c * rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + } +} +impl ::std::ops::Mul for Complex64 { + type Output = SparseObservable; + + fn mul(self, mut rhs: SparseObservable) -> SparseObservable { + rhs *= self; + rhs + } +} +impl ::std::ops::Mul<&SparseObservable> for Complex64 { + type Output = SparseObservable; + + fn mul(self, rhs: &SparseObservable) -> SparseObservable { + rhs * self + } +} +impl ::std::ops::MulAssign for SparseObservable { + fn mul_assign(&mut self, rhs: Complex64) { + if rhs == Complex64::new(0.0, 0.0) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.clear(); + self.boundaries.push(0); + } else { + self.coeffs.iter_mut().for_each(|c| *c *= rhs) + } + } +} + +impl ::std::ops::Div for SparseObservable { + type Output = SparseObservable; + + fn div(mut self, rhs: Complex64) -> SparseObservable { + self /= rhs; + self + } +} +impl ::std::ops::Div for &SparseObservable { + type Output = SparseObservable; + + fn div(self, rhs: Complex64) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c / rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::DivAssign for SparseObservable { + fn div_assign(&mut self, rhs: Complex64) { + self.coeffs.iter_mut().for_each(|c| *c /= rhs) + } +} + +impl ::std::ops::Neg for &SparseObservable { + type Output = SparseObservable; + + fn neg(self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| -c).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::Neg for SparseObservable { + type Output = SparseObservable; + + fn neg(mut self) -> SparseObservable { + self.coeffs.iter_mut().for_each(|c| *c = -*c); + self + } +} + +/// A view object onto a single term of a `SparseObservable`. /// -/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware -/// that `Python's operator-precedence rules -/// `__ may cause the -/// evaluation order to be different to your expectation. In particular, the operator ``+`` -/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. +/// The lengths of `bit_terms` and `indices` are guaranteed to be created equal, but might be zero +/// (in the case that the term is proportional to the identity). +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct SparseTermView<'a> { + pub num_qubits: u32, + pub coeff: Complex64, + pub bit_terms: &'a [BitTerm], + pub indices: &'a [u32], +} +impl SparseTermView<'_> { + /// Convert this `SparseTermView` into an owning [SparseTerm] of the same data. + pub fn to_term(&self) -> SparseTerm { + SparseTerm { + num_qubits: self.num_qubits, + coeff: self.coeff, + bit_terms: self.bit_terms.into(), + indices: self.indices.into(), + } + } + + pub fn to_sparse_str(self) -> String { + let coeff = format!("{}", self.coeff).replace('i', "j"); + let paulis = self + .indices + .iter() + .zip(self.bit_terms) + .rev() + .map(|(i, op)| format!("{}_{}", op.py_label(), i)) + .collect::>() + .join(" "); + format!("({})({})", coeff, paulis) + } +} + +/// A mutable view object onto a single term of a [SparseObservable]. /// -/// When using the operators in mixed expressions, it is safest to use parentheses to group the -/// operands of tensor products. +/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero +/// (in the case that the term is proportional to the identity). [indices] is not mutable because +/// this would allow data coherence to be broken. +#[derive(Debug)] +pub struct SparseTermViewMut<'a> { + pub num_qubits: u32, + pub coeff: &'a mut Complex64, + pub bit_terms: &'a mut [BitTerm], + pub indices: &'a [u32], +} + +/// Iterator type allowing in-place mutation of the [SparseObservable]. /// -/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex -/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined -/// analogously to the matrix representation of other Pauli operators in Qiskit. +/// Created by [SparseObservable::iter_mut]. +#[derive(Debug)] +pub struct IterMut<'a> { + num_qubits: u32, + coeffs: &'a mut [Complex64], + bit_terms: &'a mut [BitTerm], + indices: &'a [u32], + boundaries: &'a [usize], + i: usize, +} +impl<'a> From<&'a mut SparseObservable> for IterMut<'a> { + fn from(value: &mut SparseObservable) -> IterMut { + IterMut { + num_qubits: value.num_qubits, + coeffs: &mut value.coeffs, + bit_terms: &mut value.bit_terms, + indices: &value.indices, + boundaries: &value.boundaries, + i: 0, + } + } +} +impl<'a> Iterator for IterMut<'a> { + type Item = SparseTermViewMut<'a>; + + fn next(&mut self) -> Option { + // The trick here is that the lifetime of the 'self' borrow is shorter than the lifetime of + // the inner borrows. We can't give out mutable references to our inner borrow, because + // after the lifetime on 'self' expired, there'd be nothing preventing somebody using the + // 'self' borrow to see _another_ mutable borrow of the inner data, which would be an + // aliasing violation. Instead, we keep splitting the inner views we took out so the + // mutable references we return don't overlap with the ones we continue to hold. + let coeffs = ::std::mem::take(&mut self.coeffs); + let (coeff, other_coeffs) = coeffs.split_first_mut()?; + self.coeffs = other_coeffs; + + let len = self.boundaries[self.i + 1] - self.boundaries[self.i]; + self.i += 1; + + let all_bit_terms = ::std::mem::take(&mut self.bit_terms); + let all_indices = ::std::mem::take(&mut self.indices); + let (bit_terms, rest_bit_terms) = all_bit_terms.split_at_mut(len); + let (indices, rest_indices) = all_indices.split_at(len); + self.bit_terms = rest_bit_terms; + self.indices = rest_indices; + + Some(SparseTermViewMut { + num_qubits: self.num_qubits, + coeff, + bit_terms, + indices, + }) + } + + fn size_hint(&self) -> (usize, Option) { + (self.coeffs.len(), Some(self.coeffs.len())) + } +} +impl ExactSizeIterator for IterMut<'_> {} +impl ::std::iter::FusedIterator for IterMut<'_> {} + +/// A single term from a complete :class:`SparseObservable`. /// +/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. +#[derive(Clone, Debug, PartialEq)] +pub struct SparseTerm { + /// Number of qubits the entire term applies to. + num_qubits: u32, + /// The complex coefficient of the term. + coeff: Complex64, + bit_terms: Box<[BitTerm]>, + indices: Box<[u32]>, +} +impl SparseTerm { + pub fn new( + num_qubits: u32, + coeff: Complex64, + bit_terms: Box<[BitTerm]>, + indices: Box<[u32]>, + ) -> Result { + if bit_terms.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + bit_terms: bit_terms.len(), + indices: indices.len(), + }); + } + + if indices.iter().any(|index| *index >= num_qubits) { + return Err(CoherenceError::BitIndexTooHigh); + } + + Ok(Self { + num_qubits, + coeff, + bit_terms, + indices, + }) + } + + pub fn num_qubits(&self) -> u32 { + self.num_qubits + } + + pub fn coeff(&self) -> Complex64 { + self.coeff + } + + pub fn indices(&self) -> &[u32] { + &self.indices + } + + pub fn bit_terms(&self) -> &[BitTerm] { + &self.bit_terms + } + + pub fn view(&self) -> SparseTermView { + SparseTermView { + num_qubits: self.num_qubits, + coeff: self.coeff, + bit_terms: &self.bit_terms, + indices: &self.indices, + } + } + + /// Convert this term to a complete :class:`SparseObservable`. + pub fn to_observable(&self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: vec![self.coeff], + bit_terms: self.bit_terms.to_vec(), + indices: self.indices.to_vec(), + boundaries: vec![0, self.bit_terms.len()], + } + } +} + +#[derive(Error, Debug)] +struct InnerReadError; + +#[derive(Error, Debug)] +struct InnerWriteError; + +impl ::std::fmt::Display for InnerReadError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Failed acquiring lock for reading.") + } +} + +impl ::std::fmt::Display for InnerWriteError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Failed acquiring lock for writing.") + } +} + +impl From for PyErr { + fn from(value: InnerReadError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} +impl From for PyErr { + fn from(value: InnerWriteError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: BitTermFromU8Error) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} +impl From for PyErr { + fn from(value: CoherenceError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} +impl From for PyErr { + fn from(value: LabelError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} +impl From for PyErr { + fn from(value: ArithmeticError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`. /// -/// Efficiency notes -/// ---------------- +/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which +/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe +/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in +/// Numpy arrays for efficiency. /// -/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating -/// growable vectors for extending the number of terms. This means that the cost of appending to an -/// observable using ``+=`` is amortised linear in the total number of terms added, rather than -/// the quadratic complexity that the binary ``+`` would require. +/// The resulting class is attached to `SparseObservable` as a class attribute, and its +/// `__qualname__` is set to reflect this. +fn make_py_bit_term(py: Python) -> PyResult> { + let terms = [ + BitTerm::X, + BitTerm::Plus, + BitTerm::Minus, + BitTerm::Y, + BitTerm::Right, + BitTerm::Left, + BitTerm::Z, + BitTerm::Zero, + BitTerm::One, + ] + .into_iter() + .flat_map(|term| { + let mut out = vec![(term.py_name(), term as u8)]; + if term.py_name() != term.py_label() { + // Also ensure that the labels are created as aliases. These can't be (easily) accessed + // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) + // syntax, or programmatically with `getattr`. + out.push((term.py_label(), term as u8)); + } + out + }) + .collect::>(); + let obj = py.import("enum")?.getattr("IntEnum")?.call( + ("BitTerm", terms), + Some( + &[ + ("module", "qiskit.quantum_info"), + ("qualname", "SparseObservable.BitTerm"), + ] + .into_py_dict(py)?, + ), + )?; + Ok(obj.downcast_into::()?.unbind()) +} + +// Return the relevant value from the Python-space sister enumeration. These are Python-space +// singletons and subclasses of Python `int`. We only use this for interaction with "high level" +// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it +// efficiently. +impl<'py> IntoPyObject<'py> for BitTerm { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + let terms = BIT_TERM_INTO_PY.get_or_init(py, || { + let py_enum = BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .expect("creating a simple Python enum class should be infallible") + .bind(py); + ::std::array::from_fn(|val| { + ::bytemuck::checked::try_cast(val as u8) + .ok() + .map(|term: BitTerm| { + py_enum + .getattr(term.py_name()) + .expect("the created `BitTerm` enum should have matching attribute names to the terms") + .unbind() + }) + }) + }); + Ok(terms[self as usize] + .as_ref() + .expect("the lookup table initializer populated a 'Some' in all valid locations") + .bind(py) + .clone()) + } +} + +impl<'py> FromPyObject<'py> for BitTerm { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let value = ob + .extract::() + .map_err(|_| match ob.get_type().repr() { + Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {}", repr)), + Err(err) => err, + })?; + let value_error = || { + PyValueError::new_err(format!( + "value {} is not a valid letter of the single-qubit alphabet for 'BitTerm'", + value + )) + }; + let value: u8 = value.try_into().map_err(|_| value_error())?; + value.try_into().map_err(|_| value_error()) + } +} + +/// A single term from a complete :class:`SparseObservable`. /// -/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic -/// "simplification" (summing of like terms), because the majority of additions to build up an -/// observable generate only a small number of duplications, and like-term detection has additional -/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, -/// or discuss further APIs with us for better building of observables. -#[pyclass(module = "qiskit.quantum_info", sequence)] -#[derive(Clone, Debug, PartialEq)] -pub struct SparseObservable { - /// The number of qubits the operator acts on. This is not inferable from any other shape or - /// values, since identities are not stored explicitly. - num_qubits: u32, - /// The coefficients of each abstract term in in the sum. This has as many elements as terms in - /// the sum. - coeffs: Vec, - /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored - /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` - bit_terms: Vec, - /// A flat list of the qubit indices that the corresponding entries in `bit_terms` act on. This - /// list must always be term-wise sorted, where a term is a sublist as denoted by `boundaries`. - indices: Vec, - /// Indices that partition `bit_terms` and `indices` into sublists for each individual term in - /// the sum. `boundaries[0]..boundaries[1]` is the range of indices into `bit_terms` and - /// `indices` that correspond to the first term of the sum. All unspecified qubit indices are - /// implicitly the identity. This is one item longer than `coeffs`, since `boundaries[0]` is - /// always an explicit zero (for algorithmic ease). - boundaries: Vec, +/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. +#[pyclass(name = "Term", frozen, module = "qiskit.quantum_info")] +#[derive(Clone, Debug)] +struct PySparseTerm { + inner: SparseTerm, } +#[pymethods] +impl PySparseTerm { + // Mark the Python class as being defined "within" the `SparseObservable` class namespace. + #[classattr] + #[pyo3(name = "__qualname__")] + fn type_qualname() -> &'static str { + "SparseObservable.Term" + } -impl SparseObservable { - /// Create a new observable from the raw components that make it up. - /// - /// This checks the input values for data coherence on entry. If you are certain you have the - /// correct values, you can call `new_unchecked` instead. - pub fn new( + #[new] + #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))] + fn py_new( num_qubits: u32, - coeffs: Vec, + coeff: Complex64, bit_terms: Vec, indices: Vec, - boundaries: Vec, - ) -> Result { - if coeffs.len() + 1 != boundaries.len() { - return Err(CoherenceError::MismatchedTermCount { - coeffs: coeffs.len(), - boundaries: boundaries.len(), - }); - } + ) -> PyResult { if bit_terms.len() != indices.len() { return Err(CoherenceError::MismatchedItemCount { bit_terms: bit_terms.len(), indices: indices.len(), - }); - } - // We already checked that `boundaries` is at least length 1. - if *boundaries.first().unwrap() != 0 { - return Err(CoherenceError::BadInitialBoundary(boundaries[0])); - } - if *boundaries.last().unwrap() != indices.len() { - return Err(CoherenceError::BadFinalBoundary { - last: *boundaries.last().unwrap(), - items: indices.len(), - }); - } - for (&left, &right) in boundaries[..].iter().zip(&boundaries[1..]) { - if right < left { - return Err(CoherenceError::DecreasingBoundaries); - } - let indices = &indices[left..right]; - if !indices.is_empty() { - for (index_left, index_right) in indices[..].iter().zip(&indices[1..]) { - if index_left == index_right { - return Err(CoherenceError::DuplicateIndices); - } else if index_left > index_right { - return Err(CoherenceError::UnsortedIndices); - } - } } - if indices.last().map(|&ix| ix >= num_qubits).unwrap_or(false) { - return Err(CoherenceError::BitIndexTooHigh); + .into()); + } + let mut order = (0..bit_terms.len()).collect::>(); + order.sort_unstable_by_key(|a| indices[*a]); + let bit_terms = order.iter().map(|i| bit_terms[*i]).collect(); + let mut sorted_indices = Vec::::with_capacity(order.len()); + for i in order { + let index = indices[i]; + if sorted_indices + .last() + .map(|prev| *prev >= index) + .unwrap_or(false) + { + return Err(CoherenceError::UnsortedIndices.into()); } + sorted_indices.push(index) } - // SAFETY: we've just done the coherence checks. - Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) - } - - /// Create a new observable from the raw components without checking data coherence. - /// - /// # Safety - /// - /// It is up to the caller to ensure that the data-coherence requirements, as enumerated in the - /// struct-level documentation, have been upheld. - #[inline(always)] - pub unsafe fn new_unchecked( - num_qubits: u32, - coeffs: Vec, - bit_terms: Vec, - indices: Vec, - boundaries: Vec, - ) -> Self { - Self { + let inner = SparseTerm::new( num_qubits, - coeffs, + coeff, bit_terms, - indices, - boundaries, - } + sorted_indices.into_boxed_slice(), + )?; + Ok(PySparseTerm { inner }) } - /// Create a zero operator with pre-allocated space for the given number of summands and - /// single-qubit bit terms. - #[inline] - pub fn with_capacity(num_qubits: u32, num_terms: usize, num_bit_terms: usize) -> Self { - Self { - num_qubits, - coeffs: Vec::with_capacity(num_terms), - bit_terms: Vec::with_capacity(num_bit_terms), - indices: Vec::with_capacity(num_bit_terms), - boundaries: { - let mut boundaries = Vec::with_capacity(num_terms + 1); - boundaries.push(0); - boundaries - }, + /// Convert this term to a complete :class:`SparseObservable`. + fn to_observable(&self) -> PyResult { + let obs = SparseObservable::new( + self.inner.num_qubits(), + vec![self.inner.coeff()], + self.inner.bit_terms().to_vec(), + self.inner.indices().to_vec(), + vec![0, self.inner.bit_terms().len()], + )?; + Ok(obs.into()) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); } + let Ok(other) = other.downcast_into::() else { + return Ok(false); + }; + let slf = slf.borrow(); + let other = other.borrow(); + Ok(slf.inner.eq(&other.inner)) } - /// Get an iterator over the individual terms of the operator. - /// - /// Recall that two [SparseObservable]s that have different term orders can still represent the - /// same object. Use [canonicalize] to apply a canonical ordering to the terms. - pub fn iter(&'_ self) -> impl ExactSizeIterator> + '_ { - self.coeffs.iter().enumerate().map(|(i, coeff)| { - let start = self.boundaries[i]; - let end = self.boundaries[i + 1]; - SparseTermView { - num_qubits: self.num_qubits, - coeff: *coeff, - bit_terms: &self.bit_terms[start..end], - indices: &self.indices[start..end], - } - }) + fn __repr__(&self) -> PyResult { + Ok(format!( + "<{} on {} qubit{}: {}>", + Self::type_qualname(), + self.inner.num_qubits(), + if self.inner.num_qubits() == 1 { + "" + } else { + "s" + }, + self.inner.view().to_sparse_str(), + )) } - /// Get an iterator over the individual terms of the operator that allows in-place mutation. - /// - /// The length and indices of these views cannot be mutated, since both would allow breaking - /// data coherence. - pub fn iter_mut(&mut self) -> IterMut<'_> { - self.into() + fn __getnewargs__(slf_: Bound) -> PyResult> { + let py = slf_.py(); + let borrowed = slf_.borrow(); + ( + borrowed.inner.num_qubits(), + borrowed.inner.coeff(), + Self::get_bit_terms(slf_.clone()), + Self::get_indices(slf_), + ) + .into_pyobject(py) } - /// Reduce the observable to its canonical form. - /// - /// This sums like terms, removing them if the final complex coefficient's absolute value is - /// less than or equal to the tolerance. The terms are reordered to some canonical ordering. - /// - /// This function is idempotent. - pub fn canonicalize(&self, tol: f64) -> SparseObservable { - let mut terms = btree_map::BTreeMap::new(); - for term in self.iter() { - terms - .entry((term.indices, term.bit_terms)) - .and_modify(|c| *c += term.coeff) - .or_insert(term.coeff); - } - let mut out = SparseObservable::zero(self.num_qubits); - for ((indices, bit_terms), coeff) in terms { - if coeff.norm_sqr() <= tol * tol { - continue; - } - out.coeffs.push(coeff); - out.bit_terms.extend_from_slice(bit_terms); - out.indices.extend_from_slice(indices); - out.boundaries.push(out.indices.len()); - } - out + /// Get a copy of this term. + fn copy(&self) -> Self { + self.clone() } - /// Tensor product of `self` with `other`. - /// - /// The bit ordering is defined such that the qubit indices of `other` will remain the same, and - /// the indices of `self` will be offset by the number of qubits in `other`. This is the same - /// convention as used by the rest of Qiskit's `quantum_info` operators. - /// - /// Put another way, in the simplest case of two observables formed of dense labels: - /// - /// ``` - /// let mut left = SparseObservable::zero(5); - /// left.add_dense_label("IXY+Z", Complex64::new(1.0, 0.0)); - /// let mut right = SparseObservable::zero(6); - /// right.add_dense_label("IIrl01", Complex64::new(1.0, 0.0)); - /// - /// // The result is the concatenation of the two labels. - /// let mut out = SparseObservable::zero(11); - /// out.add_dense_label("IXY+ZIIrl01", Complex64::new(1.0, 0.0)); - /// - /// assert_eq!(left.tensor(right), out); - /// ``` - pub fn tensor(&self, other: &SparseObservable) -> SparseObservable { - let mut out = SparseObservable::with_capacity( - self.num_qubits + other.num_qubits, - self.coeffs.len() * other.coeffs.len(), - other.coeffs.len() * self.bit_terms.len() + self.coeffs.len() * other.bit_terms.len(), - ); - let mut self_indices = Vec::new(); - for self_term in self.iter() { - self_indices.clear(); - self_indices.extend(self_term.indices.iter().map(|i| i + other.num_qubits)); - for other_term in other.iter() { - out.coeffs.push(self_term.coeff * other_term.coeff); - out.indices.extend_from_slice(other_term.indices); - out.indices.extend_from_slice(&self_indices); - out.bit_terms.extend_from_slice(other_term.bit_terms); - out.bit_terms.extend_from_slice(self_term.bit_terms); - out.boundaries.push(out.bit_terms.len()); - } - } + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~SparseObservable.BitTerm`. + #[getter] + fn get_bit_terms(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let bit_terms = borrowed.inner.bit_terms(); + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); out } - /// Get a view onto a representation of a single sparse term. - /// - /// This is effectively an indexing operation into the [SparseObservable]. Recall that two - /// [SparseObservable]s that have different term orders can still represent the same object. - /// Use [canonicalize] to apply a canonical ordering to the terms. - /// - /// # Panics - /// - /// If the index is out of bounds. - pub fn term(&self, index: usize) -> SparseTermView { - debug_assert!(index < self.num_terms(), "index {index} out of bounds"); - let start = self.boundaries[index]; - let end = self.boundaries[index + 1]; - SparseTermView { - num_qubits: self.num_qubits, - coeff: self.coeffs[index], - bit_terms: &self.bit_terms[start..end], - indices: &self.indices[start..end], - } + /// The number of qubits the term is defined on. + #[getter] + fn get_num_qubits(&self) -> u32 { + self.inner.num_qubits() } - /// Add the term implied by a dense string label onto this observable. - pub fn add_dense_label>( - &mut self, - label: L, - coeff: Complex64, - ) -> Result<(), LabelError> { - let label = label.as_ref(); - if label.len() != self.num_qubits() as usize { - return Err(LabelError::WrongLengthDense { - num_qubits: self.num_qubits(), - label: label.len(), - }); - } - // The only valid characters in the alphabet are ASCII, so if we see something other than - // ASCII, we're already in the failure path. - for (i, letter) in label.iter().rev().enumerate() { - match BitTerm::try_from_u8(*letter) { - Ok(Some(term)) => { - self.bit_terms.push(term); - self.indices.push(i as u32); - } - Ok(None) => (), - Err(_) => { - // Undo any modifications to ourselves so we stay in a consistent state. - let num_single_terms = self.boundaries[self.boundaries.len() - 1]; - self.bit_terms.truncate(num_single_terms); - self.indices.truncate(num_single_terms); - return Err(LabelError::OutsideAlphabet); - } - } - } - self.coeffs.push(coeff); - self.boundaries.push(self.bit_terms.len()); - Ok(()) + /// The term's coefficient. + #[getter] + fn get_coeff(&self) -> Complex64 { + self.inner.coeff() } - /// Relabel the `indices` in the operator to new values. - /// - /// This fails if any of the new indices are too large, or if any mapping would cause a term to - /// contain duplicates of the same index. It may not detect if multiple qubits are mapped to - /// the same index, if those qubits never appear together in the same term. Such a mapping - /// would not cause data-coherence problems (the output observable will be valid), but is - /// unlikely to be what you intended. + /// Read-only view onto the indices of each non-identity single-qubit term. /// - /// *Panics* if `new_qubits` is not long enough to map every index used in the operator. - pub fn relabel_qubits_from_slice(&mut self, new_qubits: &[u32]) -> Result<(), CoherenceError> { - for qubit in new_qubits { - if *qubit >= self.num_qubits { - return Err(CoherenceError::BitIndexTooHigh); - } - } - let mut order = btree_map::BTreeMap::new(); - for i in 0..self.num_terms() { - let start = self.boundaries[i]; - let end = self.boundaries[i + 1]; - for j in start..end { - order.insert(new_qubits[self.indices[j] as usize], self.bit_terms[j]); - } - if order.len() != end - start { - return Err(CoherenceError::DuplicateIndices); - } - for (index, dest) in order.keys().zip(&mut self.indices[start..end]) { - *dest = *index; - } - for (bit_term, dest) in order.values().zip(&mut self.bit_terms[start..end]) { - *dest = *bit_term; - } - order.clear(); - } - Ok(()) + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let indices = borrowed.inner.indices(); + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out } - /// Add a single term to this operator. - pub fn add_term(&mut self, term: SparseTermView) -> Result<(), ArithmeticError> { - if self.num_qubits != term.num_qubits { - return Err(ArithmeticError::MismatchedQubits { - left: self.num_qubits, - right: term.num_qubits, - }); + /// Get a :class:`.Pauli` object that represents the measurement basis needed for this term. + /// + /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting + /// :class:`.Pauli` is dense, in the sense that explicit identities are stored. An identity in + /// the Pauli output does not require a concrete measurement. + /// + /// Returns: + /// :class:`.Pauli`: the Pauli operator representing the necessary measurement basis. + /// + /// See also: + /// :meth:`SparseObservable.pauli_bases` + /// A similar method for an entire observable at once. + fn pauli_base<'py>(&self, py: Python<'py>) -> PyResult> { + let mut x = vec![false; self.inner.num_qubits() as usize]; + let mut z = vec![false; self.inner.num_qubits() as usize]; + for (bit_term, index) in self + .inner + .bit_terms() + .iter() + .zip(self.inner.indices().iter()) + { + x[*index as usize] = bit_term.has_x_component(); + z[*index as usize] = bit_term.has_z_component(); } - self.coeffs.push(term.coeff); - self.bit_terms.extend_from_slice(term.bit_terms); - self.indices.extend_from_slice(term.indices); - self.boundaries.push(self.bit_terms.len()); - Ok(()) + PAULI_TYPE + .get_bound(py) + .call1(((PyArray1::from_vec(py, z), PyArray1::from_vec(py, x)),)) } +} - /// Return a suitable Python error if two observables do not have equal numbers of qubits. - fn check_equal_qubits(&self, other: &SparseObservable) -> PyResult<()> { - if self.num_qubits != other.num_qubits { - Err(PyValueError::new_err(format!( - "incompatible numbers of qubits: {} and {}", - self.num_qubits, other.num_qubits - ))) - } else { - Ok(()) - } - } +/// An observable over Pauli bases that stores its data in a qubit-sparse format. +/// +/// Mathematics +/// =========== +/// +/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate +/// projectors, with each term weighted by some complex number. That is, the full observable is +/// +/// .. math:: +/// +/// \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i +/// +/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a +/// restricted alphabet :math:`A^{(n)}_i`. The sum over :math:`i` is the sum of the individual +/// terms, and the tensor product produces the operator strings. +/// +/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the +/// Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are: +/// +/// .. _sparse-observable-alphabet: +/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable` +/// +/// ======= ======================================= =============== =========================== +/// Label Operator Numeric value :class:`.BitTerm` attribute +/// ======= ======================================= =============== =========================== +/// ``"I"`` :math:`I` (identity) Not stored. Not stored. +/// +/// ``"X"`` :math:`X` (Pauli X) ``0b0010`` (2) :attr:`~.BitTerm.X` +/// +/// ``"Y"`` :math:`Y` (Pauli Y) ``0b0011`` (3) :attr:`~.BitTerm.Y` +/// +/// ``"Z"`` :math:`Z` (Pauli Z) ``0b0001`` (1) :attr:`~.BitTerm.Z` +/// +/// ``"+"`` :math:`\lvert+\rangle\langle+\rvert` ``0b1010`` (10) :attr:`~.BitTerm.PLUS` +/// (projector to positive eigenstate of X) +/// +/// ``"-"`` :math:`\lvert-\rangle\langle-\rvert` ``0b0110`` (6) :attr:`~.BitTerm.MINUS` +/// (projector to negative eigenstate of X) +/// +/// ``"r"`` :math:`\lvert r\rangle\langle r\rvert` ``0b1011`` (11) :attr:`~.BitTerm.RIGHT` +/// (projector to positive eigenstate of Y) +/// +/// ``"l"`` :math:`\lvert l\rangle\langle l\rvert` ``0b0111`` (7) :attr:`~.BitTerm.LEFT` +/// (projector to negative eigenstate of Y) +/// +/// ``"0"`` :math:`\lvert0\rangle\langle0\rvert` ``0b1001`` (9) :attr:`~.BitTerm.ZERO` +/// (projector to positive eigenstate of Z) +/// +/// ``"1"`` :math:`\lvert1\rangle\langle1\rvert` ``0b0101`` (5) :attr:`~.BitTerm.ONE` +/// (projector to negative eigenstate of Z) +/// ======= ======================================= =============== =========================== +/// +/// The allowed alphabet forms an overcomplete basis of the operator space. This means that there +/// is not a unique summation to represent a given observable. By comparison, +/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of +/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order +/// `) there is only one representation of any operator. +/// +/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making +/// "efficiency of measurement" equivalent to "efficiency of representation". For example, the +/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on +/// hardware with simple :math:`Z` measurements, but can only be represented by +/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored +/// terms. :class:`SparseObservable` requires only a single term to store this. +/// +/// The downside to this is that it is impractical to take an arbitrary matrix or +/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation. You +/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to +/// decompose into one. +/// +/// +/// Representation +/// ============== +/// +/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit +/// operators. This makes it significantly more efficient to represent observables such as +/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of +/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically. +/// +/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse +/// matrices. In this analogy, the terms of the sum are the "rows", and the qubit terms are the +/// "columns", where an absent entry represents the identity rather than a zero. More explicitly, +/// the representation is made up of four contiguous arrays: +/// +/// .. _sparse-observable-arrays: +/// .. table:: Data arrays used to represent :class:`.SparseObservable` +/// +/// ================== =========== ============================================================= +/// Attribute Length Description +/// ================== =========== ============================================================= +/// :attr:`coeffs` :math:`t` The complex scalar multiplier for each term. +/// +/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit terms for all of the +/// operators, in order. These correspond to the non-identity +/// :math:`A^{(n)}_i` in the sum description, where the entries +/// are stored in order of increasing :math:`i` first, and in +/// order of increasing :math:`n` within each term. +/// +/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators +/// in :attr:`bit_terms`. :class:`SparseObservable` requires +/// that this list is term-wise sorted, and algorithms can rely +/// on this invariant being upheld. +/// +/// :attr:`boundaries` :math:`t+1` The indices that partition :attr:`bit_terms` and +/// :attr:`indices` into complete terms. For term number +/// :math:`i`, its complex coefficient is ``coeffs[i]``, and its +/// non-identity single-qubit operators and their corresponding +/// qubits are the slice ``boundaries[i] : boundaries[i+1]`` into +/// :attr:`bit_terms` and :attr:`indices` respectively. +/// :attr:`boundaries` always has an explicit 0 as its first +/// element. +/// ================== =========== ============================================================= +/// +/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is +/// the total number of non-identity single-qubit terms. +/// +/// As illustrative examples: +/// +/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other +/// vectors are empty. +/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``, +/// :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty. +/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``, +/// :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y, +/// BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``. The operator might act on more than +/// four qubits, depending on the :attr:`num_qubits` parameter. The :attr:`bit_terms` are integer +/// values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class. Note +/// that the single-bit terms and indices are sorted into termwise sorted order. This is a +/// requirement of the class. +/// +/// These cases are not special, they're fully consistent with the rules and should not need special +/// handling. +/// +/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte. The numeric values +/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are +/// accessible with named access by an enumeration: +/// +/// .. +/// This is documented manually here because the Python-space `Enum` is generated +/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere +/// else in this source file. The use of `autoattribute` is because it pulls in the numeric +/// value. +/// +/// .. py:class:: SparseObservable.BitTerm +/// +/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to +/// represent each of the single-qubit alphabet terms enumerated in +/// :ref:`sparse-observable-alphabet`. +/// +/// This class is attached to :class:`.SparseObservable`. Access it as +/// :class:`.SparseObservable.BitTerm`. If this is too much typing, and you are solely dealing +/// with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you +/// might want to shorten it as:: +/// +/// >>> ops = SparseObservable.BitTerm +/// >>> assert ops.X is SparseObservable.BitTerm.X +/// +/// You can access all the values of the enumeration by either their full all-capitals name, or +/// by their single-letter label. The single-letter labels are not generally valid Python +/// identifiers, so you must use indexing notation to access them:: +/// +/// >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"] +/// +/// The numeric structure of these is that they are all four-bit values of which the low two +/// bits are the (phase-less) symplectic representation of the Pauli operator related to the +/// object, where the low bit denotes a contribution by :math:`Z` and the second lowest a +/// contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01`` +/// for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X +/// +/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS +/// +/// The projector to the positive eigenstate of the :math:`X` operator: +/// :math:`\lvert+\rangle\langle+\rvert`. Uses the single-letter label ``"+"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS +/// +/// The projector to the negative eigenstate of the :math:`X` operator: +/// :math:`\lvert-\rangle\langle-\rvert`. Uses the single-letter label ``"-"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y +/// +/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT +/// +/// The projector to the positive eigenstate of the :math:`Y` operator: +/// :math:`\lvert r\rangle\langle r\rvert`. Uses the single-letter label ``"r"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT +/// +/// The projector to the negative eigenstate of the :math:`Y` operator: +/// :math:`\lvert l\rangle\langle l\rvert`. Uses the single-letter label ``"l"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z +/// +/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO +/// +/// The projector to the positive eigenstate of the :math:`Z` operator: +/// :math:`\lvert0\rangle\langle0\rvert`. Uses the single-letter label ``"0"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE +/// +/// The projector to the negative eigenstate of the :math:`Z` operator: +/// :math:`\lvert1\rangle\langle1\rvert`. Uses the single-letter label ``"1"``. +/// +/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these +/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy +/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the +/// field; this lets you easily do mathematics on the results, like bitwise operations on +/// :attr:`bit_terms`. You can assign to indices or slices of each of the attributes, but beware +/// that you must uphold :ref:`the data coherence rules ` while doing +/// this. For example:: +/// +/// >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) +/// >>> assert isinstance(obs.coeffs[:], np.ndarray) +/// >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. +/// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 +/// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) +/// +/// .. note:: +/// +/// The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`. +/// +/// .. _sparse-observable-canonical-order: +/// +/// Canonical ordering +/// ------------------ +/// +/// For any given mathematical observable, there are several ways of representing it with +/// :class:`SparseObservable`. For example, the same set of single-bit terms and their +/// corresponding indices might appear multiple times in the observable. Mathematically, this is +/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms +/// of the sum in a :class:`SparseObservable` can be in any order while representing the same +/// observable, since addition is commutative (although while floating-point addition is not +/// associative, :class:`SparseObservable` makes no guarantees about the summation order). +/// +/// These two categories of representation degeneracy can cause the ``==`` operator to claim that +/// two observables are not equal, despite representating the same object. In these cases, it can +/// be convenient to define some *canonical form*, which allows observables to be compared +/// structurally. +/// +/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. +/// The precise ordering of terms in canonical ordering is not specified, and may change between +/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables +/// structurally by comparing their simplified forms. +/// +/// .. note:: +/// +/// If you wish to account for floating-point tolerance in the comparison, it is safest to use +/// a recipe such as:: +/// +/// def equivalent(left, right, tol): +/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) +/// +/// .. note:: +/// +/// The canonical form produced by :meth:`simplify` will still not universally detect all +/// observables that are equivalent due to the over-complete basis alphabet; it is not +/// computationally feasible to do this at scale. For example, on observable built from ``+`` +/// and ``-`` components will not canonicalize to a single ``X`` term. +/// +/// Indexing +/// -------- +/// +/// :class:`SparseObservable` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The observable can be indexed by integers, and iterated +/// through to yield individual terms. +/// +/// Each term appears as an instance a self-contained class. The individual terms are copied out of +/// the base observable; mutations to them will not affect the observable. +/// +/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term +/// :members: +/// +/// Construction +/// ============ +/// +/// :class:`SparseObservable` defines several constructors. The default constructor will attempt to +/// delegate to one of the more specific constructors, based on the type of the input. You can +/// always use the specific constructors to have more control over the construction. +/// +/// .. _sparse-observable-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_label` Convert a dense string label into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_list` Sum a list of tuples of dense string labels and the associated +/// coefficients into an observable. +/// +/// :meth:`from_sparse_list` Sum a list of tuples of sparse string labels, the qubits they +/// apply to, and their coefficients into an observable. +/// +/// :meth:`from_pauli` Raise a single :class:`.Pauli` into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a :class:`SparseObservable`. +/// +/// :meth:`from_terms` Sum explicit single :class:`Term` instances. +/// +/// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays +/// `. +/// ============================ ================================================================ +/// +/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`SparseObservable`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. If +/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a +/// number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`SparseObservable`, in +/// which case the input is copied, a :class:`.Pauli` or :class:`.SparsePauliOp`, in which +/// case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it +/// can be a list in a valid format for either :meth:`from_list` or +/// :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the operator. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, there are also helper methods that construct +/// special forms of observables. +/// +/// .. table:: Construction of special observables +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`zero` The zero operator on a given number of qubits. +/// +/// :meth:`identity` The identity operator on a given number of qubits. +/// ============================ ================================================================ +/// +/// +/// Mathematical manipulation +/// ========================= +/// +/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other +/// :mod:`~qiskit.quantum_info` operators. +/// +/// In basic arithmetic, you can: +/// +/// * add two observables using ``+`` +/// * subtract two observables using ``-`` +/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` +/// * negate all the coefficients in an observable with unary ``-`` +/// +/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, +/// which mutates the left-hand side in-place. Using these is typically more efficient than the +/// infix operators, especially for building an observable in a loop. +/// +/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of +/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be +/// equivalent to :meth:`tensor`. +/// +/// .. note:: +/// +/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware +/// that `Python's operator-precedence rules +/// `__ may cause the +/// evaluation order to be different to your expectation. In particular, the operator ``+`` +/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. +/// +/// When using the operators in mixed expressions, it is safest to use parentheses to group the +/// operands of tensor products. +/// +/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex +/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined +/// analogously to the matrix representation of other Pauli operators in Qiskit. +/// +/// +/// Efficiency notes +/// ---------------- +/// +/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating +/// growable vectors for extending the number of terms. This means that the cost of appending to an +/// observable using ``+=`` is amortised linear in the total number of terms added, rather than +/// the quadratic complexity that the binary ``+`` would require. +/// +/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic +/// "simplification" (summing of like terms), because the majority of additions to build up an +/// observable generate only a small number of duplications, and like-term detection has additional +/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, +/// or discuss further APIs with us for better building of observables. +#[pyclass(name = "SparseObservable", module = "qiskit.quantum_info", sequence)] +#[derive(Debug)] +pub struct PySparseObservable { + // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. + inner: Arc>, } - #[pymethods] -impl SparseObservable { +impl PySparseObservable { #[pyo3(signature = (data, /, num_qubits=None))] #[new] fn py_new(data: &Bound, num_qubits: Option) -> PyResult { @@ -1116,11 +1902,11 @@ impl SparseObservable { if data.is_instance(PAULI_TYPE.get_bound(py))? { check_num_qubits(data)?; - return Self::py_from_pauli(data); + return Self::from_pauli(data); } if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? { check_num_qubits(data)?; - return Self::py_from_sparse_pauli_op(data); + return Self::from_sparse_pauli_op(data); } if let Ok(label) = data.extract::() { let num_qubits = num_qubits.unwrap_or(label.len() as u32); @@ -1131,407 +1917,224 @@ impl SparseObservable { label.len(), ))); } - return Self::py_from_label(&label).map_err(PyErr::from); + return Self::from_label(&label).map_err(PyErr::from); } if let Ok(observable) = data.downcast_exact::() { check_num_qubits(data)?; - return Ok(observable.borrow().clone()); + let borrowed = observable.borrow(); + let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; + return Ok(inner.clone().into()); } // The type of `vec` is inferred from the subsequent calls to `Self::py_from_list` or // `Self::py_from_sparse_list` to be either the two-tuple or the three-tuple form during the // `extract`. The empty list will pass either, but it means the same to both functions. if let Ok(vec) = data.extract() { - return Self::py_from_list(vec, num_qubits); + return Self::from_list(vec, num_qubits); } if let Ok(vec) = data.extract() { let Some(num_qubits) = num_qubits else { return Err(PyValueError::new_err( "if using the sparse-list form, 'num_qubits' must be provided", )); - }; - return Self::py_from_sparse_list(vec, num_qubits).map_err(PyErr::from); - } - if let Ok(term) = data.downcast_exact::() { - return Ok(term.borrow().to_observable()); - }; - if let Ok(observable) = Self::py_from_terms(data, num_qubits) { - return Ok(observable); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'SparseObservable': {}", - data.get_type().repr()?, - ))) - } - - /// The number of qubits the operator acts on. - /// - /// This is not inferable from any other shape or values, since identities are not stored - /// explicitly. - #[getter] - #[inline] - pub fn num_qubits(&self) -> u32 { - self.num_qubits - } - - /// The number of terms in the sum this operator is tracking. - #[getter] - #[inline] - pub fn num_terms(&self) -> usize { - self.coeffs.len() - } - - /// The coefficients of each abstract term in in the sum. This has as many elements as terms in - /// the sum. - #[getter] - fn get_coeffs(slf_: Py) -> ArrayView { - ArrayView { - base: slf_, - slot: ArraySlot::Coeffs, - } - } - - /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored - /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` - #[getter] - fn get_bit_terms(slf_: Py) -> ArrayView { - ArrayView { - base: slf_, - slot: ArraySlot::BitTerms, - } - } - - /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on. - /// This list must always be term-wise sorted, where a term is a sublist as denoted by - /// :attr:`boundaries`. - /// - /// .. warning:: - /// - /// If writing to this attribute from Python space, you *must* ensure that you only write in - /// indices that are term-wise sorted. - #[getter] - fn get_indices(slf_: Py) -> ArrayView { - ArrayView { - base: slf_, - slot: ArraySlot::Indices, - } - } - - /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each - /// individual term in the sum. ``boundaries[0] : boundaries[1]`` is the range of indices into - /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum. All - /// unspecified qubit indices are implicitly the identity. This is one item longer than - /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease). - #[getter] - fn get_boundaries(slf_: Py) -> ArrayView { - ArrayView { - base: slf_, - slot: ArraySlot::Boundaries, - } - } - - // The documentation for this is inlined into the class-level documentation of - // `SparseObservable`. - #[allow(non_snake_case)] - #[classattr] - fn BitTerm(py: Python) -> PyResult> { - BIT_TERM_PY_ENUM - .get_or_try_init(py, || make_py_bit_term(py)) - .map(|obj| obj.clone_ref(py)) - } - - // The documentation for this is inlined into the class-level documentation of - // `SparseObservable`. - #[allow(non_snake_case)] - #[classattr] - fn Term(py: Python) -> Bound { - py.get_type_bound::() - } - - /// Get the zero operator over the given number of qubits. - /// - /// The zero operator is the operator whose expectation value is zero for all quantum states. - /// It has no terms. It is the identity element for addition of two :class:`SparseObservable` - /// instances; anything added to the zero operator is equal to itself. - /// - /// If you want the projector onto the all zeros state, use:: - /// - /// >>> num_qubits = 10 - /// >>> all_zeros = SparseObservable.from_label("0" * num_qubits) - /// - /// Examples: - /// - /// Get the zero operator for 100 qubits:: - /// - /// >>> SparseObservable.zero(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn zero(num_qubits: u32) -> Self { - Self::with_capacity(num_qubits, 0, 0) - } - - /// Get the identity operator over the given number of qubits. - /// - /// Examples: - /// - /// Get the identity operator for 100 qubits:: - /// - /// >>> SparseObservable.identity(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn identity(num_qubits: u32) -> Self { - Self { - num_qubits, - coeffs: vec![Complex64::new(1.0, 0.0)], - bit_terms: vec![], - indices: vec![], - boundaries: vec![0, 0], - } - } - - /// Clear all the terms from this operator, making it equal to the zero operator again. - /// - /// This does not change the capacity of the internal allocations, so subsequent addition or - /// substraction operations may not need to reallocate. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) - /// >>> obs.clear() - /// >>> assert obs == SparseObservable.zero(obs.num_qubits) - pub fn clear(&mut self) { - self.coeffs.clear(); - self.bit_terms.clear(); - self.indices.clear(); - self.boundaries.truncate(1); - } - - fn __len__(&self) -> usize { - self.num_terms() - } - - fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult> { - let indices = match index.with_len(self.num_terms())? { - SequenceIndex::Int(index) => return Ok(self.term(index).to_term().into_py(py)), - indices => indices, - }; - let mut out = SparseObservable::zero(self.num_qubits); - for index in indices.iter() { - out.add_term(self.term(index))?; - } - Ok(out.into_py(py)) - } - - fn __repr__(&self) -> String { - let num_terms = format!( - "{} term{}", - self.num_terms(), - if self.num_terms() == 1 { "" } else { "s" } - ); - let qubits = format!( - "{} qubit{}", - self.num_qubits(), - if self.num_qubits() == 1 { "" } else { "s" } - ); - let terms = if self.num_terms() == 0 { - "0.0".to_owned() - } else { - self.iter() - .map(SparseTermView::to_sparse_str) - .collect::>() - .join(" + ") - }; - format!( - "", - num_terms, qubits, terms - ) - } - - fn __reduce__(&self, py: Python) -> PyResult> { - let bit_terms: &[u8] = ::bytemuck::cast_slice(&self.bit_terms); - Ok(( - py.get_type_bound::().getattr("from_raw_parts")?, - ( - self.num_qubits, - PyArray1::from_slice_bound(py, &self.coeffs), - PyArray1::from_slice_bound(py, bit_terms), - PyArray1::from_slice_bound(py, &self.indices), - PyArray1::from_slice_bound(py, &self.boundaries), - false, - ), - ) - .into_py(py)) - } - - fn __eq__(slf: Bound, other: Bound) -> bool { - if slf.is(&other) { - return true; - } - let Ok(other) = other.downcast_into::() else { - return false; - }; - slf.borrow().eq(&other.borrow()) - } - - fn __add__(slf_: &Bound, other: &Bound) -> PyResult> { - let py = slf_.py(); - if slf_.is(other) { - // This fast path is for consistency with the in-place `__iadd__`, which would otherwise - // struggle to do the addition to itself. - return Ok(<&SparseObservable as ::std::ops::Mul<_>>::mul( - &slf_.borrow(), - Complex64::new(2.0, 0.0), - ) - .into_py(py)); - } - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - let slf_ = slf_.borrow(); - let other = other.borrow(); - slf_.check_equal_qubits(&other)?; - Ok(<&SparseObservable as ::std::ops::Add>::add(&slf_, &other).into_py(py)) - } - fn __radd__(&self, other: &Bound) -> PyResult> { - // No need to handle the `self is other` case here, because `__add__` will get it. - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - let other = other.borrow(); - self.check_equal_qubits(&other)?; - Ok((<&SparseObservable as ::std::ops::Add>::add(&other, self)).into_py(py)) - } - fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { - if slf_.is(other) { - *slf_.borrow_mut() *= Complex64::new(2.0, 0.0); - return Ok(()); - } - let mut slf_ = slf_.borrow_mut(); - let Some(other) = coerce_to_observable(other)? else { - // This is not well behaved - we _should_ return `NotImplemented` to Python space - // without an exception, but limitations in PyO3 prevent this at the moment. See - // https://github.com/PyO3/pyo3/issues/4605. - return Err(PyTypeError::new_err(format!( - "invalid object for in-place addition of 'SparseObservable': {}", - other.repr()? - ))); - }; - let other = other.borrow(); - slf_.check_equal_qubits(&other)?; - *slf_ += &other; - Ok(()) - } - - fn __sub__(slf_: &Bound, other: &Bound) -> PyResult> { - let py = slf_.py(); - if slf_.is(other) { - return Ok(SparseObservable::zero(slf_.borrow().num_qubits).into_py(py)); + }; + return Self::from_sparse_list(vec, num_qubits).map_err(PyErr::from); } - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - let slf_ = slf_.borrow(); - let other = other.borrow(); - slf_.check_equal_qubits(&other)?; - Ok(<&SparseObservable as ::std::ops::Sub>::sub(&slf_, &other).into_py(py)) - } - fn __rsub__(&self, other: &Bound) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); + if let Ok(term) = data.downcast_exact::() { + return term.borrow().to_observable(); }; - let other = other.borrow(); - self.check_equal_qubits(&other)?; - Ok((<&SparseObservable as ::std::ops::Sub>::sub(&other, self)).into_py(py)) - } - fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { - if slf_.is(other) { - // This is not strictly the same thing as `a - a` if `a` contains non-finite - // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a - // clear view on what floating-point guarantees we're going to make right now. - slf_.borrow_mut().clear(); - return Ok(()); + if let Ok(observable) = Self::from_terms(data, num_qubits) { + return Ok(observable); } - let mut slf_ = slf_.borrow_mut(); - let Some(other) = coerce_to_observable(other)? else { - // This is not well behaved - we _should_ return `NotImplemented` to Python space - // without an exception, but limitations in PyO3 prevent this at the moment. See - // https://github.com/PyO3/pyo3/issues/4605. - return Err(PyTypeError::new_err(format!( - "invalid object for in-place subtraction of 'SparseObservable': {}", - other.repr()? - ))); - }; - let other = other.borrow(); - slf_.check_equal_qubits(&other)?; - *slf_ -= &other; - Ok(()) + Err(PyTypeError::new_err(format!( + "unknown input format for 'SparseObservable': {}", + data.get_type().repr()?, + ))) } - fn __pos__(&self) -> SparseObservable { - self.clone() + /// Get a copy of this observable. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)]) + /// >>> assert obs == obs.copy() + /// >>> assert obs is not obs.copy() + fn copy(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) } - fn __neg__(&self) -> SparseObservable { - -self + + /// The number of qubits the operator acts on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_qubits()) } - fn __mul__(&self, other: Complex64) -> SparseObservable { - self * other + /// The number of terms in the sum this operator is tracking. + #[getter] + #[inline] + pub fn num_terms(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_terms()) } - fn __rmul__(&self, other: Complex64) -> SparseObservable { - other * self + + /// The coefficients of each abstract term in in the sum. This has as many elements as terms in + /// the sum. + #[getter] + fn get_coeffs(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Coeffs, + } } - fn __imul__(&mut self, other: Complex64) { - *self *= other; + + /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored + /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` + #[getter] + fn get_bit_terms(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::BitTerms, + } } - fn __truediv__(&self, other: Complex64) -> PyResult { - if other.is_zero() { - return Err(PyZeroDivisionError::new_err("complex division by zero")); + /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on. + /// This list must always be term-wise sorted, where a term is a sublist as denoted by + /// :attr:`boundaries`. + /// + /// .. warning:: + /// + /// If writing to this attribute from Python space, you *must* ensure that you only write in + /// indices that are term-wise sorted. + #[getter] + fn get_indices(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Indices, } - Ok(self / other) } - fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { - if other.is_zero() { - return Err(PyZeroDivisionError::new_err("complex division by zero")); + + /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each + /// individual term in the sum. ``boundaries[0] : boundaries[1]`` is the range of indices into + /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum. All + /// unspecified qubit indices are implicitly the identity. This is one item longer than + /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease). + #[getter] + fn get_boundaries(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Boundaries, } - *self /= other; - Ok(()) } - fn __xor__(&self, other: &Bound) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - Ok(self.tensor(&other.borrow()).into_py(py)) + /// Get the zero operator over the given number of qubits. + /// + /// The zero operator is the operator whose expectation value is zero for all quantum states. + /// It has no terms. It is the identity element for addition of two :class:`SparseObservable` + /// instances; anything added to the zero operator is equal to itself. + /// + /// If you want the projector onto the all zeros state, use:: + /// + /// >>> num_qubits = 10 + /// >>> all_zeros = SparseObservable.from_label("0" * num_qubits) + /// + /// Examples: + /// + /// Get the zero operator for 100 qubits:: + /// + /// >>> SparseObservable.zero(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn zero(num_qubits: u32) -> Self { + SparseObservable::zero(num_qubits).into() } - fn __rxor__(&self, other: &Bound) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - Ok(other.borrow().tensor(self).into_py(py)) + + /// Get the identity operator over the given number of qubits. + /// + /// Examples: + /// + /// Get the identity operator for 100 qubits:: + /// + /// >>> SparseObservable.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + SparseObservable::identity(num_qubits).into() } - // This doesn't actually have any interaction with Python space, but uses the `py_` prefix on - // its name to make it clear it's different to the Rust concept of `Copy`. - /// Get a copy of this observable. + /// Construct a :class:`.SparseObservable` from a single :class:`.Pauli` instance. + /// + /// The output observable will have a single term, with a unitary coefficient dependent on the + /// phase. + /// + /// Args: + /// pauli (:class:`.Pauli`): the single Pauli to convert. /// /// Examples: /// /// .. code-block:: python /// - /// >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)]) - /// >>> assert obs == obs.copy() - /// >>> assert obs is not obs.copy() - #[pyo3(name = "copy")] - fn py_copy(&self) -> Self { - self.clone() + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> SparseObservable.from_pauli(pauli) + /// + /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + fn from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut bit_terms = Vec::new(); + let mut indices = Vec::new(); + let mut num_ys = 0; + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else { + continue; + }; + num_ys += (term == BitTerm::Y) as isize; + indices.push(i as u32); + bit_terms.push(term); + } + let boundaries = vec![0, indices.len()]; + // The "empty" state of a `Pauli` represents the identity, which isn't our empty state + // (that's zero), so we're always going to have a coefficient. + let group_phase = pauli + // `Pauli`'s `_phase` is a Numpy array ... + .getattr(intern!(py, "_phase"))? + // ... that should have exactly 1 element ... + .call_method0(intern!(py, "item"))? + // ... which is some integral type. + .extract::()?; + let phase = match (group_phase - num_ys).rem_euclid(4) { + 0 => Complex64::new(1.0, 0.0), + 1 => Complex64::new(0.0, -1.0), + 2 => Complex64::new(-1.0, 0.0), + 3 => Complex64::new(0.0, 1.0), + _ => unreachable!("`x % 4` has only four values"), + }; + let coeffs = vec![phase]; + let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) } /// Construct a single-term observable from a dense string label. @@ -1547,7 +2150,7 @@ impl SparseObservable { /// Examples: /// /// .. code-block:: python - /// + /// /// >>> SparseObservable.from_label("IIII+ZI") /// /// >>> label = "IYXZI" @@ -1559,11 +2162,11 @@ impl SparseObservable { /// A generalization of this method that constructs a sum operator from multiple labels /// and their corresponding coefficients. #[staticmethod] - #[pyo3(name = "from_label", signature = (label, /))] - fn py_from_label(label: &str) -> Result { - let mut out = Self::zero(label.len() as u32); - out.add_dense_label(label, Complex64::new(1.0, 0.0))?; - Ok(out) + #[pyo3(signature = (label, /))] + fn from_label(label: &str) -> Result { + let mut inner = SparseObservable::zero(label.len() as u32); + inner.add_dense_label(label, Complex64::new(1.0, 0.0))?; + Ok(inner.into()) } /// Construct an observable from a list of dense labels and coefficients. @@ -1625,8 +2228,8 @@ impl SparseObservable { /// Construct the observable from a list of labels without explicit identities, but with /// the qubits each single-qubit term applies to listed explicitly. #[staticmethod] - #[pyo3(name = "from_list", signature = (iter, /, *, num_qubits=None))] - fn py_from_list(iter: Vec<(String, Complex64)>, num_qubits: Option) -> PyResult { + #[pyo3(signature = (iter, /, *, num_qubits=None))] + fn from_list(iter: Vec<(String, Complex64)>, num_qubits: Option) -> PyResult { if iter.is_empty() && num_qubits.is_none() { return Err(PyValueError::new_err( "cannot construct an observable from an empty list without knowing `num_qubits`", @@ -1636,11 +2239,11 @@ impl SparseObservable { Some(num_qubits) => num_qubits, None => iter[0].0.len() as u32, }; - let mut out = Self::with_capacity(num_qubits, iter.len(), 0); + let mut inner = SparseObservable::with_capacity(num_qubits, iter.len(), 0); for (label, coeff) in iter { - out.add_dense_label(&label, coeff)?; + inner.add_dense_label(&label, coeff)?; } - Ok(out) + Ok(inner.into()) } /// Construct an observable from a list of labels, the qubits each item applies to, and the @@ -1687,11 +2290,11 @@ impl SparseObservable { /// ... ]) /// >>> assert from_list == from_sparse_list #[staticmethod] - #[pyo3(name = "from_sparse_list", signature = (iter, /, num_qubits))] - fn py_from_sparse_list( + #[pyo3(signature = (iter, /, num_qubits))] + fn from_sparse_list( iter: Vec<(String, Vec, Complex64)>, num_qubits: u32, - ) -> Result { + ) -> PyResult { let coeffs = iter.iter().map(|(_, _, coeff)| *coeff).collect(); let mut boundaries = Vec::with_capacity(iter.len() + 1); boundaries.push(0); @@ -1707,103 +2310,31 @@ impl SparseObservable { return Err(LabelError::WrongLengthIndices { label: label.len(), indices: indices.len(), - }); + } + .into()); } for (letter, index) in label.iter().zip(qubits) { if index >= num_qubits { - return Err(LabelError::BadIndex { index, num_qubits }); - } - let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { - return Err(LabelError::DuplicateIndex { index }); - }; - entry.insert( - BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?, - ); - } - for (index, term) in sorted.iter() { - let Some(term) = term else { - continue; - }; - indices.push(*index); - bit_terms.push(*term); - } - boundaries.push(bit_terms.len()); - } - Ok(Self { - num_qubits, - coeffs, - indices, - bit_terms, - boundaries, - }) - } - - /// Construct a :class:`.SparseObservable` from a single :class:`.Pauli` instance. - /// - /// The output observable will have a single term, with a unitary coefficient dependent on the - /// phase. - /// - /// Args: - /// pauli (:class:`.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> SparseObservable.from_pauli(pauli) - /// - /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) - #[staticmethod] - #[pyo3(name = "from_pauli", signature = (pauli, /))] - fn py_from_pauli(pauli: &Bound) -> PyResult { - let py = pauli.py(); - let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; - let z = pauli - .getattr(intern!(py, "z"))? - .extract::>()?; - let x = pauli - .getattr(intern!(py, "x"))? - .extract::>()?; - let mut bit_terms = Vec::new(); - let mut indices = Vec::new(); - let mut num_ys = 0; - for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else { - continue; - }; - num_ys += (term == BitTerm::Y) as isize; - indices.push(i as u32); - bit_terms.push(term); + return Err(LabelError::BadIndex { index, num_qubits }.into()); + } + let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { + return Err(LabelError::DuplicateIndex { index }.into()); + }; + entry.insert( + BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?, + ); + } + for (index, term) in sorted.iter() { + let Some(term) = term else { + continue; + }; + indices.push(*index); + bit_terms.push(*term); + } + boundaries.push(bit_terms.len()); } - let boundaries = vec![0, indices.len()]; - // The "empty" state of a `Pauli` represents the identity, which isn't our empty state - // (that's zero), so we're always going to have a coefficient. - let group_phase = pauli - // `Pauli`'s `_phase` is a Numpy array ... - .getattr(intern!(py, "_phase"))? - // ... that should have exactly 1 element ... - .call_method0(intern!(py, "item"))? - // ... which is some integral type. - .extract::()?; - let phase = match (group_phase - num_ys).rem_euclid(4) { - 0 => Complex64::new(1.0, 0.0), - 1 => Complex64::new(0.0, -1.0), - 2 => Complex64::new(-1.0, 0.0), - 3 => Complex64::new(0.0, 1.0), - _ => unreachable!("`x % 4` has only four values"), - }; - let coeffs = vec![phase]; - Ok(Self { - num_qubits, - coeffs, - bit_terms, - indices, - boundaries, - }) + let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) } /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance. @@ -1823,8 +2354,8 @@ impl SparseObservable { /// >>> SparseObservable.from_sparse_pauli_op(spo) /// #[staticmethod] - #[pyo3(name = "from_sparse_pauli_op", signature = (op, /))] - fn py_from_sparse_pauli_op(op: &Bound) -> PyResult { + #[pyo3(signature = (op, /))] + fn from_sparse_pauli_op(op: &Bound) -> PyResult { let py = op.py(); let pauli_list_ob = op.getattr(intern!(py, "paulis"))?; let coeffs = op @@ -1882,13 +2413,9 @@ impl SparseObservable { boundaries.push(indices.len()); } - Ok(Self { - num_qubits: num_qubits as u32, - coeffs, - bit_terms, - indices, - boundaries, - }) + let inner = + SparseObservable::new(num_qubits as u32, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) } /// Construct a :class:`SparseObservable` out of individual terms. @@ -1907,10 +2434,10 @@ impl SparseObservable { /// Returns: /// The corresponding observable. #[staticmethod] - #[pyo3(signature = (obj, /, num_qubits=None), name="from_terms")] - fn py_from_terms(obj: &Bound, num_qubits: Option) -> PyResult { - let mut iter = obj.iter()?; - let mut obs = match num_qubits { + #[pyo3(signature = (obj, /, num_qubits=None))] + fn from_terms(obj: &Bound, num_qubits: Option) -> PyResult { + let mut iter = obj.try_iter()?; + let mut inner = match num_qubits { Some(num_qubits) => SparseObservable::zero(num_qubits), None => { let Some(first) = iter.next() else { @@ -1918,13 +2445,15 @@ impl SparseObservable { "cannot construct an observable from an empty list without knowing `num_qubits`", )); }; - first?.downcast::()?.borrow().to_observable() + let py_term = first?.downcast::()?.borrow(); + py_term.inner.to_observable() } }; - for term in iter { - obs.add_term(term?.downcast::()?.borrow().view())?; + for bound_py_term in iter { + let py_term = bound_py_term?.downcast::()?.borrow(); + inner.add_term(py_term.inner.view())?; } - Ok(obs) + Ok(inner.into()) } // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then @@ -1965,13 +2494,11 @@ impl SparseObservable { /// >>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp) /// >>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries) /// - #[deny(unsafe_op_in_unsafe_fn)] #[staticmethod] #[pyo3( signature = (/, num_qubits, coeffs, bit_terms, indices, boundaries, check=true), - name = "from_raw_parts", )] - unsafe fn py_from_raw_parts<'py>( + unsafe fn from_raw_parts<'py>( num_qubits: u32, coeffs: PyArrayLike1<'py, Complex64>, bit_terms: PyArrayLike1<'py, u8>, @@ -1996,12 +2523,34 @@ impl SparseObservable { let indices = indices.as_array().to_vec(); let boundaries = boundaries.as_array().to_vec(); - if check { - Self::new(num_qubits, coeffs, bit_terms, indices, boundaries).map_err(PyErr::from) + let inner = if check { + SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries) + .map_err(PyErr::from) } else { // SAFETY: the caller promised they have upheld the coherence guarantees. - Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) - } + Ok(unsafe { + SparseObservable::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) + }) + }?; + Ok(inner.into()) + } + + /// Clear all the terms from this operator, making it equal to the zero operator again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// substraction operations may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) + /// >>> obs.clear() + /// >>> assert obs == SparseObservable.zero(obs.py_num_qubits()) + pub fn clear(&mut self) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.clear(); + Ok(()) } /// Sum any like terms in this operator, removing them if the resulting complex coefficient has @@ -2046,10 +2595,76 @@ impl SparseObservable { /// >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits) #[pyo3( signature = (/, tol=1e-8), - name = "simplify", )] - fn py_simplify(&self, tol: f64) -> SparseObservable { - self.canonicalize(tol) + fn simplify(&self, tol: f64) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let simplified = inner.canonicalize(tol); + Ok(simplified.into()) + } + + /// Calculate the adjoint of this observable. + /// + /// + /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit + /// alphabet are self-adjoint, so the result of this operation is the same observable, except + /// its coefficients are all their complex conjugates. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) + /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) + /// >>> assert left.adjoint() == right + fn adjoint(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.adjoint().into()) + } + + /// Calculate the matrix transposition of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` transposes to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) + fn transpose(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.transpose().into()) + } + + /// Calculate the complex conjugation of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` conjguates to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Additionally, all coefficients are conjugated. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) + fn conjugate(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.conjugate().into()) } /// Tensor product of two observables. @@ -2093,8 +2708,8 @@ impl SparseObservable { /// The same function, but with the order of arguments flipped. This can be useful if /// you like using the casting behavior for the argument, but you want your existing /// :class:`SparseObservable` to be on the right-hand side of the tensor ordering. - #[pyo3(signature = (other, /), name = "tensor")] - fn py_tensor(&self, other: &Bound) -> PyResult> { + #[pyo3(signature = (other, /))] + fn tensor(&self, other: &Bound) -> PyResult> { let py = other.py(); let Some(other) = coerce_to_observable(other)? else { return Err(PyTypeError::new_err(format!( @@ -2102,7 +2717,11 @@ impl SparseObservable { other.get_type().repr()? ))); }; - Ok(self.tensor(&other.borrow()).into_py(py)) + + let other = other.borrow(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.tensor(&other_inner).into_py_any(py) } /// Reverse-order tensor product. @@ -2127,8 +2746,8 @@ impl SparseObservable { /// /// The same function with the order of arguments flipped. :meth:`tensor` is the more /// standard argument ordering, and matches Qiskit's other conventions. - #[pyo3(signature = (other, /), name = "expand")] - fn py_expand(&self, other: &Bound) -> PyResult> { + #[pyo3(signature = (other, /))] + fn expand<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { let py = other.py(); let Some(other) = coerce_to_observable(other)? else { return Err(PyTypeError::new_err(format!( @@ -2136,95 +2755,11 @@ impl SparseObservable { other.get_type().repr()? ))); }; - Ok(other.borrow().tensor(self).into_py(py)) - } - - /// Calculate the adjoint of this observable. - /// - /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit - /// alphabet are self-adjoint, so the result of this operation is the same observable, except - /// its coefficients are all their complex conjugates. - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) - /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) - /// >>> assert left.adjoint() == right - fn adjoint(&self) -> SparseObservable { - SparseObservable { - num_qubits: self.num_qubits, - coeffs: self.coeffs.iter().map(|c| c.conj()).collect(), - bit_terms: self.bit_terms.clone(), - indices: self.indices.clone(), - boundaries: self.boundaries.clone(), - } - } - - /// Calculate the complex conjugation of this observable. - /// - /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the - /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related - /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their - /// alphabet terms. Precisely: - /// - /// * :math:`Y` conjguates to :math:`-Y` - /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` - /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` - /// - /// Additionally, all coefficients are conjugated. - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) - /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) - fn conjugate(&self) -> SparseObservable { - let mut out = self.transpose(); - for coeff in out.coeffs.iter_mut() { - *coeff = coeff.conj(); - } - out - } - /// Calculate the matrix transposition of this observable. - /// - /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the - /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related - /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their - /// alphabet terms. Precisely: - /// - /// * :math:`Y` transposes to :math:`-Y` - /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` - /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) - /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) - fn transpose(&self) -> SparseObservable { - let mut out = self.clone(); - for term in out.iter_mut() { - for bit_term in term.bit_terms { - match bit_term { - BitTerm::Y => { - *term.coeff = -*term.coeff; - } - BitTerm::Right => { - *bit_term = BitTerm::Left; - } - BitTerm::Left => { - *bit_term = BitTerm::Right; - } - _ => (), - } - } - } - out + let other = other.borrow(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + other_inner.tensor(&inner).into_pyobject(py) } /// Apply a transpiler layout to this :class:`SparseObservable`. @@ -2248,54 +2783,50 @@ impl SparseObservable { /// /// Returns: /// A new :class:`SparseObservable` with the provided layout applied. - #[pyo3(signature = (/, layout, num_qubits=None), name = "apply_layout")] - fn py_apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + #[pyo3(signature = (/, layout, num_qubits=None))] + fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { let py = layout.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // A utility to check the number of qubits is compatible with the observable. let check_inferred_qubits = |inferred: u32| -> PyResult { - if inferred < self.num_qubits { - return Err(PyValueError::new_err(format!( - "cannot shrink the qubit count in an observable from {} to {}", - self.num_qubits, inferred - ))); + if inferred < inner.num_qubits() { + return Err(CoherenceError::NotEnoughQubits { + current: inner.num_qubits() as usize, + target: inferred as usize, + } + .into()); } Ok(inferred) }; - if layout.is_none() { - let mut out = self.clone(); - out.num_qubits = check_inferred_qubits(num_qubits.unwrap_or(self.num_qubits))?; - return Ok(out); - } - let (num_qubits, layout) = if layout.is_instance( - &py.import_bound(intern!(py, "qiskit.transpiler"))? + + // Normalize the number of qubits in the layout and the layout itself, depending on the + // input types, before calling SparseObservable.apply_layout to do the actual work. + let (num_qubits, layout): (u32, Option>) = if layout.is_none() { + (num_qubits.unwrap_or(inner.num_qubits()), None) + } else if layout.is_instance( + &py.import(intern!(py, "qiskit.transpiler"))? .getattr(intern!(py, "TranspileLayout"))?, )? { ( check_inferred_qubits( layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 )?, - layout - .call_method0(intern!(py, "final_index_layout"))? - .extract::>()?, + Some( + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ), ) } else { ( - check_inferred_qubits(num_qubits.unwrap_or(self.num_qubits))?, - layout.extract()?, + check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, + Some(layout.extract()?), ) }; - if layout.len() < self.num_qubits as usize { - return Err(CoherenceError::IndexMapTooSmall.into()); - } - if layout.iter().any(|qubit| *qubit >= num_qubits) { - return Err(CoherenceError::BitIndexTooHigh.into()); - } - if layout.iter().collect::>().len() != layout.len() { - return Err(CoherenceError::DuplicateIndices.into()); - } - let mut out = self.clone(); - out.num_qubits = num_qubits; - out.relabel_qubits_from_slice(&layout)?; - Ok(out) + + let out = inner.apply_layout(layout.as_deref(), num_qubits)?; + Ok(out.into()) } /// Get a :class:`.PauliList` object that represents the measurement basis needed for each term @@ -2310,11 +2841,11 @@ impl SparseObservable { /// Returns: /// :class:`.PauliList`: the Pauli operator list representing the necessary measurement /// bases. - #[pyo3(name = "pauli_bases")] - fn py_pauli_bases<'py>(&self, py: Python<'py>) -> PyResult> { - let mut x = Array2::from_elem([self.num_terms(), self.num_qubits as usize], false); - let mut z = Array2::from_elem([self.num_terms(), self.num_qubits as usize], false); - for (loc, term) in self.iter().enumerate() { + fn pauli_bases<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let mut x = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); + let mut z = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); + for (loc, term) in inner.iter().enumerate() { let mut x_row = x.row_mut(loc); let mut z_row = z.row_mut(loc); for (bit_term, index) in term.bit_terms.iter().zip(term.indices) { @@ -2324,302 +2855,323 @@ impl SparseObservable { } PAULI_LIST_TYPE .get_bound(py) - .getattr(intern!(py, "from_symplectic"))? - .call1(( - PyArray2::from_owned_array_bound(py, z), - PyArray2::from_owned_array_bound(py, x), - )) - } -} - -impl ::std::ops::Add<&SparseObservable> for SparseObservable { - type Output = SparseObservable; - - fn add(mut self, rhs: &SparseObservable) -> SparseObservable { - self += rhs; - self - } -} -impl ::std::ops::Add for &SparseObservable { - type Output = SparseObservable; - - fn add(self, rhs: &SparseObservable) -> SparseObservable { - let mut out = SparseObservable::with_capacity( - self.num_qubits, - self.coeffs.len() + rhs.coeffs.len(), - self.bit_terms.len() + rhs.bit_terms.len(), - ); - out += self; - out += rhs; - out - } -} -impl ::std::ops::AddAssign<&SparseObservable> for SparseObservable { - fn add_assign(&mut self, rhs: &SparseObservable) { - if self.num_qubits != rhs.num_qubits { - panic!("attempt to add two operators with incompatible qubit counts"); - } - self.coeffs.extend_from_slice(&rhs.coeffs); - self.bit_terms.extend_from_slice(&rhs.bit_terms); - self.indices.extend_from_slice(&rhs.indices); - // We only need to write out the new endpoints, not the initial zero. - let offset = self.boundaries[self.boundaries.len() - 1]; - self.boundaries - .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + .getattr(intern!(py, "from_symplectic"))? + .call1(( + PyArray2::from_owned_array(py, z), + PyArray2::from_owned_array(py, x), + )) } -} -impl ::std::ops::Sub<&SparseObservable> for SparseObservable { - type Output = SparseObservable; - - fn sub(mut self, rhs: &SparseObservable) -> SparseObservable { - self -= rhs; - self + fn __len__(&self) -> PyResult { + self.num_terms() } -} -impl ::std::ops::Sub for &SparseObservable { - type Output = SparseObservable; - fn sub(self, rhs: &SparseObservable) -> SparseObservable { - let mut out = SparseObservable::with_capacity( - self.num_qubits, - self.coeffs.len() + rhs.coeffs.len(), - self.bit_terms.len() + rhs.bit_terms.len(), - ); - out += self; - out -= rhs; - out + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex<'py>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let indices = match index.with_len(inner.num_terms())? { + SequenceIndex::Int(index) => { + return PySparseTerm { + inner: inner.term(index).to_term(), + } + .into_bound_py_any(py) + } + indices => indices, + }; + let mut out = SparseObservable::zero(inner.num_qubits()); + for index in indices.iter() { + out.add_term(inner.term(index))?; + } + out.into_bound_py_any(py) } -} -impl ::std::ops::SubAssign<&SparseObservable> for SparseObservable { - fn sub_assign(&mut self, rhs: &SparseObservable) { - if self.num_qubits != rhs.num_qubits { - panic!("attempt to subtract two operators with incompatible qubit counts"); + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + // this is also important to check before trying to read both slf and other + if slf.is(&other) { + return Ok(true); } - self.coeffs.extend(rhs.coeffs.iter().map(|coeff| -coeff)); - self.bit_terms.extend_from_slice(&rhs.bit_terms); - self.indices.extend_from_slice(&rhs.indices); - // We only need to write out the new endpoints, not the initial zero. - let offset = self.boundaries[self.boundaries.len() - 1]; - self.boundaries - .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + let Ok(other) = other.downcast_into::() else { + return Ok(false); + }; + let slf_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.eq(&other_inner)) } -} -impl ::std::ops::Mul for SparseObservable { - type Output = SparseObservable; + fn __repr__(&self) -> PyResult { + let num_terms = self.num_terms()?; + let num_qubits = self.num_qubits()?; - fn mul(mut self, rhs: Complex64) -> SparseObservable { - self *= rhs; - self - } -} -impl ::std::ops::Mul for &SparseObservable { - type Output = SparseObservable; + let str_num_terms = format!( + "{} term{}", + num_terms, + if num_terms == 1 { "" } else { "s" } + ); + let str_num_qubits = format!( + "{} qubit{}", + num_qubits, + if num_qubits == 1 { "" } else { "s" } + ); - fn mul(self, rhs: Complex64) -> SparseObservable { - if rhs == Complex64::new(0.0, 0.0) { - SparseObservable::zero(self.num_qubits) + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let str_terms = if num_terms == 0 { + "0.0".to_owned() } else { - SparseObservable { - num_qubits: self.num_qubits, - coeffs: self.coeffs.iter().map(|c| c * rhs).collect(), - bit_terms: self.bit_terms.clone(), - indices: self.indices.clone(), - boundaries: self.boundaries.clone(), - } - } + inner + .iter() + .map(SparseTermView::to_sparse_str) + .collect::>() + .join(" + ") + }; + Ok(format!( + "", + str_num_terms, str_num_qubits, str_terms + )) } -} -impl ::std::ops::Mul for Complex64 { - type Output = SparseObservable; - fn mul(self, mut rhs: SparseObservable) -> SparseObservable { - rhs *= self; - rhs + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let bit_terms: &[u8] = ::bytemuck::cast_slice(inner.bit_terms()); + ( + py.get_type::().getattr("from_raw_parts")?, + ( + inner.num_qubits(), + PyArray1::from_slice(py, inner.coeffs()), + PyArray1::from_slice(py, bit_terms), + PyArray1::from_slice(py, inner.indices()), + PyArray1::from_slice(py, inner.boundaries()), + false, + ), + ) + .into_pyobject(py) } -} -impl ::std::ops::Mul<&SparseObservable> for Complex64 { - type Output = SparseObservable; - fn mul(self, rhs: &SparseObservable) -> SparseObservable { - rhs * self - } -} -impl ::std::ops::MulAssign for SparseObservable { - fn mul_assign(&mut self, rhs: Complex64) { - if rhs == Complex64::new(0.0, 0.0) { - self.coeffs.clear(); - self.bit_terms.clear(); - self.indices.clear(); - self.boundaries.clear(); - self.boundaries.push(0); - } else { - self.coeffs.iter_mut().for_each(|c| *c *= rhs) + fn __add__<'py>( + slf_: &Bound<'py, Self>, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + // This fast path is for consistency with the in-place `__iadd__`, which would otherwise + // struggle to do the addition to itself. + let slf_ = slf_.borrow(); + let inner = slf_.inner.read().map_err(|_| InnerReadError)?; + return <&SparseObservable as ::std::ops::Mul<_>>::mul( + &inner, + Complex64::new(2.0, 0.0), + ) + .into_bound_py_any(py); } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + let slf_ = slf_.borrow(); + let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Add>::add(&slf_inner, &other_inner).into_bound_py_any(py) } -} -impl ::std::ops::Div for SparseObservable { - type Output = SparseObservable; + fn __radd__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + // No need to handle the `self is other` case here, because `__add__` will get it. + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; - fn div(mut self, rhs: Complex64) -> SparseObservable { - self /= rhs; - self + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Add>::add(&other_inner, &inner).into_bound_py_any(py) } -} -impl ::std::ops::Div for &SparseObservable { - type Output = SparseObservable; - fn div(self, rhs: Complex64) -> SparseObservable { - SparseObservable { - num_qubits: self.num_qubits, - coeffs: self.coeffs.iter().map(|c| c / rhs).collect(), - bit_terms: self.bit_terms.clone(), - indices: self.indices.clone(), - boundaries: self.boundaries.clone(), + fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + let slf_ = slf_.borrow(); + let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; + *slf_inner *= Complex64::new(2.0, 0.0); + return Ok(()); } + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place addition of 'SparseObservable': {}", + other.repr()? + ))); + }; + let slf_ = slf_.borrow(); + let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + slf_inner.add_assign(&other_inner); + Ok(()) } -} -impl ::std::ops::DivAssign for SparseObservable { - fn div_assign(&mut self, rhs: Complex64) { - self.coeffs.iter_mut().for_each(|c| *c /= rhs) - } -} -impl ::std::ops::Neg for &SparseObservable { - type Output = SparseObservable; - - fn neg(self) -> SparseObservable { - SparseObservable { - num_qubits: self.num_qubits, - coeffs: self.coeffs.iter().map(|c| -c).collect(), - bit_terms: self.bit_terms.clone(), - indices: self.indices.clone(), - boundaries: self.boundaries.clone(), + fn __sub__<'py>( + slf_: &Bound<'py, Self>, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + return PySparseObservable::zero(slf_.borrow().num_qubits()?).into_bound_py_any(py); } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + + let slf_ = slf_.borrow(); + let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Sub>::sub(&slf_inner, &other_inner).into_bound_py_any(py) } -} -impl ::std::ops::Neg for SparseObservable { - type Output = SparseObservable; - fn neg(mut self) -> SparseObservable { - self.coeffs.iter_mut().for_each(|c| *c = -*c); - self + fn __rsub__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Sub>::sub(&other_inner, &inner).into_bound_py_any(py) } -} -/// A view object onto a single term of a `SparseObservable`. -/// -/// The lengths of `bit_terms` and `indices` are guaranteed to be created equal, but might be zero -/// (in the case that the term is proportional to the identity). -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct SparseTermView<'a> { - pub num_qubits: u32, - pub coeff: Complex64, - pub bit_terms: &'a [BitTerm], - pub indices: &'a [u32], -} -impl SparseTermView<'_> { - /// Convert this `SparseTermView` into an owning [SparseTerm] of the same data. - pub fn to_term(&self) -> SparseTerm { - SparseTerm { - num_qubits: self.num_qubits, - coeff: self.coeff, - bit_terms: self.bit_terms.into(), - indices: self.indices.into(), + fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + // This is not strictly the same thing as `a - a` if `a` contains non-finite + // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a + // clear view on what floating-point guarantees we're going to make right now. + slf_.borrow_mut().clear()?; + return Ok(()); } + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place subtraction of 'SparseObservable': {}", + other.repr()? + ))); + }; + let slf_ = slf_.borrow(); + let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + slf_inner.sub_assign(&other_inner); + Ok(()) } - fn to_sparse_str(self) -> String { - let coeff = format!("{}", self.coeff).replace('i', "j"); - let paulis = self - .indices - .iter() - .zip(self.bit_terms) - .rev() - .map(|(i, op)| format!("{}_{}", op.py_label(), i)) - .collect::>() - .join(" "); - format!("({})({})", coeff, paulis) + fn __pos__(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) } -} -/// A mutable view object onto a single term of a [SparseObservable]. -/// -/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero -/// (in the case that the term is proportional to the identity). [indices] is not mutable because -/// this would allow data coherence to be broken. -#[derive(Debug)] -pub struct SparseTermViewMut<'a> { - pub num_qubits: u32, - pub coeff: &'a mut Complex64, - pub bit_terms: &'a mut [BitTerm], - pub indices: &'a [u32], -} + fn __neg__(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let neg = <&SparseObservable as ::std::ops::Neg>::neg(&inner); + Ok(neg.into()) + } + + fn __mul__(&self, other: Complex64) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let mult = <&SparseObservable as ::std::ops::Mul<_>>::mul(&inner, other); + Ok(mult.into()) + } + fn __rmul__(&self, other: Complex64) -> PyResult { + self.__mul__(other) + } -/// Iterator type allowing in-place mutation of the [SparseObservable]. -/// -/// Created by [SparseObservable::iter_mut]. -#[derive(Debug)] -pub struct IterMut<'a> { - num_qubits: u32, - coeffs: &'a mut [Complex64], - bit_terms: &'a mut [BitTerm], - indices: &'a [u32], - boundaries: &'a [usize], - i: usize, -} -impl<'a> From<&'a mut SparseObservable> for IterMut<'a> { - fn from(value: &mut SparseObservable) -> IterMut { - IterMut { - num_qubits: value.num_qubits, - coeffs: &mut value.coeffs, - bit_terms: &mut value.bit_terms, - indices: &value.indices, - boundaries: &value.boundaries, - i: 0, + fn __imul__(&mut self, other: Complex64) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.mul_assign(other); + Ok(()) + } + + fn __truediv__(&self, other: Complex64) -> PyResult { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); } + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let div = <&SparseObservable as ::std::ops::Div<_>>::div(&inner, other); + Ok(div.into()) + } + fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.div_assign(other); + Ok(()) } -} -impl<'a> Iterator for IterMut<'a> { - type Item = SparseTermViewMut<'a>; - fn next(&mut self) -> Option { - // The trick here is that the lifetime of the 'self' borrow is shorter than the lifetime of - // the inner borrows. We can't give out mutable references to our inner borrow, because - // after the lifetime on 'self' expired, there'd be nothing preventing somebody using the - // 'self' borrow to see _another_ mutable borrow of the inner data, which would be an - // aliasing violation. Instead, we keep splitting the inner views we took out so the - // mutable references we return don't overlap with the ones we continue to hold. - let coeffs = ::std::mem::take(&mut self.coeffs); - let (coeff, other_coeffs) = coeffs.split_first_mut()?; - self.coeffs = other_coeffs; + fn __xor__(&self, other: &Bound) -> PyResult> { + // we cannot just delegate this to ``tensor`` since ``other`` might allow + // right-hand-side arithmetic and we have to try deferring to that object, + // which is done by returning ``NotImplemented`` + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; - let len = self.boundaries[self.i + 1] - self.boundaries[self.i]; - self.i += 1; + self.tensor(&other) + } - let all_bit_terms = ::std::mem::take(&mut self.bit_terms); - let all_indices = ::std::mem::take(&mut self.indices); - let (bit_terms, rest_bit_terms) = all_bit_terms.split_at_mut(len); - let (indices, rest_indices) = all_indices.split_at(len); - self.bit_terms = rest_bit_terms; - self.indices = rest_indices; + fn __rxor__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + self.expand(&other).map(|obj| obj.into_any()) + } - Some(SparseTermViewMut { - num_qubits: self.num_qubits, - coeff, - bit_terms, - indices, - }) + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn BitTerm(py: Python) -> PyResult> { + BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .map(|obj| obj.clone_ref(py)) } - fn size_hint(&self) -> (usize, Option) { - (self.coeffs.len(), Some(self.coeffs.len())) + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn Term(py: Python) -> Bound { + py.get_type::() + } +} +impl From for PySparseObservable { + fn from(val: SparseObservable) -> PySparseObservable { + PySparseObservable { + inner: Arc::new(RwLock::new(val)), + } + } +} +impl<'py> IntoPyObject<'py> for SparseObservable { + type Target = PySparseObservable; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PySparseObservable::from(self).into_pyobject(py) } } -impl ExactSizeIterator for IterMut<'_> {} -impl ::std::iter::FusedIterator for IterMut<'_> {} /// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at. #[derive(Clone, Copy, PartialEq, Eq)] @@ -2636,23 +3188,23 @@ enum ArraySlot { /// Python space. #[pyclass(frozen, sequence)] struct ArrayView { - base: Py, + base: Arc>, slot: ArraySlot, } #[pymethods] impl ArrayView { fn __repr__(&self, py: Python) -> PyResult { - let obs = self.base.borrow(py); + let obs = self.base.read().map_err(|_| InnerReadError)?; let data = match self.slot { // Simple integers look the same in Rust-space debug as Python. - ArraySlot::Indices => format!("{:?}", obs.indices), - ArraySlot::Boundaries => format!("{:?}", obs.boundaries), + ArraySlot::Indices => format!("{:?}", obs.indices()), + ArraySlot::Boundaries => format!("{:?}", obs.boundaries()), // Complexes don't have a nice repr in Rust, so just delegate the whole load to Python // and convert back. - ArraySlot::Coeffs => PyList::new_bound(py, &obs.coeffs).repr()?.to_string(), + ArraySlot::Coeffs => PyList::new(py, obs.coeffs())?.repr()?.to_string(), ArraySlot::BitTerms => format!( "[{}]", - obs.bit_terms + obs.bit_terms() .iter() .map(BitTerm::py_label) .collect::>() @@ -2671,36 +3223,36 @@ impl ArrayView { )) } - fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult> { + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex, + ) -> PyResult> { // The slightly verbose generic setup here is to allow the type of a scalar return to be // different to the type that gets put into the Numpy array, since the `BitTerm` enum can be // a direct scalar, but for Numpy, we need it to be a raw `u8`. - fn get_from_slice( - py: Python, + fn get_from_slice<'py, T, S>( + py: Python<'py>, slice: &[T], index: PySequenceIndex, - ) -> PyResult> + ) -> PyResult> where - T: ToPyObject + Copy + Into, + T: IntoPyObject<'py> + Copy + Into, S: ::numpy::Element, { match index.with_len(slice.len())? { - SequenceIndex::Int(index) => Ok(slice[index].to_object(py)), - indices => Ok(PyArray1::from_iter_bound( - py, - indices.iter().map(|index| slice[index].into()), - ) - .into_any() - .unbind()), + SequenceIndex::Int(index) => slice[index].into_bound_py_any(py), + indices => PyArray1::from_iter(py, indices.iter().map(|index| slice[index].into())) + .into_bound_py_any(py), } } - let obs = self.base.borrow(py); + let obs = self.base.read().map_err(|_| InnerReadError)?; match self.slot { - ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, &obs.coeffs, index), - ArraySlot::BitTerms => get_from_slice::<_, u8>(py, &obs.bit_terms, index), - ArraySlot::Indices => get_from_slice::<_, u32>(py, &obs.indices, index), - ArraySlot::Boundaries => get_from_slice::<_, usize>(py, &obs.boundaries, index), + ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, obs.coeffs(), index), + ArraySlot::BitTerms => get_from_slice::<_, u8>(py, obs.bit_terms(), index), + ArraySlot::Indices => get_from_slice::<_, u32>(py, obs.indices(), index), + ArraySlot::Boundaries => get_from_slice::<_, usize>(py, obs.boundaries(), index), } } @@ -2738,7 +3290,7 @@ impl ArrayView { } } else { let values = values - .iter()? + .try_iter()? .map(|value| value?.extract::()?.try_into().map_err(PyErr::from)) .collect::>>()?; if indices.len() != values.len() { @@ -2757,23 +3309,28 @@ impl ArrayView { } } - let mut obs = self.base.borrow_mut(values.py()); + let mut obs = self.base.write().map_err(|_| InnerWriteError)?; match self.slot { - ArraySlot::Coeffs => set_in_slice::<_, Complex64>(&mut obs.coeffs, index, values), - ArraySlot::BitTerms => set_in_slice::(&mut obs.bit_terms, index, values), - ArraySlot::Indices => set_in_slice::<_, u32>(&mut obs.indices, index, values), - ArraySlot::Boundaries => set_in_slice::<_, usize>(&mut obs.boundaries, index, values), + ArraySlot::Coeffs => set_in_slice::<_, Complex64>(obs.coeffs_mut(), index, values), + ArraySlot::BitTerms => set_in_slice::(obs.bit_terms_mut(), index, values), + ArraySlot::Indices => unsafe { + set_in_slice::<_, u32>(obs.indices_mut(), index, values) + }, + ArraySlot::Boundaries => unsafe { + set_in_slice::<_, usize>(obs.boundaries_mut(), index, values) + }, } } - fn __len__(&self, py: Python) -> usize { - let obs = self.base.borrow(py); - match self.slot { - ArraySlot::Coeffs => obs.coeffs.len(), - ArraySlot::BitTerms => obs.bit_terms.len(), - ArraySlot::Indices => obs.indices.len(), - ArraySlot::Boundaries => obs.boundaries.len(), - } + fn __len__(&self, _py: Python) -> PyResult { + let obs = self.base.read().map_err(|_| InnerReadError)?; + let len = match self.slot { + ArraySlot::Coeffs => obs.coeffs().len(), + ArraySlot::BitTerms => obs.bit_terms().len(), + ArraySlot::Indices => obs.indices().len(), + ArraySlot::Boundaries => obs.boundaries().len(), + }; + Ok(len) } #[pyo3(signature = (/, dtype=None, copy=None))] @@ -2792,206 +3349,20 @@ impl ArrayView { "cannot produce a safe view onto movable memory", )); } - let obs = self.base.borrow(py); + let obs = self.base.read().map_err(|_| InnerReadError)?; match self.slot { - ArraySlot::Coeffs => { - cast_array_type(py, PyArray1::from_slice_bound(py, &obs.coeffs), dtype) - } + ArraySlot::Coeffs => cast_array_type(py, PyArray1::from_slice(py, obs.coeffs()), dtype), ArraySlot::Indices => { - cast_array_type(py, PyArray1::from_slice_bound(py, &obs.indices), dtype) + cast_array_type(py, PyArray1::from_slice(py, obs.indices()), dtype) } ArraySlot::Boundaries => { - cast_array_type(py, PyArray1::from_slice_bound(py, &obs.boundaries), dtype) + cast_array_type(py, PyArray1::from_slice(py, obs.boundaries()), dtype) } ArraySlot::BitTerms => { - let bit_terms: &[u8] = ::bytemuck::cast_slice(&obs.bit_terms); - cast_array_type(py, PyArray1::from_slice_bound(py, bit_terms), dtype) - } - } - } -} - -/// A single term from a complete :class:`SparseObservable`. -/// -/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. -#[pyclass(name = "Term", frozen, module = "qiskit.quantum_info")] -#[derive(Clone, Debug, PartialEq)] -pub struct SparseTerm { - /// Number of qubits the entire term applies to. - #[pyo3(get)] - num_qubits: u32, - /// The complex coefficient of the term. - #[pyo3(get)] - coeff: Complex64, - bit_terms: Box<[BitTerm]>, - indices: Box<[u32]>, -} -impl SparseTerm { - pub fn view(&self) -> SparseTermView { - SparseTermView { - num_qubits: self.num_qubits, - coeff: self.coeff, - bit_terms: &self.bit_terms, - indices: &self.indices, - } - } -} - -#[pymethods] -impl SparseTerm { - // Mark the Python class as being defined "within" the `SparseObservable` class namespace. - #[classattr] - #[pyo3(name = "__qualname__")] - fn type_qualname() -> &'static str { - "SparseObservable.Term" - } - - #[new] - #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))] - fn py_new( - num_qubits: u32, - coeff: Complex64, - bit_terms: Vec, - indices: Vec, - ) -> PyResult { - if bit_terms.len() != indices.len() { - return Err(CoherenceError::MismatchedItemCount { - bit_terms: bit_terms.len(), - indices: indices.len(), - } - .into()); - } - let mut order = (0..bit_terms.len()).collect::>(); - order.sort_unstable_by_key(|a| indices[*a]); - let bit_terms = order.iter().map(|i| bit_terms[*i]).collect(); - let mut sorted_indices = Vec::::with_capacity(order.len()); - for i in order { - let index = indices[i]; - if sorted_indices - .last() - .map(|prev| *prev >= index) - .unwrap_or(false) - { - return Err(CoherenceError::UnsortedIndices.into()); + let bit_terms: &[u8] = ::bytemuck::cast_slice(obs.bit_terms()); + cast_array_type(py, PyArray1::from_slice(py, bit_terms), dtype) } - sorted_indices.push(index) - } - Ok(Self { - num_qubits, - coeff, - bit_terms, - indices: sorted_indices.into_boxed_slice(), - }) - } - - /// Convert this term to a complete :class:`SparseObservable`. - pub fn to_observable(&self) -> SparseObservable { - SparseObservable { - num_qubits: self.num_qubits, - coeffs: vec![self.coeff], - bit_terms: self.bit_terms.to_vec(), - indices: self.indices.to_vec(), - boundaries: vec![0, self.bit_terms.len()], - } - } - - fn __eq__(slf: Bound, other: Bound) -> bool { - if slf.is(&other) { - return true; - } - let Ok(other) = other.downcast_into::() else { - return false; - }; - slf.borrow().eq(&other.borrow()) - } - - fn __repr__(&self) -> String { - format!( - "<{} on {} qubit{}: {}>", - Self::type_qualname(), - self.num_qubits, - if self.num_qubits == 1 { "" } else { "s" }, - self.view().to_sparse_str(), - ) - } - - fn __getnewargs__(slf_: Bound, py: Python) -> Py { - let (num_qubits, coeff) = { - let slf_ = slf_.borrow(); - (slf_.num_qubits, slf_.coeff) - }; - ( - num_qubits, - coeff, - Self::get_bit_terms(slf_.clone()), - Self::get_indices(slf_), - ) - .into_py(py) - } - - /// Get a copy of this term. - #[pyo3(name = "copy")] - fn py_copy(&self) -> Self { - self.clone() - } - - /// Read-only view onto the individual single-qubit terms. - /// - /// The only valid values in the array are those with a corresponding - /// :class:`~SparseObservable.BitTerm`. - #[getter] - fn get_bit_terms(slf_: Bound) -> Bound> { - let bit_terms = &slf_.borrow().bit_terms; - let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms)); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array_bound(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// Read-only view onto the indices of each non-identity single-qubit term. - /// - /// The indices will always be in sorted order. - #[getter] - fn get_indices(slf_: Bound) -> Bound> { - let indices = &slf_.borrow().indices; - let arr = ::ndarray::aview1(indices); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array_bound(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// Get a :class:`.Pauli` object that represents the measurement basis needed for this term. - /// - /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting - /// :class:`.Pauli` is dense, in the sense that explicit identities are stored. An identity in - /// the Pauli output does not require a concrete measurement. - /// - /// Returns: - /// :class:`.Pauli`: the Pauli operator representing the necessary measurement basis. - /// - /// See also: - /// :meth:`SparseObservable.pauli_bases` - /// A similar method for an entire observable at once. - #[pyo3(name = "pauli_base")] - fn py_pauli_base<'py>(&self, py: Python<'py>) -> PyResult> { - let mut x = vec![false; self.num_qubits as usize]; - let mut z = vec![false; self.num_qubits as usize]; - for (bit_term, index) in self.bit_terms.iter().zip(self.indices.iter()) { - x[*index as usize] = bit_term.has_x_component(); - z[*index as usize] = bit_term.has_z_component(); } - PAULI_TYPE.get_bound(py).call1((( - PyArray1::from_vec_bound(py, z), - PyArray1::from_vec_bound(py, x), - ),)) } } @@ -3004,12 +3375,12 @@ fn cast_array_type<'py, T>( ) -> PyResult> { let base_dtype = array.dtype(); let dtype = dtype - .map(|dtype| PyArrayDescr::new_bound(py, dtype)) + .map(|dtype| PyArrayDescr::new(py, dtype)) .unwrap_or_else(|| Ok(base_dtype.clone()))?; if dtype.is_equiv_to(&base_dtype) { return Ok(array.into_any()); } - PyModule::import_bound(py, intern!(py, "numpy"))? + PyModule::import(py, intern!(py, "numpy"))? .getattr(intern!(py, "array"))? .call( (array,), @@ -3018,13 +3389,12 @@ fn cast_array_type<'py, T>( (intern!(py, "copy"), NUMPY_COPY_ONLY_IF_NEEDED.get_bound(py)), (intern!(py, "dtype"), dtype.as_any()), ] - .into_py_dict_bound(py), + .into_py_dict(py)?, ), ) - .map(|obj| obj.into_any()) } -/// Attempt to coerce an arbitrary Python object to a [SparseObservable]. +/// Attempt to coerce an arbitrary Python object to a [PySparseObservable]. /// /// This returns: /// @@ -3038,12 +3408,12 @@ fn cast_array_type<'py, T>( /// [PyNotImplemented] if the type is not valid for coercion. fn coerce_to_observable<'py>( value: &Bound<'py, PyAny>, -) -> PyResult>> { +) -> PyResult>> { let py = value.py(); - if let Ok(obs) = value.downcast_exact::() { + if let Ok(obs) = value.downcast_exact::() { return Ok(Some(obs.clone())); } - match SparseObservable::py_new(value, None) { + match PySparseObservable::py_new(value, None) { Ok(obs) => Ok(Some(Bound::new(py, obs)?)), Err(e) => { if e.is_instance_of::(py) { @@ -3054,9 +3424,8 @@ fn coerce_to_observable<'py>( } } } - pub fn sparse_observable(m: &Bound) -> PyResult<()> { - m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 8c9ffe97e525..02561e7061dd 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -69,8 +69,8 @@ pub fn unordered_unique(py: Python, array: PyReadonlyArray2) -> (PyObject, } } ( - indices.into_pyarray_bound(py).into(), - inverses.into_pyarray_bound(py).into(), + indices.into_pyarray(py).into_any().unbind(), + inverses.into_pyarray(py).into_any().unbind(), ) } @@ -410,14 +410,14 @@ pub fn decompose_dense( let array_view = operator.as_array(); let out = py.allow_threads(|| decompose_dense_inner(array_view, tolerance))?; Ok(ZXPaulis { - z: PyArray1::from_vec_bound(py, out.z) + z: PyArray1::from_vec(py, out.z) .reshape([out.phases.len(), out.num_qubits])? .into(), - x: PyArray1::from_vec_bound(py, out.x) + x: PyArray1::from_vec(py, out.x) .reshape([out.phases.len(), out.num_qubits])? .into(), - phases: PyArray1::from_vec_bound(py, out.phases).into(), - coeffs: PyArray1::from_vec_bound(py, out.coeffs).into(), + phases: PyArray1::from_vec(py, out.phases).into(), + coeffs: PyArray1::from_vec(py, out.coeffs).into(), }) } @@ -945,7 +945,7 @@ pub fn to_matrix_dense<'py>( let side = 1usize << paulis.num_qubits(); let parallel = !force_serial && crate::getenv_use_multiple_threads(); let out = to_matrix_dense_inner(&paulis, parallel); - PyArray1::from_vec_bound(py, out).reshape([side, side]) + PyArray1::from_vec(py, out).reshape([side, side]) } /// Inner worker of the Python-exposed [to_matrix_dense]. This is separate primarily to allow @@ -1017,17 +1017,20 @@ pub fn to_matrix_sparse( // This deliberately erases the Rust types in the output so we can return either 32- or 64-bit // indices as appropriate without breaking Rust's typing. - fn to_py_tuple(py: Python, csr_data: CSRData) -> Py + fn to_py_tuple(py: Python, csr_data: CSRData) -> PyResult> where T: numpy::Element, { let (values, indices, indptr) = csr_data; - ( - PyArray1::from_vec_bound(py, values), - PyArray1::from_vec_bound(py, indices), - PyArray1::from_vec_bound(py, indptr), - ) - .into_py(py) + Ok(PyTuple::new( + py, + [ + PyArray1::from_vec(py, values).into_any(), + PyArray1::from_vec(py, indices).into_any(), + PyArray1::from_vec(py, indptr).into_any(), + ], + )? + .unbind()) } // Pessimistic estimation of whether we can fit in `i32`. If there's any risk of overflowing @@ -1041,14 +1044,14 @@ pub fn to_matrix_sparse( } else { to_matrix_sparse_serial_32 }; - Ok(to_py_tuple(py, to_sparse(&paulis))) + to_py_tuple(py, to_sparse(&paulis)) } else { let to_sparse: ToCSRData = if crate::getenv_use_multiple_threads() && !force_serial { to_matrix_sparse_parallel_64 } else { to_matrix_sparse_serial_64 }; - Ok(to_py_tuple(py, to_sparse(&paulis))) + to_py_tuple(py, to_sparse(&paulis)) } } @@ -1158,7 +1161,7 @@ macro_rules! impl_to_matrix_sparse { // to keep threads busy by subdivision with minimizing overhead; we're setting the // chunk size such that the iterator will have as many elements as there are threads. let num_threads = rayon::current_num_threads(); - let chunk_size = (side + num_threads - 1) / num_threads; + let chunk_size = side.div_ceil(num_threads); let mut values_chunks = Vec::with_capacity(num_threads); let mut indices_chunks = Vec::with_capacity(num_threads); // SAFETY: the slice here is uninitialised data; it must not be read. diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs index f1b3065cdd3a..3343faeda13a 100644 --- a/crates/accelerate/src/split_2q_unitaries.rs +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -31,10 +31,10 @@ pub fn split_2q_unitaries( if !dag.get_op_counts().contains_key("unitary") { return Ok(()); } - let nodes: Vec = dag.op_nodes(false).collect(); + let nodes: Vec = dag.op_node_indices(false).collect(); for node in nodes { - if let NodeType::Operation(inst) = &dag.dag()[node] { + if let NodeType::Operation(inst) = &dag[node] { let qubits = dag.get_qargs(inst.qubits()).to_vec(); // We only attempt to split UnitaryGate objects, but this could be extended in future // -- however we need to ensure that we can compile the resulting single-qubit unitaries @@ -54,7 +54,7 @@ pub fn split_2q_unitaries( if matches!(decomp.specialization, Specialization::IdEquiv) { let k1r_arr = decomp.K1r(py); let k1l_arr = decomp.K1l(py); - let kwargs = PyDict::new_bound(py); + let kwargs = PyDict::new(py); kwargs.set_item(intern!(py, "num_qubits"), 1)?; let k1r_gate = UNITARY_GATE .get_bound(py) diff --git a/crates/accelerate/src/star_prerouting.rs b/crates/accelerate/src/star_prerouting.rs index dc777a844767..63ae5fd777b3 100644 --- a/crates/accelerate/src/star_prerouting.rs +++ b/crates/accelerate/src/star_prerouting.rs @@ -113,9 +113,9 @@ fn star_preroute( let final_res = ( res.map, - res.node_order.into_pyarray_bound(py).into(), + res.node_order.into_pyarray(py).into_any().unbind(), res.node_block_results, - qubit_mapping.into_pyarray_bound(py).into(), + qubit_mapping.into_pyarray(py).into_any().unbind(), ); final_res diff --git a/crates/accelerate/src/synthesis/clifford/mod.rs b/crates/accelerate/src/synthesis/clifford/mod.rs index dae85de4972d..d2b6b33a4504 100644 --- a/crates/accelerate/src/synthesis/clifford/mod.rs +++ b/crates/accelerate/src/synthesis/clifford/mod.rs @@ -63,7 +63,7 @@ fn random_clifford_tableau( seed: Option, ) -> PyResult>> { let tableau = random_clifford::random_clifford_tableau_inner(num_qubits, seed); - Ok(tableau.into_pyarray_bound(py).unbind()) + Ok(tableau.into_pyarray(py).unbind()) } /// Create a circuit that optimally synthesizes a given Clifford operator represented as diff --git a/crates/accelerate/src/synthesis/clifford/random_clifford.rs b/crates/accelerate/src/synthesis/clifford/random_clifford.rs index fee3ac8e9650..57531efcea55 100644 --- a/crates/accelerate/src/synthesis/clifford/random_clifford.rs +++ b/crates/accelerate/src/synthesis/clifford/random_clifford.rs @@ -125,17 +125,15 @@ pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option) -> Ar // Compute the full stabilizer tableau - // The code below is identical to the Python implementation, but is based on the original - // code in the paper. - + // The code below is based on the original code in the referenced paper. let mut table = Array2::from_elem((2 * num_qubits, 2 * num_qubits), false); // Apply qubit permutation for i in 0..num_qubits { - replace_row_inner(table.view_mut(), i, table2.slice(s![i, ..])); + replace_row_inner(table.view_mut(), i, table2.slice(s![perm[i], ..])); replace_row_inner( table.view_mut(), - perm[i] + num_qubits, + i + num_qubits, table2.slice(s![perm[i] + num_qubits, ..]), ); } diff --git a/crates/accelerate/src/synthesis/evolution/pauli_network.rs b/crates/accelerate/src/synthesis/evolution/pauli_network.rs index b5a73262aae8..a3a03e2fddbe 100644 --- a/crates/accelerate/src/synthesis/evolution/pauli_network.rs +++ b/crates/accelerate/src/synthesis/evolution/pauli_network.rs @@ -211,7 +211,7 @@ fn inject_rotations( if pauli_support_size == 0 { // in case of an all-identity rotation, update global phase by subtracting // the angle - global_phase = radd_param(global_phase, multiply_param(&angles[i], -1.0, py), py); + global_phase = radd_param(global_phase, multiply_param(&angles[i], -0.5, py), py); hit_paulis[i] = true; dag.remove_node(i); } else if pauli_support_size == 1 && dag.is_front_node(i) { diff --git a/crates/accelerate/src/synthesis/linear/lnn.rs b/crates/accelerate/src/synthesis/linear/lnn.rs new file mode 100644 index 000000000000..edb1ae0913a2 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/lnn.rs @@ -0,0 +1,325 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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 crate::synthesis::linear::utils::{_col_op, _row_op, _row_sum, calc_inverse_matrix_inner}; +use ndarray::{Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut2}; +use numpy::PyReadonlyArray2; +use smallvec::smallvec; + +use pyo3::prelude::*; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; + +// Optimize the synthesis of an n-qubit circuit contains only CX gates for +// linear nearest neighbor (LNN) connectivity. +// The depth of the circuit is bounded by 5*n, while the gate count is approximately 2.5*n^2 +// +// References: +// [1]: Kutin, S., Moulton, D. P., Smithline, L. (2007). +// Computation at a Distance. +// `arXiv:quant-ph/0701194 `_. + +type InstructionList = Vec<(usize, usize)>; + +/// Add a cx gate to the instructions and update the matrix mat +fn _row_op_update_instructions( + cx_instructions: &mut InstructionList, + mat: ArrayViewMut2, + a: usize, + b: usize, +) { + cx_instructions.push((a, b)); + _row_op(mat, a, b); +} + +/// Get the instructions for a lower triangular basis change of a matrix mat. +/// See the proof of Proposition 7.3 in [1]. +/// mat_inv needs to be the inverted matrix of mat +/// The outputs are the permuted versions of mat and mat_inv +fn _get_lower_triangular<'a>( + n: usize, + mat: ArrayView2, + mut mat_inv: ArrayViewMut2<'a, bool>, +) -> (Array2, ArrayViewMut2<'a, bool>) { + let mut mat = mat.to_owned(); + let mut mat_t = mat.to_owned(); + + let mut cx_instructions_rows: InstructionList = Vec::new(); + // Use the instructions in U, which contains only gates of the form cx(a,b) a>b + // to transform the matrix to a permuted lower-triangular matrix. + // The original Matrix mat is unchanged, but mat_inv is + + for i in (0..n).rev() { + // Find the last "1" in row i, use COL operations to the left in order to + // zero out all other "1"s in that row. + let cols_to_update: Vec = (0..n).rev().filter(|&j| mat[[i, j]]).collect(); + let (first_j, cols_to_update) = cols_to_update.split_first().unwrap(); + cols_to_update.iter().for_each(|j| { + _col_op(mat.view_mut(), *first_j, *j); + }); + + // Use row operations directed upwards to zero out all "1"s above the remaining "1" in row i + let rows_to_update: Vec = (0..i).rev().filter(|k| mat[[*k, *first_j]]).collect(); + rows_to_update.into_iter().for_each(|k| { + _row_op_update_instructions(&mut cx_instructions_rows, mat.view_mut(), i, k); + }); + } + // Apply only U instructions to get the permuted L + for (ctrl, trgt) in cx_instructions_rows { + _row_op(mat_t.view_mut(), ctrl, trgt); + _col_op(mat_inv.view_mut(), trgt, ctrl); // performs an inverted col_op + } + (mat_t, mat_inv) +} + +/// For each row in mat_t, save the column index of the last "1" +fn _get_label_arr(n: usize, mat_t: ArrayView2) -> Vec { + (0..n) + .map(|i| (0..n).find(|&j| mat_t[[i, n - 1 - j]]).unwrap_or(n)) + .collect() +} + +/// Check if "row" is a linear combination of all rows in mat_inv_t not including the row labeled by k +fn _in_linear_combination( + label_arr_t: &[usize], + mat_inv_t: ArrayView2, + row: ArrayView1, + k: usize, +) -> bool { + // Find the linear combination of mat_t rows which produces "row" + !(0..row.len()) + .filter(|&row_l| row[row_l]) + .fold(Array1::from_elem(row.len(), false), |w_needed, row_l| { + _row_sum(w_needed.view(), mat_inv_t.row(row_l)).unwrap() + })[label_arr_t[k]] +} + +/// Returns label_arr_t = label_arr^(-1) +fn _get_label_arr_t(n: usize, label_arr: &[usize]) -> Vec { + let mut label_arr_t: Vec = vec![0; n]; + (0..n).for_each(|i| label_arr_t[label_arr[i]] = i); + label_arr_t +} + +/// Transform an arbitrary boolean invertible matrix to a north-west triangular matrix +/// by Proposition 7.3 in [1] +fn _matrix_to_north_west( + n: usize, + mut mat: ArrayViewMut2, + mut mat_inv: ArrayViewMut2, +) -> InstructionList { + // The rows of mat_t hold all w_j vectors (see [1]). mat_inv_t is the inverted matrix of mat_t + // To save time on needless copying, we change mat_inv into mat_inv_t, since we won't need mat_inv anymore + let (mat_t, mat_inv_t) = _get_lower_triangular(n, mat.view(), mat_inv.view_mut()); + // Get all pi(i) labels + let mut label_arr = _get_label_arr(n, mat_t.view()); + + // Save the original labels, exchange index <-> value + let label_arr_t = _get_label_arr_t(n, &label_arr); + let mut first_qubit = 0; + let mut empty_layers = 0; + let mut done = false; + let mut cx_instructions_rows: InstructionList = Vec::new(); + while !done { + // At each iteration the values of i switch between even and odd + let mut at_least_one_needed = false; + for i in (first_qubit..n - 1).step_by(2) { + // "If j < k, we do nothing" (see [1]) + // "If j > k, we swap the two labels, and we also perform a box" (see [1]) + if label_arr[i] > label_arr[i + 1] { + at_least_one_needed = true; + // iterate on column indices, output rows as Vec + let row_sum = _row_sum(mat.row(i), mat.row(i + 1)).unwrap(); + // "Let W be the span of all w_l for l!=k" (see [1]) + // " We can perform a box on and that writes a vector in W to wire ." + // (see [1]) + if _in_linear_combination( + &label_arr_t, + mat_inv_t.view(), + mat.row(i + 1), + label_arr[i + 1], + ) { + // do nothing + } else if _in_linear_combination( + &label_arr_t, + mat_inv_t.view(), + row_sum.view(), + label_arr[i + 1], + ) { + _row_op_update_instructions( + &mut cx_instructions_rows, + mat.view_mut(), + i, + i + 1, + ); + } else if _in_linear_combination( + &label_arr_t, + mat_inv_t.view(), + mat.row(i), + label_arr[i + 1], + ) { + _row_op_update_instructions( + &mut cx_instructions_rows, + mat.view_mut(), + i + 1, + i, + ); + _row_op_update_instructions( + &mut cx_instructions_rows, + mat.view_mut(), + i, + i + 1, + ); + } + (label_arr[i], label_arr[i + 1]) = (label_arr[i + 1], label_arr[i]); + } + } + if !at_least_one_needed { + empty_layers += 1; + if empty_layers > 1 { + // if nothing happened twice in a row, then finished. + done = true; + } + } else { + empty_layers = 0; + } + first_qubit = 1 - first_qubit; + } + cx_instructions_rows +} + +/// Transform a north-west triangular matrix to identity in depth 3*n by Proposition 7.4 of [1] +fn _north_west_to_identity(n: usize, mut mat: ArrayViewMut2) -> InstructionList { + // At start the labels are in reversed order + let mut label_arr: Vec = (0..n).rev().collect(); + let mut first_qubit = 0; + let mut empty_layers = 0; + let mut done = false; + let mut cx_instructions_rows: InstructionList = Vec::new(); + while !done { + let mut at_least_one_needed = false; + for i in (first_qubit..n - 1).step_by(2) { + // Exchange the labels if needed + if label_arr[i] > label_arr[i + 1] { + at_least_one_needed = true; + // If row i has "1" in column i+1, swap and remove the "1" (in depth 2) + // otherwise, only do a swap (in depth 3) + if !mat[[i, label_arr[i + 1]]] { + // Adding this turns the operation to a SWAP + _row_op_update_instructions( + &mut cx_instructions_rows, + mat.view_mut(), + i + 1, + i, + ); + } + _row_op_update_instructions(&mut cx_instructions_rows, mat.view_mut(), i, i + 1); + _row_op_update_instructions(&mut cx_instructions_rows, mat.view_mut(), i + 1, i); + + (label_arr[i], label_arr[i + 1]) = (label_arr[i + 1], label_arr[i]); + } + } + + if !at_least_one_needed { + empty_layers += 1; + if empty_layers > 1 { + // if nothing happened twice in a row, then finished. + done = true; + } + } else { + empty_layers = 0; + } + first_qubit = 1 - first_qubit; + } + cx_instructions_rows +} + +/// Find instruction to synthesize CX circuit in depth bounded by 5n for LNN connectivity. +/// The algorithm [1] has two steps: +/// a) transform the original matrix to a north-west matrix (m2nw), +/// b) transform the north-west matrix to identity (nw2id). +/// +/// A square n-by-n matrix A is called north-west if A[i][j]=0 for all i+j>=n +/// For example, the following matrix is north-west: +/// [[0, 1, 0, 1] +/// [1, 1, 1, 0] +/// [0, 1, 0, 0] +/// [1, 0, 0, 0]] + +/// According to [1] the synthesis is done on the inverse matrix +/// so the matrix mat is inverted at this step + +/// References: +/// [1]: Kutin, S., Moulton, D. P., Smithline, L. (2007). +/// Computation at a Distance. +/// `arXiv:quant-ph/0701194 `_. +fn _synth_cnot_lnn_instructions(arrayview: ArrayView2) -> (InstructionList, InstructionList) { + // According to [1] the synthesis is done on the inverse matrix + // so the matrix mat is inverted at this step + let mut mat_inv: Array2 = arrayview.to_owned(); + let mut mat_cpy = calc_inverse_matrix_inner(mat_inv.view(), false).unwrap(); + + let n = mat_cpy.nrows(); + + // Transform an arbitrary invertible matrix to a north-west triangular matrix + // by Proposition 7.3 of [1] + + let cx_instructions_rows_m2nw = + _matrix_to_north_west(n, mat_cpy.view_mut(), mat_inv.view_mut()); + // Transform a north-west triangular matrix to identity in depth 3*n + // by Proposition 7.4 of [1] + + let cx_instructions_rows_nw2id = _north_west_to_identity(n, mat_cpy.view_mut()); + + (cx_instructions_rows_m2nw, cx_instructions_rows_nw2id) +} + +/// Find instruction to synthesize CX circuit in depth bounded by 5n for LNN connectivity. +/// Uses the algorithm by Kutin, Moulton, Smithline +/// described in `arXiv:quant-ph/0701194 `_. +/// Returns: Tuple with two lists of instructions for CX gates +/// Corresponding to the two parts of the algorithm +#[pyfunction] +#[pyo3(signature = (mat))] +pub fn py_synth_cnot_lnn_instructions( + mat: PyReadonlyArray2, +) -> PyResult<(InstructionList, InstructionList)> { + Ok(_synth_cnot_lnn_instructions(mat.as_array())) +} + +/// Synthesize CX circuit in depth bounded by 5n for LNN connectivity. +/// Uses the algorithm by Kutin, Moulton, Smithline +/// described in `arXiv:quant-ph/0701194 `_. +/// Returns: The CircuitData of the synthesized circuit. +#[pyfunction] +#[pyo3(signature = (mat))] +pub fn py_synth_cnot_depth_line_kms( + py: Python, + mat: PyReadonlyArray2, +) -> PyResult { + let num_qubits = mat.as_array().nrows(); // is a quadratic matrix + let (cx_instructions_rows_m2nw, cx_instructions_rows_nw2id) = + _synth_cnot_lnn_instructions(mat.as_array()); + + let instructions = cx_instructions_rows_m2nw + .into_iter() + .chain(cx_instructions_rows_nw2id) + .map(|(ctrl, target)| { + ( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(ctrl as u32), Qubit(target as u32)], + ) + }); + CircuitData::from_standard_gates(py, num_qubits as u32, instructions, Param::Float(0.0)) +} diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs index 08a0b1e104b3..688951864109 100644 --- a/crates/accelerate/src/synthesis/linear/mod.rs +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -13,7 +13,9 @@ use crate::QiskitError; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; use pyo3::prelude::*; +use pyo3::IntoPyObjectExt; +mod lnn; mod pmh; pub mod utils; @@ -39,7 +41,7 @@ fn gauss_elimination_with_perm( ) -> PyResult { let matmut = mat.as_array_mut(); let perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim); - Ok(perm.to_object(py)) + perm.into_py_any(py) } #[pyfunction] @@ -72,7 +74,7 @@ fn gauss_elimination( fn compute_rank_after_gauss_elim(py: Python, mat: PyReadonlyArray2) -> PyResult { let view = mat.as_array(); let rank = utils::compute_rank_after_gauss_elim_inner(view); - Ok(rank.to_object(py)) + rank.into_py_any(py) } #[pyfunction] @@ -84,7 +86,7 @@ fn compute_rank_after_gauss_elim(py: Python, mat: PyReadonlyArray2) -> PyR /// rank: the rank of the matrix fn compute_rank(py: Python, mat: PyReadonlyArray2) -> PyResult { let rank = utils::compute_rank_inner(mat.as_array()); - Ok(rank.to_object(py)) + rank.into_py_any(py) } #[pyfunction] @@ -105,7 +107,7 @@ pub fn calc_inverse_matrix( let view = mat.as_array(); let invmat = utils::calc_inverse_matrix_inner(view, verify.is_some()).map_err(QiskitError::new_err)?; - Ok(invmat.into_pyarray_bound(py).unbind()) + Ok(invmat.into_pyarray(py).unbind()) } #[pyfunction] @@ -126,7 +128,7 @@ pub fn binary_matmul( let view1 = mat1.as_array(); let view2 = mat2.as_array(); let result = utils::binary_matmul_inner(view1, view2).map_err(QiskitError::new_err)?; - Ok(result.into_pyarray_bound(py).unbind()) + Ok(result.into_pyarray(py).unbind()) } #[pyfunction] @@ -159,7 +161,7 @@ fn random_invertible_binary_matrix( seed: Option, ) -> PyResult>> { let matrix = utils::random_invertible_binary_matrix_inner(num_qubits, seed); - Ok(matrix.into_pyarray_bound(py).unbind()) + Ok(matrix.into_pyarray(py).unbind()) } #[pyfunction] @@ -169,10 +171,9 @@ fn random_invertible_binary_matrix( /// mat: a binary matrix. /// Returns: /// bool: True if mat in invertible and False otherwise. -fn check_invertible_binary_matrix(py: Python, mat: PyReadonlyArray2) -> PyResult { +fn check_invertible_binary_matrix(mat: PyReadonlyArray2) -> bool { let view = mat.as_array(); - let out = utils::check_invertible_binary_matrix_inner(view); - Ok(out.to_object(py)) + utils::check_invertible_binary_matrix_inner(view) } pub fn linear(m: &Bound) -> PyResult<()> { @@ -187,5 +188,7 @@ pub fn linear(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(random_invertible_binary_matrix))?; m.add_wrapped(wrap_pyfunction!(check_invertible_binary_matrix))?; m.add_wrapped(wrap_pyfunction!(pmh::synth_cnot_count_full_pmh))?; + m.add_wrapped(wrap_pyfunction!(lnn::py_synth_cnot_depth_line_kms))?; + m.add_wrapped(wrap_pyfunction!(lnn::py_synth_cnot_lnn_instructions))?; Ok(()) } diff --git a/crates/accelerate/src/synthesis/linear/utils.rs b/crates/accelerate/src/synthesis/linear/utils.rs index 5f1fb99fa682..c620acb5eec0 100644 --- a/crates/accelerate/src/synthesis/linear/utils.rs +++ b/crates/accelerate/src/synthesis/linear/utils.rs @@ -10,7 +10,9 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use ndarray::{azip, concatenate, s, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis, Zip}; +use ndarray::{ + azip, concatenate, s, Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis, Zip, +}; use rand::{Rng, SeedableRng}; use rand_pcg::Pcg64Mcg; use rayon::iter::{IndexedParallelIterator, ParallelIterator}; @@ -194,6 +196,29 @@ pub fn _add_row_or_col(mut mat: ArrayViewMut2, add_cols: &bool, ctrl: usiz row1.zip_mut_with(&row0, |x, &y| *x ^= y); } +/// Perform ROW operation on a matrix mat +pub fn _row_op(mat: ArrayViewMut2, ctrl: usize, trgt: usize) { + _add_row_or_col(mat, &false, ctrl, trgt); +} + +/// Perform COL operation on a matrix mat +pub fn _col_op(mat: ArrayViewMut2, ctrl: usize, trgt: usize) { + _add_row_or_col(mat, &true, ctrl, trgt); +} + +/// Returns the element-wise sum of two boolean rows (i.e. addition modulo 2) +pub fn _row_sum(row_1: ArrayView1, row_2: ArrayView1) -> Result, String> { + if row_1.len() != row_2.len() { + Err(format!( + "row sum performed on rows with different lengths ({} and {})", + row_1.len(), + row_2.len() + )) + } else { + Ok((0..row_1.len()).map(|i| row_1[i] ^ row_2[i]).collect()) + } +} + /// Generate a random invertible n x n binary matrix. pub fn random_invertible_binary_matrix_inner(num_qubits: usize, seed: Option) -> Array2 { let mut rng = match seed { diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index 212e1601ed64..b3647222f8d9 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/mod.rs @@ -20,27 +20,27 @@ mod permutation; use pyo3::prelude::*; pub fn synthesis(m: &Bound) -> PyResult<()> { - let linear_mod = PyModule::new_bound(m.py(), "linear")?; + let linear_mod = PyModule::new(m.py(), "linear")?; linear::linear(&linear_mod)?; m.add_submodule(&linear_mod)?; - let linear_phase_mod = PyModule::new_bound(m.py(), "linear_phase")?; + let linear_phase_mod = PyModule::new(m.py(), "linear_phase")?; linear_phase::linear_phase(&linear_phase_mod)?; m.add_submodule(&linear_phase_mod)?; - let permutation_mod = PyModule::new_bound(m.py(), "permutation")?; + let permutation_mod = PyModule::new(m.py(), "permutation")?; permutation::permutation(&permutation_mod)?; m.add_submodule(&permutation_mod)?; - let clifford_mod = PyModule::new_bound(m.py(), "clifford")?; + let clifford_mod = PyModule::new(m.py(), "clifford")?; clifford::clifford(&clifford_mod)?; m.add_submodule(&clifford_mod)?; - let mc_mod = PyModule::new_bound(m.py(), "multi_controlled")?; + let mc_mod = PyModule::new(m.py(), "multi_controlled")?; multi_controlled::multi_controlled(&mc_mod)?; m.add_submodule(&mc_mod)?; - let evolution_mod = PyModule::new_bound(m.py(), "evolution")?; + let evolution_mod = PyModule::new(m.py(), "evolution")?; evolution::evolution(&evolution_mod)?; m.add_submodule(&evolution_mod)?; diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs index 2cc0b02f2c66..daa9d4b02c10 100644 --- a/crates/accelerate/src/synthesis/permutation/mod.rs +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -39,7 +39,7 @@ pub fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult pub fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { let view = pattern.as_array(); let inverse_i64: Vec = utils::invert(&view).iter().map(|&x| x as i64).collect(); - Ok(inverse_i64.to_object(py)) + Ok(inverse_i64.into_pyobject(py)?.unbind()) } #[pyfunction] diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index f57facf28b74..42c21e6f8668 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -29,10 +29,11 @@ use pyo3::{ prelude::*, pyclass, types::{PyDict, PyList, PySet, PyTuple}, + IntoPyObjectExt, }; -use qiskit_circuit::circuit_instruction::OperationFromPython; -use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; +use qiskit_circuit::operations::{Operation, OperationRef, Param}; use qiskit_circuit::packed_instruction::PackedOperation; use smallvec::SmallVec; @@ -57,30 +58,12 @@ type GateMapState = Vec<(String, Vec<(Option, Option for TargetOperation { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Self::Normal(normal) => normal.into_py(py), - Self::Variadic(variable) => variable, - } - } -} - -impl ToPyObject for TargetOperation { - fn to_object(&self, py: Python<'_>) -> PyObject { - match self { - Self::Normal(normal) => normal.to_object(py), - Self::Variadic(variable) => variable.clone_ref(py), - } - } -} - impl TargetOperation { /// Gets the number of qubits of a [TargetOperation], will panic if the operation is [TargetOperation::Variadic]. pub fn num_qubits(&self) -> u32 { @@ -112,6 +95,26 @@ pub(crate) struct NormalOperation { op_object: PyObject, } +impl<'py> IntoPyObject<'py> for NormalOperation { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.op_object.bind(py).clone()) + } +} + +impl<'a, 'py> IntoPyObject<'py> for &'a NormalOperation { + type Target = PyAny; + type Output = Borrowed<'a, 'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.op_object.bind_borrowed(py)) + } +} + impl<'py> FromPyObject<'py> for NormalOperation { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let operation: OperationFromPython = ob.extract()?; @@ -123,18 +126,6 @@ impl<'py> FromPyObject<'py> for NormalOperation { } } -impl IntoPy for NormalOperation { - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) - } -} - -impl ToPyObject for NormalOperation { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.op_object.clone_ref(py) - } -} - /** The base class for a Python ``Target`` object. Contains data representing the constraints of a particular backend. @@ -414,9 +405,11 @@ impl Target { operation: &str, ) -> PyResult>> { match self.qargs_for_operation_name(operation) { - Ok(option_set) => { - Ok(option_set.map(|qargs| qargs.map(|qargs| qargs.to_object(py)).collect())) - } + Ok(option_set) => Ok(option_set.map(|qargs| { + qargs + .map(|qargs| qargs.into_pyobject(py).unwrap().unbind()) + .collect() + })), Err(e) => Err(PyKeyError::new_err(e.message)), } } @@ -431,9 +424,13 @@ impl Target { /// name. This also can also be the class for globally defined variable with /// operations. #[pyo3(name = "operation_from_name")] - pub fn py_operation_from_name(&self, py: Python, instruction: &str) -> PyResult { + pub fn py_operation_from_name<'py>( + &'py self, + py: Python<'py>, + instruction: &str, + ) -> PyResult> { match self._operation_from_name(instruction) { - Ok(instruction) => Ok(instruction.to_object(py)), + Ok(instruction) => instruction.into_pyobject(py), Err(e) => Err(PyKeyError::new_err(e.message)), } } @@ -462,7 +459,14 @@ impl Target { Ok(self .py_operation_names_for_qargs(qargs)? .into_iter() - .map(|x| self._gate_name_map[x].to_object(py)) + .map(|x| { + self._gate_name_map[x] + .into_pyobject(py) + .as_ref() + .unwrap() + .clone() + .unbind() + }) .collect()) } @@ -578,7 +582,7 @@ impl Target { } } TargetOperation::Normal(normal) => { - if python_is_instance(py, normal, _operation_class)? { + if normal.into_pyobject(py)?.is_instance(_operation_class)? { if let Some(parameters) = ¶meters { if parameters.len() != normal.params.len() { continue; @@ -639,7 +643,7 @@ impl Target { let mut matching_params = false; let obj_at_index = &obj_params[index]; if matches!(obj_at_index, Param::ParameterExpression(_)) - || python_compare(py, ¶ms, &obj_params[index])? + || python_compare(py, params, &obj_params[index])? { matching_params = true; } @@ -728,9 +732,11 @@ impl Target { &mut self, py: Python<'_>, strict_direction: bool, - ) -> PyObject { - self.get_non_global_operation_names(strict_direction) - .to_object(py) + ) -> PyResult { + Ok(self + .get_non_global_operation_names(strict_direction) + .into_pyobject(py)? + .unbind()) } // Instance attributes @@ -740,10 +746,10 @@ impl Target { #[pyo3(name = "qargs")] fn py_qargs(&self, py: Python) -> PyResult { if let Some(qargs) = self.qargs() { - let qargs = qargs.map(|qargs| qargs.map(|q| PyTuple::new_bound(py, q))); - let set = PySet::empty_bound(py)?; + let qargs = qargs.map(|qargs| qargs.map(|q| PyTuple::new(py, q))); + let set = PySet::empty(py)?; for qargs in qargs { - set.add(qargs)?; + set.add(qargs.transpose()?)?; } Ok(set.into_any().unbind()) } else { @@ -760,32 +766,46 @@ impl Target { #[getter] #[pyo3(name = "instructions")] pub fn py_instructions(&self, py: Python<'_>) -> PyResult> { - let list = PyList::empty_bound(py); + let list = PyList::empty(py); for (inst, qargs) in self._instructions() { - let qargs = qargs.map(|q| PyTuple::new_bound(py, q).unbind()); - list.append((inst, qargs))?; + let qargs = match qargs { + Some(q) => Some(PyTuple::new(py, q)?.unbind()), + None => None, + }; + let out_inst = match inst { + TargetOperation::Normal(op) => match op.operation.view() { + OperationRef::Standard(standard) => standard + .create_py_op(py, Some(&op.params), &ExtraInstructionAttributes::default())? + .into_any(), + OperationRef::Gate(gate) => gate.gate.clone_ref(py), + OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), + OperationRef::Operation(operation) => operation.operation.clone_ref(py), + }, + TargetOperation::Variadic(op_cls) => op_cls.clone_ref(py), + }; + list.append((out_inst, qargs))?; } Ok(list.unbind()) } /// Get the operation names in the target. #[getter] #[pyo3(name = "operation_names")] - fn py_operation_names(&self, py: Python<'_>) -> Py { - PyList::new_bound(py, self.operation_names()).unbind() + fn py_operation_names(&self, py: Python<'_>) -> PyResult> { + Ok(PyList::new(py, self.operation_names())?.unbind()) } /// Get the operation objects in the target. #[getter] #[pyo3(name = "operations")] - fn py_operations(&self, py: Python<'_>) -> Py { - PyList::new_bound(py, self._gate_name_map.values()).unbind() + fn py_operations(&self, py: Python<'_>) -> PyResult> { + Ok(PyList::new(py, self._gate_name_map.values())?.unbind()) } /// Returns a sorted list of physical qubits. #[getter] #[pyo3(name = "physical_qubits")] - fn py_physical_qubits(&self, py: Python<'_>) -> Py { - PyList::new_bound(py, self.physical_qubits()).unbind() + fn py_physical_qubits(&self, py: Python<'_>) -> PyResult> { + Ok(PyList::new(py, self.physical_qubits())?.unbind()) } // Magic methods: @@ -795,7 +815,7 @@ impl Target { } fn __getstate__(&self, py: Python<'_>) -> PyResult> { - let result_list = PyDict::new_bound(py); + let result_list = PyDict::new(py); result_list.set_item("description", self.description.clone())?; result_list.set_item("num_qubits", self.num_qubits)?; result_list.set_item("dt", self.dt)?; @@ -822,9 +842,9 @@ impl Target { ) }) .collect::() - .into_py(py), + .into_pyobject(py)?, )?; - result_list.set_item("gate_name_map", self._gate_name_map.to_object(py))?; + result_list.set_item("gate_name_map", self._gate_name_map.into_pyobject(py)?)?; result_list.set_item("global_operations", self.global_operations.clone())?; result_list.set_item( "qarg_gate_map", @@ -1151,6 +1171,12 @@ impl Target { /// Checks whether an instruction is supported by the Target based on instruction name and qargs. pub fn instruction_supported(&self, operation_name: &str, qargs: Option<&Qargs>) -> bool { + // Handle case where num_qubits is None by checking globally supported operations + let qargs: Option<&Qargs> = if self.num_qubits.is_none() { + None + } else { + qargs + }; if self.gate_map.contains_key(operation_name) { if let Some(_qargs) = qargs { let qarg_set: HashSet<&PhysicalQubit> = _qargs.iter().collect(); @@ -1250,25 +1276,13 @@ fn check_obj_params(parameters: &[Param], obj: &NormalOperation) -> bool { true } -pub fn python_compare(py: Python, obj: &T, other: &U) -> PyResult -where - T: ToPyObject, - U: ToPyObject, -{ - let obj = obj.to_object(py); - let obj_bound = obj.bind(py); - obj_bound.eq(other) -} - -pub fn python_is_instance(py: Python, obj: &T, other: &U) -> PyResult +pub fn python_compare<'a, T, U>(py: Python<'a>, obj: T, other: U) -> PyResult where - T: ToPyObject, - U: ToPyObject, + T: IntoPyObject<'a>, + U: IntoPyObject<'a>, { - let obj = obj.to_object(py); - let other_obj = other.to_object(py); - let obj_bound = obj.bind(py); - obj_bound.is_instance(other_obj.bind(py)) + let obj = obj.into_bound_py_any(py)?; + obj.eq(other.into_bound_py_any(py)?) } pub fn target(m: &Bound) -> PyResult<()> { diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs index c48386222fc0..c0f4f18b43e7 100644 --- a/crates/accelerate/src/target_transpiler/nullable_index_map.rs +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -16,8 +16,6 @@ use indexmap::{ IndexMap, }; use pyo3::prelude::*; -use pyo3::types::PyDict; -use pyo3::IntoPy; use rustworkx_core::dictmap::InitWithHasher; use std::ops::Index; use std::{hash::Hash, mem::swap}; @@ -36,7 +34,7 @@ type BaseMap = IndexMap; /// /// **Warning:** This is an experimental feature and should be used with care as it does not /// fully implement all the methods present in `IndexMap` due to API limitations. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, IntoPyObject, IntoPyObjectRef)] pub(crate) struct NullableIndexMap where K: Eq + Hash + Clone, @@ -394,8 +392,8 @@ where impl<'py, K, V> FromPyObject<'py> for NullableIndexMap where - K: IntoPy + FromPyObject<'py> + Eq + Hash + Clone, - V: IntoPy + FromPyObject<'py> + Clone, + K: IntoPyObject<'py> + FromPyObject<'py> + Eq + Hash + Clone, + V: IntoPyObject<'py> + FromPyObject<'py> + Clone, { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let map: IndexMap, V, RandomState> = ob.extract()?; @@ -415,39 +413,3 @@ where }) } } - -impl IntoPy for NullableIndexMap -where - K: IntoPy + Eq + Hash + Clone, - V: IntoPy + Clone, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - let map_object = self.map.into_py(py); - let bound_map_obj = map_object.bind(py); - let downcast_dict: &Bound = bound_map_obj.downcast().unwrap(); - if let Some(null_val) = self.null_val { - downcast_dict - .set_item(py.None(), null_val.into_py(py)) - .unwrap(); - } - map_object - } -} - -impl ToPyObject for NullableIndexMap -where - K: ToPyObject + Eq + Hash + Clone, - V: ToPyObject + Clone, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - let map_object = self.map.to_object(py); - let bound_map_obj = map_object.bind(py); - let downcast_dict: &Bound = bound_map_obj.downcast().unwrap(); - if let Some(null_val) = &self.null_val { - downcast_dict - .set_item(py.None(), null_val.to_object(py)) - .unwrap(); - } - map_object - } -} diff --git a/crates/accelerate/src/twirling.rs b/crates/accelerate/src/twirling.rs index c38b604b703a..231d3d87fe71 100644 --- a/crates/accelerate/src/twirling.rs +++ b/crates/accelerate/src/twirling.rs @@ -21,6 +21,7 @@ use pyo3::intern; use pyo3::prelude::*; use pyo3::types::PyList; use pyo3::wrap_pyfunction; +use pyo3::IntoPyObjectExt; use pyo3::Python; use rand::prelude::*; use rand_pcg::Pcg64Mcg; @@ -259,7 +260,7 @@ fn generate_twirled_circuit( custom_gate_map: Option<&CustomGateTwirlingMap>, optimizer_target: Option<&Target>, ) -> PyResult { - let mut out_circ = CircuitData::clone_empty_like(circ, None); + let mut out_circ = CircuitData::clone_empty_like(py, circ, None)?; for inst in circ.data() { if let Some(custom_gate_map) = custom_gate_map { @@ -314,11 +315,11 @@ fn generate_twirled_circuit( custom_gate_map, optimizer_target, )?; - Ok(new_block.into_py(py)) + new_block.into_py_any(py) }) .collect(); let new_blocks = new_blocks?; - let blocks_list = PyList::new_bound( + let blocks_list = PyList::new( py, new_blocks.iter().map(|block| { QUANTUM_CIRCUIT @@ -326,7 +327,7 @@ fn generate_twirled_circuit( .call_method1(intern!(py, "_from_circuit_data"), (block,)) .unwrap() }), - ); + )?; let new_inst_obj = py_inst .instruction @@ -348,8 +349,8 @@ fn generate_twirled_circuit( (!new_blocks.is_empty()).then_some( new_blocks .iter() - .map(|x| x.extract(py).unwrap()) - .collect::>(), + .map(|x| Ok(Param::Obj(x.clone().into_py_any(py)?))) + .collect::>>()?, ), inst.extra_attrs().clone(), ); diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 4410d6f35e07..f691e8333684 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -39,13 +39,13 @@ use pyo3::intern; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyList, PyTuple, PyType}; +use pyo3::IntoPyObjectExt; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ angles_from_unitary, det_one_qubit, unitary_to_gate_sequence_inner, EulerBasis, EulerBasisSet, OneQubitGateSequence, ANGLE_ZERO_EPSILON, }; -use crate::utils; use crate::QiskitError; use rand::prelude::*; @@ -59,7 +59,7 @@ use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; -use qiskit_circuit::Qubit; +use qiskit_circuit::{impl_intopyobject_for_copy_pyclass, Qubit}; const PI2: f64 = PI / 2.; const PI4: f64 = PI / 4.; @@ -164,6 +164,15 @@ fn py_trace_to_fid(trace: Complex64) -> PyResult { Ok(fid) } +/// Return indices that sort partially ordered data. +/// If `data` contains two elements that are incomparable, +/// an error will be thrown. +fn arg_sort(data: &[T]) -> Vec { + let mut indices = (0..data.len()).collect::>(); + indices.sort_by(|&a, &b| data[a].partial_cmp(&data[b]).unwrap()); + indices +} + fn decompose_two_qubit_product_gate( special_unitary: ArrayView2, ) -> PyResult<(Array2, Array2, f64)> { @@ -211,8 +220,8 @@ fn py_decompose_two_qubit_product_gate( let view = special_unitary.as_array(); let (l, r, phase) = decompose_two_qubit_product_gate(view)?; Ok(( - l.into_pyarray_bound(py).unbind().into(), - r.into_pyarray_bound(py).unbind().into(), + l.into_pyarray(py).into_any().unbind(), + r.into_pyarray(py).into_any().unbind(), phase, )) } @@ -229,8 +238,9 @@ fn weyl_coordinates(py: Python, unitary: PyReadonlyArray2) -> PyObjec let array = unitary.as_array(); __weyl_coordinates(array.into_faer_complex()) .to_vec() - .into_pyarray_bound(py) - .into() + .into_pyarray(py) + .into_any() + .unbind() } fn __weyl_coordinates(unitary: MatRef) -> [f64; 3] { @@ -250,7 +260,7 @@ fn __weyl_coordinates(unitary: MatRef) -> [f64; 3] { .map(|x| x.rem_euclid(PI2)) .map(|x| x.min(PI2 - x)) .collect(); - let mut order = utils::arg_sort(&cstemp); + let mut order = arg_sort(&cstemp); (order[0], order[1], order[2]) = (order[1], order[2], order[0]); (cs[0], cs[1], cs[2]) = (cs[order[0]], cs[order[1]], cs[order[2]]); @@ -383,7 +393,7 @@ fn ud(a: f64, b: f64, c: f64) -> Array2 { #[pyo3(name = "Ud")] fn py_ud(py: Python, a: f64, b: f64, c: f64) -> Py> { let ud_mat = ud(a, b, c); - ud_mat.into_pyarray_bound(py).unbind() + ud_mat.into_pyarray(py).unbind() } fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { @@ -443,6 +453,7 @@ pub enum Specialization { #[allow(non_camel_case_types)] fSimabmbEquiv, } +impl_intopyobject_for_copy_pyclass!(Specialization); #[pymethods] impl Specialization { @@ -461,7 +472,7 @@ impl Specialization { Self::fSimabbEquiv => 8, Self::fSimabmbEquiv => 9, }; - Ok((py.get_type_bound::().getattr("_from_u8")?, (val,)).into_py(py)) + (py.get_type::().getattr("_from_u8")?, (val,)).into_py_any(py) } #[staticmethod] @@ -660,7 +671,7 @@ impl TwoQubitWeylDecomposition { .map(|x| x.rem_euclid(PI2)) .map(|x| x.min(PI2 - x)) .collect(); - let mut order = utils::arg_sort(&cstemp); + let mut order = arg_sort(&cstemp); (order[0], order[1], order[2]) = (order[1], order[2], order[0]); (cs[0], cs[1], cs[2]) = (cs[order[0]], cs[order[1]], cs[order[2]]); (d[0], d[1], d[2]) = (d[order[0]], d[order[1]], d[order[2]]); @@ -1084,16 +1095,16 @@ impl TwoQubitWeylDecomposition { } fn __reduce__(&self, py: Python) -> PyResult> { - Ok(( - py.get_type_bound::().getattr("_from_state")?, + ( + py.get_type::().getattr("_from_state")?, ( [self.a, self.b, self.c, self.global_phase], [ - self.K1l.to_pyarray_bound(py), - self.K1r.to_pyarray_bound(py), - self.K2l.to_pyarray_bound(py), - self.K2r.to_pyarray_bound(py), - self.unitary_matrix.to_pyarray_bound(py), + self.K1l.to_pyarray(py), + self.K1r.to_pyarray(py), + self.K2l.to_pyarray(py), + self.K2r.to_pyarray(py), + self.unitary_matrix.to_pyarray(py), ], self.specialization, self.default_euler_basis, @@ -1101,7 +1112,7 @@ impl TwoQubitWeylDecomposition { self.requested_fidelity, ), ) - .into_py(py)) + .into_py_any(py) } #[new] @@ -1117,30 +1128,30 @@ impl TwoQubitWeylDecomposition { #[allow(non_snake_case)] #[getter] pub fn K1l(&self, py: Python) -> PyObject { - self.K1l.to_pyarray_bound(py).into() + self.K1l.to_pyarray(py).into_any().unbind() } #[allow(non_snake_case)] #[getter] pub fn K1r(&self, py: Python) -> PyObject { - self.K1r.to_pyarray_bound(py).into() + self.K1r.to_pyarray(py).into_any().unbind() } #[allow(non_snake_case)] #[getter] fn K2l(&self, py: Python) -> PyObject { - self.K2l.to_pyarray_bound(py).into() + self.K2l.to_pyarray(py).into_any().unbind() } #[allow(non_snake_case)] #[getter] fn K2r(&self, py: Python) -> PyObject { - self.K2r.to_pyarray_bound(py).into() + self.K2r.to_pyarray(py).into_any().unbind() } #[getter] fn unitary_matrix(&self, py: Python) -> PyObject { - self.unitary_matrix.to_pyarray_bound(py).into() + self.unitary_matrix.to_pyarray(py).into_any().unbind() } #[pyo3(signature = (euler_basis=None, simplify=false, atol=None))] @@ -1295,11 +1306,16 @@ impl TwoQubitGateSequence { fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { match idx.with_len(self.gates.len())? { - SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), - indices => Ok(PyList::new_bound( + SequenceIndex::Int(idx) => { + let item = &self.gates[idx]; + (item.0, PyList::new(py, &item.1)?, PyList::new(py, &item.2)?).into_py_any(py) + } + indices => Ok(PyList::new( py, - indices.iter().map(|pos| self.gates[pos].to_object(py)), - ) + indices + .iter() + .map(|pos| self.gates[pos].clone().into_pyobject(py).unwrap()), + )? .into_any() .unbind()), } @@ -1965,8 +1981,9 @@ impl TwoQubitBasisDecomposer { self.gate.clone(), self.basis_decomposer .unitary_matrix - .to_pyarray_bound(py) - .into(), + .to_pyarray(py) + .into_any() + .unbind(), self.basis_fidelity, self.euler_basis.as_str(), self.pulse_optimize, @@ -2023,7 +2040,7 @@ impl TwoQubitBasisDecomposer { fn decomp0(py: Python, target: &TwoQubitWeylDecomposition) -> SmallVec<[PyObject; 2]> { decomp0_inner(target) .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect() } @@ -2040,7 +2057,7 @@ impl TwoQubitBasisDecomposer { fn decomp1(&self, py: Python, target: &TwoQubitWeylDecomposition) -> SmallVec<[PyObject; 4]> { self.decomp1_inner(target) .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect() } @@ -2065,7 +2082,7 @@ impl TwoQubitBasisDecomposer { ) -> SmallVec<[PyObject; 6]> { self.decomp2_supercontrolled_inner(target) .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect() } @@ -2080,7 +2097,7 @@ impl TwoQubitBasisDecomposer { ) -> SmallVec<[PyObject; 8]> { self.decomp3_supercontrolled_inner(target) .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect() } @@ -2304,7 +2321,7 @@ fn two_qubit_decompose_up_to_diagonal( Param::Float(circ_seq.global_phase + phase), )?; real_map.mapv_inplace(|x| x.conj()); - Ok((real_map.into_pyarray_bound(py).into(), circ)) + Ok((real_map.into_pyarray(py).into_any().unbind(), circ)) } static MAGIC: GateArray2Q = [ @@ -2500,7 +2517,7 @@ impl TwoQubitControlledUDecomposer { Ok((Some(inv_gate.0), inv_gate_params, qubits)) } RXXEquivalent::CustomPython(gate_cls) => { - let gate_obj = gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; + let gate_obj = gate_cls.bind(py).call1(PyTuple::new(py, params)?)?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; let params: SmallVec<[f64; 3]> = inverse @@ -2867,7 +2884,7 @@ impl TwoQubitControlledUDecomposer { )), None => { let raw_gate_obj = - gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; + gate_cls.bind(py).call1(PyTuple::new(py, params)?)?; let op: OperationFromPython = raw_gate_obj.extract()?; Ok(( diff --git a/crates/accelerate/src/uc_gate.rs b/crates/accelerate/src/uc_gate.rs index ec79f4d4d2ba..dd131b7c84e6 100644 --- a/crates/accelerate/src/uc_gate.rs +++ b/crates/accelerate/src/uc_gate.rs @@ -150,9 +150,9 @@ pub fn dec_ucg_help( ( single_qubit_gates .into_iter() - .map(|x| x.into_pyarray_bound(py).into()) + .map(|x| x.into_pyarray(py).into_any().unbind()) .collect(), - diag.into_pyarray_bound(py).into(), + diag.into_pyarray(py).into_any().unbind(), ) } diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 6870a40cc81f..6c5d9460ebfc 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -15,7 +15,7 @@ use std::f64::consts::PI; use approx::relative_eq; use hashbrown::{HashMap, HashSet}; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; @@ -110,7 +110,7 @@ fn apply_synth_dag( synth_dag: &DAGCircuit, ) -> PyResult<()> { for out_node in synth_dag.topological_op_nodes()? { - let mut out_packed_instr = synth_dag.dag()[out_node].unwrap_operation().clone(); + let mut out_packed_instr = synth_dag[out_node].unwrap_operation().clone(); let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits()); let mapped_qargs: Vec = synth_qargs .iter() @@ -233,7 +233,7 @@ fn py_run_main_loop( // Iterate over dag nodes and determine unitary synthesis approach for node in dag.topological_op_nodes()? { - let mut packed_instr = dag.dag()[node].unwrap_operation().clone(); + let mut packed_instr = dag[node].unwrap_operation().clone(); if packed_instr.op().control_flow() { let OperationRef::Instruction(py_instr) = packed_instr.op().view() else { @@ -243,7 +243,7 @@ fn py_run_main_loop( .instruction .getattr(py, "blocks")? .bind(py) - .iter()? + .try_iter()? .collect(); let mut new_blocks = Vec::with_capacity(raw_blocks.len()); for raw_block in raw_blocks { @@ -356,7 +356,7 @@ fn py_run_main_loop( // Run 3q+ synthesis _ => { let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); - let synth_circ = qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; + let synth_circ = qs_decomposition.call1((unitary.into_pyarray(py),))?; let synth_dag = circuit_to_dag( py, QuantumCircuitData::extract_bound(&synth_circ)?, @@ -364,7 +364,8 @@ fn py_run_main_loop( None, None, )?; - out_dag = synth_dag; + let out_qargs = dag.get_qargs(packed_instr.qubits()); + apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; } } } @@ -477,7 +478,7 @@ fn run_2q_unitary_synthesis( .topological_op_nodes() .expect("Unexpected error in dag.topological_op_nodes()") .map(|node| { - let NodeType::Operation(inst) = &synth_dag.dag()[node] else { + let NodeType::Operation(inst) = &synth_dag[node] else { unreachable!("DAG node must be an instruction") }; let inst_qubits = synth_dag @@ -536,6 +537,7 @@ fn get_2q_decomposers_from_target( let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); let mut qubit_gate_map = IndexMap::new(); + match target.operation_names_for_qargs(Some(&qubits)) { Ok(direct_keys) => { qubit_gate_map.insert(&qubits, direct_keys); @@ -588,7 +590,10 @@ fn get_2q_decomposers_from_target( OperationRef::Standard(_) => (), _ => continue, } - + // Filter out non-2q-gate candidates + if op.operation.num_qubits() != 2 { + continue; + } available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); if target.contains_key(key) { @@ -614,8 +619,8 @@ fn get_2q_decomposers_from_target( } let target_basis_set = get_target_basis_set(target, qubits[0]); - let available_1q_basis: HashSet<&str> = - HashSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + let available_1q_basis: IndexSet<&str> = + IndexSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); let mut decomposers: Vec = Vec::new(); #[inline] @@ -676,10 +681,10 @@ fn get_2q_decomposers_from_target( // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer // is an ideal decomposition and there is no need to bother calculating the XX embodiments // or try the XX decomposer - let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); #[inline] - fn check_goodbye(basis_set: &HashSet<&str>) -> bool { + fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) } @@ -716,7 +721,7 @@ fn get_2q_decomposers_from_target( fidelity_value *= approx_degree; } let mut embodiment = - xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; + xx_embodiments.get_item(op.into_pyobject(py)?.getattr("base_class")?)?; if embodiment.getattr("parameters")?.len()? == 1 { embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; @@ -729,11 +734,11 @@ fn get_2q_decomposers_from_target( }, ); - let basis_2q_fidelity_dict = PyDict::new_bound(py); - let embodiments_dict = PyDict::new_bound(py); + let basis_2q_fidelity_dict = PyDict::new(py); + let embodiments_dict = PyDict::new(py); for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { basis_2q_fidelity_dict.set_item(strength, fidelity)?; - embodiments_dict.set_item(strength, embodiment.into_py(py))?; + embodiments_dict.set_item(strength, embodiment)?; } // Iterate over 2q fidelities and select decomposers @@ -765,7 +770,7 @@ fn get_2q_decomposers_from_target( let decomposer = xx_decomposer.call1(( &basis_2q_fidelity_dict, - PyString::new_bound(py, basis_1q), + PyString::new(py, basis_1q), &embodiments_dict, pi2_decomposer, ))?; @@ -974,10 +979,10 @@ fn synth_su4_dag( .into_iter() .collect(); decomposer - .call_bound( + .call( py, - (su4_mat.clone().into_pyarray_bound(py),), - Some(&kwargs.into_py_dict_bound(py)), + (su4_mat.clone().into_pyarray(py),), + Some(&kwargs.into_py_dict(py)?), )? .extract::(py)? } else { @@ -989,7 +994,7 @@ fn synth_su4_dag( Some(preferred_dir) => { let mut synth_direction: Option> = None; for node in synth_dag.topological_op_nodes()? { - let inst = &synth_dag.dag()[node].unwrap_operation(); + let inst = &synth_dag[node].unwrap_operation(); if inst.op().num_qubits() == 2 { let qargs = synth_dag.get_qargs(inst.qubits()); synth_direction = Some(vec![qargs[0].0, qargs[1].0]); @@ -1040,10 +1045,10 @@ fn reversed_synth_su4_dag( .into_iter() .collect(); decomposer - .call_bound( + .call( py, - (su4_mat.clone().into_pyarray_bound(py),), - Some(&kwargs.into_py_dict_bound(py)), + (su4_mat.clone().into_pyarray(py),), + Some(&kwargs.into_py_dict(py)?), )? .extract::(py)? } else { @@ -1053,7 +1058,7 @@ fn reversed_synth_su4_dag( let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; for node in synth_dag.topological_op_nodes()? { - let mut inst = synth_dag.dag()[node].unwrap_operation().clone(); + let mut inst = synth_dag[node].unwrap_operation().clone(); let qubits: Vec = synth_dag .qargs_interner() .get(inst.qubits()) diff --git a/crates/accelerate/src/utils.rs b/crates/accelerate/src/utils.rs deleted file mode 100644 index 598256192f83..000000000000 --- a/crates/accelerate/src/utils.rs +++ /dev/null @@ -1,47 +0,0 @@ -// 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 faer_ext::IntoFaerComplex; -use num_complex::Complex; -use numpy::{IntoPyArray, PyReadonlyArray2}; - -/// Return indices that sort partially ordered data. -/// If `data` contains two elements that are incomparable, -/// an error will be thrown. -pub fn arg_sort(data: &[T]) -> Vec { - let mut indices = (0..data.len()).collect::>(); - indices.sort_by(|&a, &b| data[a].partial_cmp(&data[b]).unwrap()); - indices -} - -/// Return the eigenvalues of `unitary` as a one-dimensional `numpy.ndarray` -/// with `dtype(complex128)`. -#[pyfunction] -#[pyo3(text_signature = "(unitary, /")] -pub fn eigenvalues(py: Python, unitary: PyReadonlyArray2>) -> PyObject { - unitary - .as_array() - .into_faer_complex() - .complex_eigenvalues() - .into_iter() - .map(|x| Complex::::new(x.re, x.im)) - .collect::>() - .into_pyarray_bound(py) - .into() -} - -pub fn utils(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(eigenvalues))?; - Ok(()) -} diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 8bb59e758786..457d64108254 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -9,6 +9,9 @@ license.workspace = true name = "qiskit_circuit" doctest = false +[lints] +workspace = true + [dependencies] rayon.workspace = true ahash.workspace = true @@ -39,3 +42,6 @@ features = ["union"] [features] cache_pygates = [] + +[dev-dependencies] +pyo3 = { workspace = true, features = ["auto-initialize"] } diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 56a2560385f2..bf9667e44214 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -92,7 +92,7 @@ where description, bits: Vec::new(), indices: HashMap::new(), - cached: PyList::empty_bound(py).unbind(), + cached: PyList::empty(py).unbind(), } } @@ -101,7 +101,7 @@ where description, bits: Vec::with_capacity(capacity), indices: HashMap::with_capacity(capacity), - cached: PyList::empty_bound(py).unbind(), + cached: PyList::empty(py).unbind(), } } @@ -189,7 +189,7 @@ where .try_insert(BitAsKey::new(bit), idx.into()) .is_ok() { - self.bits.push(bit.into_py(py)); + self.bits.push(bit.clone().unbind()); self.cached.bind(py).append(bit)?; } else if strict { return Err(PyValueError::new_err(format!( diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 629e073c8202..bd3a3426d8b8 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -28,6 +28,7 @@ use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError}; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyTuple, PyType}; +use pyo3::IntoPyObjectExt; use pyo3::{import_exception, intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; @@ -129,12 +130,12 @@ impl CircuitData { }; self_.set_global_phase(py, global_phase)?; if let Some(qubits) = qubits { - for bit in qubits.iter()? { + for bit in qubits.try_iter()? { self_.add_qubit(py, &bit?, true)?; } } if let Some(clbits) = clbits { - for bit in clbits.iter()? { + for bit in clbits.try_iter()? { self_.add_clbit(py, &bit?, true)?; } } @@ -157,7 +158,7 @@ impl CircuitData { self_.global_phase.clone(), ) }; - Ok((ty, args, None::<()>, self_.iter()?).into_py(py)) + (ty, args, None::<()>, self_.try_iter()?).into_py_any(py) } /// Returns the current sequence of registered :class:`.Qubit` instances as a list. @@ -293,7 +294,7 @@ impl CircuitData { res.param_table.clone_from(&self.param_table); if deepcopy { - let memo = PyDict::new_bound(py); + let memo = PyDict::new(py); for inst in &self.data { res.data.push(PackedInstruction::new( inst.op().py_deepcopy(py, Some(&memo))?, @@ -335,8 +336,8 @@ impl CircuitData { /// Returns: /// tuple[set[:class:`.Qubit`], set[:class:`.Clbit`]]: The active qubits and clbits. pub fn active_bits(&self, py: Python<'_>) -> PyResult> { - let qubits = PySet::empty_bound(py)?; - let clbits = PySet::empty_bound(py)?; + let qubits = PySet::empty(py)?; + let clbits = PySet::empty(py)?; for inst in self.data.iter() { for b in self.qargs_interner.get(inst.qubits()) { qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; @@ -346,7 +347,7 @@ impl CircuitData { } } - Ok((qubits, clbits).into_py(py)) + Ok((qubits, clbits).into_pyobject(py)?.unbind()) } /// Invokes callable ``func`` with each instruction's operation. @@ -503,22 +504,23 @@ impl CircuitData { let clbits = self.cargs_interner.get(inst.clbits()); CircuitInstruction { operation: inst.op().clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)).unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)).unbind(), - params: inst - .params_view() - .iter() - .map(|param| param.clone_ref(py)) - .collect(), + qubits: PyTuple::new(py, self.qubits.map_indices(qubits)) + .unwrap() + .unbind(), + clbits: PyTuple::new(py, self.clbits.map_indices(clbits)) + .unwrap() + .unbind(), + params: inst.params_view().iter().cloned().collect(), extra_attrs: inst.extra_attrs().clone(), #[cfg(feature = "cache_pygates")] py_op: inst.py_op().clone(), } - .into_py(py) + .into_py_any(py) + .unwrap() }; match index.with_len(self.data.len())? { SequenceIndex::Int(index) => Ok(get_single(index)), - indices => Ok(PyList::new_bound(py, indices.iter().map(get_single)).into_py(py)), + indices => PyList::new(py, indices.iter().map(get_single))?.into_py_any(py), } } @@ -544,7 +546,7 @@ impl CircuitData { step: 1, } => { // `list` allows setting a slice with step +1 to an arbitrary length. - let values = value.iter()?.collect::>>()?; + let values = value.try_iter()?.collect::>>()?; for (index, value) in indices.iter().zip(values.iter()) { set_single(self, index, value)?; } @@ -565,7 +567,7 @@ impl CircuitData { Ok(()) } indices => { - let values = value.iter()?.collect::>>()?; + let values = value.try_iter()?.collect::>>()?; if indices.len() == values.len() { for (index, value) in indices.iter().zip(values.iter()) { set_single(self, index, value)?; @@ -646,7 +648,7 @@ impl CircuitData { instruction: instruction_index, parameter: parameter_index, }; - for param in parameters.iter()? { + for param in parameters.try_iter()? { self.param_table.track(¶m?, Some(usage))?; } } @@ -695,7 +697,7 @@ impl CircuitData { } return Ok(()); } - for v in itr.iter()? { + for v in itr.try_iter()? { self.append(v?.downcast()?)?; } Ok(()) @@ -724,7 +726,7 @@ impl CircuitData { ) } else { let values = sequence - .iter()? + .try_iter()? .map(|ob| Param::extract_no_coerce(&ob?)) .collect::>>()?; self.assign_parameters_from_slice(sequence.py(), &values) @@ -738,7 +740,7 @@ impl CircuitData { fn assign_parameters_mapping(&mut self, mapping: Bound) -> PyResult<()> { let py = mapping.py(); let mut items = Vec::new(); - for item in mapping.call_method0("items")?.iter()? { + for item in mapping.call_method0("items")?.try_iter()? { let (param_ob, value) = item?.extract::<(Py, AssignParam)>()?; let uuid = ParameterUuid::from_parameter(param_ob.bind(py))?; // It's fine if the mapping contains parameters that we don't have - just skip those. @@ -794,8 +796,8 @@ impl CircuitData { // Implemented using generic iterators on both sides // for simplicity. - let mut ours_itr = slf.iter()?; - let mut theirs_itr = other.iter()?; + let mut ours_itr = slf.try_iter()?; + let mut theirs_itr = other.try_iter()?; loop { match (ours_itr.next(), theirs_itr.next()) { (Some(ours), Some(theirs)) => { @@ -845,7 +847,11 @@ impl CircuitData { #[setter] pub fn set_global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { if let Param::ParameterExpression(expr) = &self.global_phase { - for param_ob in expr.bind(py).getattr(intern!(py, "parameters"))?.iter()? { + for param_ob in expr + .bind(py) + .getattr(intern!(py, "parameters"))? + .try_iter()? + { match self.param_table.remove_use( ParameterUuid::from_parameter(¶m_ob?)?, ParameterUse::GlobalPhase, @@ -926,6 +932,7 @@ impl CircuitData { instruction_iter.size_hint().0, global_phase, )?; + for item in instruction_iter { let (operation, params, qargs, cargs) = item?; let qubits = res.qargs_interner.insert_owned(qargs); @@ -990,9 +997,13 @@ impl CircuitData { qubits, clbits, param_table: ParameterTable::new(), - global_phase, + global_phase: Param::Float(0.0), }; + // use the global phase setter to ensure parameters are registered + // in the parameter table + res.set_global_phase(py, global_phase)?; + for inst in instruction_iter { res.data.push(inst?); res.track_instruction_parameters(py, res.data.len() - 1)?; @@ -1034,6 +1045,7 @@ impl CircuitData { instruction_iter.size_hint().0, global_phase, )?; + let no_clbit_index = res.cargs_interner.get_default(); for (operation, params, qargs) in instruction_iter { let qubits = res.qargs_interner.insert(&qargs); @@ -1065,8 +1077,13 @@ impl CircuitData { qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), param_table: ParameterTable::new(), - global_phase, + global_phase: Param::Float(0.0), }; + + // use the global phase setter to ensure parameters are registered + // in the parameter table + res.set_global_phase(py, global_phase)?; + if num_qubits > 0 { let qubit_cls = QUBIT.get_bound(py); for _i in 0..num_qubits { @@ -1308,7 +1325,7 @@ impl CircuitData { value: &Param, coerce: bool| -> PyResult { - let new_expr = expr.call_method1(assign_attr, (param_ob, value.to_object(py)))?; + let new_expr = expr.call_method1(assign_attr, (param_ob, value.into_py_any(py)?))?; if new_expr.getattr(parameters_attr)?.len()? == 0 { let out = new_expr.call_method0(numeric_attr)?; if coerce { @@ -1369,15 +1386,12 @@ impl CircuitData { #[cfg(feature = "cache_pygates")] { // Standard gates can all rebuild their definitions, so if the - // cached py_op exists, update the `params` attribute and clear out - // any existing cache. - if let Some(borrowed) = previous.py_op().get() { - borrowed - .bind(py) - .getattr(params_attr)? - .set_item(parameter, new_param)?; - borrowed.bind(py).setattr("_definition", py.None())? - } + // cached py_op exists, discard it to prompt the instruction + // to rebuild its cached python gate upon request later on. This is + // done to avoid an unintentional duplicated reference to the same gate + // instance in python. For more information, see + // https://github.com/Qiskit/qiskit/issues/13504 + previous.py_op_mut().take(); } } else { // Track user operations we've seen so we can rebind their definitions. @@ -1428,10 +1442,10 @@ impl CircuitData { Param::extract_no_coerce( &obj.call_method( assign_parameters_attr, - ([(¶m_ob, value.as_ref())].into_py_dict_bound(py),), + ([(¶m_ob, value.as_ref())].into_py_dict(py)?,), Some( &[("inplace", false), ("flat_input", true)] - .into_py_dict_bound(py), + .into_py_dict(py)?, ), )?, )? @@ -1454,7 +1468,9 @@ impl CircuitData { } let assign_kwargs = (!user_operations.is_empty()).then(|| { - [("inplace", true), ("flat_input", true), ("strict", false)].into_py_dict_bound(py) + [("inplace", true), ("flat_input", true), ("strict", false)] + .into_py_dict(py) + .unwrap() }); for (instruction, bindings) in user_operations { // We only put non-standard gates in `user_operations`, so we're not risking creating a @@ -1483,7 +1499,7 @@ impl CircuitData { if !definition_cache.is_none() { definition_cache.call_method( assign_parameters_attr, - (bindings.into_py_dict_bound(py),), + (bindings.into_py_dict(py)?.into_any().unbind(),), assign_kwargs.as_ref(), )?; } @@ -1515,16 +1531,18 @@ impl CircuitData { /// * capacity - The capacity for instructions to use in the output `CircuitData` /// If `None` the length of `other` will be used, if `Some` the integer /// value will be used as the capacity. - pub fn clone_empty_like(other: &Self, capacity: Option) -> Self { - CircuitData { + pub fn clone_empty_like(py: Python, other: &Self, capacity: Option) -> PyResult { + let mut empty = CircuitData { data: Vec::with_capacity(capacity.unwrap_or(other.data.len())), qargs_interner: other.qargs_interner.clone(), cargs_interner: other.cargs_interner.clone(), qubits: other.qubits.clone(), clbits: other.clbits.clone(), param_table: ParameterTable::new(), - global_phase: other.global_phase.clone(), - } + global_phase: Param::Float(0.0), + }; + empty.set_global_phase(py, other.global_phase.clone())?; + Ok(empty) } /// Append a PackedInstruction to the circuit data. diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index e449ad660e38..3ded8da806a5 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -13,13 +13,15 @@ #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; -use numpy::IntoPyArray; +use numpy::{IntoPyArray, PyArray2}; use pyo3::basic::CompareOp; use pyo3::exceptions::{PyDeprecationWarning, PyTypeError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PyString, PyTuple, PyType}; -use pyo3::{intern, IntoPy, PyObject, PyResult}; +use pyo3::types::{PyBool, PyList, PyString, PyTuple, PyType}; +use pyo3::IntoPyObjectExt; +use pyo3::{intern, PyObject, PyResult}; +use num_complex::Complex64; use smallvec::SmallVec; use crate::imports::{ @@ -80,7 +82,7 @@ impl ExtraInstructionAttributes { attrs .unit .as_deref() - .map(|unit| <&str as IntoPy>>::into_py(unit, py)) + .map(|unit| unit.into_pyobject(py).unwrap().unbind()) }) .unwrap_or_else(|| Self::default_unit(py).clone().unbind()) } @@ -281,7 +283,7 @@ impl CircuitInstruction { params: op_parts.params, extra_attrs: op_parts.extra_attrs, #[cfg(feature = "cache_pygates")] - py_op: operation.into_py(py).into(), + py_op: operation.clone().unbind().into(), }) } @@ -297,7 +299,7 @@ impl CircuitInstruction { Ok(Self { operation: standard.into(), qubits: as_tuple(py, qubits)?.unbind(), - clbits: PyTuple::empty_bound(py).unbind(), + clbits: PyTuple::empty(py).unbind(), params, extra_attrs: ExtraInstructionAttributes::new(label, None, None, None), #[cfg(feature = "cache_pygates")] @@ -347,19 +349,19 @@ impl CircuitInstruction { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> PyObject { - self.operation.name().to_object(py) + fn get_name(&self) -> &str { + self.operation.name() } #[getter] - fn get_params(&self, py: Python) -> PyObject { - self.params.to_object(py) + fn get_params(&self) -> &[Param] { + self.params.as_slice() } #[getter] - fn matrix(&self, py: Python) -> Option { + fn matrix<'py>(&'py self, py: Python<'py>) -> Option>> { let matrix = self.operation.view().matrix(&self.params); - matrix.map(|mat| mat.into_pyarray_bound(py).into()) + matrix.map(move |mat| mat.into_pyarray(py)) } #[getter] @@ -454,7 +456,7 @@ impl CircuitInstruction { params: params.unwrap_or(op_parts.params), extra_attrs: op_parts.extra_attrs, #[cfg(feature = "cache_pygates")] - py_op: operation.into_py(py).into(), + py_op: operation.clone().unbind().into(), }) } else { Ok(Self { @@ -470,12 +472,12 @@ impl CircuitInstruction { } pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { - Ok(( + ( self.get_operation(py)?, self.qubits.bind(py), self.clbits.bind(py), ) - .into_py(py)) + .into_py_any(py) } pub fn __repr__(self_: &Bound, py: Python<'_>) -> PyResult { @@ -497,24 +499,30 @@ impl CircuitInstruction { // like that via unpacking or similar. That means that the `parameters` field is completely // absent, and the qubits and clbits must be converted to lists. pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult> { - Ok(PyTuple::new_bound( + PyTuple::new( py, [ self.get_operation(py)?, self.qubits.bind(py).to_list().into(), self.clbits.bind(py).to_list().into(), ], - )) + ) } pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { warn_on_legacy_circuit_instruction_iteration(py)?; - Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) + self._legacy_format(py)? + .as_any() + .get_item(key)? + .into_py_any(py) } pub fn __iter__(&self, py: Python<'_>) -> PyResult { warn_on_legacy_circuit_instruction_iteration(py)?; - Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) + self._legacy_format(py)? + .as_any() + .try_iter()? + .into_py_any(py) } pub fn __len__(&self, py: Python) -> PyResult { @@ -582,15 +590,17 @@ impl CircuitInstruction { )) } - match op { - CompareOp::Eq => Ok(eq(py, self_, other)? - .map(|b| b.into_py(py)) - .unwrap_or_else(|| py.NotImplemented())), - CompareOp::Ne => Ok(eq(py, self_, other)? - .map(|b| (!b).into_py(py)) - .unwrap_or_else(|| py.NotImplemented())), - _ => Ok(py.NotImplemented()), - } + Ok(match op { + CompareOp::Eq => match eq(py, self_, other)? { + Some(res) => PyBool::new(py, res).to_owned().into_any().unbind(), + None => py.NotImplemented(), + }, + CompareOp::Ne => match eq(py, self_, other)? { + Some(res) => PyBool::new(py, !res).to_owned().into_any().unbind(), + None => py.NotImplemented(), + }, + _ => py.NotImplemented(), + }) } } @@ -702,7 +712,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { clbits: 0, params: params.len() as u32, op_name: ob.getattr(intern!(py, "name"))?.extract()?, - gate: ob.into_py(py), + gate: ob.clone().unbind(), }); return Ok(OperationFromPython { operation: PackedOperation::from_gate(gate), @@ -718,7 +728,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { params: params.len() as u32, op_name: ob.getattr(intern!(py, "name"))?.extract()?, control_flow: ob.is_instance(CONTROL_FLOW_OP.get_bound(py))?, - instruction: ob.into_py(py), + instruction: ob.clone().unbind(), }); return Ok(OperationFromPython { operation: PackedOperation::from_instruction(instruction), @@ -733,7 +743,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?, params: params.len() as u32, op_name: ob.getattr(intern!(py, "name"))?.extract()?, - operation: ob.into_py(py), + operation: ob.clone().unbind(), }); return Ok(OperationFromPython { operation: PackedOperation::from_operation(operation), @@ -748,7 +758,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { /// Convert a sequence-like Python object to a tuple. fn as_tuple<'py>(py: Python<'py>, seq: Option>) -> PyResult> { let Some(seq) = seq else { - return Ok(PyTuple::empty_bound(py)); + return Ok(PyTuple::empty(py)); }; if seq.is_instance_of::() { Ok(seq.downcast_into_exact::()?) @@ -756,12 +766,12 @@ fn as_tuple<'py>(py: Python<'py>, seq: Option>) -> PyResult()?.to_tuple()) } else { // New tuple from iterable. - Ok(PyTuple::new_bound( + PyTuple::new( py, - seq.iter()? + seq.try_iter()? .map(|o| Ok(o?.unbind())) .collect::>>()?, - )) + ) } } @@ -783,7 +793,7 @@ fn warn_on_legacy_circuit_instruction_iteration(py: Python) -> PyResult<()> { " Instead, use the `operation`, `qubits` and `clbits` named attributes." ) ), - py.get_type_bound::(), + py.get_type::(), // Stack level. Compared to Python-space calls to `warn`, this is unusually low // beacuse all our internal call structure is now Rust-space and invisible to Python. 1, diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index 1c61e6705f38..9b3467804688 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -10,8 +10,8 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use ::pyo3::prelude::*; use hashbrown::HashMap; +use pyo3::prelude::*; use pyo3::{ intern, types::{PyDict, PyList}, @@ -59,15 +59,15 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { .ok(), input_vars: ob .call_method0(intern!(py, "iter_input_vars"))? - .iter()? + .try_iter()? .collect::>>()?, captured_vars: ob .call_method0(intern!(py, "iter_captured_vars"))? - .iter()? + .try_iter()? .collect::>>()?, declared_vars: ob .call_method0(intern!(py, "iter_declared_vars"))? - .iter()? + .try_iter()? .collect::>>()?, }) } @@ -103,7 +103,7 @@ pub fn dag_to_circuit( dag.qargs_interner().clone(), dag.cargs_interner().clone(), dag.topological_op_nodes()?.map(|node_index| { - let NodeType::Operation(ref instr) = dag.dag()[node_index] else { + let NodeType::Operation(ref instr) = dag[node_index] else { unreachable!( "The received node from topological_op_nodes() is not an Operation node." ) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index c11eb63d17c0..ed0bf6b6e690 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -40,6 +40,7 @@ use pyo3::exceptions::{ }; use pyo3::intern; use pyo3::prelude::*; +use pyo3::IntoPyObjectExt; use pyo3::types::{ IntoPyDict, PyDict, PyInt, PyIterator, PyList, PySequence, PySet, PyString, PyTuple, PyType, @@ -146,13 +147,13 @@ pub enum Wire { } impl Wire { - fn to_pickle(&self, py: Python) -> PyObject { + fn to_pickle(&self, py: Python) -> PyResult { match self { - Self::Qubit(bit) => (0, bit.0.into_py(py)), - Self::Clbit(bit) => (1, bit.0.into_py(py)), - Self::Var(var) => (2, var.0.into_py(py)), + Self::Qubit(bit) => (0, bit.0.into_py_any(py)?), + Self::Clbit(bit) => (1, bit.0.into_py_any(py)?), + Self::Var(var) => (2, var.0.into_py_any(py)?), } - .into_py(py) + .into_py_any(py) } fn from_pickle(b: &Bound) -> PyResult { @@ -255,7 +256,7 @@ struct PyLegacyResources { impl PyControlFlowModule { fn new(py: Python) -> PyResult { - let module = PyModule::import_bound(py, "qiskit.circuit.controlflow")?; + let module = PyModule::import(py, "qiskit.circuit.controlflow")?; Ok(PyControlFlowModule { condition_resources: module.getattr("condition_resources")?.unbind(), node_resources: module.getattr("node_resources")?.unbind(), @@ -282,6 +283,7 @@ impl PyControlFlowModule { } } +#[derive(IntoPyObject)] struct PyVariableMapper { mapper: Py, } @@ -301,7 +303,7 @@ impl PyVariableMapper { .get_bound(py) .call( (target_cregs, bit_map, var_map), - Some(&kwargs.into_py_dict_bound(py)), + Some(&kwargs.into_py_dict(py)?), )? .unbind(), }) @@ -313,12 +315,11 @@ impl PyVariableMapper { allow_reorder: bool, ) -> PyResult> { let py = condition.py(); - let kwargs: HashMap<&str, Py> = - HashMap::from_iter([("allow_reorder", allow_reorder.into_py(py))]); + let kwargs: HashMap<&str, bool> = HashMap::from_iter([("allow_reorder", allow_reorder)]); self.mapper.bind(py).call_method( intern!(py, "map_condition"), (condition,), - Some(&kwargs.into_py_dict_bound(py)), + Some(&kwargs.into_py_dict(py)?), ) } @@ -330,12 +331,6 @@ impl PyVariableMapper { } } -impl IntoPy> for PyVariableMapper { - fn into_py(self, _py: Python<'_>) -> Py { - self.mapper - } -} - #[pyfunction] fn reject_new_register(reg: &Bound) -> PyResult<()> { Err(DAGCircuitError::new_err(format!( @@ -374,11 +369,11 @@ impl DAGCircuit { pub fn new(py: Python<'_>) -> PyResult { Ok(DAGCircuit { name: None, - metadata: Some(PyDict::new_bound(py).unbind().into()), + metadata: Some(PyDict::new(py).unbind().into()), calibrations: HashMap::new(), dag: StableDiGraph::default(), - qregs: PyDict::new_bound(py).unbind(), - cregs: PyDict::new_bound(py).unbind(), + qregs: PyDict::new(py).unbind(), + cregs: PyDict::new(py).unbind(), qargs_interner: Interner::new(), cargs_interner: Interner::new(), qubits: BitData::new(py, "qubits".to_string()), @@ -387,8 +382,8 @@ impl DAGCircuit { global_phase: Param::Float(0.), duration: None, unit: "dt".to_string(), - qubit_locations: PyDict::new_bound(py).unbind(), - clbit_locations: PyDict::new_bound(py).unbind(), + qubit_locations: PyDict::new(py).unbind(), + clbit_locations: PyDict::new(py).unbind(), qubit_io_map: Vec::new(), clbit_io_map: Vec::new(), var_io_map: Vec::new(), @@ -396,9 +391,9 @@ impl DAGCircuit { control_flow_module: PyControlFlowModule::new(py)?, vars_info: HashMap::new(), vars_by_type: [ - PySet::empty_bound(py)?.unbind(), - PySet::empty_bound(py)?.unbind(), - PySet::empty_bound(py)?.unbind(), + PySet::empty(py)?.unbind(), + PySet::empty(py)?.unbind(), + PySet::empty(py)?.unbind(), ], }) } @@ -417,7 +412,7 @@ impl DAGCircuit { "deprecated as of Qiskit 1.3.0. It will be removed in Qiskit 2.0.0.", ) ), - py.get_type_bound::(), + py.get_type::(), 2, ))?; Ok(self.duration.as_ref().map(|x| x.clone_ref(py))) @@ -436,7 +431,7 @@ impl DAGCircuit { "deprecated as of Qiskit 1.3.0. It will be removed in Qiskit 2.0.0.", ) ), - py.get_type_bound::(), + py.get_type::(), 2, ))?; Ok(self.unit.clone()) @@ -444,7 +439,7 @@ impl DAGCircuit { #[getter] fn input_map(&self, py: Python) -> PyResult> { - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); for (qubit, indices) in self .qubit_io_map .iter() @@ -483,7 +478,7 @@ impl DAGCircuit { #[getter] fn output_map(&self, py: Python) -> PyResult> { - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); for (qubit, indices) in self .qubit_io_map .iter() @@ -521,7 +516,7 @@ impl DAGCircuit { } fn __getstate__(&self, py: Python) -> PyResult> { - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); out_dict.set_item("name", self.name.as_ref().map(|x| x.clone_ref(py)))?; out_dict.set_item("metadata", self.metadata.as_ref().map(|x| x.clone_ref(py)))?; out_dict.set_item("_calibrations_prop", self.calibrations.clone())?; @@ -534,7 +529,7 @@ impl DAGCircuit { .iter() .enumerate() .map(|(k, v)| (k, [v[0].index(), v[1].index()])) - .into_py_dict_bound(py), + .into_py_dict(py)?, )?; out_dict.set_item( "clbit_io_map", @@ -542,7 +537,7 @@ impl DAGCircuit { .iter() .enumerate() .map(|(k, v)| (k, [v[0].index(), v[1].index()])) - .into_py_dict_bound(py), + .into_py_dict(py)?, )?; out_dict.set_item( "var_io_map", @@ -550,7 +545,7 @@ impl DAGCircuit { .iter() .enumerate() .map(|(k, v)| (k, [v[0].index(), v[1].index()])) - .into_py_dict_bound(py), + .into_py_dict(py)?, )?; out_dict.set_item("op_name", self.op_names.clone())?; out_dict.set_item( @@ -568,7 +563,7 @@ impl DAGCircuit { ), ) }) - .into_py_dict_bound(py), + .into_py_dict(py)?, )?; out_dict.set_item("vars_by_type", self.vars_by_type.clone())?; out_dict.set_item("qubits", self.qubits.bits())?; @@ -577,7 +572,7 @@ impl DAGCircuit { let mut nodes: Vec = Vec::with_capacity(self.dag.node_count()); for node_idx in self.dag.node_indices() { let node_data = self.get_node(py, node_idx)?; - nodes.push((node_idx.index(), node_data).to_object(py)); + nodes.push((node_idx.index(), node_data).into_py_any(py)?); } out_dict.set_item("nodes", nodes)?; out_dict.set_item( @@ -594,9 +589,9 @@ impl DAGCircuit { ( endpoints.0.index(), endpoints.1.index(), - edge_w.clone().to_pickle(py), + edge_w.clone().to_pickle(py)?, ) - .to_object(py) + .into_py_any(py)? } None => py.None(), }; @@ -792,7 +787,7 @@ impl DAGCircuit { .iter() .chain(self.clbits.bits().iter()) .collect(); - let out_list = PyList::new_bound(py, wires); + let out_list = PyList::new(py, wires)?; for var_type_set in &self.vars_by_type { for var in var_type_set.bind(py).iter() { out_list.append(var)?; @@ -910,9 +905,10 @@ impl DAGCircuit { } let params_tuple = if let Some(operands) = params { - let add_calibration = PyModule::from_code_bound( + let add_calibration = PyModule::from_code( py, - r#" + std::ffi::CString::new( + r#" import numpy as np def _format(operand): @@ -932,29 +928,32 @@ def _format(operand): # Unassigned parameter return operand "#, - "add_calibration.py", - "add_calibration", + )? + .as_c_str(), + std::ffi::CString::new("add_calibration.py")?.as_c_str(), + std::ffi::CString::new("add_calibration")?.as_c_str(), )?; let format = add_calibration.getattr("_format")?; - let mapped: PyResult> = operands.iter()?.map(|p| format.call1((p?,))).collect(); - PyTuple::new_bound(py, mapped?).into_any() + let mapped: PyResult> = + operands.try_iter()?.map(|p| format.call1((p?,))).collect(); + PyTuple::new(py, mapped?)?.into_any() } else { - PyTuple::empty_bound(py).into_any() + PyTuple::empty(py).into_any() }; let calibrations = self .calibrations .entry(gate.extract()?) - .or_insert_with(|| PyDict::new_bound(py).unbind()) + .or_insert_with(|| PyDict::new(py).unbind()) .bind(py); let qubits = if let Ok(qubits) = qubits.downcast::() { qubits.to_tuple()?.into_any() } else { - PyTuple::new_bound(py, [qubits]).into_any() + PyTuple::new(py, [qubits])?.into_any() }; - let key = PyTuple::new_bound(py, &[qubits.unbind(), params_tuple.into_any().unbind()]); + let key = PyTuple::new(py, &[qubits.unbind(), params_tuple.into_any().unbind()])?; calibrations.set_item(key, schedule)?; Ok(()) } @@ -989,18 +988,18 @@ def _format(operand): continue; } } - params.push(p.to_object(py)); + params.push(p.into_py_any(py)?); } let qubits: Vec = self .qubits .map_bits(node.instruction.qubits.bind(py).iter())? .map(|bit| bit.0) .collect(); - let qubits = PyTuple::new_bound(py, qubits); - let params = PyTuple::new_bound(py, params); + let qubits = PyTuple::new(py, qubits)?; + let params = PyTuple::new(py, params)?; self.calibrations[node.instruction.operation.name()] .bind(py) - .contains((qubits, params).to_object(py)) + .contains((qubits, params)) } /// Remove all operation nodes with the given name. @@ -1075,7 +1074,7 @@ def _format(operand): } self.qregs.bind(py).set_item(®ister_name, qreg)?; - for (index, bit) in qreg.iter()?.enumerate() { + for (index, bit) in qreg.try_iter()?.enumerate() { let bit = bit?; if self.qubits.find(&bit).is_none() { self.add_qubit_unchecked(py, &bit)?; @@ -1108,7 +1107,7 @@ def _format(operand): } self.cregs.bind(py).set_item(register_name, creg)?; - for (index, bit) in creg.iter()?.enumerate() { + for (index, bit) in creg.try_iter()?.enumerate() { let bit = bit?; if self.clbits.find(&bit).is_none() { self.add_clbit_unchecked(py, &bit)?; @@ -1141,7 +1140,11 @@ def _format(operand): /// Raises: /// DAGCircuitError: If the supplied :obj:`~Bit` was of an unknown type. /// DAGCircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. - fn find_bit<'py>(&self, py: Python<'py>, bit: &Bound) -> PyResult> { + fn find_bit<'py>( + &self, + py: Python<'py>, + bit: &Bound<'py, PyAny>, + ) -> PyResult> { if bit.is_instance(imports::QUBIT.get_bound(py))? { return self.qubit_locations.bind(py).get_item(bit)?.ok_or_else(|| { DAGCircuitError::new_err(format!( @@ -1222,7 +1225,7 @@ def _format(operand): // Remove any references to bits. let mut cregs_to_remove = Vec::new(); for creg in self.cregs.bind(py).values() { - for bit in creg.iter()? { + for bit in creg.try_iter()? { let bit = bit?; if clbits.contains(&self.clbits.find(&bit).unwrap()) { cregs_to_remove.push(creg); @@ -1230,7 +1233,7 @@ def _format(operand): } } } - self.remove_cregs(py, &PyTuple::new_bound(py, cregs_to_remove))?; + self.remove_cregs(py, &PyTuple::new(py, cregs_to_remove)?)?; // Remove DAG in/out nodes etc. for bit in clbits.iter() { @@ -1354,7 +1357,7 @@ def _format(operand): self.cregs .bind(py) .del_item(creg.getattr(intern!(py, "name"))?)?; - for (i, bit) in creg.iter()?.enumerate() { + for (i, bit) in creg.try_iter()?.enumerate() { let bit = bit?; let bit_position = self .clbit_locations @@ -1430,7 +1433,7 @@ def _format(operand): // Remove any references to bits. let mut qregs_to_remove = Vec::new(); for qreg in self.qregs.bind(py).values() { - for bit in qreg.iter()? { + for bit in qreg.try_iter()? { let bit = bit?; if qubits.contains(&self.qubits.find(&bit).unwrap()) { qregs_to_remove.push(qreg); @@ -1438,7 +1441,7 @@ def _format(operand): } } } - self.remove_qregs(py, &PyTuple::new_bound(py, qregs_to_remove))?; + self.remove_qregs(py, &PyTuple::new(py, qregs_to_remove)?)?; // Remove DAG in/out nodes etc. for bit in qubits.iter() { @@ -1562,7 +1565,7 @@ def _format(operand): self.qregs .bind(py) .del_item(qreg.getattr(intern!(py, "name"))?)?; - for (i, bit) in qreg.iter()?.enumerate() { + for (i, bit) in qreg.try_iter()?.enumerate() { let bit = bit?; let bit_position = self .qubit_locations @@ -1892,13 +1895,13 @@ def _format(operand): .bits() .iter() .zip(slf.qubits.bits()) - .into_py_dict_bound(py); + .into_py_dict(py)?; let identity_clbit_map = other .clbits .bits() .iter() .zip(slf.clbits.bits()) - .into_py_dict_bound(py); + .into_py_dict(py)?; let qubit_map: Bound = match qubits { None => identity_qubit_map.clone(), @@ -1912,7 +1915,7 @@ def _format(operand): let self_qubits = slf.qubits.cached().bind(py); let other_qubits = other.qubits.cached().bind(py); - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); for (i, q) in qubits.iter().enumerate() { let q = if q.is_instance_of::() { self_qubits.get_item(q.extract()?)? @@ -1938,7 +1941,7 @@ def _format(operand): let self_clbits = slf.clbits.cached().bind(py); let other_clbits = other.clbits.cached().bind(py); - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); for (i, q) in clbits.iter().enumerate() { let q = if q.is_instance_of::() { self_clbits.get_item(q.extract()?)? @@ -1957,18 +1960,15 @@ def _format(operand): identity_qubit_map .iter() .chain(identity_clbit_map.iter()) - .into_py_dict_bound(py) + .into_py_dict(py)? } else { - qubit_map - .iter() - .chain(clbit_map.iter()) - .into_py_dict_bound(py) + qubit_map.iter().chain(clbit_map.iter()).into_py_dict(py)? }; // Chck duplicates in wire map. { let edge_map_values: Vec<_> = edge_map.values().iter().collect(); - if PySet::new_bound(py, edge_map_values.as_slice())?.len() != edge_map.len() { + if PySet::new(py, edge_map_values.as_slice())?.len() != edge_map.len() { return Err(DAGCircuitError::new_err("duplicates in wire_map")); } } @@ -1987,7 +1987,7 @@ def _format(operand): Some(calibrations) => calibrations, None => { dag.calibrations - .insert(gate.clone(), PyDict::new_bound(py).unbind()); + .insert(gate.clone(), PyDict::new(py).unbind()); &dag.calibrations[gate] } }; @@ -2026,7 +2026,7 @@ def _format(operand): dag.cregs.bind(py).values().into_any(), Some(edge_map.clone()), None, - Some(wrap_pyfunction_bound!(reject_new_register, py)?.to_object(py)), + Some(wrap_pyfunction!(reject_new_register, py)?.into_py_any(py)?), )?; for node in other.topological_nodes()? { @@ -2073,7 +2073,7 @@ def _format(operand): .unwrap_or_else(|| bit.bind(py).clone()), ); } - PyTuple::new_bound(py, mapped) + PyTuple::new(py, mapped) }; let m_cargs = { let clbits = other @@ -2087,7 +2087,7 @@ def _format(operand): .unwrap_or_else(|| bit.bind(py).clone()), ); } - PyTuple::new_bound(py, mapped) + PyTuple::new(py, mapped) }; // We explicitly create a mutable py_op here since we might @@ -2118,8 +2118,8 @@ def _format(operand): dag.py_apply_operation_back( py, py_op, - Some(TupleLikeArg { value: m_qargs }), - Some(TupleLikeArg { value: m_cargs }), + Some(TupleLikeArg { value: m_qargs? }), + Some(TupleLikeArg { value: m_cargs? }), false, )?; } @@ -2132,7 +2132,8 @@ def _format(operand): } if !inplace { - Ok(Some(dag.into_py(py))) + let out_obj = dag.into_py_any(py)?; + Ok(Some(out_obj)) } else { Ok(None) } @@ -2142,7 +2143,7 @@ def _format(operand): /// /// Returns: /// DAGCircuit: the reversed dag. - fn reverse_ops<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { + fn reverse_ops<'py>(slf: PyRef<'py, Self>, py: Python<'py>) -> PyResult> { let qc = imports::DAG_TO_CIRCUIT.get_bound(py).call1((slf,))?; let reversed = qc.call_method0("reverse_ops")?; imports::CIRCUIT_TO_DAG.get_bound(py).call1((reversed,)) @@ -2203,7 +2204,7 @@ def _format(operand): } } } - Ok(PyTuple::new_bound(py, result).into_any().iter()?.unbind()) + Ok(PyTuple::new(py, result)?.into_any().try_iter()?.unbind()) } /// Return the number of operations. If there is control flow present, this count may only @@ -2263,7 +2264,7 @@ def _format(operand): || inst_bound.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { let blocks = inst_bound.getattr("blocks")?; - for block in blocks.iter()? { + for block in blocks.try_iter()? { let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; length += inner_dag.size(py, true)?; } @@ -2339,7 +2340,7 @@ def _format(operand): } else { let blocks = inst_bound.getattr("blocks")?; let mut block_weights: Vec = Vec::with_capacity(blocks.len()?); - for block in blocks.iter()? { + for block in blocks.try_iter()? { let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; block_weights.push(inner_dag.depth(py, true)?); } @@ -2461,7 +2462,7 @@ def _format(operand): .chain(self.clbits.bits()) .enumerate() .map(|(idx, bit)| (bit, idx)); - indices.into_py_dict_bound(py) + indices.into_py_dict(py)? }; let other_bit_indices = { @@ -2472,7 +2473,7 @@ def _format(operand): .chain(other.clbits.bits()) .enumerate() .map(|(idx, bit)| (bit, idx)); - indices.into_py_dict_bound(py) + indices.into_py_dict(py)? }; // Check if qregs are the same. @@ -2669,9 +2670,8 @@ def _format(operand): /// /// Args: /// key (Callable): A callable which will take a DAGNode object and - /// return a string sort key. If not specified the - /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - /// used as the sort key for each node. + /// return a string sort key. If not specified the bit qargs and + /// cargs of a node will be used for sorting. /// /// Returns: /// generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order @@ -2692,9 +2692,9 @@ def _format(operand): .collect() }; - Ok(PyTuple::new_bound(py, nodes?) + Ok(PyTuple::new(py, nodes?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -2705,9 +2705,8 @@ def _format(operand): /// /// Args: /// key (Callable): A callable which will take a DAGNode object and - /// return a string sort key. If not specified the - /// :attr:`~qiskit.dagcircuit.DAGNode.sort_key` attribute will be - /// used as the sort key for each node. + /// return a string sort key. If not specified the qargs and + /// cargs of a node will be used for sorting. /// /// Returns: /// generator(DAGOpNode): op node in topological order @@ -2731,9 +2730,9 @@ def _format(operand): .collect() }; - Ok(PyTuple::new_bound(py, nodes?) + Ok(PyTuple::new(py, nodes?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -2891,7 +2890,7 @@ def _format(operand): } let mut qubit_wire_map = HashMap::new(); let mut clbit_wire_map = HashMap::new(); - let var_map = PyDict::new_bound(py); + let var_map = PyDict::new(py); for (index, wire) in wires.iter().enumerate() { if wire.is_instance(imports::QUBIT.get_bound(py))? { if index >= qargs_len { @@ -2928,7 +2927,7 @@ def _format(operand): Ok(bound_wires) => { let mut qubit_wire_map = HashMap::new(); let mut clbit_wire_map = HashMap::new(); - let var_map = PyDict::new_bound(py); + let var_map = PyDict::new(py); for (source_wire, target_wire) in bound_wires.iter() { if source_wire.is_instance(imports::QUBIT.get_bound(py))? { qubit_wire_map.insert( @@ -2974,7 +2973,7 @@ def _format(operand): let node_vars = if self.may_have_additional_wires(py, &node) { let (_additional_clbits, additional_vars) = self.additional_wires(py, node.op().view(), node.condition())?; - let var_set = PySet::new_bound(py, &additional_vars)?; + let var_set = PySet::new(py, &additional_vars)?; if input_dag_var_set .call_method1(intern!(py, "difference"), (var_set.clone(),))? .is_truthy()? @@ -2986,7 +2985,7 @@ def _format(operand): } var_set } else { - PySet::empty_bound(py)? + PySet::empty(py)? }; let bound_var_map = var_map.bind(py); for var in input_dag_var_set.iter() { @@ -3042,7 +3041,7 @@ def _format(operand): // in favour of the new-style conditional blocks. The extra logic in here to add // additional wires into the map as necessary would hugely complicate matters if we tried // to abstract it out into the `VariableMapper` used elsewhere. - let wire_map = PyDict::new_bound(py); + let wire_map = PyDict::new(py); for (source_qubit, target_qubit) in &qubit_wire_map { wire_map.set_item( in_dag.qubits.get(*source_qubit).unwrap().clone_ref(py), @@ -3057,7 +3056,7 @@ def _format(operand): } wire_map.update(var_map.bind(py).as_mapping())?; - let reverse_wire_map = wire_map.iter().map(|(k, v)| (v, k)).into_py_dict_bound(py); + let reverse_wire_map = wire_map.iter().map(|(k, v)| (v, k)).into_py_dict(py)?; let (py_target, py_value): (Bound, Bound) = condition.bind(py).extract()?; let (py_new_target, target_cargs) = @@ -3073,18 +3072,18 @@ def _format(operand): reverse_wire_map.set_item(&py_target, &new_target)?; Ok(new_target) })?; - (new_target.clone(), PySet::new_bound(py, &[new_target])?) + (new_target.clone(), PySet::new(py, &[new_target])?) } else { // ClassicalRegister let target_bits: Vec> = - py_target.iter()?.collect::>()?; + py_target.try_iter()?.collect::>()?; let mapped_bits: Vec>> = target_bits .iter() .map(|b| reverse_wire_map.get_item(b)) .collect::>()?; let mut new_target = Vec::with_capacity(target_bits.len()); - let target_cargs = PySet::empty_bound(py)?; + let target_cargs = PySet::empty(py)?; for (ours, theirs) in target_bits.into_iter().zip(mapped_bits) { if let Some(theirs) = theirs { // Target bit was in node's wires. @@ -3100,14 +3099,14 @@ def _format(operand): target_cargs.add(theirs)?; } } - let kwargs = [("bits", new_target.into_py(py))].into_py_dict_bound(py); + let kwargs = [("bits", new_target.into_pyobject(py)?)].into_py_dict(py)?; let new_target_register = imports::CLASSICAL_REGISTER .get_bound(py) .call((), Some(&kwargs))?; in_dag.add_creg(py, &new_target_register)?; (new_target_register, target_cargs) }; - let new_condition = PyTuple::new_bound(py, [py_new_target, py_value]); + let new_condition = PyTuple::new(py, [py_new_target, py_value])?; qubit_wire_map.clear(); clbit_wire_map.clear(); @@ -3185,7 +3184,7 @@ def _format(operand): }; self.global_phase = add_global_phase(py, &self.global_phase, &input_dag.global_phase)?; - let wire_map_dict = PyDict::new_bound(py); + let wire_map_dict = PyDict::new(py); for (source, target) in clbit_wire_map.iter() { let source_bit = match new_input_dag { Some(ref in_dag) => in_dag.clbits.get(*source), @@ -3200,7 +3199,7 @@ def _format(operand): // measure until qiskit.expr is ported to Rust. It is necessary because we cannot easily // have Python call back to DAGCircuit::add_creg while we're currently borrowing // the DAGCircuit. - let new_registers = PyList::empty_bound(py); + let new_registers = PyList::empty(py); let add_new_register = new_registers.getattr("append")?.unbind(); let flush_new_registers = |dag: &mut DAGCircuit| -> PyResult<()> { for reg in &new_registers { @@ -3228,7 +3227,7 @@ def _format(operand): if old_op.name() == "switch_case" { let raw_target = old_op.instruction.getattr(py, "target")?; let target = raw_target.bind(py); - let kwargs = PyDict::new_bound(py); + let kwargs = PyDict::new(py); kwargs.set_item("label", old_inst.extra_attrs().label())?; let new_op = imports::SWITCH_CASE_OP.get_bound(py).call( ( @@ -3290,7 +3289,7 @@ def _format(operand): } } } - let out_dict = PyDict::new_bound(py); + let out_dict = PyDict::new(py); for (old_index, new_index) in node_map { out_dict.set_item(old_index.index(), self.get_node(py, new_index)?)?; } @@ -3349,7 +3348,7 @@ def _format(operand): { node.instruction.py_op = new_weight.py_op().clone(); } - Ok(node.into_py(py)) + node.into_py_any(py) } else { self.get_node(py, node_index) } @@ -3377,7 +3376,7 @@ def _format(operand): vars_mode: &str, ) -> PyResult> { let connected_components = rustworkx_core::connectivity::connected_components(&self.dag); - let dags = PyList::empty_bound(py); + let dags = PyList::empty(py); for comp_nodes in connected_components.iter() { let mut new_dag = self.copy_empty_like(py, vars_mode)?; @@ -3484,7 +3483,7 @@ def _format(operand): .filter(|e| e.is_instance(imports::QUBIT.get_bound(py)).unwrap()) .collect(); - let qubits = PyTuple::new_bound(py, idle_wires); + let qubits = PyTuple::new(py, idle_wires)?; new_dag.remove_qubits(py, &qubits)?; // TODO: this does not really work, some issue with remove_qubits itself } @@ -3596,8 +3595,8 @@ def _format(operand): .node_references() .map(|(node, weight)| self.unpack_into(py, node, weight)) .collect(); - let tup = PyTuple::new_bound(py, result?); - Ok(tup.into_any().iter().unwrap().unbind()) + let tup = PyTuple::new(py, result?)?; + Ok(tup.into_any().try_iter().unwrap().unbind()) } /// Iterator for edge values with source and destination node. @@ -3626,7 +3625,7 @@ def _format(operand): if let Ok(node) = get_node_index(&nodes) { out.push(node); } else { - for node in nodes.iter()? { + for node in nodes.try_iter()? { out.push(get_node_index(&node?)?); } } @@ -3649,9 +3648,9 @@ def _format(operand): } } - Ok(PyTuple::new_bound(py, edges) + Ok(PyTuple::new(py, edges)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3767,12 +3766,9 @@ def _format(operand): /// Get list of 2 qubit operations. Ignore directives like snapshot and barrier. #[pyo3(name = "two_qubit_ops")] pub fn py_two_qubit_ops(&self, py: Python) -> PyResult>> { - let mut nodes = Vec::new(); - for node in self.two_qubit_ops() { - let weight = self.dag.node_weight(node).expect("NodeIndex in graph"); - nodes.push(self.unpack_into(py, node, weight)?); - } - Ok(nodes) + self.two_qubit_ops() + .map(|(index, _)| self.unpack_into(py, index, &self.dag[index])) + .collect() } /// Get list of 3+ qubit operations. Ignore directives like snapshot and barrier. @@ -3813,9 +3809,9 @@ def _format(operand): .unique() .map(|i| self.get_node(py, i)) .collect(); - Ok(PyTuple::new_bound(py, successors?) + Ok(PyTuple::new(py, successors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3828,9 +3824,9 @@ def _format(operand): .unique() .map(|i| self.get_node(py, i)) .collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3846,9 +3842,9 @@ def _format(operand): _ => None, }) .collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3864,9 +3860,9 @@ def _format(operand): _ => None, }) .collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3893,9 +3889,9 @@ def _format(operand): .quantum_predecessors(node.node.unwrap()) .map(|i| self.get_node(py, i)) .collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3908,9 +3904,9 @@ def _format(operand): .quantum_successors(node.node.unwrap()) .map(|i| self.get_node(py, i)) .collect(); - Ok(PyTuple::new_bound(py, successors?) + Ok(PyTuple::new(py, successors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3925,9 +3921,9 @@ def _format(operand): }); let predecessors: PyResult> = filtered.unique().map(|i| self.get_node(py, i)).collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -3939,7 +3935,7 @@ def _format(operand): .ancestors(node.node.unwrap()) .map(|node| self.get_node(py, node)) .collect(); - Ok(PySet::new_bound(py, &ancestors?)?.unbind()) + Ok(PySet::new(py, &ancestors?)?.unbind()) } /// Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes. @@ -3949,7 +3945,7 @@ def _format(operand): .descendants(node.node.unwrap()) .map(|node| self.get_node(py, node)) .collect(); - Ok(PySet::new_bound(py, &descendants?)?.unbind()) + Ok(PySet::new(py, &descendants?)?.unbind()) } /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node @@ -3968,9 +3964,9 @@ def _format(operand): )) }) .collect(); - Ok(PyList::new_bound(py, successor_index?) + Ok(PyList::new(py, successor_index?)? .into_any() - .iter()? + .try_iter()? .unbind()) } @@ -3984,9 +3980,9 @@ def _format(operand): }); let predecessors: PyResult> = filtered.unique().map(|i| self.get_node(py, i)).collect(); - Ok(PyTuple::new_bound(py, predecessors?) + Ok(PyTuple::new(py, predecessors?)? .into_any() - .iter() + .try_iter() .unwrap() .unbind()) } @@ -4078,7 +4074,7 @@ def _format(operand): #[pyo3(name = "front_layer")] fn py_front_layer(&self, py: Python) -> PyResult> { let native_front_layer = self.front_layer(); - let front_layer_list = PyList::empty_bound(py); + let front_layer_list = PyList::empty(py); for node in native_front_layer { front_layer_list.append(self.get_node(py, node)?)?; } @@ -4103,14 +4099,14 @@ def _format(operand): /// the desired behavior. #[pyo3(signature = (*, vars_mode="captures"))] fn layers(&self, py: Python, vars_mode: &str) -> PyResult> { - let layer_list = PyList::empty_bound(py); + let layer_list = PyList::empty(py); let mut graph_layers = self.multigraph_layers(); if graph_layers.next().is_none() { - return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + return Ok(PyIterator::from_object(&layer_list)?.into()); } for graph_layer in graph_layers { - let layer_dict = PyDict::new_bound(py); + let layer_dict = PyDict::new(py); // Sort to make sure they are in the order they were added to the original DAG // It has to be done by node_id as graph_layer is just a list of nodes // with no implied topology @@ -4128,36 +4124,31 @@ def _format(operand): op_nodes.sort_by_key(|(_, node_index)| **node_index); if op_nodes.is_empty() { - return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + return Ok(PyIterator::from_object(&layer_list)?.into()); } let mut new_layer = self.copy_empty_like(py, vars_mode)?; new_layer.extend(py, op_nodes.iter().map(|(inst, _)| (*inst).clone()))?; - let new_layer_op_nodes = new_layer.op_nodes(false).filter_map(|node_index| { - match new_layer.dag.node_weight(node_index) { - Some(NodeType::Operation(ref node)) => Some(node), - _ => None, - } - }); - let support_iter = new_layer_op_nodes.into_iter().map(|node| { - PyTuple::new_bound( + let support_iter = new_layer.op_nodes(false).map(|(_, instruction)| { + PyTuple::new( py, new_layer .qubits - .map_indices(new_layer.qargs_interner.get(node.qubits())), + .map_indices(new_layer.qargs_interner.get(instruction.qubits())), ) + .unwrap() }); - let support_list = PyList::empty_bound(py); + let support_list = PyList::empty(py); for support_qarg in support_iter { support_list.append(support_qarg)?; } - layer_dict.set_item("graph", new_layer.into_py(py))?; + layer_dict.set_item("graph", new_layer)?; layer_dict.set_item("partition", support_list)?; layer_list.append(layer_dict)?; } - Ok(layer_list.into_any().iter()?.into()) + Ok(layer_list.into_any().try_iter()?.into()) } /// Yield a layer for all gates of this circuit. @@ -4166,7 +4157,7 @@ def _format(operand): /// same structure as in layers(). #[pyo3(signature = (*, vars_mode="captures"))] fn serial_layers(&self, py: Python, vars_mode: &str) -> PyResult> { - let layer_list = PyList::empty_bound(py); + let layer_list = PyList::empty(py); for next_node in self.topological_op_nodes()? { let retrieved_node: &PackedInstruction = match self.dag.node_weight(next_node) { Some(NodeType::Operation(node)) => node, @@ -4175,14 +4166,14 @@ def _format(operand): let mut new_layer = self.copy_empty_like(py, vars_mode)?; // Save the support of the operation we add to the layer - let support_list = PyList::empty_bound(py); - let qubits = PyTuple::new_bound( + let support_list = PyList::empty(py); + let qubits = PyTuple::new( py, self.qargs_interner .get(retrieved_node.qubits()) .iter() .map(|qubit| self.qubits.get(*qubit)), - ) + )? .unbind(); new_layer.push_back(py, retrieved_node.clone())?; @@ -4191,14 +4182,14 @@ def _format(operand): } let layer_dict = [ - ("graph", new_layer.into_py(py)), + ("graph", new_layer.into_py_any(py)?), ("partition", support_list.into_any().unbind()), ] - .into_py_dict_bound(py); + .into_py_dict(py)?; layer_list.append(layer_dict)?; } - Ok(layer_list.into_any().iter()?.into()) + Ok(layer_list.into_any().try_iter()?.into()) } /// Yield layers of the multigraph. @@ -4210,9 +4201,8 @@ def _format(operand): .filter_map(|index| self.get_node(py, index).ok()) .collect() }); - let list: Bound = - PyList::new_bound(py, graph_layers.collect::>>()); - Ok(PyIterator::from_bound_object(&list)?.unbind()) + let list: Bound = PyList::new(py, graph_layers.collect::>>())?; + Ok(PyIterator::from_object(&list)?.unbind()) } /// Return a set of non-conditional runs of "op" nodes with the given names. @@ -4232,14 +4222,14 @@ def _format(operand): name_list_set.insert(name.extract::()?); } - let out_set = PySet::empty_bound(py)?; + let out_set = PySet::empty(py)?; for run in self.collect_runs(name_list_set) { - let run_tuple = PyTuple::new_bound( + let run_tuple = PyTuple::new( py, run.into_iter() .map(|node_index| self.get_node(py, node_index).unwrap()), - ); + )?; out_set.add(run_tuple)?; } Ok(out_set.unbind()) @@ -4251,15 +4241,16 @@ def _format(operand): match self.collect_1q_runs() { Some(runs) => { let runs_iter = runs.map(|node_indices| { - PyList::new_bound( + PyList::new( py, node_indices .into_iter() .map(|node_index| self.get_node(py, node_index).unwrap()), ) + .unwrap() .unbind() }); - let out_list = PyList::empty_bound(py); + let out_list = PyList::empty(py); for run_list in runs_iter { out_list.append(run_list)?; } @@ -4277,15 +4268,16 @@ def _format(operand): match self.collect_2q_runs() { Some(runs) => { let runs_iter = runs.into_iter().map(|node_indices| { - PyList::new_bound( + PyList::new( py, node_indices .into_iter() .map(|node_index| self.get_node(py, node_index).unwrap()), ) + .unwrap() .unbind() }); - let out_list = PyList::empty_bound(py); + let out_list = PyList::empty(py); for run_list in runs_iter { out_list.append(run_list)?; } @@ -4334,7 +4326,7 @@ def _format(operand): .into_iter() .map(|n| self.get_node(py, n)) .collect::>>()?; - Ok(PyTuple::new_bound(py, nodes).into_any().iter()?.unbind()) + Ok(PyTuple::new(py, nodes)?.into_any().try_iter()?.unbind()) } /// Count the occurrences of operation names. @@ -4349,7 +4341,7 @@ def _format(operand): /// Mapping[str, int]: a mapping of operation names to the number of times it appears. #[pyo3(name = "count_ops", signature = (*, recurse=true))] fn py_count_ops(&self, py: Python, recurse: bool) -> PyResult { - self.count_ops(py, recurse).map(|x| x.into_py(py)) + self.count_ops(py, recurse)?.into_py_any(py) } /// Count the occurrences of operation names on the longest path. @@ -4469,18 +4461,18 @@ def _format(operand): let qubits_in_cone_vec: Vec<_> = qubits_in_cone.iter().map(|&&qubit| qubit).collect(); let elements = self.qubits.map_indices(&qubits_in_cone_vec[..]); - Ok(PySet::new_bound(py, elements)?.unbind()) + Ok(PySet::new(py, elements)?.unbind()) } /// Return a dictionary of circuit properties. fn properties(&self, py: Python) -> PyResult> { Ok(HashMap::from_iter([ - ("size", self.size(py, false)?.into_py(py)), - ("depth", self.depth(py, false)?.into_py(py)), - ("width", self.width().into_py(py)), - ("qubits", self.num_qubits().into_py(py)), - ("bits", self.num_clbits().into_py(py)), - ("factors", self.num_tensor_factors().into_py(py)), + ("size", self.size(py, false)?.into_py_any(py)?), + ("depth", self.depth(py, false)?.into_py_any(py)?), + ("width", self.width().into_py_any(py)?), + ("qubits", self.num_qubits().into_py_any(py)?), + ("bits", self.num_clbits().into_py_any(py)?), + ("factors", self.num_tensor_factors().into_py_any(py)?), ("operations", self.py_count_ops(py, true)?), ])) } @@ -4512,7 +4504,7 @@ def _format(operand): filename: Option<&str>, style: &str, ) -> PyResult> { - let module = PyModule::import_bound(py, "qiskit.visualization.dag_visualization")?; + let module = PyModule::import(py, "qiskit.visualization.dag_visualization")?; module.call_method1("dag_drawer", (slf, scale, filename, style)) } @@ -4526,7 +4518,7 @@ def _format(operand): ) -> PyResult> { let mut buffer = Vec::::new(); build_dot(py, self, &mut buffer, graph_attrs, node_attrs, edge_attrs)?; - Ok(PyString::new_bound(py, std::str::from_utf8(&buffer)?)) + Ok(PyString::new(py, std::str::from_utf8(&buffer)?)) } /// Add an input variable to the circuit. @@ -4624,7 +4616,7 @@ def _format(operand): .bind(py) .clone() .into_any() - .iter()? + .try_iter()? .unbind()) } @@ -4634,7 +4626,7 @@ def _format(operand): .bind(py) .clone() .into_any() - .iter()? + .try_iter()? .unbind()) } @@ -4644,19 +4636,19 @@ def _format(operand): .bind(py) .clone() .into_any() - .iter()? + .try_iter()? .unbind()) } /// Iterable over all the classical variables tracked by the circuit. fn iter_vars(&self, py: Python) -> PyResult> { - let out_set = PySet::empty_bound(py)?; + let out_set = PySet::empty(py)?; for var_type_set in &self.vars_by_type { for var in var_type_set.bind(py).iter() { out_set.add(var)?; } } - Ok(out_set.into_any().iter()?.unbind()) + Ok(out_set.into_any().try_iter()?.unbind()) } fn _has_edge(&self, source: usize, target: usize) -> bool { @@ -4681,7 +4673,9 @@ def _format(operand): Wire::Var(var) => self.vars.get(*var).unwrap(), }, ) - .into_py(py) + .into_pyobject(py) + .unwrap() + .unbind() }) .collect() } @@ -4699,7 +4693,9 @@ def _format(operand): Wire::Var(var) => self.vars.get(*var).unwrap(), }, ) - .into_py(py) + .into_pyobject(py) + .unwrap() + .unbind() }) .collect() } @@ -4750,15 +4746,15 @@ def _format(operand): Ok(result) } - fn _edges(&self, py: Python) -> Vec { + fn _edges(&self, py: Python) -> PyResult> { self.dag .edge_indices() .map(|index| { let wire = self.dag.edge_weight(index).unwrap(); match wire { - Wire::Qubit(qubit) => self.qubits.get(*qubit).to_object(py), - Wire::Clbit(clbit) => self.clbits.get(*clbit).to_object(py), - Wire::Var(var) => self.vars.get(*var).to_object(py), + Wire::Qubit(qubit) => self.qubits.get(*qubit).into_py_any(py), + Wire::Clbit(clbit) => self.clbits.get(*clbit).into_py_any(py), + Wire::Var(var) => self.vars.get(*var).into_py_any(py), } }) .collect() @@ -5250,13 +5246,17 @@ impl DAGCircuit { let wires_from_expr = |node: &Bound| -> PyResult<(Vec, Vec)> { let mut clbits = Vec::new(); let mut vars = Vec::new(); - for var in imports::ITER_VARS.get_bound(py).call1((node,))?.iter()? { + for var in imports::ITER_VARS + .get_bound(py) + .call1((node,))? + .try_iter()? + { let var = var?; let var_var = var.getattr("var")?; if var_var.is_instance(imports::CLBIT.get_bound(py))? { clbits.push(self.clbits.find(&var_var).unwrap()); } else if var_var.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { - for bit in var_var.iter().unwrap() { + for bit in var_var.try_iter().unwrap() { clbits.push(self.clbits.find(&bit?).unwrap()); } } else { @@ -5295,7 +5295,7 @@ impl DAGCircuit { if let OperationRef::Instruction(inst) = op { let op = inst.instruction.bind(py); if inst.control_flow() { - for var in op.call_method0("iter_captured_vars")?.iter()? { + for var in op.call_method0("iter_captured_vars")?.try_iter()? { vars.push(var?.unbind()) } if op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { @@ -5303,7 +5303,7 @@ impl DAGCircuit { if target.is_instance(imports::CLBIT.get_bound(py))? { clbits.push(self.clbits.find(&target).unwrap()); } else if target.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { - for bit in target.iter()? { + for bit in target.try_iter()? { clbits.push(self.clbits.find(&bit?).unwrap()); } } else { @@ -5438,7 +5438,7 @@ impl DAGCircuit { py, BitLocations { index: (self.qubits.len() - 1), - registers: PyList::empty_bound(py).unbind(), + registers: PyList::empty(py).unbind(), }, )?, )?; @@ -5454,7 +5454,7 @@ impl DAGCircuit { py, BitLocations { index: (self.clbits.len() - 1), - registers: PyList::empty_bound(py).unbind(), + registers: PyList::empty(py).unbind(), }, )?, )?; @@ -5570,22 +5570,22 @@ impl DAGCircuit { let dag_node = match weight { NodeType::QubitIn(qubit) => Py::new( py, - DAGInNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + DAGInNode::new(id, self.qubits.get(*qubit).unwrap().clone_ref(py)), )? .into_any(), NodeType::QubitOut(qubit) => Py::new( py, - DAGOutNode::new(py, id, self.qubits.get(*qubit).unwrap().clone_ref(py)), + DAGOutNode::new(id, self.qubits.get(*qubit).unwrap().clone_ref(py)), )? .into_any(), NodeType::ClbitIn(clbit) => Py::new( py, - DAGInNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + DAGInNode::new(id, self.clbits.get(*clbit).unwrap().clone_ref(py)), )? .into_any(), NodeType::ClbitOut(clbit) => Py::new( py, - DAGOutNode::new(py, id, self.clbits.get(*clbit).unwrap().clone_ref(py)), + DAGOutNode::new(id, self.clbits.get(*clbit).unwrap().clone_ref(py)), )? .into_any(), NodeType::Operation(packed) => { @@ -5597,20 +5597,13 @@ impl DAGCircuit { DAGOpNode { instruction: CircuitInstruction { operation: packed.op().clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)) - .unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)) - .unbind(), - params: packed - .params_view() - .iter() - .map(|param| param.clone_ref(py)) - .collect(), + qubits: PyTuple::new(py, self.qubits.map_indices(qubits))?.unbind(), + clbits: PyTuple::new(py, self.clbits.map_indices(clbits))?.unbind(), + params: packed.params_view().iter().cloned().collect(), extra_attrs: packed.extra_attrs().clone(), #[cfg(feature = "cache_pygates")] py_op: packed.py_op().clone(), }, - sort_key: format!("{:?}", self.sort_key(id)).into_py(py), }, DAGNode { node: Some(id) }, ), @@ -5619,54 +5612,50 @@ impl DAGCircuit { } NodeType::VarIn(var) => Py::new( py, - DAGInNode::new(py, id, self.vars.get(*var).unwrap().clone_ref(py)), + DAGInNode::new(id, self.vars.get(*var).unwrap().clone_ref(py)), )? .into_any(), NodeType::VarOut(var) => Py::new( py, - DAGOutNode::new(py, id, self.vars.get(*var).unwrap().clone_ref(py)), + DAGOutNode::new(id, self.vars.get(*var).unwrap().clone_ref(py)), )? .into_any(), }; Ok(dag_node) } - /// Returns an iterator over all the indices that refer to an `Operation` node in the `DAGCircuit.` - pub fn op_nodes<'a>( - &'a self, + /// An iterator of the DAG indices and corresponding `PackedInstruction` references for + /// the `NodeType::Operation` variants stored in the DAG. + /// + /// See also [op_node_indices], which provides only the indices. + pub fn op_nodes( + &self, include_directives: bool, - ) -> Box + 'a> { - let node_ops_iter = self - .dag + ) -> impl Iterator + '_ { + self.dag .node_references() - .filter_map(|(node_index, node_type)| match node_type { - NodeType::Operation(ref node) => Some((node_index, node)), - _ => None, - }); - if !include_directives { - Box::new(node_ops_iter.filter_map(|(index, node)| { - if !node.op().directive() { - Some(index) - } else { - None + .filter_map(move |(node_index, node_type)| match node_type { + NodeType::Operation(ref node) => { + (include_directives || !node.op().directive()).then_some((node_index, node)) } - })) - } else { - Box::new(node_ops_iter.map(|(index, _)| index)) - } + _ => None, + }) + } + + /// An iterator of the DAG indices corresponding to `NodeType::Operation` variants. + /// + /// See also [op_nodes], which also provides a reference to the contained `PackedInstruction`. + pub fn op_node_indices( + &self, + include_directives: bool, + ) -> impl Iterator + '_ { + self.op_nodes(include_directives).map(|(index, _)| index) } /// Return an iterator of 2 qubit operations. Ignore directives like snapshot and barrier. - pub fn two_qubit_ops(&self) -> impl Iterator + '_ { - Box::new(self.op_nodes(false).filter(|index| { - let weight = self.dag.node_weight(*index).expect("NodeIndex in graph"); - if let NodeType::Operation(ref packed) = weight { - let qargs = self.qargs_interner.get(packed.qubits()); - qargs.len() == 2 - } else { - false - } - })) + pub fn two_qubit_ops(&self) -> impl Iterator + '_ { + self.op_nodes(false) + .filter(|(_, instruction)| self.qargs_interner.get(instruction.qubits()).len() == 2) } // Filter any nodes that don't match a given predicate function @@ -5674,42 +5663,15 @@ impl DAGCircuit { where F: FnMut(&PackedInstruction) -> bool, { - let mut remove_nodes: Vec = Vec::new(); - for node in self.op_nodes(true) { - let NodeType::Operation(op) = &self.dag[node] else { - unreachable!() - }; - if !predicate(op) { - remove_nodes.push(node); - } - } - for node in remove_nodes { + let remove_indices = self + .op_nodes(true) + .filter_map(|(index, instruction)| (!predicate(instruction)).then_some(index)) + .collect::>(); + for node in remove_indices { self.remove_op_node(node); } } - pub fn op_nodes_by_py_type<'a>( - &'a self, - op: &'a Bound, - include_directives: bool, - ) -> impl Iterator + 'a { - self.dag - .node_references() - .filter_map(move |(node, weight)| { - if let NodeType::Operation(ref packed) = weight { - if !include_directives && packed.op().directive() { - None - } else if packed.op().py_op_is_instance(op).unwrap() { - Some(node) - } else { - None - } - } else { - None - } - }) - } - /// Returns an iterator over a list layers of the `DAGCircuit``. pub fn multigraph_layers(&self) -> impl Iterator> + '_ { let mut first_layer: Vec<_> = self.qubit_io_map.iter().map(|x| x[0]).collect(); @@ -5827,7 +5789,7 @@ impl DAGCircuit { qubit_map.iter().map(|(x, y)| (*y, *x)).collect(); let reverse_clbit_map: HashMap = clbit_map.iter().map(|(x, y)| (*y, *x)).collect(); - let reverse_var_map = PyDict::new_bound(py); + let reverse_var_map = PyDict::new(py); for (k, v) in bound_var_map.iter() { reverse_var_map.set_item(v, k)?; } @@ -6101,11 +6063,11 @@ impl DAGCircuit { Ok(Self { name: None, - metadata: Some(PyDict::new_bound(py).unbind().into()), + metadata: Some(PyDict::new(py).unbind().into()), calibrations: HashMap::default(), dag: StableDiGraph::with_capacity(num_nodes, num_edges), - qregs: PyDict::new_bound(py).unbind(), - cregs: PyDict::new_bound(py).unbind(), + qregs: PyDict::new(py).unbind(), + cregs: PyDict::new(py).unbind(), qargs_interner: Interner::with_capacity(num_qubits), cargs_interner: Interner::with_capacity(num_clbits), qubits: BitData::with_capacity(py, "qubits".to_string(), num_qubits), @@ -6114,8 +6076,8 @@ impl DAGCircuit { global_phase: Param::Float(0.), duration: None, unit: "dt".to_string(), - qubit_locations: PyDict::new_bound(py).unbind(), - clbit_locations: PyDict::new_bound(py).unbind(), + qubit_locations: PyDict::new(py).unbind(), + clbit_locations: PyDict::new(py).unbind(), qubit_io_map: Vec::with_capacity(num_qubits), clbit_io_map: Vec::with_capacity(num_clbits), var_io_map: Vec::with_capacity(num_vars), @@ -6123,9 +6085,9 @@ impl DAGCircuit { control_flow_module: PyControlFlowModule::new(py)?, vars_info: HashMap::with_capacity(num_vars), vars_by_type: [ - PySet::empty_bound(py)?.unbind(), - PySet::empty_bound(py)?.unbind(), - PySet::empty_bound(py)?.unbind(), + PySet::empty(py)?.unbind(), + PySet::empty(py)?.unbind(), + PySet::empty(py)?.unbind(), ], }) } @@ -6290,7 +6252,7 @@ impl DAGCircuit { return Ok(false); } let params = if instruction.params_view().is_empty() { - PyTuple::empty_bound(py) + PyTuple::empty(py) } else { let mut out_params = Vec::new(); for p in instruction.params_view().iter() { @@ -6302,15 +6264,15 @@ impl DAGCircuit { continue; } } - out_params.push(p.to_object(py)); + out_params.push(p.into_pyobject(py)?.into_any().unbind()); } - PyTuple::new_bound(py, out_params) + PyTuple::new(py, out_params)? }; let qargs = self.qargs_interner.get(instruction.qubits()); - let qubits = PyTuple::new_bound(py, qargs.iter().map(|x| x.0)); + let qubits = PyTuple::new(py, qargs.iter().map(|x| x.0))?; self.calibrations[instruction.op().name()] .bind(py) - .contains((qubits, params).to_object(py)) + .contains((qubits, params).into_py_any(py)?) } else { Err(DAGCircuitError::new_err("Specified node is not an op node")) } @@ -6352,7 +6314,7 @@ impl DAGCircuit { panic!("control flow op must be an instruction") }; let blocks = inst.instruction.bind(py).getattr("blocks")?; - for block in blocks.iter()? { + for block in blocks.try_iter()? { let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; inner(py, inner_dag, counts)?; } @@ -6386,7 +6348,7 @@ impl DAGCircuit { let mut clbit_last_nodes: HashMap = HashMap::default(); // TODO: Refactor once Vars are in rust // Dict [ Var: (int, VarWeight)] - let vars_last_nodes: Bound = PyDict::new_bound(py); + let vars_last_nodes: Bound = PyDict::new(py); // Consume into iterator to obtain size hint let iter = iter.into_iter(); @@ -6954,6 +6916,14 @@ impl DAGCircuit { } } +impl ::std::ops::Index for DAGCircuit { + type Output = NodeType; + + fn index(&self, index: NodeIndex) -> &Self::Output { + self.dag.index(index) + } +} + /// Add to global phase. Global phase can only be Float or ParameterExpression so this /// does not handle the full possibility of parameter values. pub(crate) fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { @@ -6983,7 +6953,7 @@ type SortKeyType<'a> = (&'a [Qubit], &'a [Clbit]); /// Emit a Python `DeprecationWarning` for pulse-related dependencies. fn emit_pulse_dependency_deprecation(py: Python, msg: &str) { let _ = imports::WARNINGS_WARN.get_bound(py).call1(( - PyString::new_bound( + PyString::new( py, &format!( "The {} is deprecated as of Qiskit 1.3.0. It will be removed in Qiskit 2.0.0. \ @@ -6992,7 +6962,206 @@ fn emit_pulse_dependency_deprecation(py: Python, msg: &str) { msg ), ), - py.get_type_bound::(), + py.get_type::(), 1, )); } + +#[cfg(all(test, not(miri)))] +mod test { + use crate::circuit_instruction::OperationFromPython; + use crate::dag_circuit::{DAGCircuit, Wire}; + use crate::imports::{CLASSICAL_REGISTER, MEASURE, QUANTUM_REGISTER}; + use crate::operations::StandardGate; + use crate::packed_instruction::{PackedInstruction, PackedOperation}; + use crate::{Clbit, Qubit}; + use ahash::HashSet; + use pyo3::prelude::*; + use rustworkx_core::petgraph::prelude::*; + use rustworkx_core::petgraph::visit::IntoEdgeReferences; + + fn new_dag(py: Python, qubits: u32, clbits: u32) -> DAGCircuit { + let qreg = QUANTUM_REGISTER.get_bound(py).call1((qubits,)).unwrap(); + let creg = CLASSICAL_REGISTER.get_bound(py).call1((clbits,)).unwrap(); + let mut dag = DAGCircuit::new(py).unwrap(); + dag.add_qreg(py, &qreg).unwrap(); + dag.add_creg(py, &creg).unwrap(); + dag + } + + macro_rules! cx_gate { + ($dag:expr, $q0:expr, $q1:expr) => { + PackedInstruction::new( + PackedOperation::from_standard(StandardGate::CXGate), + $dag.qargs_interner + .insert_owned(vec![Qubit($q0), Qubit($q1)]), + $dag.cargs_interner.get_default(), + None, + Default::default(), + ) + }; + } + + macro_rules! measure { + ($dag:expr, $qarg:expr, $carg:expr) => {{ + Python::with_gil(|py| { + let py_op = MEASURE.get_bound(py).call0().unwrap(); + let op_from_py: OperationFromPython = py_op.extract().unwrap(); + let qubits = $dag.qargs_interner.insert_owned(vec![Qubit($qarg)]); + let clbits = $dag.cargs_interner.insert_owned(vec![Clbit($qarg)]); + PackedInstruction::new( + op_from_py.operation, + qubits, + clbits, + Some(op_from_py.params), + op_from_py.extra_attrs, + ) + }) + }}; + } + + #[test] + fn test_push_back() -> PyResult<()> { + Python::with_gil(|py| { + let mut dag = new_dag(py, 2, 2); + + // IO nodes. + let [q0_in_node, q0_out_node] = dag.qubit_io_map[0]; + let [q1_in_node, q1_out_node] = dag.qubit_io_map[1]; + let [c0_in_node, c0_out_node] = dag.clbit_io_map[0]; + let [c1_in_node, c1_out_node] = dag.clbit_io_map[1]; + + // Add a CX to the otherwise empty circuit. + let cx = cx_gate!(dag, 0, 1); + let cx_node = dag.push_back(py, cx)?; + assert!(matches!(dag.op_names.get("cx"), Some(1))); + + let expected_wires = HashSet::from_iter([ + // q0In => CX => q0Out + (q0_in_node, cx_node, Wire::Qubit(Qubit(0))), + (cx_node, q0_out_node, Wire::Qubit(Qubit(0))), + // q1In => CX => q1Out + (q1_in_node, cx_node, Wire::Qubit(Qubit(1))), + (cx_node, q1_out_node, Wire::Qubit(Qubit(1))), + // No clbits used, so in goes straight to out. + (c0_in_node, c0_out_node, Wire::Clbit(Clbit(0))), + (c1_in_node, c1_out_node, Wire::Clbit(Clbit(1))), + ]); + + let actual_wires: HashSet<_> = dag + .dag + .edge_references() + .map(|e| (e.source(), e.target(), e.weight().clone())) + .collect(); + + assert_eq!(actual_wires, expected_wires, "unexpected DAG structure"); + + // Add measures after CX. + let measure_q0 = measure!(dag, 0, 0); + let measure_q0_node = dag.push_back(py, measure_q0)?; + + let measure_q1 = measure!(dag, 1, 1); + let measure_q1_node = dag.push_back(py, measure_q1)?; + + let expected_wires = HashSet::from_iter([ + // q0In -> CX -> M -> q0Out + (q0_in_node, cx_node, Wire::Qubit(Qubit(0))), + (cx_node, measure_q0_node, Wire::Qubit(Qubit(0))), + (measure_q0_node, q0_out_node, Wire::Qubit(Qubit(0))), + // q1In -> CX -> M -> q1Out + (q1_in_node, cx_node, Wire::Qubit(Qubit(1))), + (cx_node, measure_q1_node, Wire::Qubit(Qubit(1))), + (measure_q1_node, q1_out_node, Wire::Qubit(Qubit(1))), + // c0In -> M -> c0Out + (c0_in_node, measure_q0_node, Wire::Clbit(Clbit(0))), + (measure_q0_node, c0_out_node, Wire::Clbit(Clbit(0))), + // c1In -> M -> c1Out + (c1_in_node, measure_q1_node, Wire::Clbit(Clbit(1))), + (measure_q1_node, c1_out_node, Wire::Clbit(Clbit(1))), + ]); + + let actual_wires: HashSet<_> = dag + .dag + .edge_references() + .map(|e| (e.source(), e.target(), e.weight().clone())) + .collect(); + + assert_eq!(actual_wires, expected_wires, "unexpected DAG structure"); + Ok(()) + }) + } + + #[test] + fn test_push_front() -> PyResult<()> { + Python::with_gil(|py| { + let mut dag = new_dag(py, 2, 2); + + // IO nodes. + let [q0_in_node, q0_out_node] = dag.qubit_io_map[0]; + let [q1_in_node, q1_out_node] = dag.qubit_io_map[1]; + let [c0_in_node, c0_out_node] = dag.clbit_io_map[0]; + let [c1_in_node, c1_out_node] = dag.clbit_io_map[1]; + + // Add measures first (we'll add something before them afterwards). + let measure_q0 = measure!(dag, 0, 0); + let measure_q0_node = dag.push_back(py, measure_q0)?; + + let measure_q1 = measure!(dag, 1, 1); + let measure_q1_node = dag.push_back(py, measure_q1)?; + + let expected_wires = HashSet::from_iter([ + // q0In => M => q0Out + (q0_in_node, measure_q0_node, Wire::Qubit(Qubit(0))), + (measure_q0_node, q0_out_node, Wire::Qubit(Qubit(0))), + // q1In => M => q1Out + (q1_in_node, measure_q1_node, Wire::Qubit(Qubit(1))), + (measure_q1_node, q1_out_node, Wire::Qubit(Qubit(1))), + // c0In -> M -> c0Out + (c0_in_node, measure_q0_node, Wire::Clbit(Clbit(0))), + (measure_q0_node, c0_out_node, Wire::Clbit(Clbit(0))), + // c1In -> M -> c1Out + (c1_in_node, measure_q1_node, Wire::Clbit(Clbit(1))), + (measure_q1_node, c1_out_node, Wire::Clbit(Clbit(1))), + ]); + + let actual_wires: HashSet<_> = dag + .dag + .edge_references() + .map(|e| (e.source(), e.target(), e.weight().clone())) + .collect(); + + assert_eq!(actual_wires, expected_wires); + + // Add a CX before the measures. + let cx = cx_gate!(dag, 0, 1); + let cx_node = dag.push_front(py, cx)?; + assert!(matches!(dag.op_names.get("cx"), Some(1))); + + let expected_wires = HashSet::from_iter([ + // q0In -> CX -> M -> q0Out + (q0_in_node, cx_node, Wire::Qubit(Qubit(0))), + (cx_node, measure_q0_node, Wire::Qubit(Qubit(0))), + (measure_q0_node, q0_out_node, Wire::Qubit(Qubit(0))), + // q1In -> CX -> M -> q1Out + (q1_in_node, cx_node, Wire::Qubit(Qubit(1))), + (cx_node, measure_q1_node, Wire::Qubit(Qubit(1))), + (measure_q1_node, q1_out_node, Wire::Qubit(Qubit(1))), + // c0In -> M -> c0Out + (c0_in_node, measure_q0_node, Wire::Clbit(Clbit(0))), + (measure_q0_node, c0_out_node, Wire::Clbit(Clbit(0))), + // c1In -> M -> c1Out + (c1_in_node, measure_q1_node, Wire::Clbit(Clbit(1))), + (measure_q1_node, c1_out_node, Wire::Clbit(Clbit(1))), + ]); + + let actual_wires: HashSet<_> = dag + .dag + .edge_references() + .map(|e| (e.source(), e.target(), e.weight().clone())) + .collect(); + + assert_eq!(actual_wires, expected_wires, "unexpected DAG structure"); + Ok(()) + }) + } +} diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 2fdfcdcbaef2..b2361fbc1615 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -21,13 +21,16 @@ use crate::TupleLikeArg; use ahash::AHasher; use approx::relative_eq; +use num_complex::Complex64; use rustworkx_core::petgraph::stable_graph::NodeIndex; use numpy::IntoPyArray; +use numpy::PyArray2; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyString, PyTuple}; -use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; +use pyo3::types::PyTuple; +use pyo3::IntoPyObjectExt; +use pyo3::{intern, PyObject, PyResult}; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] @@ -103,7 +106,7 @@ impl DAGNode { } fn __hash__(&self, py: Python) -> PyResult { - self.py_nid().into_py(py).bind(py).hash() + self.py_nid().into_pyobject(py)?.hash() } } @@ -111,8 +114,6 @@ impl DAGNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGOpNode { pub instruction: CircuitInstruction, - #[pyo3(get)] - pub sort_key: PyObject, } #[pymethods] @@ -127,9 +128,8 @@ impl DAGOpNode { #[allow(unused_variables)] dag: Option>, ) -> PyResult> { let py_op = op.extract::()?; - let qargs = qargs.map_or_else(|| PyTuple::empty_bound(py), |q| q.value); - let sort_key = qargs.str().unwrap().into(); - let cargs = cargs.map_or_else(|| PyTuple::empty_bound(py), |c| c.value); + let qargs = qargs.map_or_else(|| PyTuple::empty(py), |q| q.value); + let cargs = cargs.map_or_else(|| PyTuple::empty(py), |c| c.value); let instruction = CircuitInstruction { operation: py_op.operation, qubits: qargs.unbind(), @@ -140,16 +140,7 @@ impl DAGOpNode { py_op: op.unbind().into(), }; - Py::new( - py, - ( - DAGOpNode { - instruction, - sort_key, - }, - DAGNode { node: None }, - ), - ) + Py::new(py, (DAGOpNode { instruction }, DAGNode { node: None })) } fn __hash__(slf: PyRef<'_, Self>) -> PyResult { @@ -236,7 +227,6 @@ impl DAGOpNode { mut instruction: CircuitInstruction, deepcopy: bool, ) -> PyResult { - let sort_key = instruction.qubits.bind(py).str().unwrap().into(); if deepcopy { instruction.operation = instruction.operation.py_deepcopy(py, None)?; #[cfg(feature = "cache_pygates")] @@ -245,31 +235,23 @@ impl DAGOpNode { } } let base = PyClassInitializer::from(DAGNode { node: None }); - let sub = base.add_subclass(DAGOpNode { - instruction, - sort_key, - }); - Ok(Py::new(py, sub)?.to_object(py)) + let sub = base.add_subclass(DAGOpNode { instruction }); + Py::new(py, sub)?.into_py_any(py) } fn __reduce__(slf: PyRef, py: Python) -> PyResult { - let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); - Ok(( - py.get_type_bound::(), - ( - slf.instruction.get_operation(py)?, - &slf.instruction.qubits, - &slf.instruction.clbits, - ), - state, - ) - .into_py(py)) + let state = slf.as_ref().node.map(|node| node.index()); + let temp = ( + slf.instruction.get_operation(py)?, + &slf.instruction.qubits, + &slf.instruction.clbits, + ); + (py.get_type::(), temp, state).into_py_any(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (index, sort_key): (Option, PyObject) = state.extract()?; + let index: Option = state.extract()?; slf.as_mut().node = index.map(NodeIndex::new); - slf.sort_key = sort_key; Ok(()) } @@ -347,13 +329,13 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> Py { - self.instruction.operation.name().into_py(py) + fn get_name(&self) -> &str { + self.instruction.operation.name() } #[getter] - fn get_params(&self, py: Python) -> PyObject { - self.instruction.params.to_object(py) + fn get_params(&self) -> &[Param] { + self.instruction.params.as_slice() } #[setter] @@ -362,9 +344,9 @@ impl DAGOpNode { } #[getter] - fn matrix(&self, py: Python) -> Option { + fn matrix<'py>(&'py self, py: Python<'py>) -> Option>> { let matrix = self.instruction.operation.matrix(&self.instruction.params); - matrix.map(|mat| mat.into_pyarray_bound(py).into()) + matrix.map(|mat| mat.into_pyarray(py)) } #[getter] @@ -462,44 +444,29 @@ impl DAGOpNode { pub struct DAGInNode { #[pyo3(get)] pub wire: PyObject, - #[pyo3(get)] - sort_key: PyObject, } impl DAGInNode { - pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { - ( - DAGInNode { - wire, - sort_key: intern!(py, "[]").clone().into(), - }, - DAGNode { node: Some(node) }, - ) + pub fn new(node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + (DAGInNode { wire }, DAGNode { node: Some(node) }) } } #[pymethods] impl DAGInNode { #[new] - fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { - Ok(( - DAGInNode { - wire, - sort_key: intern!(py, "[]").clone().into(), - }, - DAGNode { node: None }, - )) + fn py_new(wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok((DAGInNode { wire }, DAGNode { node: None })) } - fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); - (py.get_type_bound::(), (&slf.wire,), state).into_py(py) + fn __reduce__<'py>(slf: PyRef<'py, Self>, py: Python<'py>) -> PyResult> { + let state = slf.as_ref().node.map(|node| node.index()); + (py.get_type::(), (&slf.wire,), state).into_pyobject(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (index, sort_key): (Option, PyObject) = state.extract()?; + let index: Option = state.extract()?; slf.as_mut().node = index.map(NodeIndex::new); - slf.sort_key = sort_key; Ok(()) } @@ -535,44 +502,29 @@ impl DAGInNode { pub struct DAGOutNode { #[pyo3(get)] pub wire: PyObject, - #[pyo3(get)] - sort_key: PyObject, } impl DAGOutNode { - pub fn new(py: Python, node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { - ( - DAGOutNode { - wire, - sort_key: intern!(py, "[]").clone().into(), - }, - DAGNode { node: Some(node) }, - ) + pub fn new(node: NodeIndex, wire: PyObject) -> (Self, DAGNode) { + (DAGOutNode { wire }, DAGNode { node: Some(node) }) } } #[pymethods] impl DAGOutNode { #[new] - fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { - Ok(( - DAGOutNode { - wire, - sort_key: intern!(py, "[]").clone().into(), - }, - DAGNode { node: None }, - )) + fn py_new(wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok((DAGOutNode { wire }, DAGNode { node: None })) } - fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); - (py.get_type_bound::(), (&slf.wire,), state).into_py(py) + fn __reduce__(slf: PyRef, py: Python) -> PyResult { + let state = slf.as_ref().node.map(|node| node.index()); + (py.get_type::(), (&slf.wire,), state).into_py_any(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (index, sort_key): (Option, PyObject) = state.extract()?; + let index: Option = state.extract()?; slf.as_mut().node = index.map(NodeIndex::new); - slf.sort_key = sort_key; Ok(()) } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs index c31488e92c81..e969e0287460 100644 --- a/crates/circuit/src/dot_utils.rs +++ b/crates/circuit/src/dot_utils.rs @@ -77,16 +77,22 @@ where static ATTRS_TO_ESCAPE: [&str; 2] = ["label", "tooltip"]; /// Convert an attr map to an output string -fn attr_map_to_string( - py: Python, - attrs: Option<&PyObject>, +fn attr_map_to_string<'py, T: IntoPyObject<'py>>( + py: Python<'py>, + attrs: Option<&'py PyObject>, weight: T, -) -> PyResult { +) -> PyResult +where + >::Output: pyo3::IntoPyObject<'py>, + >::Error: std::fmt::Debug, +{ if attrs.is_none() { return Ok("".to_string()); } let attr_callable = |node: T| -> PyResult> { - let res = attrs.unwrap().call1(py, (node.to_object(py),))?; + let res = attrs + .unwrap() + .call1(py, (node.into_pyobject(py).unwrap(),))?; res.extract(py) }; diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 67ae9f85897f..abe0d15baf03 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -41,7 +41,7 @@ impl ImportOnceCell { #[inline] pub fn get(&self, py: Python) -> &Py { self.cell.get_or_init(py, || { - py.import_bound(self.module) + py.import(self.module) .unwrap() .getattr(self.object) .unwrap() @@ -112,6 +112,7 @@ pub static FOR_LOOP_OP_CHECK: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq"); pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID"); pub static BARRIER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Barrier"); +pub static MEASURE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Measure"); pub static UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( "qiskit.circuit.library.generalized_gates.unitary", "UnitaryGate", @@ -255,42 +256,65 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ /// /// NOTE: the order here is significant it must match the StandardGate variant's number must match /// index of it's entry in this table. This is all done statically for performance -static mut STDGATE_PYTHON_GATES: GILOnceCell<[Option; STANDARD_GATE_SIZE]> = - GILOnceCell::new(); - -#[inline] -pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObject) { - let gate_map = unsafe { - match STDGATE_PYTHON_GATES.get_mut() { - Some(gate_map) => gate_map, - None => { - let array: [Option; STANDARD_GATE_SIZE] = std::array::from_fn(|_| None); - STDGATE_PYTHON_GATES.set(py, array).unwrap(); - STDGATE_PYTHON_GATES.get_mut().unwrap() - } - } - }; - let gate_cls = &gate_map[rs_gate as usize]; - if gate_cls.is_none() { - gate_map[rs_gate as usize] = Some(py_gate.clone_ref(py)); - } -} +static STDGATE_PYTHON_GATES: [GILOnceCell; STANDARD_GATE_SIZE] = [ + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), + GILOnceCell::new(), +]; #[inline] -pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { - let gate_map = - unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || std::array::from_fn(|_| None)) }; - let gate = &gate_map[rs_gate as usize]; - let populate = gate.is_none(); - let out_gate = match gate { - Some(gate) => gate.clone_ref(py), - None => { - let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize]; - py.import_bound(py_mod)?.getattr(py_class)?.unbind() - } - }; - if populate { - populate_std_gate_map(py, rs_gate, out_gate.clone_ref(py)); - } - Ok(out_gate) +pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult<&'static Py> { + STDGATE_PYTHON_GATES[rs_gate as usize].get_or_try_init(py, || { + let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize]; + Ok(py.import(py_mod)?.getattr(py_class)?.unbind()) + }) } diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index a72efb037afb..b77ecb51fa98 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -206,7 +206,8 @@ mod test { fn default_key_exists() { let mut interner = Interner::<[u32]>::new(); assert_eq!(interner.get_default(), interner.get_default()); - assert_eq!(interner.get(interner.get_default()), &[]); + let res: &[u32] = &[]; + assert_eq!(interner.get(interner.get_default()), res); assert_eq!(interner.insert_owned(Vec::new()), interner.get_default()); assert_eq!(interner.insert(&[]), interner.get_default()); diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index a4064d44b917..ba120acecefb 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -87,12 +87,12 @@ impl<'py> FromPyObject<'py> for TupleLikeArg<'py> { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let value = match ob.downcast::() { Ok(seq) => seq.to_tuple()?, - Err(_) => PyTuple::new_bound( + Err(_) => PyTuple::new( ob.py(), - ob.iter()? + ob.try_iter()? .map(|o| Ok(o?.unbind())) .collect::>>()?, - ), + )?, }; Ok(TupleLikeArg { value }) } @@ -122,6 +122,40 @@ impl From for BitType { } } +/// Implement `IntoPyObject` for the reference to a struct or enum declared as `#[pyclass]` that is +/// also `Copy`. +/// +/// For example: +/// ``` +/// #[derive(Clone, Copy)] +/// #[pyclass(frozen)] +/// struct MyStruct(u32); +/// +/// impl_intopyobject_for_copy_pyclass!(MyStruct); +/// ``` +/// +/// The `pyclass` attribute macro already ensures that `IntoPyObject` is implemented for `MyStruct`, +/// but it doesn't implement it for `&MyStruct` - for non-copy structs, the implementation of that +/// is not obvious and may be surprising to users if it existed. If the struct is `Copy`, though, +/// it's explicitly "free" to make new copies and convert them, so we can do that and delegate. +/// +/// Usually this doesn't matter much to code authors, but it can help a lot when dealing with +/// references nested in ad-hoc structures, like `(&T1, &T2)`. +#[macro_export] +macro_rules! impl_intopyobject_for_copy_pyclass { + ($ty:ty) => { + impl<'py> ::pyo3::conversion::IntoPyObject<'py> for &$ty { + type Target = <$ty as ::pyo3::conversion::IntoPyObject<'py>>::Target; + type Output = <$ty as ::pyo3::conversion::IntoPyObject<'py>>::Output; + type Error = <$ty as ::pyo3::conversion::IntoPyObject<'py>>::Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } + }; +} + pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 59adfd9e0e8c..61fea09c20b0 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -18,19 +18,20 @@ use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; use crate::imports::get_std_gate_class; use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; -use crate::{gate_matrix, Qubit}; +use crate::{gate_matrix, impl_intopyobject_for_copy_pyclass, Qubit}; use ndarray::{aview2, Array2}; use num_complex::Complex64; use smallvec::{smallvec, SmallVec}; use numpy::IntoPyArray; +use numpy::PyArray2; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyList, PyTuple}; -use pyo3::{intern, IntoPy, Python}; +use pyo3::{intern, Python}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, IntoPyObject, IntoPyObjectRef)] pub enum Param { ParameterExpression(PyObject), Float(f64), @@ -72,26 +73,6 @@ impl<'py> FromPyObject<'py> for Param { } } -impl IntoPy for Param { - fn into_py(self, py: Python) -> PyObject { - match &self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), - } - } -} - -impl ToPyObject for Param { - fn to_object(&self, py: Python) -> PyObject { - match self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), - } - } -} - impl Param { /// Get an iterator over any Python-space `Parameter` instances tracked within this `Param`. pub fn iter_parameters<'py>(&self, py: Python<'py>) -> PyResult> { @@ -99,13 +80,13 @@ impl Param { match self { Param::Float(_) => Ok(ParamParameterIter(None)), Param::ParameterExpression(expr) => Ok(ParamParameterIter(Some( - expr.bind(py).getattr(parameters_attr)?.iter()?, + expr.bind(py).getattr(parameters_attr)?.try_iter()?, ))), Param::Obj(obj) => { let obj = obj.bind(py); if obj.is_instance(QUANTUM_CIRCUIT.get_bound(py))? { Ok(ParamParameterIter(Some( - obj.getattr(parameters_attr)?.iter()?, + obj.getattr(parameters_attr)?.try_iter()?, ))) } else { Ok(ParamParameterIter(None)) @@ -333,6 +314,7 @@ pub enum StandardGate { C3SXGate = 50, RC3XGate = 51, } +impl_intopyobject_for_copy_pyclass!(StandardGate); unsafe impl ::bytemuck::CheckedBitPattern for StandardGate { type Bits = u8; @@ -343,12 +325,6 @@ unsafe impl ::bytemuck::CheckedBitPattern for StandardGate { } unsafe impl ::bytemuck::NoUninit for StandardGate {} -impl ToPyObject for StandardGate { - fn to_object(&self, py: Python) -> Py { - (*self).into_py(py) - } -} - static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0-9 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 10-19 @@ -431,6 +407,11 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "rcccx", // 51 ("rc3x") ]; +/// Get a slice of all standard gate names. +pub fn get_standard_gate_names() -> &'static [&'static str] { + &STANDARD_GATE_NAME +} + impl StandardGate { pub fn create_py_op( &self, @@ -440,8 +421,8 @@ impl StandardGate { ) -> PyResult> { let gate_class = get_std_gate_class(py, *self)?; let args = match params.unwrap_or(&[]) { - &[] => PyTuple::empty_bound(py), - params => PyTuple::new_bound(py, params), + &[] => PyTuple::empty(py), + params => PyTuple::new(py, params.iter().map(|x| x.into_pyobject(py).unwrap()))?, }; let (label, unit, duration, condition) = ( extra_attrs.label(), @@ -450,8 +431,8 @@ impl StandardGate { extra_attrs.condition(), ); if label.is_some() || unit.is_some() || duration.is_some() || condition.is_some() { - let kwargs = [("label", label.to_object(py))].into_py_dict_bound(py); - let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; + let kwargs = [("label", label.into_pyobject(py)?)].into_py_dict(py)?; + let mut out = gate_class.call(py, args, Some(&kwargs))?; let mut mutable = false; if let Some(condition) = condition { if !mutable { @@ -475,7 +456,7 @@ impl StandardGate { } Ok(out) } else { - gate_class.call_bound(py, args, None) + gate_class.call(py, args, None) } } @@ -683,9 +664,12 @@ impl StandardGate { } // These pymethods are for testing: - pub fn _to_matrix(&self, py: Python, params: Vec) -> Option { - self.matrix(¶ms) - .map(|x| x.into_pyarray_bound(py).into()) + pub fn _to_matrix<'py>( + &self, + py: Python<'py>, + params: Vec, + ) -> Option>> { + self.matrix(¶ms).map(|x| x.into_pyarray(py)) } pub fn _num_params(&self) -> u32 { @@ -731,13 +715,13 @@ impl StandardGate { } #[getter] - pub fn get_gate_class(&self, py: Python) -> PyResult> { + pub fn get_gate_class(&self, py: Python) -> PyResult<&'static Py> { get_std_gate_class(py, *self) } #[staticmethod] - pub fn all_gates(py: Python) -> Bound { - PyList::new_bound( + pub fn all_gates(py: Python) -> PyResult> { + PyList::new( py, (0..STANDARD_GATE_SIZE as u8).map(::bytemuck::checked::cast::<_, Self>), ) @@ -2340,8 +2324,14 @@ pub fn add_param(param: &Param, summand: f64, py: Python) -> Param { } pub fn radd_param(param1: Param, param2: Param, py: Python) -> Param { - match [param1, param2] { + match [¶m1, ¶m2] { [Param::Float(theta), Param::Float(lambda)] => Param::Float(theta + lambda), + [Param::Float(theta), Param::ParameterExpression(_lambda)] => { + add_param(¶m2, *theta, py) + } + [Param::ParameterExpression(_theta), Param::Float(lambda)] => { + add_param(¶m1, *lambda, py) + } [Param::ParameterExpression(theta), Param::ParameterExpression(lambda)] => { Param::ParameterExpression( theta diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 36a7cea7a9a8..bc51ffae5aca 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -10,7 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::cell::OnceCell; +use std::sync::OnceLock; use hashbrown::hash_map::Entry; use hashbrown::{HashMap, HashSet}; @@ -130,12 +130,12 @@ pub struct ParameterTable { /// calculate this on demand and cache it. /// /// Any method that adds or removes a parameter needs to invalidate this. - order_cache: OnceCell>, + order_cache: OnceLock>, /// Cache of a Python-space list of the parameter objects, in order. We only generate this /// specifically when asked. /// /// Any method that adds or removes a parameter needs to invalidate this. - py_parameters_cache: OnceCell>, + py_parameters_cache: OnceLock>, } impl ParameterTable { @@ -234,13 +234,14 @@ impl ParameterTable { pub fn py_parameters<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { self.py_parameters_cache .get_or_init(|| { - PyList::new_bound( + PyList::new( py, self.order_cache .get_or_init(|| self.sorted_order()) .iter() .map(|uuid| self.by_uuid[uuid].object.bind(py).clone()), ) + .unwrap() .unbind() }) .bind(py) @@ -249,7 +250,7 @@ impl ParameterTable { /// Get a Python set of all tracked `Parameter` objects. pub fn py_parameters_unsorted<'py>(&self, py: Python<'py>) -> PyResult> { - PySet::new_bound(py, self.by_uuid.values().map(|info| &info.object)) + PySet::new(py, self.by_uuid.values().map(|info| &info.object)) } /// Get the sorted order of the `ParameterTable`. This does not access the cache. @@ -380,8 +381,8 @@ impl ParameterTable { .by_uuid .get(&uuid) .ok_or(ParameterTableError::ParameterNotTracked(uuid))?; - // PyO3's `PySet::new_bound` only accepts iterables of references. - let out = PySet::empty_bound(py)?; + // PyO3's `PySet::new` only accepts iterables of references. + let out = PySet::empty(py)?; for usage in info.uses.iter() { match usage { ParameterUse::GlobalPhase => out.add((py.None(), py.None()))?, diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index eccc3ba8a87a..42962edb2fb9 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -10,6 +10,9 @@ name = "qiskit_pyext" doctest = false crate-type = ["cdylib"] +[lints] +workspace = true + [features] # We always need to activate PyO3's `extension-module` for this crate to be useful at all, but we # need it *not* to be active for the tests of other crates to work. If the feature is active, diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index d8e59e04e51e..d2a0db8d19fd 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -18,7 +18,7 @@ fn add_submodule(m: &Bound, constructor: F, name: &str) -> PyResult where F: FnOnce(&Bound) -> PyResult<()>, { - let new_mod = PyModule::new_bound(m.py(), name)?; + let new_mod = PyModule::new(m.py(), name)?; constructor(&new_mod)?; m.add_submodule(&new_mod) } @@ -66,7 +66,6 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::two_qubit_decompose::two_qubit_decompose, "two_qubit_decompose")?; add_submodule(m, ::qiskit_accelerate::unitary_synthesis::unitary_synthesis, "unitary_synthesis")?; add_submodule(m, ::qiskit_accelerate::uc_gate::uc_gate, "uc_gate")?; - add_submodule(m, ::qiskit_accelerate::utils::utils, "utils")?; add_submodule(m, ::qiskit_accelerate::vf2_layout::vf2_layout, "vf2_layout")?; add_submodule(m, ::qiskit_circuit::circuit, "circuit")?; add_submodule(m, ::qiskit_circuit::converters::converters, "converters")?; diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 681693c4a17d..a6ce9c0cf6fd 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -9,6 +9,9 @@ license.workspace = true name = "qiskit_qasm2" doctest = false +[lints] +workspace = true + [dependencies] num-bigint.workspace = true hashbrown.workspace = true diff --git a/crates/qasm2/src/bytecode.rs b/crates/qasm2/src/bytecode.rs index df3897aa5b71..6da03887c44e 100644 --- a/crates/qasm2/src/bytecode.rs +++ b/crates/qasm2/src/bytecode.rs @@ -211,89 +211,108 @@ pub enum InternalBytecode { }, } -impl IntoPy for InternalBytecode { +impl<'py> IntoPyObject<'py> for InternalBytecode { + type Target = Bytecode; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + /// Convert the internal bytecode representation to a Python-space one. - fn into_py(self, py: Python<'_>) -> Bytecode { - match self { - InternalBytecode::Gate { - id, - arguments, - qubits, - } => Bytecode { - opcode: OpCode::Gate, - operands: (id, arguments, qubits).into_py(py), - }, - InternalBytecode::ConditionedGate { - id, - arguments, - qubits, - creg, - value, - } => Bytecode { - opcode: OpCode::ConditionedGate, - operands: (id, arguments, qubits, creg, value).into_py(py), - }, - InternalBytecode::Measure { qubit, clbit } => Bytecode { - opcode: OpCode::Measure, - operands: (qubit, clbit).into_py(py), - }, - InternalBytecode::ConditionedMeasure { - qubit, - clbit, - creg, - value, - } => Bytecode { - opcode: OpCode::ConditionedMeasure, - operands: (qubit, clbit, creg, value).into_py(py), - }, - InternalBytecode::Reset { qubit } => Bytecode { - opcode: OpCode::Reset, - operands: (qubit,).into_py(py), - }, - InternalBytecode::ConditionedReset { qubit, creg, value } => Bytecode { - opcode: OpCode::ConditionedReset, - operands: (qubit, creg, value).into_py(py), - }, - InternalBytecode::Barrier { qubits } => Bytecode { - opcode: OpCode::Barrier, - operands: (qubits,).into_py(py), - }, - InternalBytecode::DeclareQreg { name, size } => Bytecode { - opcode: OpCode::DeclareQreg, - operands: (name, size).into_py(py), + fn into_pyobject(self, py: Python<'py>) -> Result { + Bound::new( + py, + match self { + InternalBytecode::Gate { + id, + arguments, + qubits, + } => Bytecode { + opcode: OpCode::Gate, + operands: (id, arguments, qubits) + .into_pyobject(py)? + .into_any() + .unbind(), + }, + InternalBytecode::ConditionedGate { + id, + arguments, + qubits, + creg, + value, + } => Bytecode { + opcode: OpCode::ConditionedGate, + operands: (id, arguments, qubits, creg, value) + .into_pyobject(py)? + .into_any() + .unbind(), + }, + InternalBytecode::Measure { qubit, clbit } => Bytecode { + opcode: OpCode::Measure, + operands: (qubit, clbit).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::ConditionedMeasure { + qubit, + clbit, + creg, + value, + } => Bytecode { + opcode: OpCode::ConditionedMeasure, + operands: (qubit, clbit, creg, value) + .into_pyobject(py)? + .into_any() + .unbind(), + }, + InternalBytecode::Reset { qubit } => Bytecode { + opcode: OpCode::Reset, + operands: (qubit,).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::ConditionedReset { qubit, creg, value } => Bytecode { + opcode: OpCode::ConditionedReset, + operands: (qubit, creg, value).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::Barrier { qubits } => Bytecode { + opcode: OpCode::Barrier, + operands: (qubits,).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::DeclareQreg { name, size } => Bytecode { + opcode: OpCode::DeclareQreg, + operands: (name, size).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::DeclareCreg { name, size } => Bytecode { + opcode: OpCode::DeclareCreg, + operands: (name, size).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::DeclareGate { name, num_qubits } => Bytecode { + opcode: OpCode::DeclareGate, + operands: (name, num_qubits).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::GateInBody { + id, + arguments, + qubits, + } => Bytecode { + // In Python space, we don't have to be worried about the types of the + // parameters changing here, so we can just use `OpCode::Gate` unlike in the + // internal bytecode. + opcode: OpCode::Gate, + operands: (id, arguments.into_pyobject(py)?, qubits) + .into_pyobject(py)? + .into_any() + .unbind(), + }, + InternalBytecode::EndDeclareGate {} => Bytecode { + opcode: OpCode::EndDeclareGate, + operands: ().into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::DeclareOpaque { name, num_qubits } => Bytecode { + opcode: OpCode::DeclareOpaque, + operands: (name, num_qubits).into_pyobject(py)?.into_any().unbind(), + }, + InternalBytecode::SpecialInclude { indices } => Bytecode { + opcode: OpCode::SpecialInclude, + operands: (indices,).into_pyobject(py)?.into_any().unbind(), + }, }, - InternalBytecode::DeclareCreg { name, size } => Bytecode { - opcode: OpCode::DeclareCreg, - operands: (name, size).into_py(py), - }, - InternalBytecode::DeclareGate { name, num_qubits } => Bytecode { - opcode: OpCode::DeclareGate, - operands: (name, num_qubits).into_py(py), - }, - InternalBytecode::GateInBody { - id, - arguments, - qubits, - } => Bytecode { - // In Python space, we don't have to be worried about the types of the - // parameters changing here, so we can just use `OpCode::Gate` unlike in the - // internal bytecode. - opcode: OpCode::Gate, - operands: (id, arguments.into_py(py), qubits).into_py(py), - }, - InternalBytecode::EndDeclareGate {} => Bytecode { - opcode: OpCode::EndDeclareGate, - operands: ().into_py(py), - }, - InternalBytecode::DeclareOpaque { name, num_qubits } => Bytecode { - opcode: OpCode::DeclareOpaque, - operands: (name, num_qubits).into_py(py), - }, - InternalBytecode::SpecialInclude { indices } => Bytecode { - opcode: OpCode::SpecialInclude, - operands: (indices,).into_py(py), - }, - } + ) } } @@ -348,7 +367,9 @@ impl BytecodeIterator { self.buffer_used += 1; Ok(self.buffer[self.buffer_used - 1] .take() - .map(|bytecode| bytecode.into_py(py))) + .map(|bytecode| bytecode.into_pyobject(py)) + .transpose()? + .map(|x| x.get().clone())) } } } diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index 0186253b2f84..a8ef33d2bd59 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -146,57 +146,76 @@ pub enum Expr { CustomFunction(PyObject, Vec), } -impl IntoPy for Expr { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Expr::Constant(value) => bytecode::ExprConstant { value }.into_py(py), - Expr::Parameter(index) => bytecode::ExprArgument { index }.into_py(py), +impl<'py> IntoPyObject<'py> for Expr { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(match self { + Expr::Constant(value) => bytecode::ExprConstant { value } + .into_pyobject(py)? + .into_any(), + Expr::Parameter(index) => bytecode::ExprArgument { index } + .into_pyobject(py)? + .into_any(), Expr::Negate(expr) => bytecode::ExprUnary { opcode: bytecode::UnaryOpCode::Negate, - argument: expr.into_py(py), + argument: expr.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Add(left, right) => bytecode::ExprBinary { opcode: bytecode::BinaryOpCode::Add, - left: left.into_py(py), - right: right.into_py(py), + left: left.into_pyobject(py)?.unbind(), + right: right.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Subtract(left, right) => bytecode::ExprBinary { opcode: bytecode::BinaryOpCode::Subtract, - left: left.into_py(py), - right: right.into_py(py), + left: left.into_pyobject(py)?.unbind(), + right: right.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Multiply(left, right) => bytecode::ExprBinary { opcode: bytecode::BinaryOpCode::Multiply, - left: left.into_py(py), - right: right.into_py(py), + left: left.into_pyobject(py)?.unbind(), + right: right.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Divide(left, right) => bytecode::ExprBinary { opcode: bytecode::BinaryOpCode::Divide, - left: left.into_py(py), - right: right.into_py(py), + left: left.into_pyobject(py)?.unbind(), + right: right.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Power(left, right) => bytecode::ExprBinary { opcode: bytecode::BinaryOpCode::Power, - left: left.into_py(py), - right: right.into_py(py), + left: left.into_pyobject(py)?.unbind(), + right: right.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::Function(func, expr) => bytecode::ExprUnary { opcode: func.into(), - argument: expr.into_py(py), + argument: expr.into_pyobject(py)?.unbind(), } - .into_py(py), + .into_pyobject(py)? + .into_any(), Expr::CustomFunction(func, exprs) => bytecode::ExprCustom { callable: func, - arguments: exprs.into_iter().map(|expr| expr.into_py(py)).collect(), + arguments: exprs + .into_iter() + .map(|expr| expr.into_pyobject(py).unwrap().unbind()) + .collect(), } - .into_py(py), - } + .into_pyobject(py)? + .into_any(), + }) } } @@ -424,7 +443,7 @@ impl ExprParser<'_> { // going to have to acquire the GIL and call the Python object the user gave us right // now. We need to explicitly handle any exceptions that might occur from that. Python::with_gil(|py| { - let args = PyTuple::new_bound( + let args = PyTuple::new( py, exprs.iter().map(|x| { if let Expr::Constant(val) = x { @@ -433,7 +452,7 @@ impl ExprParser<'_> { unreachable!() } }), - ); + )?; match callable.call1(py, args) { Ok(retval) => { match retval.extract::(py) { diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index cfac9e98fce0..fc92e4e6c860 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -337,7 +337,7 @@ pub struct TokenStream { /// backing file or other named resource. pub filename: std::ffi::OsString, strict: bool, - source: Box, + source: Box, line_buffer: Vec, done: bool, line: usize, @@ -352,7 +352,7 @@ impl TokenStream { /// Create and initialise a generic [TokenStream], given a source that implements /// [std::io::BufRead] and a filename (or resource path) that describes its source. fn new( - source: Box, + source: Box, filename: std::ffi::OsString, strict: bool, ) -> Self { diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index ec8c61152bab..032251f85d4b 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -17,7 +17,7 @@ use hashbrown::{HashMap, HashSet}; use num_bigint::BigUint; -use pyo3::prelude::{PyObject, PyResult, Python}; +use pyo3::prelude::*; use crate::bytecode::InternalBytecode; use crate::error::{ @@ -64,7 +64,7 @@ const BUILTIN_CLASSICAL: [&str; 6] = ["cos", "exp", "ln", "sin", "sqrt", "tan"]; /// the second is whether to also define addition to make offsetting the newtype easier. macro_rules! newtype_id { ($id:ident, false) => { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPyObject, IntoPyObjectRef)] pub struct $id(usize); impl $id { @@ -72,12 +72,6 @@ macro_rules! newtype_id { Self(value) } } - - impl pyo3::IntoPy for $id { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } - } }; ($id:ident, true) => { diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index a7be442f67e3..34bfd4304d17 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -9,6 +9,9 @@ license.workspace = true name = "qiskit_qasm3" doctest = false +[lints] +workspace = true + [dependencies] indexmap.workspace = true hashbrown.workspace = true diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 912066df839e..37659187df3e 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -11,7 +11,7 @@ // that they have been altered from the originals. use pyo3::prelude::*; -use pyo3::types::{PySequence, PyString, PyTuple}; +use pyo3::types::{PySequence, PyTuple}; use ahash::RandomState; @@ -145,7 +145,7 @@ impl BuilderState { let gate = self.symbols.gates.get(gate_id).ok_or_else(|| { QASM3ImporterError::new_err(format!("internal error: unknown gate {:?}", gate_id)) })?; - let params = PyTuple::new_bound( + let params = PyTuple::new( py, call.params() .as_ref() @@ -154,7 +154,7 @@ impl BuilderState { .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!( @@ -209,7 +209,7 @@ impl BuilderState { } } } - PyTuple::new_bound(py, qubits.values()) + 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 @@ -320,9 +320,9 @@ impl BuilderState { } } - fn add_qreg>>( - &mut self, - py: Python, + fn add_qreg<'a, T: IntoPyObject<'a>>( + &'a mut self, + py: Python<'a>, ast_symbol: SymbolId, name: T, size: usize, @@ -338,9 +338,9 @@ impl BuilderState { } } - fn add_creg>>( + fn add_creg<'py, T: IntoPyObject<'py>>( &mut self, - py: Python, + py: Python<'py>, ast_symbol: SymbolId, name: T, size: usize, diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 8b46e2bdaf96..5e77b2a8c380 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -11,7 +11,8 @@ // that they have been altered from the originals. use pyo3::prelude::*; -use pyo3::types::{PyList, PyString, PyTuple, PyType}; +use pyo3::types::{PyAny, PyList, PyString, PyTuple, PyType}; +use pyo3::IntoPyObjectExt; use crate::error::QASM3ImporterError; @@ -27,6 +28,7 @@ pub trait PyRegister { macro_rules! register_type { ($name: ident) => { /// Rust-space wrapper around Qiskit `Register` objects. + #[derive(Clone)] pub struct $name { /// The actual register instance. object: Py, @@ -43,18 +45,13 @@ macro_rules! register_type { } } - impl ::pyo3::IntoPy> for $name { - fn into_py(self, _py: Python) -> Py { - self.object - } - } + impl<'py> IntoPyObject<'py> for $name { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; - 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_ref(py) + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.object.bind(py).clone()) } } }; @@ -86,15 +83,22 @@ pub struct PyGate { } impl PyGate { - pub fn new>, S: AsRef>( - py: Python, + pub fn new< + 'py, + T: IntoPyObject<'py, Target = PyAny, Output = Bound<'py, PyAny>>, + S: AsRef, + >( + py: Python<'py>, constructor: T, name: S, num_params: usize, num_qubits: usize, - ) -> Self { + ) -> Self + where + >::Error: std::fmt::Debug, + { Self { - constructor: constructor.into_py(py), + constructor: constructor.into_pyobject(py).unwrap().unbind(), name: name.as_ref().to_owned(), num_params, num_qubits, @@ -102,14 +106,14 @@ impl PyGate { } /// Construct a Python-space instance of the custom gate. - pub fn construct(&self, py: Python, args: A) -> PyResult> + pub fn construct<'py, A>(&'py self, py: Python<'py>, args: A) -> PyResult> where - A: IntoPy>, + A: pyo3::IntoPyObject<'py, Target = PyTuple, Output = Bound<'py, PyTuple>>, { - let args = args.into_py(py); - let received_num_params = args.bind(py).len(); + let args = args.into_pyobject_or_pyerr(py)?; + let received_num_params = args.len(); if received_num_params == self.num_params { - self.constructor.call1(py, args.bind(py)) + self.constructor.call1(py, args) } else { Err(QASM3ImporterError::new_err(format!( "internal error: wrong number of params for {} (got {}, expected {})", @@ -145,19 +149,19 @@ impl PyGate { } fn __repr__<'py>(&self, py: Python<'py>) -> PyResult> { - PyString::new_bound(py, "CustomGate(name={!r}, num_params={}, num_qubits={})").call_method1( + PyString::new(py, "CustomGate(name={!r}, num_params={}, num_qubits={})").call_method1( "format", ( - PyString::new_bound(py, &self.name), + PyString::new(py, &self.name), self.num_params, self.num_qubits, ), ) } - fn __reduce__(&self, py: Python) -> Py { + fn __reduce__(&self, py: Python) -> PyResult> { ( - PyType::new_bound::(py), + PyType::new::(py), ( self.constructor.clone_ref(py), &self.name, @@ -165,7 +169,8 @@ impl PyGate { self.num_qubits, ), ) - .into_py(py) + .into_pyobject(py) + .map(|x| x.unbind()) } } @@ -189,7 +194,7 @@ pub struct PyCircuitModule { impl PyCircuitModule { /// Import the necessary components from `qiskit.circuit`. pub fn import(py: Python) -> PyResult { - let module = PyModule::import_bound(py, "qiskit.circuit")?; + let module = PyModule::import(py, "qiskit.circuit")?; Ok(Self { circuit: module .getattr("QuantumCircuit")? @@ -214,7 +219,7 @@ impl PyCircuitModule { .downcast_into::()? .unbind(), // Measure is a singleton, so just store the object. - measure: module.getattr("Measure")?.call0()?.into_py(py), + measure: module.getattr("Measure")?.call0()?.into_py_any(py)?, }) } @@ -222,13 +227,13 @@ impl PyCircuitModule { self.circuit.call0(py).map(PyCircuit) } - pub fn new_qreg>>( - &self, - py: Python, + pub fn new_qreg<'a, T: IntoPyObject<'a>>( + &'a self, + py: Python<'a>, name: T, size: usize, ) -> PyResult { - let qreg = self.qreg.call1(py, (size, name.into_py(py)))?; + let qreg = self.qreg.call1(py, (size, name))?; Ok(PyQuantumRegister { items: qreg .bind(py) @@ -243,13 +248,13 @@ impl PyCircuitModule { self.qubit.call0(py) } - pub fn new_creg>>( + pub fn new_creg<'py, T: IntoPyObject<'py>>( &self, - py: Python, + py: Python<'py>, name: T, size: usize, ) -> PyResult { - let creg = self.creg.call1(py, (size, name.into_py(py)))?; + let creg = self.creg.call1(py, (size, name))?; Ok(PyClassicalRegister { items: creg .bind(py) @@ -264,24 +269,34 @@ impl PyCircuitModule { self.clbit.call0(py) } - pub fn new_instruction( - &self, - py: Python, + pub fn new_instruction<'a, O, Q, C>( + &'a self, + py: Python<'a>, operation: O, qubits: Q, clbits: C, ) -> PyResult> where - O: IntoPy>, - Q: IntoPy>, - C: IntoPy>, + O: IntoPyObject<'a>, + Q: IntoPyObject<'a>, + C: IntoPyObject<'a>, + >::Output: pyo3::IntoPyObject<'a>, + >::Output: pyo3::IntoPyObject<'a>, { - self.circuit_instruction - .call1(py, (operation, qubits.into_py(py), clbits.into_py(py))) + self.circuit_instruction.call1( + py, + ( + operation, + qubits.into_pyobject_or_pyerr(py)?, + clbits.into_pyobject_or_pyerr(py)?, + ), + ) } pub fn new_barrier(&self, py: Python, num_qubits: usize) -> PyResult> { - self.barrier.call1(py, (num_qubits,)).map(|x| x.into_py(py)) + self.barrier + .call1(py, (num_qubits,)) + .map(|x| x.into_pyobject(py).unwrap().unbind()) } pub fn measure(&self, py: Python) -> Py { @@ -293,6 +308,8 @@ impl PyCircuitModule { /// 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 stabilize. + +#[derive(IntoPyObject, IntoPyObjectRef)] pub struct PyCircuit(Py); impl PyCircuit { @@ -303,7 +320,7 @@ impl PyCircuit { pub fn add_qreg(&self, py: Python, qreg: &PyQuantumRegister) -> PyResult<()> { self.inner(py) - .call_method1("add_register", (qreg.to_object(py),)) + .call_method1("add_register", (qreg.clone().into_pyobject(py)?,)) .map(|_| ()) } @@ -315,25 +332,27 @@ impl PyCircuit { pub fn add_creg(&self, py: Python, creg: &PyClassicalRegister) -> PyResult<()> { self.inner(py) - .call_method1("add_register", (creg.to_object(py),)) + .call_method1("add_register", (creg.clone().into_pyobject(py)?,)) .map(|_| ()) } - pub fn add_clbit>>(&self, py: Python, clbit: T) -> PyResult<()> { + pub fn add_clbit<'a, T: IntoPyObject<'a>>(&'a self, py: Python<'a>, clbit: T) -> PyResult<()> { self.inner(py) .call_method1("add_bits", ((clbit,),)) - .map(|_| ()) + .map(move |_| ()) } - pub fn append>>(&self, py: Python, instruction: T) -> PyResult<()> { + pub fn append<'py, T: IntoPyObject<'py>>( + &'py self, + py: Python<'py>, + instruction: T, + ) -> PyResult<()> + where + >::Output: pyo3::IntoPyObject<'py>, + PyErr: From<>::Error>, + { self.inner(py) - .call_method1("_append", (instruction.into_py(py),)) + .call_method1("_append", (instruction.into_pyobject(py)?,)) .map(|_| ()) } } - -impl ::pyo3::IntoPy> for PyCircuit { - fn into_py(self, py: Python) -> Py { - self.0.clone_ref(py) - } -} diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index 40d2da4af2dc..de39e365b2a5 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -12,6 +12,7 @@ use pyo3::prelude::*; use pyo3::types::PyTuple; +use pyo3::IntoPyObjectExt; use hashbrown::HashMap; @@ -122,10 +123,7 @@ impl<'py> Iterator for BroadcastQubitsIter<'py> { BroadcastItem::Register(bits) => bits[offset].clone_ref(self.py), }; self.offset += 1; - Some(PyTuple::new_bound( - self.py, - self.items.iter().map(to_scalar), - )) + Some(PyTuple::new(self.py, self.items.iter().map(to_scalar)).unwrap()) } fn size_hint(&self) -> (usize, Option) { @@ -156,8 +154,8 @@ impl<'py> Iterator for BroadcastMeasureIter<'_, 'py> { }; self.offset += 1; Some(( - PyTuple::new_bound(self.py, &[to_scalar(self.qarg)]), - PyTuple::new_bound(self.py, &[to_scalar(self.carg)]), + PyTuple::new(self.py, &[to_scalar(self.qarg)]).unwrap(), + PyTuple::new(self.py, &[to_scalar(self.carg)]).unwrap(), )) } @@ -177,7 +175,10 @@ fn broadcast_bits_for_identifier( Ok(BroadcastItem::Bit(bit.clone())) } else if let Some(reg) = registers.get(iden_symbol) { Ok(BroadcastItem::Register( - reg.bit_list(py).iter().map(|obj| obj.into_py(py)).collect(), + reg.bit_list(py) + .iter() + .map(|obj| obj.into_py_any(py).unwrap()) + .collect(), )) } else { Err(QASM3ImporterError::new_err(format!( diff --git a/crates/qasm3/src/lib.rs b/crates/qasm3/src/lib.rs index 14ab9525e5bd..2a4f05566835 100644 --- a/crates/qasm3/src/lib.rs +++ b/crates/qasm3/src/lib.rs @@ -62,7 +62,7 @@ pub fn loads( include_path: Option>, ) -> PyResult { let default_include_path = || -> PyResult> { - let filename: PyBackedStr = py.import_bound("qiskit")?.filename()?.try_into()?; + let filename: PyBackedStr = py.import("qiskit")?.filename()?.try_into()?; Ok(vec![Path::new(filename.deref()) .parent() .unwrap() @@ -83,9 +83,9 @@ pub fn loads( .map(|gate| (gate.name().to_owned(), gate)) .collect(), None => py - .import_bound("qiskit.qasm3")? + .import("qiskit.qasm3")? .getattr("STDGATES_INC_GATES")? - .iter()? + .try_iter()? .map(|obj| { let gate = obj?.extract::()?; Ok((gate.name().to_owned(), gate)) @@ -133,21 +133,20 @@ pub fn load( custom_gates: Option>, include_path: Option>, ) -> PyResult { - let source = if pathlike_or_filelike - .is_instance(&PyModule::import_bound(py, "io")?.getattr("TextIOBase")?)? - { - pathlike_or_filelike - .call_method0("read")? - .extract::()? - } else { - let path = PyModule::import_bound(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)) - })? - }; + 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(py, source, custom_gates, include_path) } diff --git a/docs/source_images/depth.gif b/docs/source_images/depth.gif deleted file mode 100644 index 4437049fc882..000000000000 Binary files a/docs/source_images/depth.gif and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index bdabeda6f146..a89f191fe848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "HalfAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisC04" "HalfAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisV95" "HalfAdder.qft_d00" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisD00" -"FullAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" +"FullAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisDefault" "FullAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" "FullAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisV95" "Multiplier.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisR17" @@ -184,7 +184,6 @@ environment = 'PATH="$PATH:$HOME/.cargo/bin" CARGO_NET_GIT_FETCH_WITH_CLI="true" repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel}" [tool.cibuildwheel.macos] -environment = "MACOSX_DEPLOYMENT_TARGET=10.12" repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel}" [tool.cibuildwheel.windows] diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index f4d533b7a2f2..fc9e7a9af80e 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -64,6 +64,7 @@ .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit @@ -1144,6 +1145,7 @@ def __array__(self, dtype=None, copy=None): drawing: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: :show-source-link: False diff --git a/qiskit/circuit/controlledgate.py b/qiskit/circuit/controlledgate.py index a2038be9bb0c..54d0cd817d28 100644 --- a/qiskit/circuit/controlledgate.py +++ b/qiskit/circuit/controlledgate.py @@ -70,6 +70,7 @@ def __init__( Create a controlled standard gate and apply it to a circuit. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit, QuantumRegister @@ -84,6 +85,7 @@ def __init__( Create a controlled custom gate and apply it to a circuit. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit, QuantumRegister diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 969ccfc5e5a6..7033d7eb758b 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -26,6 +26,7 @@ For example, to append a multi-controlled CNOT: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import MCXGate diff --git a/qiskit/circuit/library/arithmetic/adders/adder.py b/qiskit/circuit/library/arithmetic/adders/adder.py index 7fa3411d0436..435fab109476 100644 --- a/qiskit/circuit/library/arithmetic/adders/adder.py +++ b/qiskit/circuit/library/arithmetic/adders/adder.py @@ -116,6 +116,15 @@ def num_state_qubits(self) -> int: """ return self._num_state_qubits + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_qft_d00 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = adder_qft_d00(self.num_state_qubits, kind="half") + class ModularAdderGate(Gate): r"""Compute the sum modulo :math:`2^n` of two :math:`n`-sized qubit registers. @@ -162,6 +171,15 @@ def num_state_qubits(self) -> int: """ return self._num_state_qubits + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_qft_d00 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = adder_qft_d00(self.num_state_qubits, kind="fixed") + class FullAdderGate(Gate): r"""Compute the sum of two :math:`n`-sized qubit registers, including carry-in and -out bits. @@ -208,3 +226,10 @@ def num_state_qubits(self) -> int: The number of state qubits. """ return self._num_state_qubits + + def _define(self): + """Populates self.definition with a decomposition of this gate.""" + from qiskit.synthesis.arithmetic import adder_ripple_c04 + + # In the case of a full adder, this method does not use any ancilla qubits + self.definition = adder_ripple_c04(self.num_state_qubits, kind="full") diff --git a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py index 4089cc35452a..38f362cf34d3 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py +++ b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py @@ -190,3 +190,12 @@ def num_result_qubits(self) -> int: The number of result qubits. """ return self._num_result_qubits + + def _define(self): + """Populates self.definition with some decomposition of this gate.""" + from qiskit.synthesis.arithmetic import multiplier_qft_r17 + + # This particular decomposition does not use any ancilla qubits. + # Note that the transpiler may choose a different decomposition + # based on the number of ancilla qubits available. + self.definition = multiplier_qft_r17(self.num_state_qubits) diff --git a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py index cc34d3631f59..666f05c0323c 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py +++ b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py @@ -37,6 +37,7 @@ class PiecewiseChebyshev(BlueprintCircuit): Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: import numpy as np diff --git a/qiskit/circuit/library/basis_change/qft.py b/qiskit/circuit/library/basis_change/qft.py index cb14977b4463..42cc43ced17f 100644 --- a/qiskit/circuit/library/basis_change/qft.py +++ b/qiskit/circuit/library/basis_change/qft.py @@ -38,6 +38,7 @@ class QFT(BlueprintCircuit): For 4 qubits, the circuit that implements this transformation is: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import QFT from qiskit.visualization.library import _generate_circuit_library_visualization @@ -48,6 +49,7 @@ class QFT(BlueprintCircuit): The respective circuit diagram is: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import QFT from qiskit.visualization.library import _generate_circuit_library_visualization @@ -64,6 +66,7 @@ class QFT(BlueprintCircuit): on 5 qubits with approximation degree 2 yields (the barriers are dropped in this example): .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import QFT from qiskit.visualization.library import _generate_circuit_library_visualization diff --git a/qiskit/circuit/library/boolean_logic/inner_product.py b/qiskit/circuit/library/boolean_logic/inner_product.py index 84b3807fb56c..757cfe3a48de 100644 --- a/qiskit/circuit/library/boolean_logic/inner_product.py +++ b/qiskit/circuit/library/boolean_logic/inner_product.py @@ -55,6 +55,7 @@ class InnerProduct(QuantumCircuit): Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import InnerProduct from qiskit.visualization.library import _generate_circuit_library_visualization @@ -121,6 +122,7 @@ class InnerProductGate(Gate): Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import InnerProductGate diff --git a/qiskit/circuit/library/boolean_logic/quantum_and.py b/qiskit/circuit/library/boolean_logic/quantum_and.py index 53099aa7be84..9f3033fc3024 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_and.py +++ b/qiskit/circuit/library/boolean_logic/quantum_and.py @@ -32,6 +32,7 @@ class AND(QuantumCircuit): The AND gate without special flags equals the multi-controlled-X gate: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import AND from qiskit.visualization.library import _generate_circuit_library_visualization @@ -43,6 +44,7 @@ class AND(QuantumCircuit): ``[-1, 0, 0, 1, 1]``. .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import AND from qiskit.visualization.library import _generate_circuit_library_visualization @@ -116,6 +118,7 @@ class AndGate(Gate): The AndGate gate without special flags equals the multi-controlled-X gate: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import AndGate @@ -129,6 +132,7 @@ class AndGate(Gate): ``[-1, 0, 0, 1, 1]``. .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import AndGate diff --git a/qiskit/circuit/library/boolean_logic/quantum_or.py b/qiskit/circuit/library/boolean_logic/quantum_or.py index 95b346b34846..5a4f4d06b867 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_or.py +++ b/qiskit/circuit/library/boolean_logic/quantum_or.py @@ -33,6 +33,7 @@ class OR(QuantumCircuit): The OR gate without special flags: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import OR from qiskit.visualization.library import _generate_circuit_library_visualization @@ -44,6 +45,7 @@ class OR(QuantumCircuit): flags ``[-1, 0, 0, 1, 1]``. .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import OR from qiskit.visualization.library import _generate_circuit_library_visualization @@ -117,6 +119,7 @@ class OrGate(Gate): The OrGate gate without special flags: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import OrGate @@ -130,6 +133,7 @@ class OrGate(Gate): flags ``[-1, 0, 0, 1, 1]``. .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import OrGate diff --git a/qiskit/circuit/library/boolean_logic/quantum_xor.py b/qiskit/circuit/library/boolean_logic/quantum_xor.py index 73a2178830bc..9d4f459be4d6 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_xor.py +++ b/qiskit/circuit/library/boolean_logic/quantum_xor.py @@ -53,6 +53,7 @@ def __init__( Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import XOR from qiskit.visualization.library import _generate_circuit_library_visualization @@ -88,6 +89,7 @@ class BitwiseXorGate(Gate): Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import BitwiseXorGate diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index b511edd9513e..5a87f6602bcf 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -125,9 +125,9 @@ def pauli_feature_map( q_1: ┤ H ├────────────┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├───────────── └───┘ └───┘└────────────────────────────────┘└───┘ - >>> from qiskit.circuit.library import EfficientSU2 + >>> from qiskit.circuit.library import efficient_su2 >>> prep = pauli_feature_map(3, reps=3, paulis=["Z", "YY", "ZXZ"]) - >>> wavefunction = EfficientSU2(3) + >>> wavefunction = efficient_su2(3) >>> classifier = prep.compose(wavefunction) >>> classifier.num_parameters 27 @@ -286,8 +286,8 @@ def zz_feature_map( q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ └───┘└─────────────┘└───┘└────────────────────────────────┘└───┘ - >>> from qiskit.circuit.library import EfficientSU2 - >>> classifier = zz_feature_map(3) + EfficientSU2(3) + >>> from qiskit.circuit.library import efficient_su2 + >>> classifier = zz_feature_map(3).compose(efficient_su2(3)) >>> classifier.num_parameters 15 >>> classifier.parameters # 'x' for the data preparation, 'θ' for the SU2 parameters diff --git a/qiskit/circuit/library/fourier_checking.py b/qiskit/circuit/library/fourier_checking.py index b5012b4beeef..cce2dbe0e2a9 100644 --- a/qiskit/circuit/library/fourier_checking.py +++ b/qiskit/circuit/library/fourier_checking.py @@ -70,6 +70,7 @@ def __init__(self, f: Sequence[int], g: Sequence[int]) -> None: Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import FourierChecking from qiskit.visualization.library import _generate_circuit_library_visualization @@ -124,6 +125,7 @@ def fourier_checking(f: Sequence[int], g: Sequence[int]) -> QuantumCircuit: **Reference Circuit:** .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import fourier_checking diff --git a/qiskit/circuit/library/generalized_gates/gms.py b/qiskit/circuit/library/generalized_gates/gms.py index bdf01757abd0..d79b9bf79fcb 100644 --- a/qiskit/circuit/library/generalized_gates/gms.py +++ b/qiskit/circuit/library/generalized_gates/gms.py @@ -45,6 +45,7 @@ class GMS(QuantumCircuit): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GMS from qiskit.visualization.library import _generate_circuit_library_visualization diff --git a/qiskit/circuit/library/generalized_gates/gr.py b/qiskit/circuit/library/generalized_gates/gr.py index 3ababe25f3dd..d4189f7c0bd6 100644 --- a/qiskit/circuit/library/generalized_gates/gr.py +++ b/qiskit/circuit/library/generalized_gates/gr.py @@ -45,6 +45,7 @@ class GR(QuantumCircuit): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GR from qiskit.visualization.library import _generate_circuit_library_visualization @@ -99,6 +100,7 @@ class GRX(GR): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GRX from qiskit.visualization.library import _generate_circuit_library_visualization @@ -147,6 +149,7 @@ class GRY(GR): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GRY from qiskit.visualization.library import _generate_circuit_library_visualization @@ -195,6 +198,7 @@ class GRZ(QuantumCircuit): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GRZ from qiskit.visualization.library import _generate_circuit_library_visualization diff --git a/qiskit/circuit/library/generalized_gates/mcmt.py b/qiskit/circuit/library/generalized_gates/mcmt.py index 496b391d38e8..8e649b6a36cb 100644 --- a/qiskit/circuit/library/generalized_gates/mcmt.py +++ b/qiskit/circuit/library/generalized_gates/mcmt.py @@ -129,6 +129,7 @@ class MCMTVChain(MCMT): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import MCMTVChain, ZGate from qiskit.visualization.library import _generate_circuit_library_visualization diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index bea863f78242..0dd8e0f9df7f 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -54,6 +54,7 @@ def __init__( Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import Permutation A = [2,4,3,0,1] @@ -62,6 +63,7 @@ def __init__( Expanded Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import Permutation from qiskit.visualization.library import _generate_circuit_library_visualization @@ -116,6 +118,7 @@ def __init__( Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.library import PermutationGate @@ -127,6 +130,7 @@ def __init__( Expanded Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.library import PermutationGate @@ -182,7 +186,7 @@ def inverse(self, annotated: bool = False) -> PermutationGate: return PermutationGate(pattern=_inverse_pattern(self.pattern)) - def _qasm2_decomposition(self): + def _qasm_decomposition(self): # pylint: disable=cyclic-import from qiskit.synthesis.permutation import synth_permutation_basic diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index d9f4dbe9cf1f..f443522c8c21 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -34,7 +34,7 @@ from qiskit.exceptions import QiskitError from qiskit._accelerate import uc_gate -from .diagonal import Diagonal +from .diagonal import DiagonalGate _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -276,7 +276,7 @@ def _dec_ucg(self): # Important: the diagonal gate is given in the computational basis of the qubits # q[k-1],...,q[0],q_target (ordered with decreasing significance), # where q[i] are the control qubits and t denotes the target qubit. - diagonal = Diagonal(diag) + diagonal = DiagonalGate(diag) circuit.append(diagonal, [q_target] + q_controls) return circuit, diag diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 40271ed4f594..a1550d1a7acb 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -202,7 +202,7 @@ def control( ) return gate - def _qasm2_decomposition(self): + def _qasm_decomposition(self): """Return an unparameterized version of ourselves, so the OQ2 exporter doesn't choke on the non-standard things in our `params` field.""" out = self.definition.to_gate() diff --git a/qiskit/circuit/library/graph_state.py b/qiskit/circuit/library/graph_state.py index 79c0dfbc864d..9f84f6fcbb4a 100644 --- a/qiskit/circuit/library/graph_state.py +++ b/qiskit/circuit/library/graph_state.py @@ -41,6 +41,7 @@ class GraphState(QuantumCircuit): **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import GraphState from qiskit.visualization.library import _generate_circuit_library_visualization @@ -105,6 +106,7 @@ class GraphStateGate(Gate): **Reference Circuit:** .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit diff --git a/qiskit/circuit/library/grover_operator.py b/qiskit/circuit/library/grover_operator.py index d55a59249a3d..9b0cef56745f 100644 --- a/qiskit/circuit/library/grover_operator.py +++ b/qiskit/circuit/library/grover_operator.py @@ -103,6 +103,7 @@ def grover_operator( We can construct a Grover operator just from the phase oracle: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: @@ -117,8 +118,9 @@ def grover_operator( We can also modify the state preparation: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs oracle = QuantumCircuit(1) oracle.z(0) # the qubit state |1> is the good state @@ -132,8 +134,9 @@ def grover_operator( the oracle: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs oracle = QuantumCircuit(4) oracle.z(3) @@ -149,8 +152,9 @@ def grover_operator( objects: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs from qiskit.quantum_info import Statevector, DensityMatrix, Operator diff --git a/qiskit/circuit/library/hidden_linear_function.py b/qiskit/circuit/library/hidden_linear_function.py index 7acaaa7aa72e..c3bfad38c54d 100644 --- a/qiskit/circuit/library/hidden_linear_function.py +++ b/qiskit/circuit/library/hidden_linear_function.py @@ -55,6 +55,7 @@ class HiddenLinearFunction(QuantumCircuit): **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import HiddenLinearFunction from qiskit.visualization.library import _generate_circuit_library_visualization @@ -123,6 +124,7 @@ def hidden_linear_function(adjacency_matrix: list | np.ndarray) -> QuantumCircui **Reference Circuit:** .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import hidden_linear_function diff --git a/qiskit/circuit/library/iqp.py b/qiskit/circuit/library/iqp.py index 62082fd5c446..ac8624ec7015 100644 --- a/qiskit/circuit/library/iqp.py +++ b/qiskit/circuit/library/iqp.py @@ -38,6 +38,7 @@ class IQP(QuantumCircuit): **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import IQP A = [[6, 5, 3], [5, 4, 5], [3, 5, 1]] @@ -47,6 +48,7 @@ class IQP(QuantumCircuit): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import IQP from qiskit.visualization.library import _generate_circuit_library_visualization @@ -97,6 +99,7 @@ def iqp( **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import iqp A = [[6, 5, 3], [5, 4, 5], [3, 5, 1]] @@ -106,6 +109,7 @@ def iqp( **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import iqp from qiskit.visualization.library import _generate_circuit_library_visualization @@ -154,6 +158,7 @@ def random_iqp( Example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import random_iqp diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index 53698a3e18a6..a24fb4e1d88b 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -71,6 +71,7 @@ def efficient_su2( CX gates, is equivalent to an all-to-all entanglement: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: @@ -84,8 +85,9 @@ def efficient_su2( For example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs circuit = efficient_su2(4, su2_gates=["rx", "y"], entanglement="circular", reps=1) circuit.draw("mpl") @@ -114,10 +116,13 @@ def efficient_su2( if su2_gates is None: su2_gates = ["ry", "rz"] + # Set entanglement_blocks to None when num_qubits == 1 + entanglement_blocks = ["cx"] if num_qubits > 1 else [] + return n_local( num_qubits, su2_gates, - ["cx"], + entanglement_blocks, entanglement, reps, insert_barriers, diff --git a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py index 086bbda31749..386d05f16f7b 100644 --- a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py @@ -61,6 +61,7 @@ def evolved_operator_ansatz( Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import evolved_operator_ansatz @@ -213,6 +214,7 @@ def hamiltonian_variational_ansatz( A single operator will be split into commuting terms automatically: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.quantum_info import SparsePauliOp @@ -226,6 +228,7 @@ def hamiltonian_variational_ansatz( Alternatively, we can directly provide the terms: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.quantum_info import SparsePauliOp diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index 49bfdb07f017..7d4e0c74b057 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -70,8 +70,9 @@ def excitation_preserving( With linear entanglement, this circuit is given by: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs from qiskit.circuit.library import excitation_preserving @@ -83,6 +84,7 @@ def excitation_preserving( in each block: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: @@ -112,17 +114,21 @@ def excitation_preserving( raise ValueError(f"Unsupported mode {mode}, choose one of {supported_modes}") theta = Parameter("θ") - swap = QuantumCircuit(2, name="Interaction") - swap.rxx(theta, 0, 1) - swap.ryy(theta, 0, 1) - if mode == "fsim": - phi = Parameter("φ") - swap.cp(phi, 0, 1) + if num_qubits > 1: + swap = QuantumCircuit(2, name="Interaction") + swap.rxx(theta, 0, 1) + swap.ryy(theta, 0, 1) + if mode == "fsim": + phi = Parameter("φ") + swap.cp(phi, 0, 1) + entanglement_blocks = [swap.to_gate()] + else: + entanglement_blocks = [] return n_local( num_qubits, ["rz"], - [swap.to_gate()], + entanglement_blocks, entanglement, reps, insert_barriers, diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 8c0b4d285086..057290ac98eb 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -148,6 +148,7 @@ def n_local( are made up of a single block per layer: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: @@ -160,8 +161,9 @@ def n_local( Pauli-Y and Pauli-Z rotations in the rotation layer: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs circuit = n_local(3, ["ry", "rz"], "cz", "full", reps=1, insert_barriers=True) circuit.draw("mpl") @@ -169,8 +171,9 @@ def n_local( To omit rotation or entanglement layers, the block can be set to an empty list: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs circuit = n_local(4, [], "cry", reps=2) circuit.draw("mpl") @@ -178,8 +181,9 @@ def n_local( The entanglement can be set explicitly via the ``entanglement`` argument: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs entangler_map = [[0, 1], [2, 0]] circuit = n_local(3, "x", "crx", entangler_map, reps=2) @@ -189,9 +193,10 @@ def n_local( as input the current layer index, and returns the entanglement structure. For example, the following uses different entanglements for odd and even layers: - .. plot: + .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs def entanglement(layer_index): if layer_index % 2 == 0: diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index f0deeb68b287..8d9b64fc37ad 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -59,6 +59,7 @@ def pauli_two_design( Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import pauli_two_design @@ -88,19 +89,20 @@ def pauli_two_design( """ rng = np.random.default_rng(seed) random_block = Block.from_callable(1, 1, lambda params: _random_pauli_builder(params, rng)) - cz_block = Block.from_standard_gate(CZGate._standard_gate) + entanglement_block = [Block.from_standard_gate(CZGate._standard_gate)] if num_qubits > 1 else [] data = py_n_local( num_qubits=num_qubits, reps=reps, rotation_blocks=[random_block], - entanglement_blocks=[cz_block], + entanglement_blocks=entanglement_block, entanglement=["pairwise"], insert_barriers=insert_barriers, skip_final_rotation_layer=False, skip_unentangled_qubits=False, parameter_prefix=parameter_prefix, ) + two_design = QuantumCircuit._from_circuit_data(data) circuit = QuantumCircuit(num_qubits, name=name) @@ -147,6 +149,7 @@ class PauliTwoDesign(TwoLocal): Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.library import PauliTwoDesign diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index b200d7ea7e15..508840391445 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -47,6 +47,7 @@ def qaoa_ansatz( optimization problem: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.quantum_info import SparsePauliOp diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index f1ba10604e9d..96f7001443a7 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -68,6 +68,7 @@ def real_amplitudes( Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: @@ -77,22 +78,25 @@ def real_amplitudes( ansatz.draw("mpl") .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(3, entanglement="full", reps=2) # it is the same unitary as above ansatz.draw("mpl") .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(3, entanglement="linear", reps=2, insert_barriers=True) ansatz.draw("mpl") .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(4, reps=2, entanglement=[[0,3], [0,2]], skip_unentangled_qubits=True) ansatz.draw("mpl") @@ -114,11 +118,13 @@ def real_amplitudes( Returns: A real-amplitudes circuit. """ + # Set entanglement_blocks to None when num_qubits == 1 + entanglement_blocks = ["cx"] if num_qubits > 1 else [] return n_local( num_qubits, ["ry"], - ["cx"], + entanglement_blocks, entanglement, reps, insert_barriers, diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index 6f444bebf0fb..eb31ee679440 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -128,13 +128,14 @@ def unitary_overlap( **Reference Circuit:** .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: import numpy as np - from qiskit.circuit.library import EfficientSU2, unitary_overlap + from qiskit.circuit.library import efficient_su2, unitary_overlap # get two circuit to prepare states of which we compute the overlap - circuit = EfficientSU2(2, reps=1) + circuit = efficient_su2(2, reps=1) unitary1 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) unitary2 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) diff --git a/qiskit/circuit/library/phase_estimation.py b/qiskit/circuit/library/phase_estimation.py index 8ff441eb705e..f5f3abc22c96 100644 --- a/qiskit/circuit/library/phase_estimation.py +++ b/qiskit/circuit/library/phase_estimation.py @@ -76,6 +76,7 @@ def __init__( Reference Circuit: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import PhaseEstimation @@ -131,6 +132,7 @@ def phase_estimation( **Reference Circuit:** .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit diff --git a/qiskit/circuit/library/quantum_volume.py b/qiskit/circuit/library/quantum_volume.py index 644a85d9af13..1ff480e7ef0d 100644 --- a/qiskit/circuit/library/quantum_volume.py +++ b/qiskit/circuit/library/quantum_volume.py @@ -35,6 +35,7 @@ class QuantumVolume(QuantumCircuit): **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import QuantumVolume circuit = QuantumVolume(5, 6, seed=10) @@ -43,6 +44,7 @@ class QuantumVolume(QuantumCircuit): **Expanded Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import QuantumVolume from qiskit.visualization.library import _generate_circuit_library_visualization @@ -150,6 +152,7 @@ def quantum_volume( **Reference Circuit:** .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit.circuit.library import quantum_volume circuit = quantum_volume(5, 6, seed=10) diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index be0e9dd04449..729772723418 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -43,7 +43,6 @@ from .y import YGate, CYGate from .z import ZGate, CZGate, CCZGate from .global_phase import GlobalPhaseGate -from .multi_control_rotation_gates import mcrx, mcry, mcrz def get_standard_gate_name_mapping(): diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py deleted file mode 100644 index 8746e51c48db..000000000000 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ /dev/null @@ -1,405 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# 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. -""" -Multiple-Controlled U3 gate. Not using ancillary qubits. -""" - -from math import pi -import math -from typing import Optional, Union, Tuple, List -import numpy as np - -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, ParameterExpression -from qiskit.circuit.library.standard_gates.x import MCXGate -from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code -from qiskit.circuit.parameterexpression import ParameterValueType -from qiskit.exceptions import QiskitError - - -def _apply_cu(circuit, theta, phi, lam, control, target, use_basis_gates=True): - if use_basis_gates: - # pylint: disable=cyclic-import - # ┌──────────────┐ - # control: ┤ P(λ/2 + φ/2) ├──■──────────────────────────────────■──────────────── - # ├──────────────┤┌─┴─┐┌────────────────────────────┐┌─┴─┐┌────────────┐ - # target: ┤ P(λ/2 - φ/2) ├┤ X ├┤ U(-0.5*0,0,-0.5*λ - 0.5*φ) ├┤ X ├┤ U(0/2,φ,0) ├ - # └──────────────┘└───┘└────────────────────────────┘└───┘└────────────┘ - circuit.p((lam + phi) / 2, [control]) - circuit.p((lam - phi) / 2, [target]) - circuit.cx(control, target) - circuit.u(-theta / 2, 0, -(phi + lam) / 2, [target]) - circuit.cx(control, target) - circuit.u(theta / 2, phi, 0, [target]) - else: - circuit.cu(theta, phi, lam, 0, control, target) - - -def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): - """Apply multi-controlled u gate from ctls to tgt using graycode - pattern with single-step angles theta, phi, lam.""" - - n = len(ctls) - - gray_code = _generate_gray_code(n) - last_pattern = None - - for pattern in gray_code: - if "1" not in pattern: - continue - if last_pattern is None: - last_pattern = pattern - # find left most set bit - lm_pos = list(pattern).index("1") - - # find changed bit - comp = [i != j for i, j in zip(pattern, last_pattern)] - if True in comp: - pos = comp.index(True) - else: - pos = None - if pos is not None: - if pos != lm_pos: - circuit.cx(ctls[pos], ctls[lm_pos]) - else: - indices = [i for i, x in enumerate(pattern) if x == "1"] - for idx in indices[1:]: - circuit.cx(ctls[idx], ctls[lm_pos]) - # check parity and undo rotation - if pattern.count("1") % 2 == 0: - # inverse CU: u(theta, phi, lamb)^dagger = u(-theta, -lam, -phi) - _apply_cu( - circuit, -theta, -lam, -phi, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates - ) - else: - _apply_cu(circuit, theta, phi, lam, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates) - last_pattern = pattern - - -def _mcsu2_real_diagonal( - unitary: np.ndarray, - num_controls: int, - ctrl_state: Optional[str] = None, - use_basis_gates: bool = False, -) -> QuantumCircuit: - """ - Return a multi-controlled SU(2) gate [1]_ with a real main diagonal or secondary diagonal. - - Args: - unitary: SU(2) unitary matrix with one real diagonal. - num_controls: The number of control qubits. - ctrl_state: The state on which the SU(2) operation is controlled. Defaults to all - control qubits being in state 1. - use_basis_gates: If ``True``, use ``[p, u, cx]`` gates to implement the decomposition. - - Returns: - A :class:`.QuantumCircuit` implementing the multi-controlled SU(2) gate. - - Raises: - QiskitError: If the input matrix is invalid. - - References: - - .. [1]: R. Vale et al. Decomposition of Multi-controlled Special Unitary Single-Qubit Gates - `arXiv:2302.06377 (2023) `__ - - """ - # pylint: disable=cyclic-import - from .x import MCXVChain - from qiskit.circuit.library.generalized_gates import UnitaryGate - from qiskit.quantum_info.operators.predicates import is_unitary_matrix - from qiskit.compiler import transpile - - if unitary.shape != (2, 2): - raise QiskitError(f"The unitary must be a 2x2 matrix, but has shape {unitary.shape}.") - - if not is_unitary_matrix(unitary): - raise QiskitError(f"The unitary in must be an unitary matrix, but is {unitary}.") - - if not np.isclose(1.0, np.linalg.det(unitary)): - raise QiskitError("Invalid Value _mcsu2_real_diagonal requires det(unitary) equal to one.") - - is_main_diag_real = np.isclose(unitary[0, 0].imag, 0.0) and np.isclose(unitary[1, 1].imag, 0.0) - is_secondary_diag_real = np.isclose(unitary[0, 1].imag, 0.0) and np.isclose( - unitary[1, 0].imag, 0.0 - ) - - if not is_main_diag_real and not is_secondary_diag_real: - raise QiskitError("The unitary must have one real diagonal.") - - if is_secondary_diag_real: - x = unitary[0, 1] - z = unitary[1, 1] - else: - x = -unitary[0, 1].real - z = unitary[1, 1] - unitary[0, 1].imag * 1.0j - - if np.isclose(z, -1): - s_op = [[1.0, 0.0], [0.0, 1.0j]] - else: - alpha_r = math.sqrt((math.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) - alpha_i = z.imag / ( - 2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0)) - ) - alpha = alpha_r + 1.0j * alpha_i - beta = x / (2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0))) - - # S gate definition - s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) - - s_gate = UnitaryGate(s_op) - - k_1 = math.ceil(num_controls / 2.0) - k_2 = math.floor(num_controls / 2.0) - - ctrl_state_k_1 = None - ctrl_state_k_2 = None - - if ctrl_state is not None: - str_ctrl_state = f"{ctrl_state:0{num_controls}b}" - ctrl_state_k_1 = str_ctrl_state[::-1][:k_1][::-1] - ctrl_state_k_2 = str_ctrl_state[::-1][k_1:][::-1] - - circuit = QuantumCircuit(num_controls + 1, name="MCSU2") - controls = list(range(num_controls)) # control indices, defined for code legibility - target = num_controls # target index, defined for code legibility - - if not is_secondary_diag_real: - circuit.h(target) - - mcx_1 = MCXVChain(num_ctrl_qubits=k_1, dirty_ancillas=True, ctrl_state=ctrl_state_k_1) - circuit.append(mcx_1, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2]) - circuit.append(s_gate, [target]) - - mcx_2 = MCXVChain( - num_ctrl_qubits=k_2, - dirty_ancillas=True, - ctrl_state=ctrl_state_k_2, - # action_only=general_su2_optimization # Requires PR #9687 - ) - circuit.append(mcx_2.inverse(), controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1]) - circuit.append(s_gate.inverse(), [target]) - - mcx_3 = MCXVChain(num_ctrl_qubits=k_1, dirty_ancillas=True, ctrl_state=ctrl_state_k_1) - circuit.append(mcx_3, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2]) - circuit.append(s_gate, [target]) - - mcx_4 = MCXVChain(num_ctrl_qubits=k_2, dirty_ancillas=True, ctrl_state=ctrl_state_k_2) - circuit.append(mcx_4, controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1]) - circuit.append(s_gate.inverse(), [target]) - - if not is_secondary_diag_real: - circuit.h(target) - - if use_basis_gates: - circuit = transpile(circuit, basis_gates=["p", "u", "cx"], qubits_initially_zero=False) - - return circuit - - -def mcrx( - self, - theta: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled X rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcrx gate on. - theta (float): angle theta - q_controls (QuantumRegister or list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .rx import RXGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - all_qubits = control_qubits + target_qubit - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - n_c = len(control_qubits) - if n_c == 1: # cu - _apply_cu( - self, - theta, - -pi / 2, - pi / 2, - control_qubits[0], - target_qubit, - use_basis_gates=use_basis_gates, - ) - elif n_c < 4: - theta_step = theta * (1 / (2 ** (n_c - 1))) - _apply_mcu_graycode( - self, - theta_step, - -pi / 2, - pi / 2, - control_qubits, - target_qubit, - use_basis_gates=use_basis_gates, - ) - else: - if isinstance(theta, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRX with unbound parameter: {theta}.") - - cgate = _mcsu2_real_diagonal( - RXGate(theta).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - - -def mcry( - self, - theta: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - q_ancillae: Optional[Union[QuantumRegister, Tuple[QuantumRegister, int]]] = None, - mode: Optional[str] = None, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled Y rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcry gate on. - theta (float): angle theta - q_controls (list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - q_ancillae (QuantumRegister or tuple(QuantumRegister, int)): The list of ancillary qubits. - mode (string): The implementation mode to use - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .ry import RYGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - ancillary_qubits = [] if q_ancillae is None else self._qbit_argument_conversion(q_ancillae) - all_qubits = control_qubits + target_qubit + ancillary_qubits - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - # auto-select the best mode - if mode is None: - # if enough ancillary qubits are provided, use the 'v-chain' method - additional_vchain = MCXGate.get_num_ancilla_qubits(len(control_qubits), "v-chain") - if len(ancillary_qubits) >= additional_vchain: - mode = "basic" - else: - mode = "noancilla" - - if mode == "basic": - self.ry(theta / 2, q_target) - self.mcx(q_controls, q_target, q_ancillae, mode="v-chain") - self.ry(-theta / 2, q_target) - self.mcx(q_controls, q_target, q_ancillae, mode="v-chain") - elif mode == "noancilla": - n_c = len(control_qubits) - if n_c == 1: # cu - _apply_cu( - self, theta, 0, 0, control_qubits[0], target_qubit, use_basis_gates=use_basis_gates - ) - elif n_c < 4: - theta_step = theta * (1 / (2 ** (n_c - 1))) - _apply_mcu_graycode( - self, - theta_step, - 0, - 0, - control_qubits, - target_qubit, - use_basis_gates=use_basis_gates, - ) - else: - if isinstance(theta, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRY with unbound parameter: {theta}.") - - cgate = _mcsu2_real_diagonal( - RYGate(theta).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - else: - raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") - - -def mcrz( - self, - lam: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled Z rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcrz gate on. - lam (float): angle lambda - q_controls (list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .rz import CRZGate, RZGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - all_qubits = control_qubits + target_qubit - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - n_c = len(control_qubits) - if n_c == 1: - if use_basis_gates: - self.u(0, 0, lam / 2, target_qubit) - self.cx(control_qubits[0], target_qubit) - self.u(0, 0, -lam / 2, target_qubit) - self.cx(control_qubits[0], target_qubit) - else: - self.append(CRZGate(lam), control_qubits + [target_qubit]) - else: - if isinstance(lam, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRZ with unbound parameter: {lam}.") - - cgate = _mcsu2_real_diagonal( - RZGate(lam).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - - -QuantumCircuit.mcrx = mcrx -QuantumCircuit.mcry = mcry -QuantumCircuit.mcrz = mcrz diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index a3ea7167a34f..479d7959f942 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -372,24 +372,16 @@ def _define(self): qc.cp(self.params[0], 0, 1) else: lam = self.params[0] - if type(lam) in [float, int]: - q_controls = list(range(self.num_ctrl_qubits)) - q_target = self.num_ctrl_qubits - new_target = q_target - for k in range(self.num_ctrl_qubits): - # Note: it's better *not* to run transpile recursively - qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=False) - new_target = q_controls.pop() - qc.p(lam / (2**self.num_ctrl_qubits), new_target) - else: # in this case type(lam) is ParameterValueType - from .u3 import _gray_code_chain - - scaled_lam = self.params[0] / (2 ** (self.num_ctrl_qubits - 1)) - bottom_gate = CPhaseGate(scaled_lam) - for operation, qubits, clbits in _gray_code_chain( - qr, self.num_ctrl_qubits, bottom_gate - ): - qc._append(operation, qubits, clbits) + + q_controls = list(range(self.num_ctrl_qubits)) + q_target = self.num_ctrl_qubits + new_target = q_target + for k in range(self.num_ctrl_qubits): + # Note: it's better *not* to run transpile recursively + qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=False) + new_target = q_controls.pop() + qc.p(lam / (2**self.num_ctrl_qubits), new_target) + self.definition = qc def control( diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index cc8a72cd06dd..5c6db8ce7cbc 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -22,7 +22,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -104,11 +104,6 @@ def control( gate = CRXGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 6e6ba7142498..67c27007c1a6 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -21,7 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -103,11 +103,6 @@ def control( gate = CRYGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index e7efeafd24c5..ed0207658441 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -19,7 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -115,11 +115,6 @@ def control( gate = CRZGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/operation.py b/qiskit/circuit/operation.py index 8856222b2663..6a00a2a2811c 100644 --- a/qiskit/circuit/operation.py +++ b/qiskit/circuit/operation.py @@ -34,6 +34,7 @@ class Operation(ABC): Add a Clifford and a Toffoli gate to a :class:`QuantumCircuit`. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 8723445cdef4..1ccfb103412f 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -41,6 +41,7 @@ class Parameter(ParameterExpression): Construct a variable-rotation X gate using circuit parameters. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit, Parameter diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index fe786762c096..feaa0b772c7c 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -340,7 +340,7 @@ def _apply_operation( either a constant or a second ParameterExpression. Args: - operation: One of operator.{add,sub,mul,truediv}. + operation: An operator, such as add, sub, mul, and truediv. other: The second argument to be used with self in operation. reflected: Optional - The default ordering is "self operator other". If reflected is True, this is switched to "other operator self". diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index dee2f3e72276..ab0ad758efb9 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ Literal, overload, ) +from math import pi import numpy as np from qiskit._accelerate.circuit import CircuitData from qiskit._accelerate.circuit import StandardGate @@ -835,6 +836,7 @@ class QuantumCircuit: Consider the following circuit: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit @@ -887,25 +889,6 @@ class QuantumCircuit: assert qc.size() == 19 - A particularly important circuit property is known as the circuit :meth:`depth`. The depth - of a quantum circuit is a measure of how many "layers" of quantum gates, executed in - parallel, it takes to complete the computation defined by the circuit. Because quantum - gates take time to implement, the depth of a circuit roughly corresponds to the amount of - time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit - is one important quantity used to measure if a quantum circuit can be run on a device. - - The depth of a quantum circuit has a mathematical definition as the longest path in a - directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for - experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar - with playing `Tetris `_. Lets see how to compute this - graphically: - - .. image:: /source_images/depth.gif - - We can verify our graphical result using :meth:`QuantumCircuit.depth`:: - - assert qc.depth() == 9 - .. automethod:: count_ops .. automethod:: depth .. automethod:: get_instructions @@ -2169,6 +2152,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu Examples: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit @@ -3425,6 +3409,7 @@ def draw( Example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit @@ -3484,6 +3469,14 @@ def depth( ) -> int: """Return circuit depth (i.e., length of critical path). + The depth of a quantum circuit is a measure of how many + "layers" of quantum gates, executed in parallel, it takes to + complete the computation defined by the circuit. Because + quantum gates take time to implement, the depth of a circuit + roughly corresponds to the amount of time it takes the quantum + computer to execute the circuit. + + .. warning:: This operation is not well defined if the circuit contains control-flow operations. @@ -4327,6 +4320,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc Create a parameterized circuit and assign the parameters in-place. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit, Parameter @@ -4342,6 +4336,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc Bind the values out-of-place by list and get a copy of the original circuit. .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit, ParameterVector @@ -4683,6 +4678,198 @@ def mcp( copy=False, ) + def mcrx( + self, + theta: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled X rotation gate + + Args: + theta: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + use_basis_gates: use p, u, cx basis gates. + """ + # pylint: disable=cyclic-import + from .library.standard_gates.rx import RXGate + from qiskit.synthesis.multi_controlled import ( + _apply_cu, + _apply_mcu_graycode, + _mcsu2_real_diagonal, + ) + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcrx gate needs a single qubit as target.") + all_qubits = control_qubits + target_qubit + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + n_c = len(control_qubits) + if n_c == 1: # cu + _apply_cu( + self, + theta, + -pi / 2, + pi / 2, + control_qubits[0], + target_qubit, + use_basis_gates=use_basis_gates, + ) + elif n_c < 4: + theta_step = theta * (1 / (2 ** (n_c - 1))) + _apply_mcu_graycode( + self, + theta_step, + -pi / 2, + pi / 2, + control_qubits, + target_qubit, + use_basis_gates=use_basis_gates, + ) + else: + cgate = _mcsu2_real_diagonal( + RXGate(theta), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + + def mcry( + self, + theta: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + q_ancillae: QubitSpecifier | Sequence[QubitSpecifier] | None = None, + mode: str | None = None, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled Y rotation gate + + Args: + theta: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + q_ancillae: The list of ancillary qubits. + mode: The implementation mode to use. + use_basis_gates: use p, u, cx basis gates + """ + # pylint: disable=cyclic-import + from .library.standard_gates.ry import RYGate + from .library.standard_gates.x import MCXGate + from qiskit.synthesis.multi_controlled import ( + _apply_cu, + _apply_mcu_graycode, + _mcsu2_real_diagonal, + ) + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcry gate needs a single qubit as target.") + ancillary_qubits = [] if q_ancillae is None else self._qbit_argument_conversion(q_ancillae) + all_qubits = control_qubits + target_qubit + ancillary_qubits + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + # auto-select the best mode + if mode is None: + # if enough ancillary qubits are provided, use the 'v-chain' method + additional_vchain = MCXGate.get_num_ancilla_qubits(len(control_qubits), "v-chain") + if len(ancillary_qubits) >= additional_vchain: + mode = "basic" + else: + mode = "noancilla" + + if mode == "basic": + self.ry(theta / 2, q_target) + self.mcx(list(q_controls), q_target, q_ancillae, mode="v-chain") + self.ry(-theta / 2, q_target) + self.mcx(list(q_controls), q_target, q_ancillae, mode="v-chain") + elif mode == "noancilla": + n_c = len(control_qubits) + if n_c == 1: # cu + _apply_cu( + self, + theta, + 0, + 0, + control_qubits[0], + target_qubit, + use_basis_gates=use_basis_gates, + ) + elif n_c < 4: + theta_step = theta * (1 / (2 ** (n_c - 1))) + _apply_mcu_graycode( + self, + theta_step, + 0, + 0, + control_qubits, + target_qubit, + use_basis_gates=use_basis_gates, + ) + else: + cgate = _mcsu2_real_diagonal( + RYGate(theta), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + else: + raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") + + def mcrz( + self, + lam: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled Z rotation gate + + Args: + lam: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + use_basis_gates: use p, u, cx basis gates. + """ + # pylint: disable=cyclic-import + from .library.standard_gates.rz import CRZGate, RZGate + from qiskit.synthesis.multi_controlled import _mcsu2_real_diagonal + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcrz gate needs a single qubit as target.") + all_qubits = control_qubits + target_qubit + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + n_c = len(control_qubits) + if n_c == 1: + if use_basis_gates: + self.u(0, 0, lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + self.u(0, 0, -lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + else: + self.append(CRZGate(lam), control_qubits + [target_qubit]) + else: + cgate = _mcsu2_real_diagonal( + RZGate(lam), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + def r( self, theta: ParameterValueType, phi: ParameterValueType, qubit: QubitSpecifier ) -> InstructionSet: diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 5caba8ca2ae0..7ce48bed01ee 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -37,6 +37,7 @@ def random_circuit( from the set of standard gates in :mod:`qiskit.circuit.library.standard_gates`. For example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.random import random_circuit @@ -295,6 +296,7 @@ def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): gates from the set of standard gates in :mod:`qiskit.circuit.library.standard_gates`. For example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit.random import random_clifford_circuit diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 27de0a7e1811..5e361503091f 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -191,7 +191,7 @@ def transpile( # pylint: disable=too-many-return-statements This can also be the external plugin name to use for the ``routing`` stage. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"routing"`` for the ``stage_name`` argument. - translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') + translation_method: Name of translation pass (``"translator"`` or ``"synthesis"``) This can also be the external plugin name to use for the ``translation`` stage. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"translation"`` for the ``stage_name`` argument. diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 6ffa74527aa3..5c81dd07a6bf 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -34,6 +34,7 @@ def dag_to_circuit(dag, copy_operations=True): Example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit diff --git a/qiskit/dagcircuit/collect_blocks.py b/qiskit/dagcircuit/collect_blocks.py index 99c51d2e3600..972688ec3eb7 100644 --- a/qiskit/dagcircuit/collect_blocks.py +++ b/qiskit/dagcircuit/collect_blocks.py @@ -139,7 +139,9 @@ def _have_uncollected_nodes(self): """Returns whether there are uncollected (pending) nodes""" return len(self._pending_nodes) > 0 - def collect_matching_block(self, filter_fn: Callable) -> list[DAGOpNode | DAGDepNode]: + def collect_matching_block( + self, filter_fn: Callable, max_block_width: int | None + ) -> list[DAGOpNode | DAGDepNode]: """Iteratively collects the largest block of input nodes (that is, nodes with ``_in_degree`` equal to 0) that match a given filtering function. Examples of this include collecting blocks of swap gates, @@ -150,6 +152,7 @@ def collect_matching_block(self, filter_fn: Callable) -> list[DAGOpNode | DAGDep Returns the block of collected nodes. """ current_block = [] + current_block_qargs = set() unprocessed_pending_nodes = self._pending_nodes self._pending_nodes = [] @@ -158,19 +161,28 @@ def collect_matching_block(self, filter_fn: Callable) -> list[DAGOpNode | DAGDep # - any node that match filter_fn is added to the current_block, # and some of its successors may be moved to unprocessed_pending_nodes. while unprocessed_pending_nodes: - new_pending_nodes = [] - for node in unprocessed_pending_nodes: - if filter_fn(node): - current_block.append(node) - - # update the _in_degree of node's successors - for suc in self._direct_succs(node): - self._in_degree[suc] -= 1 - if self._in_degree[suc] == 0: - new_pending_nodes.append(suc) - else: - self._pending_nodes.append(node) - unprocessed_pending_nodes = new_pending_nodes + node = unprocessed_pending_nodes.pop() + + if max_block_width is not None: + # for efficiency, only update new_qargs when max_block_width is specified + new_qargs = current_block_qargs.copy() + new_qargs.update(node.qargs) + width_within_budget = len(new_qargs) <= max_block_width + else: + new_qargs = set() + width_within_budget = True + + if filter_fn(node) and width_within_budget: + current_block.append(node) + current_block_qargs = new_qargs + + # update the _in_degree of node's successors + for suc in self._direct_succs(node): + self._in_degree[suc] -= 1 + if self._in_degree[suc] == 0: + unprocessed_pending_nodes.append(suc) + else: + self._pending_nodes.append(node) return current_block @@ -181,6 +193,7 @@ def collect_all_matching_blocks( min_block_size=2, split_layers=False, collect_from_back=False, + max_block_width=None, ): """Collects all blocks that match a given filtering function filter_fn. This iteratively finds the largest block that does not match filter_fn, @@ -193,6 +206,8 @@ def collect_all_matching_blocks( qubit subsets. The option ``split_layers`` allows to split collected blocks into layers of non-overlapping instructions. The option ``min_block_size`` specifies the minimum number of gates in the block for the block to be collected. + The option ``max_block_width`` specificies the maximum number of qubits over + which a block can be defined. By default, blocks are collected in the direction from the inputs towards the outputs of the circuit. The option ``collect_from_back`` allows to change this direction, @@ -212,8 +227,8 @@ def not_filter_fn(node): # Iteratively collect non-matching and matching blocks. matching_blocks: list[list[DAGOpNode | DAGDepNode]] = [] while self._have_uncollected_nodes(): - self.collect_matching_block(not_filter_fn) - matching_block = self.collect_matching_block(filter_fn) + self.collect_matching_block(not_filter_fn, max_block_width=None) + matching_block = self.collect_matching_block(filter_fn, max_block_width=max_block_width) if matching_block: matching_blocks.append(matching_block) diff --git a/qiskit/dagcircuit/dagdependency_v2.py b/qiskit/dagcircuit/dagdependency_v2.py index 0bfdd53d40c4..6389ecddad89 100644 --- a/qiskit/dagcircuit/dagdependency_v2.py +++ b/qiskit/dagcircuit/dagdependency_v2.py @@ -13,6 +13,7 @@ """_DAGDependencyV2 class for representing non-commutativity in a circuit. """ +import itertools import math from collections import OrderedDict, defaultdict, namedtuple from typing import Dict, List, Generator, Any @@ -459,7 +460,9 @@ def topological_nodes(self, key=None) -> Generator[DAGOpNode, Any, Any]: """ def _key(x): - return x.sort_key + return ",".join( + f"{self.find_bit(q).index:04d}" for q in itertools.chain(x.qargs, x.cargs) + ) if key is None: key = _key diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index b458b3d49232..e06971b16942 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -314,6 +314,29 @@ def from_samples( array = np.frombuffer(data, dtype=np.uint8, count=len(data)) return BitArray(array.reshape(-1, num_bytes), num_bits) + def to_bool_array(self, order: Literal["big", "little"] = "big") -> NDArray[np.bool_]: + """Convert this :class:`~BitArray` to a boolean array. + + Args: + order: One of ``"big"`` or ``"little"``, respectively indicating whether the most significant + bit or the least significant bit of each bitstring should be placed at ``[..., 0]``. + + Returns: + A NumPy array of bools. + + Raises: + ValueError: If the order is not one of ``"big"`` or ``"little"``. + """ + if order not in ("big", "little"): + raise ValueError( + f"Invalid value for order: '{order}'. Valid values are 'big' and 'little'." + ) + + arr = np.unpackbits(self.array, axis=-1)[..., -self.num_bits :] + if order == "little": + arr = arr[..., ::-1] + return arr.astype(np.bool_) + def get_counts(self, loc: int | tuple[int, ...] | None = None) -> dict[str, int]: """Return a counts dictionary with bitstring keys. diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 21c415d75899..f339589ac79d 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -97,8 +97,25 @@ def __repr__(self): array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50) return prefix + array + suffix - def tolist(self) -> list: - """Convert to a nested list""" + def tolist(self) -> list | ObservableLike: + """Convert to a nested list. + + Similar to Numpy's ``tolist`` method, the level of nesting + depends on the dimension of the observables array. In the + case of dimension 0 the method returns a single observable + (``dict`` in the case of a weighted sum of Paulis) instead of a list. + + Examples:: + Return values for a one-element list vs one element: + + >>> from qiskit.primitives.containers.observables_array import ObservablesArray + >>> oa = ObservablesArray.coerce(["Z"]) + >>> print(type(oa.tolist())) + + >>> oa = ObservablesArray.coerce("Z") + >>> print(type(oa.tolist())) + + """ return self._array.tolist() def __array__(self, dtype=None, copy=None): diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index f96d409631a3..bdfc2c4df721 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -49,6 +49,7 @@ class StatevectorEstimator(BaseEstimatorV2): ``StatevectorEstimator(seed=123)``. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.circuit import Parameter, QuantumCircuit diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 9526793f0e10..a119f8b77e99 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -29,6 +29,7 @@ Here is an example of using a simulated backend for transpilation and running. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 9b2c9208ac72..bc057980cf09 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -39,6 +39,7 @@ .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse @@ -61,6 +62,7 @@ a pulse: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse @@ -90,6 +92,7 @@ automatically lowered to be run as a pulse program: .. plot:: + :alt: Output from the previous code. :include-source: from math import pi @@ -277,6 +280,7 @@ Pulse instructions are available within the builder interface. Here's an example: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse @@ -325,6 +329,7 @@ be used to align all pulses as late as possible in a pulse program. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse @@ -1188,6 +1193,7 @@ def align_equispaced(duration: int | ParameterExpression) -> Generator[None, Non Examples: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse @@ -1242,6 +1248,7 @@ def align_func( Examples: .. plot:: + :alt: Output from the previous code. :include-source: import numpy as np @@ -2116,6 +2123,7 @@ def macro(func: Callable): Examples: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import pulse diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index f18a10805739..560e1d529958 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -330,6 +330,7 @@ class SymbolicPulse(Pulse): without knowing the envelope definition. Now you need to provide the envelope. .. plot:: + :alt: Output from the previous code. :include-source: import sympy diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 7c655dfd432b..fbe9b533ea9a 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -314,8 +314,8 @@ def _define_custom_operation(operation, gates_to_define): # definition, but still continue to return the given object as the call-site object. if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) - elif hasattr(operation, "_qasm2_decomposition"): - new_op = operation._qasm2_decomposition() + elif hasattr(operation, "_qasm_decomposition"): + new_op = operation._qasm_decomposition() parameterized_operation = operation = new_op.copy(name=_escape_name(new_op.name, "gate_")) else: parameterized_operation = operation diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 8f2ef60369bf..1dc6ff3feae8 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -130,6 +130,7 @@ convert it into a :class:`.QuantumCircuit`: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: import qiskit.qasm3 diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 8997770c14e7..b08b2a9719ef 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1180,13 +1180,16 @@ def build_gate_call(self, instruction: CircuitInstruction): This will also push the gate into the symbol table (if required), including recursively defining the gate blocks.""" - ident = self.symbols.get_gate(instruction.operation) + operation = instruction.operation + if hasattr(operation, "_qasm_decomposition"): + operation = operation._qasm_decomposition() + ident = self.symbols.get_gate(operation) if ident is None: - ident = self.define_gate(instruction.operation) + ident = self.define_gate(operation) qubits = [self._lookup_bit(qubit) for qubit in instruction.qubits] parameters = [ ast.StringifyAndPray(self._rebind_scoped_parameters(param)) - for param in instruction.operation.params + for param in operation.params ] if not self.disable_constants: for parameter in parameters: diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index ffaaa5f56142..60922f3d3ec2 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -475,10 +475,10 @@ def open(*args): * - ``u`` - substitution -If the type value is ``f`` ,``c`` or ``i``, the corresponding ``lhs`` or `rhs`` +If the type value is ``f``, ``c``, or ``i``, the corresponding ``lhs`` or ``rhs`` field widths are 128 bits each. In the case of floats, the literal value is encoded as a double with 0 padding, while complex numbers are encoded as real part followed by imaginary part, -taking up 64 bits each. For ``i`, the value is encoded as a 64 bit signed integer with 0 padding +taking up 64 bits each. For ``i``, the value is encoded as a 64 bit signed integer with 0 padding for the full 128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used as it indicates an argument that's not used. For ``p`` the data is the UUID for the :class:`.Parameter` which can be looked up in the symbol map described in the @@ -574,7 +574,7 @@ def open(*args): Changes to EXPR_VAR ~~~~~~~~~~~~~~~~~~~ -The EXPR_VAR variable has gained a new type code and payload, in addition to the pre-existing ones: +The ``EXPR_VAR`` variable has gained a new type code and payload, in addition to the pre-existing ones: =========================== ========= ============================================================ Python class Type code Payload @@ -721,9 +721,9 @@ def open(*args): ====================== ========= ======================================================= ======== Qiskit class Type code Payload Children ====================== ========= ======================================================= ======== -:class:`~.expr.Var` ``x`` One EXPR_VAR. 0 +:class:`~.expr.Var` ``x`` One ``EXPR_VAR``. 0 -:class:`~.expr.Value` ``v`` One EXPR_VALUE. 0 +:class:`~.expr.Value` ``v`` One ``EXPR_VALUE``. 0 :class:`~.expr.Cast` ``c`` One ``_Bool`` that corresponds to the value of 1 ``implicit``. diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index af2a0ed9407f..8e23b6f31a87 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -451,16 +451,14 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: f"Index {ind} is greater than number of qubits" f" in the PauliList ({self.num_qubits})" ) - if len(value) == 1: - # Pad blocks to correct size - value_x = np.vstack(size * [value.x]) - value_z = np.vstack(size * [value.z]) - value_phase = np.vstack(size * [value.phase]) - elif len(value) == size: + if len(value) == size: # Blocks are already correct size value_x = value.x value_z = value.z - value_phase = value.phase + elif len(value) == 1: + # Pad blocks to correct size + value_x = np.vstack(size * [value.x]) + value_z = np.vstack(size * [value.z]) else: # Blocks are incorrect size raise QiskitError( @@ -471,7 +469,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: # Build new array by blocks z = np.hstack([self.z[:, :ind], value_z, self.z[:, ind:]]) x = np.hstack([self.x[:, :ind], value_x, self.x[:, ind:]]) - phase = self.phase + value_phase + phase = self.phase + value.phase return PauliList.from_symplectic(z, x, phase) @@ -1131,6 +1129,8 @@ def from_symplectic( Returns: PauliList: the constructed PauliList. """ + if isinstance(phase, np.ndarray) and np.ndim(phase) > 1: + raise ValueError(f"phase should be at most 1D but has {np.ndim(phase)} dimensions.") base_z, base_x, base_phase = cls._from_array(z, x, phase) return cls(BasePauli(base_z, base_x, base_phase)) diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 34e524348bf8..50b80ec89051 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -167,7 +167,16 @@ def __init__( # move the phase of `pauli_list` to `self._coeffs` phase = pauli_list._phase count_y = pauli_list._count_y() - self._coeffs = np.asarray((-1j) ** (phase - count_y) * coeffs, dtype=coeffs.dtype) + + # Compute exponentiation via integer arithmetic and lookup table to avoid + # floating point errors + exponent = (phase - count_y) % 4 + lookup = np.array([1 + 0j, -1j, -1 + 0j, 1j], dtype=coeffs.dtype) + + vals = lookup[exponent] + self._coeffs = vals * coeffs + + # Update pauli_list phase pauli_list._phase = np.mod(count_y, 4) self._pauli_list = pauli_list diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index b6bc8243b407..dfedb285fffa 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -24,7 +24,7 @@ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.op_shape import OpShape from qiskit.quantum_info.operators.operator import Operator -from qiskit.quantum_info.operators.symplectic import Clifford, Pauli, PauliList +from qiskit.quantum_info.operators.symplectic import Clifford, Pauli, PauliList, SparsePauliOp from qiskit.quantum_info.operators.symplectic.clifford_circuits import _append_x from qiskit.quantum_info.states.quantum_state import QuantumState from qiskit.circuit import QuantumCircuit, Instruction @@ -259,7 +259,34 @@ def evolve( ret._data = self.clifford.compose(other.clifford, qargs=qargs) return ret - def expectation_value(self, oper: Pauli, qargs: None | list = None) -> complex: + def expectation_value(self, oper: Pauli | SparsePauliOp, qargs: None | list = None) -> complex: + """Compute the expectation value of a Pauli or SparsePauliOp operator. + + Args: + oper: A Pauli or SparsePauliOp operator to evaluate the expectation value. + qargs: Subsystems to apply the operator on. + + Returns: + The expectation value. + + Raises: + QiskitError: if oper is not a Pauli or SparsePauliOp operator. + """ + if isinstance(oper, Pauli): + return self._expectation_value_pauli(oper, qargs) + + if isinstance(oper, SparsePauliOp): + return sum( + coeff * self._expectation_value_pauli(Pauli((z, x)), qargs) + for z, x, coeff in zip(oper.paulis.z, oper.paulis.x, oper.coeffs) + ) + + raise QiskitError( + "Operator for expectation value is not a Pauli or SparsePauliOp operator, " + f"but {type(oper)}." + ) + + def _expectation_value_pauli(self, oper: Pauli, qargs: None | list = None) -> complex: """Compute the expectation value of a Pauli operator. Args: diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index 087d942cba43..15519fb2e24d 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -181,6 +181,7 @@ def draw(self, output: str | None = None, **drawer_args): Plot one of the Bell states .. plot:: + :alt: Output from the previous code. :include-source: from numpy import sqrt diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index adea95d4260c..a86ec6681400 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -147,7 +147,7 @@ Multipliers ----------- -.. autofunction:: multiplier_cumulative_h18 +.. autofunction:: multiplier_cumulative_h18 .. autofunction:: multiplier_qft_r17 """ diff --git a/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py b/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py index 676d57ce4342..fa7b87b6c533 100644 --- a/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py +++ b/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py @@ -27,6 +27,7 @@ def multiplier_cumulative_h18( performs a non-modular multiplication on two 3-qubit sized registers is: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.synthesis.arithmetic import multiplier_cumulative_h18 diff --git a/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py b/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py index 550fc44694d0..d69eafe0c427 100644 --- a/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py +++ b/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py @@ -35,6 +35,7 @@ def multiplier_qft_r17( For example, on 3 state qubits, a full multiplier is given by: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.synthesis.arithmetic import multiplier_qft_r17 diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index b314d5ec5e62..980d8cdfefd9 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -191,12 +191,6 @@ def settings(self) -> dict[str, typing.Any]: "preserve_order": self.preserve_order, } - def _normalize_coefficients( - self, paulis: list[str | list[int], float | complex | ParameterExpression] - ) -> list[str | list[int] | ParameterValueType]: - """Ensure the coefficients are real (or parameter expressions).""" - return [[(op, qubits, real_or_fail(coeff)) for op, qubits, coeff in ops] for ops in paulis] - def _custom_evolution(self, num_qubits, pauli_rotations): """Implement the evolution for the non-standard path. diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 13b6b6dd79e6..02e06edae8f7 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -32,7 +32,7 @@ class QDrift(ProductFormula): - r"""The QDrift Trotterization method, which selects each each term in the + r"""The QDrift Trotterization method, which selects each term in the Trotterization randomly, with a probability proportional to its weight. Based on the work of Earl Campbell in Ref. [1]. diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index 209f377351a7..194e986aeb0d 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -18,7 +18,9 @@ import typing from collections.abc import Callable from itertools import chain +import numpy as np +from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg @@ -138,7 +140,7 @@ def expand( .. code-block:: text - ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], 2) + ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], t) Note that the rotation angle contains a factor of 2, such that that evolution of a Pauli :math:`P` over time :math:`t`, which is :math:`e^{itP}`, is represented @@ -157,7 +159,10 @@ def expand( time = evolution.time def to_sparse_list(operator): - paulis = (time * (2 / self.reps) * operator).to_sparse_list() + paulis = [ + (pauli, indices, real_or_fail(coeff) * time * 2 / self.reps) + for pauli, indices, coeff in operator.to_sparse_list() + ] if not self.preserve_order: return reorder_paulis(paulis) @@ -171,9 +176,6 @@ def to_sparse_list(operator): # here would be the location to do so. non_commuting = [[op] for op in to_sparse_list(operators)] - # normalize coefficients, i.e. ensure they are float or ParameterExpression - non_commuting = self._normalize_coefficients(non_commuting) - # we're already done here since Lie Trotter does not do any operator repetition product_formula = self._recurse(self.order, non_commuting) flattened = self.reps * list(chain.from_iterable(product_formula)) @@ -213,3 +215,18 @@ def _recurse(order, grouped_paulis): ], ) return outer + inner + outer + + +def real_or_fail(value, tol=100): + """Return real if close, otherwise fail. Unbound parameters are left unchanged. + + Based on NumPy's ``real_if_close``, i.e. ``tol`` is in terms of machine precision for float. + """ + if isinstance(value, ParameterExpression): + return value + + abstol = tol * np.finfo(float).eps + if abs(np.imag(value)) < abstol: + return np.real(value) + + raise ValueError(f"Encountered complex value {value}, but expected real.") diff --git a/qiskit/synthesis/linear/linear_depth_lnn.py b/qiskit/synthesis/linear/linear_depth_lnn.py index 7c7360915e0f..fdbabd68f025 100644 --- a/qiskit/synthesis/linear/linear_depth_lnn.py +++ b/qiskit/synthesis/linear/linear_depth_lnn.py @@ -25,217 +25,8 @@ import numpy as np from qiskit.exceptions import QiskitError from qiskit.circuit import QuantumCircuit -from qiskit.synthesis.linear.linear_matrix_utils import ( - calc_inverse_matrix, - check_invertible_binary_matrix, - col_op, - row_op, -) - - -def _row_op_update_instructions(cx_instructions, mat, a, b): - # Add a cx gate to the instructions and update the matrix mat - cx_instructions.append((a, b)) - row_op(mat, a, b) - - -def _get_lower_triangular(n, mat, mat_inv): - # Get the instructions for a lower triangular basis change of a matrix mat. - # See the proof of Proposition 7.3 in [1]. - mat = mat.copy() - mat_t = mat.copy() - mat_inv_t = mat_inv.copy() - - cx_instructions_rows = [] - - # Use the instructions in U, which contains only gates of the form cx(a,b) a>b - # to transform the matrix to a permuted lower-triangular matrix. - # The original Matrix is unchanged. - for i in reversed(range(0, n)): - found_first = False - # Find the last "1" in row i, use COL operations to the left in order to - # zero out all other "1"s in that row. - for j in reversed(range(0, n)): - if mat[i, j]: - if not found_first: - found_first = True - first_j = j - else: - # cx_instructions_cols (L instructions) are not needed - col_op(mat, j, first_j) - # Use row operations directed upwards to zero out all "1"s above the remaining "1" in row i - for k in reversed(range(0, i)): - if mat[k, first_j]: - _row_op_update_instructions(cx_instructions_rows, mat, i, k) - - # Apply only U instructions to get the permuted L - for inst in cx_instructions_rows: - row_op(mat_t, inst[0], inst[1]) - col_op(mat_inv_t, inst[0], inst[1]) - return mat_t, mat_inv_t - - -def _get_label_arr(n, mat_t): - # For each row in mat_t, save the column index of the last "1" - label_arr = [] - for i in range(n): - j = 0 - while not mat_t[i, n - 1 - j]: - j += 1 - label_arr.append(j) - return label_arr - - -def _in_linear_combination(label_arr_t, mat_inv_t, row, k): - # Check if "row" is a linear combination of all rows in mat_inv_t not including the row labeled by k - indx_k = label_arr_t[k] - w_needed = np.zeros(len(row), dtype=bool) - # Find the linear combination of mat_t rows which produces "row" - for row_l, _ in enumerate(row): - if row[row_l]: - # mat_inv_t can be thought of as a set of instructions. Row l in mat_inv_t - # indicates which rows from mat_t are necessary to produce the elementary vector e_l - w_needed = w_needed ^ mat_inv_t[row_l] - # If the linear combination requires the row labeled by k - if w_needed[indx_k]: - return False - return True - - -def _get_label_arr_t(n, label_arr): - # Returns label_arr_t = label_arr^(-1) - label_arr_t = [None] * n - for i in range(n): - label_arr_t[label_arr[i]] = i - return label_arr_t - - -def _matrix_to_north_west(n, mat, mat_inv): - # Transform an arbitrary boolean invertible matrix to a north-west triangular matrix - # by Proposition 7.3 in [1] - - # The rows of mat_t hold all w_j vectors (see [1]). mat_inv_t is the inverted matrix of mat_t - mat_t, mat_inv_t = _get_lower_triangular(n, mat, mat_inv) - - # Get all pi(i) labels - label_arr = _get_label_arr(n, mat_t) - - # Save the original labels, exchange index <-> value - label_arr_t = _get_label_arr_t(n, label_arr) - - first_qubit = 0 - empty_layers = 0 - done = False - cx_instructions_rows = [] - - while not done: - # At each iteration the values of i switch between even and odd - at_least_one_needed = False - - for i in range(first_qubit, n - 1, 2): - # "If j < k, we do nothing" (see [1]) - # "If j > k, we swap the two labels, and we also perform a box" (see [1]) - if label_arr[i] > label_arr[i + 1]: - at_least_one_needed = True - # "Let W be the span of all w_l for l!=k" (see [1]) - # " We can perform a box on and that writes a vector in W to wire ." - # (see [1]) - if _in_linear_combination(label_arr_t, mat_inv_t, mat[i + 1], label_arr[i + 1]): - pass - - elif _in_linear_combination( - label_arr_t, mat_inv_t, mat[i + 1] ^ mat[i], label_arr[i + 1] - ): - _row_op_update_instructions(cx_instructions_rows, mat, i, i + 1) - - elif _in_linear_combination(label_arr_t, mat_inv_t, mat[i], label_arr[i + 1]): - _row_op_update_instructions(cx_instructions_rows, mat, i + 1, i) - _row_op_update_instructions(cx_instructions_rows, mat, i, i + 1) - - label_arr[i], label_arr[i + 1] = label_arr[i + 1], label_arr[i] - - if not at_least_one_needed: - empty_layers += 1 - if empty_layers > 1: # if nothing happened twice in a row, then finished. - done = True - else: - empty_layers = 0 - - first_qubit = int(not first_qubit) - - return cx_instructions_rows - - -def _north_west_to_identity(n, mat): - # Transform a north-west triangular matrix to identity in depth 3*n by Proposition 7.4 of [1] - - # At start the labels are in reversed order - label_arr = list(reversed(range(n))) - first_qubit = 0 - empty_layers = 0 - done = False - cx_instructions_rows = [] - - while not done: - at_least_one_needed = False - - for i in range(first_qubit, n - 1, 2): - # Exchange the labels if needed - if label_arr[i] > label_arr[i + 1]: - at_least_one_needed = True - - # If row i has "1" in column i+1, swap and remove the "1" (in depth 2) - # otherwise, only do a swap (in depth 3) - if not mat[i, label_arr[i + 1]]: - # Adding this turns the operation to a SWAP - _row_op_update_instructions(cx_instructions_rows, mat, i + 1, i) - - _row_op_update_instructions(cx_instructions_rows, mat, i, i + 1) - _row_op_update_instructions(cx_instructions_rows, mat, i + 1, i) - - label_arr[i], label_arr[i + 1] = label_arr[i + 1], label_arr[i] - - if not at_least_one_needed: - empty_layers += 1 - if empty_layers > 1: # if nothing happened twice in a row, then finished. - done = True - else: - empty_layers = 0 - - first_qubit = int(not first_qubit) - - return cx_instructions_rows - - -def _optimize_cx_circ_depth_5n_line(mat): - # Optimize CX circuit in depth bounded by 5n for LNN connectivity. - # The algorithm [1] has two steps: - # a) transform the original matrix to a north-west matrix (m2nw), - # b) transform the north-west matrix to identity (nw2id). - # - # A square n-by-n matrix A is called north-west if A[i][j]=0 for all i+j>=n - # For example, the following matrix is north-west: - # [[0, 1, 0, 1] - # [1, 1, 1, 0] - # [0, 1, 0, 0] - # [1, 0, 0, 0]] - - # According to [1] the synthesis is done on the inverse matrix - # so the matrix mat is inverted at this step - mat_inv = mat.astype(bool, copy=True) - mat_cpy = calc_inverse_matrix(mat_inv) - - n = len(mat_cpy) - - # Transform an arbitrary invertible matrix to a north-west triangular matrix - # by Proposition 7.3 of [1] - cx_instructions_rows_m2nw = _matrix_to_north_west(n, mat_cpy, mat_inv) - - # Transform a north-west triangular matrix to identity in depth 3*n - # by Proposition 7.4 of [1] - cx_instructions_rows_nw2id = _north_west_to_identity(n, mat_cpy) - - return cx_instructions_rows_m2nw, cx_instructions_rows_nw2id +from qiskit.synthesis.linear.linear_matrix_utils import check_invertible_binary_matrix +from qiskit._accelerate.synthesis.linear import py_synth_cnot_depth_line_kms as fast_kms def synth_cnot_depth_line_kms(mat: np.ndarray[bool]) -> QuantumCircuit: @@ -264,13 +55,7 @@ def synth_cnot_depth_line_kms(mat: np.ndarray[bool]) -> QuantumCircuit: if not check_invertible_binary_matrix(mat): raise QiskitError("The input matrix is not invertible.") - # Returns the quantum circuit constructed from the instructions - # that we got in _optimize_cx_circ_depth_5n_line - num_qubits = len(mat) - cx_inst = _optimize_cx_circ_depth_5n_line(mat) - qc = QuantumCircuit(num_qubits) - for pair in cx_inst[0]: - qc.cx(pair[0], pair[1]) - for pair in cx_inst[1]: - qc.cx(pair[0], pair[1]) - return qc + circuit_data = fast_kms(mat) + + # construct circuit from the data + return QuantumCircuit._from_circuit_data(circuit_data, add_regs=True) diff --git a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py index c0956ea3bc7c..9773d1d63f84 100644 --- a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py @@ -33,7 +33,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.synthesis.linear.linear_matrix_utils import calc_inverse_matrix -from qiskit.synthesis.linear.linear_depth_lnn import _optimize_cx_circ_depth_5n_line +from qiskit._accelerate.synthesis.linear import py_synth_cnot_lnn_instructions def _initialize_phase_schedule(mat_z): @@ -245,7 +245,7 @@ def synth_cx_cz_depth_line_my(mat_x: np.ndarray, mat_z: np.ndarray) -> QuantumCi n = len(mat_x) mat_x = calc_inverse_matrix(mat_x) - cx_instructions_rows_m2nw, cx_instructions_rows_nw2id = _optimize_cx_circ_depth_5n_line(mat_x) + cx_instructions_rows_m2nw, cx_instructions_rows_nw2id = py_synth_cnot_lnn_instructions(mat_x) # Meanwhile, also build the -CZ- circuit via Phase gate insertions as per Algorithm 2 [2] phase_schedule = _initialize_phase_schedule(mat_z) diff --git a/qiskit/synthesis/multi_controlled/__init__.py b/qiskit/synthesis/multi_controlled/__init__.py index 925793fc5dc1..0fa29553e7ed 100644 --- a/qiskit/synthesis/multi_controlled/__init__.py +++ b/qiskit/synthesis/multi_controlled/__init__.py @@ -22,3 +22,4 @@ synth_c3x, synth_c4x, ) +from .multi_control_rotation_gates import _apply_cu, _apply_mcu_graycode, _mcsu2_real_diagonal diff --git a/qiskit/synthesis/multi_controlled/mcx_synthesis.py b/qiskit/synthesis/multi_controlled/mcx_synthesis.py index 10680f0fee88..221d6adaf736 100644 --- a/qiskit/synthesis/multi_controlled/mcx_synthesis.py +++ b/qiskit/synthesis/multi_controlled/mcx_synthesis.py @@ -53,7 +53,10 @@ def synth_mcx_n_dirty_i15( `arXiv:1501.06911 `_ """ - num_qubits = 2 * num_ctrl_qubits - 1 + if num_ctrl_qubits == 1: + num_qubits = 2 + else: + num_qubits = 2 * num_ctrl_qubits - 1 q = QuantumRegister(num_qubits, name="q") qc = QuantumCircuit(q, name="mcx_vchain") q_controls = q[:num_ctrl_qubits] diff --git a/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py b/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py new file mode 100644 index 000000000000..520bf1722a41 --- /dev/null +++ b/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py @@ -0,0 +1,206 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 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. +""" +Multiple-Controlled U3 gate utilities. Not using ancillary qubits. +""" + +import math +import numpy as np + +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code +from qiskit.exceptions import QiskitError + + +def _apply_cu(circuit, theta, phi, lam, control, target, use_basis_gates=True): + if use_basis_gates: + # ┌──────────────┐ + # control: ┤ P(λ/2 + φ/2) ├──■──────────────────────────────────■──────────────── + # ├──────────────┤┌─┴─┐┌────────────────────────────┐┌─┴─┐┌────────────┐ + # target: ┤ P(λ/2 - φ/2) ├┤ X ├┤ U(-0.5*0,0,-0.5*λ - 0.5*φ) ├┤ X ├┤ U(0/2,φ,0) ├ + # └──────────────┘└───┘└────────────────────────────┘└───┘└────────────┘ + circuit.p((lam + phi) / 2, [control]) + circuit.p((lam - phi) / 2, [target]) + circuit.cx(control, target) + circuit.u(-theta / 2, 0, -(phi + lam) / 2, [target]) + circuit.cx(control, target) + circuit.u(theta / 2, phi, 0, [target]) + else: + circuit.cu(theta, phi, lam, 0, control, target) + + +def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): + """Apply multi-controlled u gate from ctls to tgt using graycode + pattern with single-step angles theta, phi, lam.""" + + n = len(ctls) + + gray_code = _generate_gray_code(n) + last_pattern = None + + for pattern in gray_code: + if "1" not in pattern: + continue + if last_pattern is None: + last_pattern = pattern + # find left most set bit + lm_pos = list(pattern).index("1") + + # find changed bit + comp = [i != j for i, j in zip(pattern, last_pattern)] + if True in comp: + pos = comp.index(True) + else: + pos = None + if pos is not None: + if pos != lm_pos: + circuit.cx(ctls[pos], ctls[lm_pos]) + else: + indices = [i for i, x in enumerate(pattern) if x == "1"] + for idx in indices[1:]: + circuit.cx(ctls[idx], ctls[lm_pos]) + # check parity and undo rotation + if pattern.count("1") % 2 == 0: + # inverse CU: u(theta, phi, lamb)^dagger = u(-theta, -lam, -phi) + _apply_cu( + circuit, -theta, -lam, -phi, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates + ) + else: + _apply_cu(circuit, theta, phi, lam, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates) + last_pattern = pattern + + +def _mcsu2_real_diagonal( + gate: Gate, + num_controls: int, + use_basis_gates: bool = False, +) -> QuantumCircuit: + """ + Return a multi-controlled SU(2) gate [1]_ with a real main diagonal or secondary diagonal. + + Args: + gate: SU(2) Gate whose unitary matrix has one real diagonal. + num_controls: The number of control qubits. + use_basis_gates: If ``True``, use ``[p, u, cx]`` gates to implement the decomposition. + + Returns: + A :class:`.QuantumCircuit` implementing the multi-controlled SU(2) gate. + + Raises: + QiskitError: If the input matrix is invalid. + + References: + + .. [1]: R. Vale et al. Decomposition of Multi-controlled Special Unitary Single-Qubit Gates + `arXiv:2302.06377 (2023) `__ + + """ + # pylint: disable=cyclic-import + from qiskit.circuit.library.standard_gates import RXGate, RYGate, RZGate + from qiskit.circuit.library.generalized_gates import UnitaryGate + from qiskit.quantum_info.operators.predicates import is_unitary_matrix + from qiskit.compiler import transpile + from qiskit.synthesis.multi_controlled import synth_mcx_n_dirty_i15 + + if isinstance(gate, RYGate): + theta = gate.params[0] + s_gate = RYGate(-theta / 4) + is_secondary_diag_real = True + elif isinstance(gate, RZGate): + theta = gate.params[0] + s_gate = RZGate(-theta / 4) + is_secondary_diag_real = True + elif isinstance(gate, RXGate): + theta = gate.params[0] + s_gate = RZGate(-theta / 4) + is_secondary_diag_real = False + + else: + unitary = gate.to_matrix() + if unitary.shape != (2, 2): + raise QiskitError(f"The unitary must be a 2x2 matrix, but has shape {unitary.shape}.") + + if not is_unitary_matrix(unitary): + raise QiskitError(f"The unitary in must be an unitary matrix, but is {unitary}.") + + if not np.isclose(1.0, np.linalg.det(unitary)): + raise QiskitError( + "Invalid Value _mcsu2_real_diagonal requires det(unitary) equal to one." + ) + + is_main_diag_real = np.isclose(unitary[0, 0].imag, 0.0) and np.isclose( + unitary[1, 1].imag, 0.0 + ) + is_secondary_diag_real = np.isclose(unitary[0, 1].imag, 0.0) and np.isclose( + unitary[1, 0].imag, 0.0 + ) + + if not is_main_diag_real and not is_secondary_diag_real: + raise QiskitError("The unitary must have one real diagonal.") + + if is_secondary_diag_real: + x = unitary[0, 1] + z = unitary[1, 1] + else: + x = -unitary[0, 1].real + z = unitary[1, 1] - unitary[0, 1].imag * 1.0j + + if np.isclose(z, -1): + s_op = [[1.0, 0.0], [0.0, 1.0j]] + else: + alpha_r = math.sqrt((math.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) + alpha_i = z.imag / ( + 2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0)) + ) + alpha = alpha_r + 1.0j * alpha_i + beta = x / (2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0))) + + # S gate definition + s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) + s_gate = UnitaryGate(s_op) + + k_1 = math.ceil(num_controls / 2.0) + k_2 = math.floor(num_controls / 2.0) + + circuit = QuantumCircuit(num_controls + 1, name="MCSU2") + controls = list(range(num_controls)) # control indices, defined for code legibility + target = num_controls # target index, defined for code legibility + + if not is_secondary_diag_real: + circuit.h(target) + + mcx_1 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_1) + circuit.compose(mcx_1, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2], inplace=True) + circuit.append(s_gate, [target]) + + # TODO: improve CX count by using action_only=True (based on #9687) + mcx_2 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_2).to_gate() + circuit.compose( + mcx_2.inverse(), controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1], inplace=True + ) + circuit.append(s_gate.inverse(), [target]) + + mcx_3 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_1).to_gate() + circuit.compose(mcx_3, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2], inplace=True) + circuit.append(s_gate, [target]) + + mcx_4 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_2).to_gate() + circuit.compose(mcx_4, controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1], inplace=True) + circuit.append(s_gate.inverse(), [target]) + + if not is_secondary_diag_real: + circuit.h(target) + + if use_basis_gates: + circuit = transpile(circuit, basis_gates=["p", "u", "cx"], qubits_initially_zero=False) + + return circuit diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 7924d4034aef..4b1446915081 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -33,6 +33,8 @@ compilation flow follows the structure given below: .. image:: /source_images/transpiling_core_steps.png + :alt: The transpilation process takes the input circuit, applies the transpilation \ + passes, then produces the output circuit. .. raw:: html @@ -336,7 +338,8 @@ .. plot:: :include-source: - + :alt: Output from the previous code. + from qiskit.circuit import Parameter, Measure from qiskit.transpiler import Target, InstructionProperties from qiskit.circuit.library import UGate, RZGate, RXGate, RYGate, CXGate, CZGate @@ -395,6 +398,7 @@ :meth:`.CouplingMap.build_coupling_map`: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.circuit import Parameter, Measure @@ -450,6 +454,7 @@ target.build_coupling_map('cx').draw() .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.circuit import Parameter, Measure @@ -539,6 +544,7 @@ For example, to run a simple phase estimation circuit: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: import numpy as np @@ -564,6 +570,7 @@ a fake backend with a specified number of qubits for test purposes): .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: :context: reset @@ -621,6 +628,7 @@ ['id', 'rz', 'sx', 'x', 'cx', 'measure', 'delay'] .. plot: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit @@ -641,6 +649,7 @@ this gate must be decomposed. This decomposition is quite costly: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit @@ -666,6 +675,7 @@ manner to the "physical" qubits in an actual quantum device. .. image:: /source_images/mapping.png + :alt: Diagram illustrating how virtual qubits are mapped to physical qubits. By default, qiskit will do this mapping for you. The choice of mapping depends on the @@ -713,6 +723,7 @@ :func:`qiskit.visualization.plot_circuit_layout`: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -731,6 +742,7 @@ - **Layout Using Optimization Level 0** .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -750,6 +762,7 @@ - **Layout Using Optimization Level 3** .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -773,6 +786,7 @@ corresponding value is the label for the physical qubit to map onto: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -820,6 +834,7 @@ ``initial_layout`` in a heavy hex coupling map: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit import QuantumCircuit, transpile @@ -829,6 +844,7 @@ ghz.draw(output='mpl') .. plot:: + :alt: Output from the previous code. :include-source: import matplotlib.pyplot as plt @@ -901,6 +917,7 @@ .. plot:: + :alt: Diagram illustrating the previously described circuit. import matplotlib.pyplot as plt from qiskit import QuantumCircuit, transpile @@ -913,6 +930,7 @@ ghz.draw(output='mpl') .. plot:: + :alt: Output from the previous code. :include-source: import matplotlib.pyplot as plt @@ -959,6 +977,7 @@ circuit such as: .. plot:: + :alt: Diagram illustrating the previously described circuit. from qiskit import QuantumCircuit @@ -970,6 +989,7 @@ we can then call :func:`~.transpile` on it with ``scheduling_method`` set: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -989,6 +1009,7 @@ also look at it with the :func:`.timeline.draw` function: .. plot:: + :alt: Output from circuit timeline drawer. from qiskit.visualization.timeline import draw as timeline_draw diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index bece19671794..4f7e8a98bf1a 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -455,6 +455,7 @@ class TranspileLayout: let the input circuit be: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit, QuantumRegister @@ -470,6 +471,7 @@ class TranspileLayout: Suppose that during the layout stage the transpiler reorders the qubits to be: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit @@ -498,6 +500,7 @@ class TranspileLayout: becomes: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index 1772cbd65544..4466dd492944 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -17,8 +17,10 @@ from collections.abc import Sequence from typing import Type from fnmatch import fnmatch +import warnings from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.utils import control_flow from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.converters.circuit_to_dag import circuit_to_dag @@ -58,7 +60,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: output dag where ``gate`` was expanded. """ # We might use HLS to synthesize objects that do not have a definition - hls = HighLevelSynthesis() if self.apply_synthesis else None + hls = HighLevelSynthesis(qubits_initially_zero=False) if self.apply_synthesis else None # Walk through the DAG and expand each non-basis node for node in dag.op_nodes(): @@ -66,12 +68,18 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if not self._should_decompose(node): continue - if getattr(node.op, "definition", None) is None: + if node.is_control_flow(): + decomposition = control_flow.map_blocks(self.run, node.op) + dag.substitute_node(node, decomposition, inplace=True) + + elif getattr(node.op, "definition", None) is None: # if we try to synthesize, turn the node into a DAGCircuit and run HLS if self.apply_synthesis: + # note that node_as_dag does not include the condition, which will + # be propagated in ``substitute_node_with_dag`` node_as_dag = _node_to_dag(node) synthesized = hls.run(node_as_dag) - dag.substitute_node_with_dag(node, synthesized) + dag.substitute_node_with_dag(node, synthesized, propagate_condition=True) # else: no definition and synthesis not enabled, so we do nothing else: @@ -123,9 +131,21 @@ def _should_decompose(self, node: DAGOpNode) -> bool: def _node_to_dag(node: DAGOpNode) -> DAGCircuit: + # Control flow is already handled separately, however that does not capture + # c_if, which we are treating here. We explicitly ignore the condition attribute, + # which will be handled by ``substitute_node_with_dag``, so we create a copy of the node + # and set the condition to None. Once ``c_if`` is removed for 2.0, this block can go, too. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + if getattr(node.op, "condition", None) is not None: + op = node.op.copy() + op.condition = None + node = DAGOpNode(op, node.qargs, node.cargs) + + # create new dag and apply the operation dag = DAGCircuit() dag.add_qubits(node.qargs) dag.add_clbits(node.cargs) - dag.apply_operation_back(node.op, node.qargs, node.cargs) + return dag diff --git a/qiskit/transpiler/passes/optimization/collect_and_collapse.py b/qiskit/transpiler/passes/optimization/collect_and_collapse.py index d8644f5831d9..94b096484cc8 100644 --- a/qiskit/transpiler/passes/optimization/collect_and_collapse.py +++ b/qiskit/transpiler/passes/optimization/collect_and_collapse.py @@ -95,6 +95,7 @@ def collect_using_filter_function( min_block_size, split_layers=False, collect_from_back=False, + max_block_width=None, ): """Corresponds to an important block collection strategy that greedily collects maximal blocks of nodes matching a given ``filter_function``. @@ -105,6 +106,7 @@ def collect_using_filter_function( min_block_size=min_block_size, split_layers=split_layers, collect_from_back=collect_from_back, + max_block_width=max_block_width, ) diff --git a/qiskit/transpiler/passes/optimization/collect_cliffords.py b/qiskit/transpiler/passes/optimization/collect_cliffords.py index 8b26d04045c1..b5183963896b 100644 --- a/qiskit/transpiler/passes/optimization/collect_cliffords.py +++ b/qiskit/transpiler/passes/optimization/collect_cliffords.py @@ -39,6 +39,7 @@ def __init__( split_layers=False, collect_from_back=False, matrix_based=False, + max_block_width=None, ): """CollectCliffords initializer. @@ -55,6 +56,9 @@ def __init__( from the end of the circuit. matrix_based (bool): specifies whether to collect unitary gates which are Clifford gates only for certain parameters (based on their unitary matrix). + max_block_width (int | None): specifies the maximum width of the block + (that is, the number of qubits over which the block is defined) + for the block to be collected. """ collect_function = partial( @@ -64,6 +68,7 @@ def __init__( min_block_size=min_block_size, split_layers=split_layers, collect_from_back=collect_from_back, + max_block_width=max_block_width, ) collapse_function = partial(collapse_to_operation, collapse_function=_collapse_to_clifford) diff --git a/qiskit/transpiler/passes/optimization/collect_linear_functions.py b/qiskit/transpiler/passes/optimization/collect_linear_functions.py index 25a66e2bf9dc..a358874a122d 100644 --- a/qiskit/transpiler/passes/optimization/collect_linear_functions.py +++ b/qiskit/transpiler/passes/optimization/collect_linear_functions.py @@ -34,6 +34,7 @@ def __init__( min_block_size=2, split_layers=False, collect_from_back=False, + max_block_width=None, ): """CollectLinearFunctions initializer. @@ -48,6 +49,9 @@ def __init__( over disjoint qubit subsets. collect_from_back (bool): specifies if blocks should be collected started from the end of the circuit. + max_block_width (int | None): specifies the maximum width of the block + (that is, the number of qubits over which the block is defined) + for the block to be collected. """ collect_function = partial( @@ -57,6 +61,7 @@ def __init__( min_block_size=min_block_size, split_layers=split_layers, collect_from_back=collect_from_back, + max_block_width=max_block_width, ) collapse_function = partial( collapse_to_operation, collapse_function=_collapse_to_linear_function diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index 34d51a17fe4a..c05d5f023b44 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -33,13 +33,18 @@ class CollectMultiQBlocks(AnalysisPass): Some gates may not be present in any block (e.g. if the number of operands is greater than ``max_block_size``) + By default, blocks are collected in the direction from the inputs towards the + outputs of the DAG. The option ``collect_from_back`` allows to change this + direction, that is to collect blocks from the outputs towards the inputs. + Note that the blocks are still reported in a valid topological order. + A Disjoint Set Union data structure (DSU) is used to maintain blocks as gates are processed. This data structure points each qubit to a set at all times and the sets correspond to current blocks. These change over time and the data structure allows these changes to be done quickly. """ - def __init__(self, max_block_size=2): + def __init__(self, max_block_size=2, collect_from_back=False): super().__init__() self.parent = {} # parent array for the union @@ -49,6 +54,7 @@ def __init__(self, max_block_size=2): self.gate_groups = {} # current gate lists for the groups self.max_block_size = max_block_size # maximum block size + self.collect_from_back = collect_from_back # backward collection def find_set(self, index): """DSU function for finding root of set of items @@ -127,6 +133,10 @@ def collect_key(x): op_nodes = dag.topological_op_nodes(key=collect_key) + # When collecting from the back, the order of nodes is reversed + if self.collect_from_back: + op_nodes = reversed(list(op_nodes)) + for nd in op_nodes: can_process = True makes_too_big = False @@ -222,6 +232,11 @@ def collect_key(x): if item == index and len(self.gate_groups[index]) != 0: block_list.append(self.gate_groups[index][:]) + # When collecting from the back, both the order of the blocks + # and the order of nodes in each block should be reversed. + if self.collect_from_back: + block_list = [block[::-1] for block in block_list[::-1]] + self.property_set["block_list"] = block_list return dag diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index 259d79ba636b..b3c55929d636 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -11,12 +11,20 @@ # that they have been altered from the originals. """Search for star connectivity patterns and replace them with.""" +import itertools from typing import Iterable, Union, Optional, List, Tuple from math import floor, log10 from qiskit.circuit import SwitchCaseOp, Clbit, ClassicalRegister, Barrier from qiskit.circuit.controlflow import condition_resources, node_resources -from qiskit.dagcircuit import DAGOpNode, DAGDepNode, DAGDependency, DAGCircuit +from qiskit.dagcircuit import ( + DAGOpNode, + DAGDepNode, + DAGDependency, + DAGCircuit, + DAGOutNode, + DAGInNode, +) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes.routing.sabre_swap import _build_sabre_dag, _apply_sabre_result @@ -89,6 +97,7 @@ class StarPreRouting(TransformationPass): For example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit.circuit import QuantumCircuit @@ -330,7 +339,14 @@ def star_preroute(self, dag, blocks, processing_order): } def tie_breaker_key(node): - return processing_order_index_map.get(node, node.sort_key) + processing_order = processing_order_index_map.get(node, None) + if processing_order is not None: + return processing_order + if isinstance(node, (DAGInNode, DAGOutNode)): + return str(node.wire) + return ",".join( + f"{dag.find_bit(q).index:04d}" for q in itertools.chain(node.qargs, node.cargs) + ) rust_processing_order = _extract_nodes(dag.topological_op_nodes(key=tie_breaker_key), dag) diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 7ae27ddf03d0..08371c488422 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -47,6 +47,7 @@ class DynamicalDecoupling(TransformationPass): (including global phase). .. plot:: + :alt: Output from the previous code. :include-source: import numpy as np diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 0a692a85621b..6159253e93e7 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -53,6 +53,7 @@ class PadDynamicalDecoupling(BasePadding): (including global phase). .. plot:: + :alt: Output from the previous code. :include-source: import numpy as np diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index a609b11f0fef..11f9958f076c 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -300,6 +300,10 @@ - :class:`.ModularAdderSynthesisD00` - 0 - a QFT-based adder + * - ``"default"`` + - :class:`~.ModularAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ @@ -307,6 +311,7 @@ ModularAdderSynthesisC04 ModularAdderSynthesisD00 ModularAdderSynthesisV95 + ModularAdderSynthesisDefault Half Adder Synthesis '''''''''''''''''''' @@ -330,6 +335,10 @@ - :class:`.HalfAdderSynthesisD00` - 0 - a QFT-based adder + * - ``"default"`` + - :class:`~.HalfAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ @@ -337,6 +346,7 @@ HalfAdderSynthesisC04 HalfAdderSynthesisD00 HalfAdderSynthesisV95 + HalfAdderSynthesisDefault Full Adder Synthesis '''''''''''''''''''' @@ -356,12 +366,17 @@ - :class:`.FullAdderSynthesisV95` - :math:`n-1`, for :math:`n`-bit numbers - a ripple-carry adder + * - ``"default"`` + - :class:`~.FullAdderSynthesisDefault` + - any + - chooses the best algorithm based on the ancillas available .. autosummary:: :toctree: ../stubs/ FullAdderSynthesisC04 FullAdderSynthesisV95 + FullAdderSynthesisDefault Multiplier Synthesis @@ -405,12 +420,13 @@ C3XGate, C4XGate, PauliEvolutionGate, + PermutationGate, + MCMTGate, ModularAdderGate, HalfAdderGate, FullAdderGate, MultiplierGate, ) -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.coupling import CouplingMap from qiskit.synthesis.clifford import ( @@ -452,6 +468,7 @@ multiplier_qft_r17, multiplier_cumulative_h18, ) +from qiskit.quantum_info.operators import Clifford from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from .plugin import HighLevelSynthesisPlugin @@ -469,6 +486,9 @@ class DefaultSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_full(high_level_object) return decomposition @@ -482,6 +502,9 @@ class AGSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_ag(high_level_object) return decomposition @@ -498,10 +521,14 @@ class BMSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + if high_level_object.num_qubits <= 3: decomposition = synth_clifford_bm(high_level_object) else: decomposition = None + return decomposition @@ -515,6 +542,9 @@ class GreedySynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_greedy(high_level_object) return decomposition @@ -529,6 +559,9 @@ class LayerSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_layers(high_level_object) return decomposition @@ -544,6 +577,9 @@ class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_depth_lnn(high_level_object) return decomposition @@ -557,6 +593,9 @@ class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" + if not isinstance(high_level_object, LinearFunction): + return None + decomposition = synth_cnot_count_full_pmh(high_level_object.linear) return decomposition @@ -580,11 +619,8 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" - if not isinstance(high_level_object, LinearFunction): - raise TranspilerError( - "PMHSynthesisLinearFunction only accepts objects of type LinearFunction" - ) + return None use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) @@ -631,11 +667,8 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" - if not isinstance(high_level_object, LinearFunction): - raise TranspilerError( - "PMHSynthesisLinearFunction only accepts objects of type LinearFunction" - ) + return None section_size = options.get("section_size", 2) use_inverted = options.get("use_inverted", False) @@ -667,6 +700,9 @@ class KMSSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) return decomposition @@ -680,6 +716,9 @@ class BasicSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_basic(high_level_object.pattern) return decomposition @@ -693,6 +732,9 @@ class ACGSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_acg(high_level_object.pattern) return decomposition @@ -843,6 +885,9 @@ class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + trials = options.get("trials", 5) seed = options.get("seed", 0) parallel_threshold = options.get("parallel_threshold", 50) @@ -1141,6 +1186,9 @@ class MCMTSynthesisDefault(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): # first try to use the V-chain synthesis if enough auxiliary qubits are available + if not isinstance(high_level_object, MCMTGate): + return None + if ( decomposition := MCMTSynthesisVChain().run( high_level_object, coupling_map, target, qubits, **options @@ -1155,6 +1203,9 @@ class MCMTSynthesisNoAux(HighLevelSynthesisPlugin): """A V-chain based synthesis for ``MCMTGate``.""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MCMTGate): + return None + base_gate = high_level_object.base_gate ctrl_state = options.get("ctrl_state", None) @@ -1180,6 +1231,9 @@ class MCMTSynthesisVChain(HighLevelSynthesisPlugin): """A V-chain based synthesis for ``MCMTGate``.""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MCMTGate): + return None + if options.get("num_clean_ancillas", 0) < high_level_object.num_ctrl_qubits - 1: return None # insufficient number of auxiliary qubits @@ -1212,10 +1266,26 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** if not isinstance(high_level_object, ModularAdderGate): return None - if options.get("num_clean_ancillas", 0) >= 1: - return adder_ripple_c04(high_level_object.num_state_qubits, kind="fixed") + # For up to 5 qubits, the QFT-based adder is best + if high_level_object.num_state_qubits <= 5: + decomposition = ModularAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition - return adder_qft_d00(high_level_object.num_state_qubits, kind="fixed") + # Otherwise, the following decomposition is best (if there are enough ancillas) + if ( + decomposition := ModularAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + # Otherwise, use the QFT-adder again + return ModularAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) class ModularAdderSynthesisC04(HighLevelSynthesisPlugin): @@ -1264,8 +1334,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="fixed") @@ -1309,10 +1379,26 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** if not isinstance(high_level_object, HalfAdderGate): return None - if options.get("num_clean_ancillas", 0) >= 1: - return adder_ripple_c04(high_level_object.num_state_qubits, kind="half") + # For up to 3 qubits, ripple_v95 is better (if there are enough ancilla qubits) + if high_level_object.num_state_qubits <= 3: + decomposition = HalfAdderSynthesisV95().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition - return adder_qft_d00(high_level_object.num_state_qubits, kind="half") + # The next best option is to use ripple_c04 (if there are enough ancilla qubits) + if ( + decomposition := HalfAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + # The QFT-based adder does not require ancilla qubits and should always succeed + return HalfAdderSynthesisD00().run( + high_level_object, coupling_map, target, qubits, **options + ) class HalfAdderSynthesisC04(HighLevelSynthesisPlugin): @@ -1360,8 +1446,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="half") @@ -1381,18 +1467,38 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** return adder_qft_d00(high_level_object.num_state_qubits, kind="half") -class FullAdderSynthesisC04(HighLevelSynthesisPlugin): +class FullAdderSynthesisDefault(HighLevelSynthesisPlugin): """A ripple-carry adder with a carry-in and a carry-out bit. - This plugin name is:``FullAdder.ripple_c04`` which can be used as the key on + This plugin name is:``FullAdder.default`` which can be used as the key on an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ - This plugin requires at least one clean auxiliary qubit. + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, FullAdderGate): + return None - The plugin supports the following plugin-specific options: + # FullAdderSynthesisC04 requires no ancilla qubits and returns better results + # than FullAdderSynthesisV95 in all cases except for n=1. + if high_level_object.num_state_qubits == 1: + decomposition = FullAdderSynthesisV95().run( + high_level_object, coupling_map, target, qubits, **options + ) + if decomposition is not None: + return decomposition - * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + return FullAdderSynthesisC04().run( + high_level_object, coupling_map, target, qubits, **options + ) + +class FullAdderSynthesisC04(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-in and a carry-out bit. + + This plugin name is:``FullAdder.ripple_c04`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + This plugin requires no auxiliary qubits. """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): @@ -1409,7 +1515,7 @@ class FullAdderSynthesisV95(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. For an adder on 2 registers with :math:`n` qubits each, this plugin requires at - least :math:`n-1` clean auxiliary qubit. + least :math:`n-1` clean auxiliary qubits. The plugin supports the following plugin-specific options: @@ -1422,8 +1528,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** num_state_qubits = high_level_object.num_state_qubits - # for more than 1 state qubit, we need an ancilla - if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + # The synthesis method needs n-1 clean ancilla qubits + if num_state_qubits - 1 > options.get("num_clean_ancillas", 0): return None return adder_ripple_v95(num_state_qubits, kind="full") diff --git a/qiskit/transpiler/passes/utils/filter_op_nodes.py b/qiskit/transpiler/passes/utils/filter_op_nodes.py index 75b824332aee..4da15d45385a 100644 --- a/qiskit/transpiler/passes/utils/filter_op_nodes.py +++ b/qiskit/transpiler/passes/utils/filter_op_nodes.py @@ -35,9 +35,10 @@ class FilterOpNodes(TransformationPass): Example: - Filter out operations that are labelled ``"foo"`` + Filter out operations that are labeled ``"foo"`` .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/transpiler/passes/utils/remove_barriers.py b/qiskit/transpiler/passes/utils/remove_barriers.py index 53b500ff31c6..cf374435c8e7 100644 --- a/qiskit/transpiler/passes/utils/remove_barriers.py +++ b/qiskit/transpiler/passes/utils/remove_barriers.py @@ -25,6 +25,7 @@ class RemoveBarriers(TransformationPass): Example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index cad512c07a52..fbbd5a6c202a 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -23,11 +23,11 @@ transformations as part of Qiskit's compiler inside the :func:`~.transpile` function at the different optimization levels, but can also be used in a standalone manner. -The functionality here is divided into two parts, the first includes the -functions used generate the entire pass manager which is used by -:func:`~.transpile` (:ref:`preset_pass_manager_generators`) and the -second includes functions which are used to build (either entirely or in -part) the stages which the preset pass managers are composed of +The functionality here is divided into two parts. The first includes the +functions used to generate the entire pass manager, which is used by +:func:`~.transpile` (:ref:`preset_pass_manager_generators`), and the +second includes functions that are used to build (either entirely or in +part) the stages that comprise the preset pass managers (:ref:`stage_generators`). .. _preset_pass_manager_generators: diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 85b0c05d5cae..1e5ca4519864 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -238,7 +238,6 @@ class BasicSwapPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map backend_properties = pass_manager_config.backend_properties @@ -257,7 +256,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -268,7 +267,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -280,7 +279,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -291,7 +290,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -324,7 +323,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -335,7 +334,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -347,7 +346,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -358,7 +357,6 @@ class LookaheadSwapPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map coupling_map_routing = target @@ -376,7 +374,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -388,7 +386,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -401,7 +399,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -413,7 +411,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -448,7 +446,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 1: @@ -466,7 +464,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, check_trivial=True, use_barrier_before_measurement=True, ) @@ -486,7 +484,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) if optimization_level == 3: @@ -504,7 +502,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana vf2_call_limit=vf2_call_limit, vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") @@ -515,7 +513,6 @@ class NoneRoutingPassManager(PassManagerStagePlugin): def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: """Build routing stage PassManager.""" - seed_transpiler = pass_manager_config.seed_transpiler target = pass_manager_config.target coupling_map = pass_manager_config.coupling_map routing_pass = Error( @@ -527,7 +524,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana routing_pass, target, coupling_map=coupling_map, - seed_transpiler=seed_transpiler, + seed_transpiler=-1, use_barrier_before_measurement=True, ) @@ -793,7 +790,7 @@ def _swap_mapped(property_set): ) choose_layout_1 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, @@ -826,7 +823,7 @@ def _swap_mapped(property_set): elif optimization_level == 2: choose_layout_0 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(5e6), # Set call limit to ~10s with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, @@ -861,7 +858,7 @@ def _swap_mapped(property_set): elif optimization_level == 3: choose_layout_0 = VF2Layout( coupling_map=pass_manager_config.coupling_map, - seed=pass_manager_config.seed_transpiler, + seed=-1, call_limit=int(3e7), # Set call limit to ~60s with rustworkx 0.10.2 properties=pass_manager_config.backend_properties, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index c9bcc9a7904c..431089f657dd 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -281,7 +281,7 @@ def generate_routing_passmanager( coupling_map=None, vf2_call_limit=None, backend_properties=None, - seed_transpiler=None, + seed_transpiler=-1, check_trivial=False, use_barrier_before_measurement=True, vf2_max_trials=None, @@ -300,7 +300,10 @@ def generate_routing_passmanager( backend_properties (BackendProperties): Properties of a backend to synthesize for (e.g. gate fidelities). seed_transpiler (int): Sets random seed for the stochastic parts of - the transpiler. + the transpiler. This is currently only used for :class:`.VF2PostLayout` and the + default value of ``-1`` is strongly recommended (which is no randomization). + If a value of ``None`` is provided this will seed from system + entropy. check_trivial (bool): If set to true this will condition running the :class:`~.VF2PostLayout` pass after routing on whether a trivial layout was tried and was found to not be perfect. This is only @@ -358,7 +361,7 @@ def _swap_condition(property_set): target, coupling_map, backend_properties, - seed_transpiler, + seed=seed_transpiler, call_limit=vf2_call_limit, max_trials=vf2_max_trials, strict_direction=False, diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index ac3b2b618cc9..920460a2b5f5 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -79,7 +79,7 @@ ``second_final_layout.compose(first_final_layout, dag.qubits)``). * - ``translation`` - ``qiskit.transpiler.translation`` - - ``translator``, ``synthesis``, ``unroller`` + - ``translator``, ``synthesis`` - The output of this stage is expected to have every operation be a native instruction on the target backend. * - ``optimization`` diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index fb15a8039d3c..51e1967473f3 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -725,7 +725,7 @@ def operation_names(self): @property def instructions(self): - """Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))`` + """Get the list of tuples (:class:`~qiskit.circuit.Instruction`, (qargs)) for the target For globally defined variable width operations the tuple will be of the form diff --git a/qiskit/visualization/__init__.py b/qiskit/visualization/__init__.py index 3fdaa9ffaa53..6d1b4e86622d 100644 --- a/qiskit/visualization/__init__.py +++ b/qiskit/visualization/__init__.py @@ -47,6 +47,7 @@ The following example demonstrates the common usage of these arguments: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_histogram @@ -60,6 +61,7 @@ You can specify ``legend``, ``title``, ``figsize`` and ``color`` by passing to the kwargs. .. plot:: + :alt: Output from the previous code. :include-source: :context: reset @@ -103,6 +105,7 @@ Here is an example of using :func:`plot_histogram` to visualize measurement outcome counts: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_histogram @@ -144,6 +147,7 @@ Here is an example of using :func:`plot_state_city` to visualize a quantum state: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_state_city @@ -157,6 +161,7 @@ obtained from a :class:`~qiskit.circuit.QuantumCircuit`: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit @@ -172,6 +177,7 @@ plot_state_city(state) .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index 82c3307e36c2..dc01e69dc41d 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -189,6 +189,7 @@ def circuit_drawer( Example: .. plot:: + :alt: Circuit diagram output by the previous code. :include-source: from qiskit import QuantumCircuit diff --git a/qiskit/visualization/counts_visualization.py b/qiskit/visualization/counts_visualization.py index 6d93ff6bfac0..b20c0b20e24e 100644 --- a/qiskit/visualization/counts_visualization.py +++ b/qiskit/visualization/counts_visualization.py @@ -111,6 +111,7 @@ def plot_histogram( Examples: .. plot:: + :alt: Output from the previous code. :include-source: # Plot two counts in the same figure with legends and colors specified. @@ -215,6 +216,7 @@ def plot_distribution( Examples: .. plot:: + :alt: Output from the previous code. :include-source: # Plot two counts in the same figure with legends and colors specified. diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index 8f8b8fc89097..c80a753174c1 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -100,6 +100,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): Example: .. plot:: :include-source: + :nofigs: from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.dagcircuit import DAGCircuit diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index eb20623f0c8c..06dc3c66e425 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -75,13 +75,14 @@ def plot_gate_map( Figure: A Matplotlib figure instance. Raises: - QiskitError: if tried to pass a simulator, or if the backend is None, + QiskitError: If you tried to pass a simulator or the backend is None, but one of num_qubits, mpl_data, or cmap is None. - MissingOptionalLibraryError: if matplotlib not installed. + MissingOptionalLibraryError: If matplotlib not installed. Example: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.providers.fake_provider import GenericBackendV2 @@ -997,11 +998,12 @@ def plot_coupling_map( Raises: MissingOptionalLibraryError: If matplotlib or graphviz is not installed. - QiskitError: If length of qubit labels does not match number of qubits. + QiskitError: If the length of qubit labels does not match the number of qubits. Example: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_coupling_map @@ -1168,6 +1170,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None Example: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile @@ -1270,6 +1273,7 @@ def plot_error_map(backend, figsize=(15, 12), show_title=True, qubit_coordinates Example: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_error_map diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py index 9d781bd6e457..b879c73224dd 100644 --- a/qiskit/visualization/pulse_v2/interface.py +++ b/qiskit/visualization/pulse_v2/interface.py @@ -306,12 +306,13 @@ def draw( Examples: To visualize a pulse program, you can call this function with set of - control arguments. Most of appearance of the output image can be controlled by the + control arguments. Most of the appearance of the output image can be controlled by the stylesheet. Drawing with the default stylesheet. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule @@ -330,6 +331,7 @@ def draw( Drawing with the stylesheet suited for publication. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule @@ -348,6 +350,7 @@ def draw( Drawing with the stylesheet suited for program debugging. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 0e47a5fe6d72..ac003055d2c8 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -70,10 +70,11 @@ def plot_state_hinton(state, title="", figsize=None, ax_real=None, ax_imag=None, Raises: MissingOptionalLibraryError: Requires matplotlib. - VisualizationError: if input is not a valid N-qubit state. + VisualizationError: Input is not a valid N-qubit state. Examples: .. plot:: + :alt: Output from the previous code. :include-source: import numpy as np @@ -214,6 +215,7 @@ def plot_bloch_vector( Examples: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit.visualization import plot_bloch_vector @@ -221,6 +223,7 @@ def plot_bloch_vector( plot_bloch_vector([0,1,0], title="New Bloch Sphere") .. plot:: + :alt: Output from the previous code. :include-source: import numpy as np @@ -290,6 +293,7 @@ def plot_bloch_multivector( Examples: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit @@ -304,6 +308,7 @@ def plot_bloch_multivector( plot_bloch_multivector(state) .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit @@ -406,6 +411,7 @@ def plot_state_city( Examples: .. plot:: + :alt: Output from the previous code. :include-source: # You can choose different colors for the real and imaginary parts of the density matrix. @@ -422,6 +428,7 @@ def plot_state_city( plot_state_city(state, color=['midnightblue', 'crimson'], title="New State City") .. plot:: + :alt: Output from the previous code. :include-source: # You can make the bars more transparent to better see the ones that are behind @@ -643,6 +650,7 @@ def plot_state_paulivec(state, title="", figsize=None, color=None, ax=None, *, f Examples: .. plot:: + :alt: Output from the previous code. :include-source: # You can set a color for all the bars. @@ -659,6 +667,7 @@ def plot_state_paulivec(state, title="", figsize=None, color=None, ax=None, *, f plot_state_paulivec(state, color='midnightblue', title="New PauliVec plot") .. plot:: + :alt: Output from the previous code. :include-source: # If you introduce a list with less colors than bars, the color of the bars will @@ -816,12 +825,13 @@ def plot_state_qsphere( Raises: MissingOptionalLibraryError: Requires matplotlib. - VisualizationError: if input is not a valid N-qubit state. + VisualizationError: Input is not a valid N-qubit state. QiskitError: Input statevector does not have valid dimensions. Examples: .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit @@ -836,6 +846,7 @@ def plot_state_qsphere( plot_state_qsphere(state) .. plot:: + :alt: Output from the previous code. :include-source: # You can show the phase of each state and use diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 686e0fe1d810..50dd006633a7 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -291,13 +291,14 @@ def draw( for more details. No default layout is set. (default `None`). Examples: - To visualize a scheduled circuit program, you can call this function with set of - control arguments. Most of appearance of the output image can be controlled by the + To visualize a scheduled circuit program, you can call this function with a set of + control arguments. Most of the appearance of the output image can be controlled by the stylesheet. Drawing with the default stylesheet. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule @@ -314,6 +315,7 @@ def draw( Drawing with the simple stylesheet. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule @@ -330,6 +332,7 @@ def draw( Drawing with the stylesheet suited for program debugging. .. plot:: + :alt: Output from the previous code. :include-source: from qiskit import QuantumCircuit, transpile, schedule @@ -356,7 +359,7 @@ def draw( In the same way as above, you can create custom generator or layout functions and update existing stylesheet with custom functions. - This feature enables you to control the most of appearance of the output image + This feature enables you to control the most of the appearance of the output image without modifying the codebase of the scheduled circuit drawer. """ del show_idle diff --git a/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml b/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml new file mode 100644 index 000000000000..6632cb914f96 --- /dev/null +++ b/releasenotes/notes/64-bit-only-4132f330ec7804b3.yaml @@ -0,0 +1,16 @@ +--- +upgrade: + - | + Qiskit no longer supports Linux i686 and 32 bit Windows. Starting in Qiskit + 2.0 a 64 bit platform is needed to run Qiskit. The user base for 32bit + architectures is relatively small and as Qiskit continues to focus on + improving performance to handle the increased scale in the complexity + of quantum computing hardware maintaining support for 32 bit platforms + is proving increasingly difficult. This coupled with the larger + scientific/numeric Python community's trend away from 32 bit platform + support maintaining support for 32bit platforms is no longer viable. + Qiskit 1.x will still continue to support 32 bit platforms, but starting + in this 2.0.0 release Qiskit no longer supports these platforms, will + not publish pre-compiled binaries for these platforms any longer, and + there is no longer any guarantee of being able to build from source on + 32 bit platforms. diff --git a/releasenotes/notes/add-bool-bitarray-ddc30e5280f21c67.yaml b/releasenotes/notes/add-bool-bitarray-ddc30e5280f21c67.yaml new file mode 100644 index 000000000000..bbecb9501637 --- /dev/null +++ b/releasenotes/notes/add-bool-bitarray-ddc30e5280f21c67.yaml @@ -0,0 +1,5 @@ +--- +features_primitives: + - | + Added :meth:`~BitArray.to_bool_array` method to :class:`~BitArray` class that returns the bit array + as a boolean NumPy array. The ``order`` argument can be used to specify the endianness of the output array. diff --git a/releasenotes/notes/add-max-block-width-arg-e3677a2d26575a73.yaml b/releasenotes/notes/add-max-block-width-arg-e3677a2d26575a73.yaml new file mode 100644 index 000000000000..2dc97302b203 --- /dev/null +++ b/releasenotes/notes/add-max-block-width-arg-e3677a2d26575a73.yaml @@ -0,0 +1,27 @@ +--- +features_transpiler: + - | + Added a new argument ``max_block_width`` to the class :class:`.BlockCollector` + and to the transpiler passes :class:`.CollectLinearFunctions` and :class:`.CollectCliffords`. + This argument allows to restrict the maximum number of qubits over which a block of nodes is + defined. + + For example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import CollectLinearFunctions + + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + + # Collects all CX-gates into a single block + qc1 = CollectLinearFunctions()(qc) + qc1.draw(output='mpl') + + # Collects CX-gates into two blocks of width 3 + qc2 = CollectLinearFunctions(max_block_width=3)(qc) + qc2.draw(output='mpl') diff --git a/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml b/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml new file mode 100644 index 000000000000..684b095905a0 --- /dev/null +++ b/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml @@ -0,0 +1,11 @@ +--- +features_transpiler: + - | + Added a new option, ``collect_from_back``, to + :class:`~qiskit.transpiler.passes.CollectMultiQBlocks`. + When set to ``True``, the blocks are collected in the reverse direction, + from the outputs towards the inputs of the circuit. The blocks are still + reported following the normal topological order. + This leads to an additional flexibility provided by the pass, and + additional optimization opportunities when combined with a circuit + resynthesis method. diff --git a/releasenotes/notes/bump-msrv-f655886d03b493f2.yaml b/releasenotes/notes/bump-msrv-f655886d03b493f2.yaml new file mode 100644 index 000000000000..bc58887fbe7a --- /dev/null +++ b/releasenotes/notes/bump-msrv-f655886d03b493f2.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The minimum supported Rust version for building Qiskit from source is now + 1.79. This has been raised from the previous minimum supported Rust + version of 1.70 in the Qiskit 1.x release series. diff --git a/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml b/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml new file mode 100644 index 000000000000..dd741f981110 --- /dev/null +++ b/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Commutation relations of :class:`~.circuit.Instruction`\ s with float-only ``params`` + were eagerly cached by the :class:`.CommutationChecker`, using the ``params`` as key to + query the relation. This could lead to faulty results, if the instruction's definition + depended on additional information that just the :attr:`~.circuit.Instruction.params` + attribute, such as e.g. the case for :class:`.PauliEvolutionGate`. + This behavior is now fixed, and the commutation checker only conservatively caches + commutations for Qiskit-native standard gates. This can incur a performance cost if you were + relying on your custom gates being cached, however, we cannot guarantee safe caching for + custom gates, as they might rely on information beyond :attr:`~.circuit.Instruction.params`. + - | + Fixed a bug in the :class:`.CommmutationChecker`, where checking commutation of instruction + with non-numeric values in the :attr:`~.circuit.Instruction.params` attribute (such as the + :class:`.PauliGate`) could raise an error. + Fixed `#13570 `__. + diff --git a/releasenotes/notes/decompose-controlflow-7a7e38d402aed260.yaml b/releasenotes/notes/decompose-controlflow-7a7e38d402aed260.yaml new file mode 100644 index 000000000000..3ac215e29ea8 --- /dev/null +++ b/releasenotes/notes/decompose-controlflow-7a7e38d402aed260.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Fixed a bug where calling :meth:`.QuantumCircuit.decompose` on an instruction + that had no definition inside a ``c_if`` block would raise an error. + Fixed `#13493 `__. + - | + Operations inside a control flow (e.g. :meth:`.QuantumCircuit.for_loop`) were not + correctly decomposed when calling :meth:`.QuantumCircuit.decompose`. This + behavior is now fixed and instructions are unrolled. + Fixed `#13544 `__. diff --git a/releasenotes/notes/fix-4pi-periodic-commutations-3b89d1813513f613.yaml b/releasenotes/notes/fix-4pi-periodic-commutations-3b89d1813513f613.yaml new file mode 100644 index 000000000000..289c43cb422f --- /dev/null +++ b/releasenotes/notes/fix-4pi-periodic-commutations-3b89d1813513f613.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The :class:`.CommutationChecker` did not handle commutations of the :class:`.CRXGate`, + :class:`.CRYGate` and :class:`.CRZGate` correctly for angles + :math:`\pi(4k + 2)` for :math:`k \in \mathbb Z`. + In these cases, the controlled rotations were falsely assumed to commute with any gate. + Now these gates correctly commute with any gate if the rotation angle is a multiple of + :math:`4\pi`. diff --git a/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml b/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml new file mode 100644 index 000000000000..d4b792062352 --- /dev/null +++ b/releasenotes/notes/fix-adder-gates-39cf3d5f683e8880.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Added default definitions for :class:`.FullAdderGate`, :class:`.HalfAdderGate`, + :class:`.ModularAdderGate` and :class:`.MultiplierGate` gates, allowing to + contruct :class:`.Operator`\s from quantum circuits containing these gates. + - | + Fixed the number of clean ancilla qubits required by + :class:`.FullAdderSynthesisV95`, :class:`.HalfAdderSynthesisV95`, and + :class:`.ModularAdderSynthesisV95` plugins. + - | + Added missing :class:`.FullAdderSynthesisDefault` plugin that chooses the best + decomposition for :class:`.FullAdderGate` based on the number of clean ancilla qubits + available. + - | + Fixed :class:`.HalfAdderSynthesisDefault` and :class:`.ModularAdderSynthesisDefault` + plugins, for :class:`.HalfAdderGate` and :class:`.ModularAdderGate` respectively, + to choose the best decomposition based on the number of clean ancilla qubits available. diff --git a/releasenotes/notes/fix-assign-parameters-ffa284ebde429704.yaml b/releasenotes/notes/fix-assign-parameters-ffa284ebde429704.yaml new file mode 100644 index 000000000000..b1e1dc5f67d6 --- /dev/null +++ b/releasenotes/notes/fix-assign-parameters-ffa284ebde429704.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fix incorrect behavior in :class:`.CircuitData` in which, upon parameter assignment, + we attempted to modify the cached operation inside of a ``PackedInstruction``. Now + we instead discard said cache prompting the ``PackedInstruction`` to build a new Python + operation should it be needed. \ No newline at end of file diff --git a/releasenotes/notes/fix-efficient-su2-numqubits-issue-2b2c91c1186f82ac.yaml b/releasenotes/notes/fix-efficient-su2-numqubits-issue-2b2c91c1186f82ac.yaml new file mode 100644 index 000000000000..530c4438de3d --- /dev/null +++ b/releasenotes/notes/fix-efficient-su2-numqubits-issue-2b2c91c1186f82ac.yaml @@ -0,0 +1,6 @@ +fixes: + - | + Fixed a bug that caused the circuit library functions :func:`.efficient_su2`, + :func:`.real_amplitudes`, :func:`.excitation_preserving` and :func:`.pauli_two_design` + to error out when constructed for ``num_qubits==1``. For a single qubit these + circuits will not contain any 2-qubit gates. \ No newline at end of file diff --git a/releasenotes/notes/fix-global-phase-assign-d05f182ed9ddcf57.yaml b/releasenotes/notes/fix-global-phase-assign-d05f182ed9ddcf57.yaml new file mode 100644 index 000000000000..5000a9b03697 --- /dev/null +++ b/releasenotes/notes/fix-global-phase-assign-d05f182ed9ddcf57.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + Fixed a series of bugs when processing circuit with parameterized global phases, + where upon assignment the global phase was not correctly assigned. + Known cases this affected include: + + * assigning parameters after calling :meth:`.QuantumCircuit.decompose` on a circuit, + where the decomposition introduces a global phase + * assigning parameters on a circuit constructed from a DAG via :func:`.dag_to_circuit` + * assigning parameters on circuits created with :func:`.pauli_twirl_2q_gates`, where + the circuit to be twirled had a parameterized global phase + + Fixed `#13534 `__. \ No newline at end of file diff --git a/releasenotes/notes/fix-inverse-cancellation-c7f4debcde4a705a.yaml b/releasenotes/notes/fix-inverse-cancellation-c7f4debcde4a705a.yaml index 7d6999677b2b..ad9f7a0b8334 100644 --- a/releasenotes/notes/fix-inverse-cancellation-c7f4debcde4a705a.yaml +++ b/releasenotes/notes/fix-inverse-cancellation-c7f4debcde4a705a.yaml @@ -1,3 +1,6 @@ fixes: - | - The transpilation pass :class`.InverseCancellation` now runs inside of flow controlled blocks. Previously, it ignores the pairs of gates in classical blocks that can be cancelled. Refer to `#13437 ` for more details. + The transpilation pass :class:`.InverseCancellation` now runs inside of flow + controlled blocks. Previously, it ignored the pairs of gates in classical blocks + that could be cancelled. Refer to `#13437 `__ + for more details. diff --git a/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml b/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml new file mode 100644 index 000000000000..04fb9cc3f1b5 --- /dev/null +++ b/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + Fixed a bug where any instruction called ``"mcmt"`` would be passed into the high-level + synthesis routine for a :class:`.MCMTGate`, which causes a failure or invalid result. + In particular, this could happen accidentally when handling the :class:`.MCMT` _circuit_, + named ``"mcmt"``, and implicitly converting it into an instruction e.g. when appending + it to a circuit. + Fixed `#13563 `__. +upgrade_synthesis: + - | + The plugins for :class:`.LinearFunction` no longer raise an error if another object + than :class:`.LinearFunction` is passed into the ``run`` method. Instead, ``None`` is + returned, which is consistent with the other plugins. If you relied on this error being raised, + you can manually perform an instance-check. \ No newline at end of file diff --git a/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml b/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml new file mode 100644 index 000000000000..7653507c98a2 --- /dev/null +++ b/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fix a bug in the multi-controlled rotation circuit methods :meth:`.QuantumCircuit.mcrx`, + :meth:`.QuantumCircuit.mcry`, and :meth:`.QuantumCircuit.mcrz`, when the user provides an unbounded parameter, + as well as when calling :meth:`.RXGate.control`, :meth:`.RYGate.control` or :meth:`.RZGate.control` where the + rotation angle is a :class:`.ParameterExpression`. + Previously, the user got an error that this gate cannot be synthesized with unbound parameter, + and now these multi-controlled rotation circuits can be synthesized without raising an error. diff --git a/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml new file mode 100644 index 000000000000..03cbe8654cb9 --- /dev/null +++ b/releasenotes/notes/fix-pauli-evo-all-identity-b129acd854d8c391.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The :class:`.PauliEvolutionGate`, if used with a product formula synthesis (this is the default), + did not correctly handle all-identity terms in the operator. The all-identity term + should introduce a global phase equal to ``-evolution_time``, but was off by a factor of 2 + and could break for parameterized times. This behavior is now fixed. + Fixed `#13625 `__. diff --git a/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml new file mode 100644 index 000000000000..248a09b6f88e --- /dev/null +++ b/releasenotes/notes/fix-pauli-sympify-ea9acceb2a923aff.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed an inconsistency in the circuit generated by a Pauli evolution synthesis + with :class:`.SuzukiTrotter` or :class:`.LieTrotter` (the default) method. + For parameterized evolution times, the resulting circuits contained parameters + with a spurious, zero complex part, which affected the output of + :meth:`.ParameterExpression.sympify`. The output now correctly is only real. + Fixed `#13642 `__. diff --git a/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml b/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml new file mode 100644 index 000000000000..a1a59708003c --- /dev/null +++ b/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug that caused :meth:`.PauliList.insert` with ``qubit=True`` to produce a `phase` + attribute with the wrong shape when the original object was length 1. + Fixed `#13623 `__. diff --git a/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml b/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml new file mode 100644 index 000000000000..3cd59ff5e427 --- /dev/null +++ b/releasenotes/notes/fix-qasm-3-unitary-2da190be6ba25bbd.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix a bug in :class:`.qasm3.Exporter` that caused the exporter to crash when + handling a unitary gate due to incorrect processing of its ``params`` field. diff --git a/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml b/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml new file mode 100644 index 000000000000..42a77da5210b --- /dev/null +++ b/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :func:`~qiskit.quantum_info.random_clifford` that stopped it + from sampling the full Clifford group. diff --git a/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml b/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml new file mode 100644 index 000000000000..5db23e66fed3 --- /dev/null +++ b/releasenotes/notes/fix-target-instr-supported-900a1caa76e30655.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug in the :meth:`.Target.instruction_supported` method where + targets with ``self.num_qubits==None`` would always return ``false`` + independently of the supported basis set. \ No newline at end of file diff --git a/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml b/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml new file mode 100644 index 000000000000..248ff0bc2195 --- /dev/null +++ b/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.UnitarySynthesis` transpiler pass, where blocks of + :class:`.UnitaryGate`\s on 3 qubits or more were not correctly synthesized. + This led, e.g., to the circuit being overwritten with the last processed block or + to internal panics when encountering measurements after such a block. + Fixed `#13586 `__. diff --git a/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml b/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml new file mode 100644 index 000000000000..21310a4729a8 --- /dev/null +++ b/releasenotes/notes/fix-unitary-synthesis-global-gates-19b93840b28cfcf7.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.UnitarySynthesis` transpiler pass where + non-2-qubit gates would be included in the available 2 qubit basis, + causing the ``TwoQubitWeylDecomposition`` to panic because of + the dimension mismatch. diff --git a/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml b/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml new file mode 100644 index 000000000000..061dc8cfa4ce --- /dev/null +++ b/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml @@ -0,0 +1,6 @@ +--- +features_primitives: + - | + Expanded the docstring of :meth:`.ObservablesArray.tolist` + to make it clear it might return a scalar in the case + the observables array is of dimension 0. diff --git a/releasenotes/notes/remove-deprecated-sort-key-8921c52db826c8ba.yaml b/releasenotes/notes/remove-deprecated-sort-key-8921c52db826c8ba.yaml new file mode 100644 index 000000000000..3907153e16cd --- /dev/null +++ b/releasenotes/notes/remove-deprecated-sort-key-8921c52db826c8ba.yaml @@ -0,0 +1,15 @@ +--- +upgrade_transpiler: + - | + Removed the deprecated ``DAGNode.sort_key`` attribute. This attribute was deprecated + in the Qiskit 1.4.0 release. As the lexicographical topological sorting is done internally + and rust and the sort key attribute was unused this was removed to remove the overhead + from DAG node creation. If you were relying on the sort key you can reproduce it from + a given node using something like:: + + def get_sort_key(node: DAGNode): + if isinstance(node, (DAGInNode, DAGOutNode)): + return str(node.wire) + return ",".join( + f"{dag.find_bit(q).index:04d}" for q in itertools.chain(node.qargs, node.cargs) + ) diff --git a/releasenotes/notes/rust-linear-depth-lnn-69532c9c6b86ffe8.yaml b/releasenotes/notes/rust-linear-depth-lnn-69532c9c6b86ffe8.yaml new file mode 100644 index 000000000000..24f7f0b710e2 --- /dev/null +++ b/releasenotes/notes/rust-linear-depth-lnn-69532c9c6b86ffe8.yaml @@ -0,0 +1,5 @@ +--- +features_transpiler: + - | + The :meth:`.synth_cnot_depth_line_kms` pass was ported into rust, with preliminary + benchmarks pointing at a factor of 20x speedup. \ No newline at end of file diff --git a/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml b/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml new file mode 100644 index 000000000000..d285c6fb6857 --- /dev/null +++ b/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml @@ -0,0 +1,7 @@ +fixes: + - | + Fixed a bug where a initializing :class:`.SparsePauliOp` with a large + number of Pauli-``Y`` terms (typically :math:`\geq 100`) and no explicit + ``coeffs`` would result in a coefficient close to 1 but with a floating point + error. The coefficient is now correctly 1 per default. + Fixed `#13522 `__. \ No newline at end of file diff --git a/releasenotes/notes/stabilizerstate-expval-sparsepauliop-3e32a871d8e908ce.yaml b/releasenotes/notes/stabilizerstate-expval-sparsepauliop-3e32a871d8e908ce.yaml new file mode 100644 index 000000000000..135156272dc4 --- /dev/null +++ b/releasenotes/notes/stabilizerstate-expval-sparsepauliop-3e32a871d8e908ce.yaml @@ -0,0 +1,5 @@ +--- +features_quantum_info: + - | + The method :meth:`.StabilizerState.expectation_value` can now accept an operator of type + :class:`.SparsePauliOp`. diff --git a/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml b/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml new file mode 100644 index 000000000000..3d573a21901a --- /dev/null +++ b/releasenotes/notes/vf2-order-3ef2b4ca5ebd0588.yaml @@ -0,0 +1,12 @@ +--- +upgrade_transpiler: + - | + The default value for the :func:`.generate_routing_passmanager` argument + ``seed_transpiler`` has changed from ``None`` to ``-1``. This was done + because this flag was only used to configure the :class:`.VF2PostLayout` + transpiler pass in the output, and for that pass in particular the + randomization typically only hurts performance and is not desirable. + If you were relying on the previous default value you can restore this + behavior by explicitly setting the argument ``seed_transpiler=None``. If + you were explicitly setting a seed value for this parameter there is no + change in behavior. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6f581c2f9ab3..36612820ed38 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # Keep in sync with Cargo.toml's `rust-version`. -channel = "1.70" +channel = "1.79" components = [ "cargo", "clippy", diff --git a/test/benchmarks/circuit_construction.py b/test/benchmarks/circuit_construction.py index bc673a716ff4..abd0634ff6ef 100644 --- a/test/benchmarks/circuit_construction.py +++ b/test/benchmarks/circuit_construction.py @@ -19,7 +19,7 @@ from qiskit.quantum_info import random_clifford from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import Parameter -from qiskit.circuit.library import EfficientSU2, QuantumVolume +from qiskit.circuit.library import efficient_su2, quantum_volume from .utils import dtc_unitary, multi_control_circuit SEED = 12345 @@ -112,7 +112,7 @@ def time_QV100_build(self, circuit_size, num_qubits): """Measures an SDKs ability to build a 100Q QV circit from scratch. """ - return QuantumVolume(circuit_size, num_qubits, seed=SEED) + return quantum_volume(circuit_size, num_qubits, seed=SEED) def time_DTC100_set_build(self, circuit_size, num_qubits): """Measures an SDKs ability to build a set @@ -154,7 +154,7 @@ def time_param_circSU2_100_build(self, num_qubits): over 100Q utilizing 4 repetitions. This will yield a circuit with 1000 parameters """ - out = EfficientSU2(num_qubits, reps=4, entanglement="circular", flatten=True) + out = efficient_su2(num_qubits, reps=4, entanglement="circular") out._build() return out diff --git a/test/benchmarks/utility_scale.py b/test/benchmarks/utility_scale.py index 21ed4c88229b..403edb45cc68 100644 --- a/test/benchmarks/utility_scale.py +++ b/test/benchmarks/utility_scale.py @@ -16,7 +16,7 @@ import os from qiskit import QuantumCircuit -from qiskit.circuit.library import EfficientSU2 +from qiskit.circuit.library import efficient_su2 from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import CouplingMap @@ -48,7 +48,7 @@ def setup(self, basis_gate): self.qaoa_qasm = os.path.join(qasm_dir, "qaoa_barabasi_albert_N100_3reps.qasm") self.qaoa_qc = QuantumCircuit.from_qasm_file(self.qaoa_qasm) self.qv_qc = build_qv_model_circuit(50, 50, SEED) - self.circSU2 = EfficientSU2(100, reps=3, entanglement="circular") + self.circSU2 = efficient_su2(100, reps=3, entanglement="circular") self.bv_100 = bv_all_ones(100) self.bv_like_100 = trivial_bvlike_circuit(100) diff --git a/test/python/circuit/library/test_adders.py b/test/python/circuit/library/test_adders.py index eacf8057775a..5c960cedb171 100644 --- a/test/python/circuit/library/test_adders.py +++ b/test/python/circuit/library/test_adders.py @@ -168,7 +168,7 @@ def test_raises_on_wrong_num_bits(self, adder): _ = adder(-1) def test_plugins(self): - """Test setting the HLS plugins for the modular adder.""" + """Test calling HLS plugins for various adder types.""" # all gates with the plugins we check modes = { @@ -204,6 +204,106 @@ def test_plugins(self): self.assertTrue(expected_ops[plugin] in ops) + def test_plugins_when_do_not_apply(self): + """Test that plugins do not do anything when not enough + clean ancilla qubits are available. + """ + with self.subTest(name="FullAdder"): + adder = FullAdderGate(3) + circuit = QuantumCircuit(9) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(FullAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"FullAdder": 1}) + with self.subTest(name="HalfAdder"): + adder = HalfAdderGate(3) + circuit = QuantumCircuit(8) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(HalfAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"HalfAdder": 1}) + with self.subTest(name="ModularAdder"): + adder = ModularAdderGate(3) + circuit = QuantumCircuit(7) + circuit.append(adder, range(adder.num_qubits)) + hls_config = HLSConfig(ModularAdder=["ripple_v95"]) + hls = HighLevelSynthesis(hls_config=hls_config) + synth = hls(circuit) + self.assertEqual(synth.count_ops(), {"ModularAdder": 1}) + + def test_default_plugins(self): + """Tests covering different branches in the default synthesis plugins.""" + + # Test's name indicates which synthesis method should get used. + with self.subTest(name="HalfAdder_use_ripple_v95"): + adder = HalfAdderGate(3) + circuit = QuantumCircuit(9) + circuit.append(adder, range(7)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("Carry" in ops) + with self.subTest(name="HalfAdder_use_ripple_c04"): + adder = HalfAdderGate(4) + circuit = QuantumCircuit(12) + circuit.append(adder, range(9)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + with self.subTest(name="HalfAdder_use_qft_d00"): + adder = HalfAdderGate(4) + circuit = QuantumCircuit(9) + circuit.append(adder, range(9)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + + with self.subTest(name="FullAdder_use_ripple_c04"): + adder = FullAdderGate(4) + circuit = QuantumCircuit(10) + circuit.append(adder, range(10)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + with self.subTest(name="FullAdder_use_ripple_v95"): + adder = FullAdderGate(1) + circuit = QuantumCircuit(10) + circuit.append(adder, range(4)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("Carry" in ops) + + with self.subTest(name="ModularAdder_use_qft_d00"): + adder = ModularAdderGate(4) + circuit = QuantumCircuit(8) + circuit.append(adder, range(8)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + with self.subTest(name="ModularAdder_also_use_qft_d00"): + adder = ModularAdderGate(6) + circuit = QuantumCircuit(12) + circuit.append(adder, range(12)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("cp" in ops) + with self.subTest(name="ModularAdder_use_ripple_c04"): + adder = ModularAdderGate(6) + circuit = QuantumCircuit(16) + circuit.append(adder, range(12)) + hls = HighLevelSynthesis() + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + self.assertTrue("MAJ" in ops) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 1b55ea920289..f56da1995e4a 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -479,6 +479,50 @@ def atomic_evolution(pauli, time): decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) + def test_all_identity(self): + """Test circuit with all identity Paulis works correctly.""" + evo = PauliEvolutionGate(I ^ I, time=1).definition + expected = QuantumCircuit(2, global_phase=-1) + self.assertEqual(expected, evo) + + def test_global_phase(self): + """Test a circuit with parameterized global phase terms. + + Regression test of #13625. + """ + pauli = (X ^ X) + (I ^ I) + (I ^ X) + time = Parameter("t") + evo = PauliEvolutionGate(pauli, time=time) + + expected = QuantumCircuit(2, global_phase=-time) + expected.rxx(2 * time, 0, 1) + expected.rx(2 * time, 0) + + with self.subTest(msg="check circuit"): + self.assertEqual(expected, evo.definition) + + # since all terms in the Pauli operator commute, we can compare to an + # exact matrix exponential + time_value = 1.76123 + bound = evo.definition.assign_parameters([time_value]) + exact = scipy.linalg.expm(-1j * time_value * pauli.to_matrix()) + with self.subTest(msg="check correctness"): + self.assertEqual(Operator(exact), Operator(bound)) + + def test_sympify_is_real(self): + """Test converting the parameters to sympy is real. + + Regression test of #13642, where the parameters in the Pauli evolution had a spurious + zero complex part. Even though this is not noticable upon binding or printing the parameter, + it does affect the output of Parameter.sympify. + """ + time = Parameter("t") + evo = PauliEvolutionGate(Z, time=time) + + angle = evo.definition.data[0].operation.params[0] + expected = (2.0 * time).sympify() + self.assertEqual(expected, angle.sympify()) + def exact_atomic_evolution(circuit, pauli, time): """An exact atomic evolution for Suzuki-Trotter. diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 9c923764f408..2e7865a856c4 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -199,6 +199,21 @@ def test_detect_commutation(self): # this Hamiltonian should be split into 2 commuting groups, hence we get 2 parameters self.assertEqual(2, circuit.num_parameters) + def test_evolution_with_identity(self): + """Test a Hamiltonian containing an identity term. + + Regression test of #13644. + """ + hamiltonian = SparsePauliOp(["III", "IZZ", "IXI"]) + ansatz = hamiltonian_variational_ansatz(hamiltonian, reps=1) + bound = ansatz.assign_parameters([1, 1]) # we have two non-commuting groups, hence 2 params + + expected = QuantumCircuit(3, global_phase=-1) + expected.rzz(2, 0, 1) + expected.rx(2, 1) + + self.assertEqual(expected, bound) + def evolve(pauli_string, time): """Get the reference evolution circuit for a single Pauli string.""" diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index 73befb19db46..435ee0629593 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -285,6 +285,19 @@ def test_gate_with_parameters_vchain(self): self.assertEqual(circuit.num_parameters, 1) self.assertIs(circuit.parameters[0], theta) + def test_mcmt_circuit_as_gate(self): + """Test the MCMT plugin is only triggered for the gate, not the same-named circuit. + + Regression test of #13563. + """ + circuit = QuantumCircuit(2) + gate = RYGate(0.1) + mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) + circuit.append(mcmt, circuit.qubits) # append the MCMT circuit as gate called "MCMT" + + transpiled = transpile(circuit, basis_gates=["u", "cx"]) + self.assertEqual(Operator(transpiled), Operator(gate.control(1))) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 22bfa391612d..b171a728d4c9 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -784,12 +784,24 @@ def test_real_amplitudes(self): expected = n_local(4, "ry", "cx", "reverse_linear", reps=3) self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + def test_real_amplitudes_numqubits_equal1(self): + """Test the real amplitudes circuit for a single qubit.""" + circuit = real_amplitudes(1) + expected = n_local(1, "ry", []) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + def test_efficient_su2(self): """Test the efficient SU(2) circuit.""" circuit = efficient_su2(4) expected = n_local(4, ["ry", "rz"], "cx", "reverse_linear", reps=3) self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + def test_efficient_su2_numqubits_equal1(self): + """Test the efficient SU(2) circuit for a single qubit.""" + circuit = efficient_su2(1) + expected = n_local(1, ["ry", "rz"], []) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + @data("fsim", "iswap") def test_excitation_preserving(self, mode): """Test the excitation preserving circuit.""" @@ -808,6 +820,15 @@ def test_excitation_preserving(self, mode): expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose() ) + @data("fsim", "iswap") + def test_excitation_preserving_numqubits_equal1(self, mode): + """Test the excitation preserving circuit for a single qubit.""" + circuit = excitation_preserving(1, mode=mode) + expected = n_local(1, "rz", []) + self.assertEqual( + expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose() + ) + def test_excitation_preserving_invalid_mode(self): """Test an error is raised for an invalid mode.""" with self.assertRaises(ValueError): @@ -824,6 +845,14 @@ def test_two_design(self): self.assertTrue(circuit_ops.issubset(expected_ops)) + def test_two_design_numqubits_equal1(self): + """Test the Pauli 2-design circuit for a single qubit.""" + circuit = pauli_two_design(1) + expected_ops = {"rx", "ry", "rz", "id"} + circuit_ops = set(circuit.count_ops().keys()) + + self.assertTrue(circuit_ops.issubset(expected_ops)) + def test_two_design_seed(self): """Test the seed""" seed1 = 123 diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 9568c1c42084..8918a9f2192b 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -320,10 +320,6 @@ def test_delitem_slice(self, sli): del data_list[sli] del data[sli] - if data_list[sli] != data[sli]: - print(f"data_list: {data_list}") - print(f"data: {list(data)}") - self.assertEqual(data[sli], data_list[sli]) @ddt.data( diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 9759b5bffd1e..d0a7fc8eeb47 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -27,6 +27,7 @@ Parameter, QuantumRegister, Qubit, + QuantumCircuit, ) from qiskit.circuit.commutation_library import SessionCommutationChecker as scc from qiskit.circuit.library import ( @@ -37,9 +38,11 @@ CRYGate, CRZGate, CXGate, + CUGate, LinearFunction, MCXGate, Measure, + PauliGate, PhaseGate, Reset, RXGate, @@ -53,6 +56,7 @@ XGate, ZGate, HGate, + UnitaryGate, ) from qiskit.dagcircuit import DAGOpNode @@ -61,14 +65,14 @@ RYGate, RZGate, PhaseGate, - CRXGate, - CRYGate, - CRZGate, - CPhaseGate, RXXGate, RYYGate, RZZGate, RZXGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, ] @@ -82,6 +86,22 @@ def to_matrix(self): return np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex) +class MyEvilRXGate(Gate): + """A RX gate designed to annoy the caching mechanism (but a realistic gate nevertheless).""" + + def __init__(self, evil_input_not_in_param: float): + """ + Args: + evil_input_not_in_param: The RX rotation angle. + """ + self.value = evil_input_not_in_param + super().__init__("", 1, []) + + def _define(self): + self.definition = QuantumCircuit(1) + self.definition.rx(self.value, 0) + + @ddt class TestCommutationChecker(QiskitTestCase): """Test CommutationChecker class.""" @@ -137,7 +157,7 @@ def test_standard_gates_commutations(self): def test_caching_positive_results(self): """Check that hashing positive results in commutativity checker works as expected.""" scc.clear_cached_commutations() - self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertTrue(scc.commute(ZGate(), [0], [], CUGate(1, 2, 3, 0), [0, 1], [])) self.assertGreater(scc.num_cached_entries(), 0) def test_caching_lookup_with_non_overlapping_qubits(self): @@ -150,16 +170,17 @@ def test_caching_lookup_with_non_overlapping_qubits(self): def test_caching_store_and_lookup_with_non_overlapping_qubits(self): """Check that commutations storing and lookup with non-overlapping qubits works as expected.""" scc_lenm = scc.num_cached_entries() - self.assertTrue(scc.commute(NewGateCX(), [0, 2], [], CXGate(), [0, 1], [])) - self.assertFalse(scc.commute(NewGateCX(), [0, 1], [], CXGate(), [1, 2], [])) - self.assertTrue(scc.commute(NewGateCX(), [1, 4], [], CXGate(), [1, 6], [])) - self.assertFalse(scc.commute(NewGateCX(), [5, 3], [], CXGate(), [3, 1], [])) + cx_like = CUGate(np.pi, 0, np.pi, 0) + self.assertTrue(scc.commute(cx_like, [0, 2], [], CXGate(), [0, 1], [])) + self.assertFalse(scc.commute(cx_like, [0, 1], [], CXGate(), [1, 2], [])) + self.assertTrue(scc.commute(cx_like, [1, 4], [], CXGate(), [1, 6], [])) + self.assertFalse(scc.commute(cx_like, [5, 3], [], CXGate(), [3, 1], [])) self.assertEqual(scc.num_cached_entries(), scc_lenm + 2) def test_caching_negative_results(self): """Check that hashing negative results in commutativity checker works as expected.""" scc.clear_cached_commutations() - self.assertFalse(scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertFalse(scc.commute(XGate(), [0], [], CUGate(1, 2, 3, 0), [0, 1], [])) self.assertGreater(scc.num_cached_entries(), 0) def test_caching_different_qubit_sets(self): @@ -167,10 +188,11 @@ def test_caching_different_qubit_sets(self): scc.clear_cached_commutations() # All the following should be cached in the same way # though each relation gets cached twice: (A, B) and (B, A) - scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], []) - scc.commute(XGate(), [10], [], NewGateCX(), [10, 20], []) - scc.commute(XGate(), [10], [], NewGateCX(), [10, 5], []) - scc.commute(XGate(), [5], [], NewGateCX(), [5, 7], []) + cx_like = CUGate(np.pi, 0, np.pi, 0) + scc.commute(XGate(), [0], [], cx_like, [0, 1], []) + scc.commute(XGate(), [10], [], cx_like, [10, 20], []) + scc.commute(XGate(), [10], [], cx_like, [10, 5], []) + scc.commute(XGate(), [5], [], cx_like, [5, 7], []) self.assertEqual(scc.num_cached_entries(), 1) def test_zero_rotations(self): @@ -377,12 +399,14 @@ def test_serialization(self): """Test that the commutation checker is correctly serialized""" import pickle + cx_like = CUGate(np.pi, 0, np.pi, 0) + scc.clear_cached_commutations() - self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertTrue(scc.commute(ZGate(), [0], [], cx_like, [0, 1], [])) cc2 = pickle.loads(pickle.dumps(scc)) self.assertEqual(cc2.num_cached_entries(), 1) dop1 = DAGOpNode(ZGate(), qargs=[0], cargs=[]) - dop2 = DAGOpNode(NewGateCX(), qargs=[0, 1], cargs=[]) + dop2 = DAGOpNode(cx_like, qargs=[0, 1], cargs=[]) cc2.commute_nodes(dop1, dop2) dop1 = DAGOpNode(ZGate(), qargs=[0], cargs=[]) dop2 = DAGOpNode(CXGate(), qargs=[0, 1], cargs=[]) @@ -410,26 +434,55 @@ def test_cutoff_angles(self, gate_cls): self.assertFalse(scc.commute(generic_gate, [0, 1], [], gate, qargs, [])) @idata(ROTATION_GATES) - def test_rotation_mod_2pi(self, gate_cls): - """Test the rotations modulo 2pi commute with any gate.""" + def test_controlled_rotation_mod_4pi(self, gate_cls): + """Test the rotations modulo 2pi (4pi for controlled-rx/y/z) commute with any gate.""" generic_gate = HGate() # does not commute with any rotation gate - even = np.arange(-6, 7, 2) + multiples = np.arange(-6, 7) - with self.subTest(msg="even multiples"): - for multiple in even: + for multiple in multiples: + with self.subTest(multiple=multiple): gate = gate_cls(multiple * np.pi) - self.assertTrue( - scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) - ) + numeric = UnitaryGate(gate.to_matrix()) - odd = np.arange(-5, 6, 2) - with self.subTest(msg="odd multiples"): - for multiple in odd: - gate = gate_cls(multiple * np.pi) - self.assertFalse( - scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) + # compute a numeric reference, that doesn't go through any special cases and + # uses a matrix-based commutation check + expected = scc.commute( + generic_gate, [0], [], numeric, list(range(gate.num_qubits)), [] ) + result = scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) + self.assertEqual(expected, result) + + def test_custom_gate(self): + """Test a custom gate.""" + my_cx = NewGateCX() + + self.assertTrue(scc.commute(my_cx, [0, 1], [], XGate(), [1], [])) + self.assertFalse(scc.commute(my_cx, [0, 1], [], XGate(), [0], [])) + self.assertTrue(scc.commute(my_cx, [0, 1], [], ZGate(), [0], [])) + + self.assertFalse(scc.commute(my_cx, [0, 1], [], my_cx, [1, 0], [])) + self.assertTrue(scc.commute(my_cx, [0, 1], [], my_cx, [0, 1], [])) + + def test_custom_gate_caching(self): + """Test a custom gate is correctly handled on consecutive runs.""" + + all_commuter = MyEvilRXGate(0) # this will commute with anything + some_rx = MyEvilRXGate(1.6192) # this should not commute with H + + # the order here is important: we're testing whether the gate that commutes with + # everything is used after the first commutation check, regardless of the internal + # gate parameters + self.assertTrue(scc.commute(all_commuter, [0], [], HGate(), [0], [])) + self.assertFalse(scc.commute(some_rx, [0], [], HGate(), [0], [])) + + def test_nonfloat_param(self): + """Test commutation-checking on a gate that has non-float ``params``.""" + pauli_gate = PauliGate("XX") + rx_gate_theta = RXGate(Parameter("Theta")) + self.assertTrue(scc.commute(pauli_gate, [0, 1], [], rx_gate_theta, [0], [])) + self.assertTrue(scc.commute(rx_gate_theta, [0], [], pauli_gate, [0, 1], [])) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 997fa2fdb034..8ad5ca7d473a 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -86,7 +86,7 @@ ) from qiskit.circuit._utils import _compute_control_matrix import qiskit.circuit.library.standard_gates as allGates -from qiskit.circuit.library.standard_gates.multi_control_rotation_gates import _mcsu2_real_diagonal +from qiskit.synthesis.multi_controlled.multi_control_rotation_gates import _mcsu2_real_diagonal from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, ) @@ -553,9 +553,9 @@ def test_mcsu2_real_diagonal(self): """Test mcsu2_real_diagonal""" num_ctrls = 6 theta = 0.3 - ry_matrix = RYGate(theta).to_matrix() - qc = _mcsu2_real_diagonal(ry_matrix, num_ctrls) + qc = _mcsu2_real_diagonal(RYGate(theta), num_ctrls) + ry_matrix = RYGate(theta).to_matrix() mcry_matrix = _compute_control_matrix(ry_matrix, 6) self.assertTrue(np.allclose(mcry_matrix, Operator(qc).to_matrix())) @@ -685,6 +685,23 @@ def test_mcry_defaults_to_vchain(self): dag = circuit_to_dag(circuit) self.assertEqual(len(list(dag.idle_wires())), 0) + @combine(num_controls=[1, 2, 3], base_gate=[RXGate, RYGate, RZGate, CPhaseGate]) + def test_multi_controlled_rotation_gate_with_parameter(self, num_controls, base_gate): + """Test multi-controlled rotation gates and MCPhase gate with Parameter synthesis.""" + theta = Parameter("theta") + params = [theta] + val = 0.4123 + rot_matrix = base_gate(val).to_matrix() + mc_matrix = _compute_control_matrix(rot_matrix, num_controls) + + mc_gate = base_gate(*params).control(num_controls) + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) + + bound = circuit.assign_parameters([val]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + self.assertTrue(np.allclose(mc_matrix, Operator(unrolled).to_matrix())) + @data(1, 2) def test_mcx_gates_yield_explicit_gates(self, num_ctrl_qubits): """Test the creating a MCX gate yields the explicit definition if we know it.""" @@ -1443,8 +1460,6 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): self.assertEqual(Operator(controlled), Operator(target)) @data( - RXGate, - RYGate, RXXGate, RYYGate, RZXGate, @@ -1454,8 +1469,8 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): XXMinusYYGate, XXPlusYYGate, ) - def test_mc_failure_without_annotation(self, gate_cls): - """Test error for gates that cannot be multi-controlled without annotation.""" + def test_mc_without_annotation(self, gate_cls): + """Test multi-controlled gates with and without annotation.""" theta = Parameter("theta") num_params = len(_get_free_params(gate_cls.__init__, ignore=["self"])) params = [theta] + (num_params - 1) * [1.234] @@ -1463,22 +1478,17 @@ def test_mc_failure_without_annotation(self, gate_cls): for annotated in [False, None]: with self.subTest(annotated=annotated): # if annotated is False, check that a sensible error is raised - if annotated is False: - with self.assertRaisesRegex(QiskitError, "unbound parameter"): - _ = gate_cls(*params).control(5, annotated=False) - # else, check that the gate can be synthesized after all parameters # have been bound - else: - mc_gate = gate_cls(*params).control(5) + mc_gate = gate_cls(*params).control(5) - circuit = QuantumCircuit(mc_gate.num_qubits) - circuit.append(mc_gate, circuit.qubits) + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) - bound = circuit.assign_parameters([0.5123]) - unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + bound = circuit.assign_parameters([0.5123]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) - self.assertEqual(unrolled.num_parameters, 0) + self.assertEqual(unrolled.num_parameters, 0) def assertEqualTranslated(self, circuit, unrolled_reference, basis): """Assert that the circuit is equal to the unrolled reference circuit.""" diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 3497ef9fe46a..1b78861c6c00 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -65,6 +65,11 @@ CSXGate, RVGate, XXMinusYYGate, + FullAdderGate, + HalfAdderGate, + ModularAdderGate, + LinearFunction, + MultiplierGate, ) from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, @@ -199,6 +204,41 @@ def test_ucpaulirotgate_repeat(self): operator = Operator(gate) self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator)) + def test_linear_function_definition(self): + """Test LinearFunction gate matrix and definition.""" + circ = QuantumCircuit(3) + circ.append(LinearFunction([[1, 1], [0, 1]]), [0, 2]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_full_adder_definition(self): + """Test FullAdder gate matrix and definition.""" + circ = QuantumCircuit(4) + circ.append(FullAdderGate(1), [0, 1, 2, 3]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_half_adder_definition(self): + """Test HalfAdder gate matrix and definition.""" + circ = QuantumCircuit(3) + circ.append(HalfAdderGate(1), [0, 1, 2]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_modular_adder_definition(self): + """Test ModularAdder gate matrix and definition.""" + circ = QuantumCircuit(2) + circ.append(ModularAdderGate(1), [0, 1]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + + def test_multiplier_gate_definition(self): + """Test Multiplier gate matrix and definition.""" + circ = QuantumCircuit(4) + circ.append(MultiplierGate(1), [0, 1, 2, 3]) + decomposed_circ = circ.decompose() + self.assertTrue(Operator(circ).equiv(Operator(decomposed_circ))) + @ddt class TestStandardGates(QiskitTestCase): diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 2b342cae1968..560ce7630dff 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1246,6 +1246,18 @@ def test_repeated_gates_to_dag_and_back(self): bound_test_qc = test_qc.assign_parameters({theta: 1}) self.assertEqual(len(bound_test_qc.parameters), 0) + def test_global_phase_to_dag_and_back(self): + """Test global phase parameters are correctly handled to dag and back.""" + from qiskit.converters import circuit_to_dag, dag_to_circuit + + theta = Parameter("theta") + qc = QuantumCircuit(global_phase=theta) + + test_qc = dag_to_circuit(circuit_to_dag(qc)) + + bound_test_qc = test_qc.assign_parameters({theta: 1}) + self.assertEqual(bound_test_qc.global_phase, 1) + def test_rebinding_instruction_copy(self): """Test rebinding a copied instruction does not modify the original.""" @@ -1617,6 +1629,22 @@ def test_sub_allow_unknown_parameters(self): subbed = x.subs({y: z}, allow_unknown_parameters=True) self.assertEqual(subbed, x) + def test_decompose_with_global_phase(self): + """Test decomposing a circuit which introduces a global phase is correctly bound. + + Regression test of #13534. + """ + x = Parameter("x") + qc = QuantumCircuit(1) + qc.rz(x, 0) + + bound = qc.decompose().assign_parameters([1]) + + expect = QuantumCircuit(1) + expect.rz(1, 0) + + self.assertEqual(expect.decompose(), bound) + def _construct_circuit(param, qr): qc = QuantumCircuit(qr) diff --git a/test/python/circuit/test_twirling.py b/test/python/circuit/test_twirling.py index 59e6b0c41fe2..da3aa25ceb9c 100644 --- a/test/python/circuit/test_twirling.py +++ b/test/python/circuit/test_twirling.py @@ -15,7 +15,7 @@ import ddt import numpy as np -from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates, Gate +from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates, Gate, Parameter from qiskit.circuit.library import ( CXGate, ECRGate, @@ -210,3 +210,13 @@ def test_error_on_parameterized_gate(self): qc = QuantumCircuit(5) with self.assertRaises(QiskitError): pauli_twirl_2q_gates(qc, [RZXGate(3.24)]) + + def test_with_global_phase(self): + """Test twirling a circuit with parameterized global phase.""" + + x = Parameter("x") + qc = QuantumCircuit(2, global_phase=x) + res = pauli_twirl_2q_gates(qc, seed=2) + bound = res.assign_parameters([1]) + + self.assertEqual(bound.global_phase, 1) diff --git a/test/python/dagcircuit/test_collect_blocks.py b/test/python/dagcircuit/test_collect_blocks.py index d8178fdb3a54..4b2d02786c43 100644 --- a/test/python/dagcircuit/test_collect_blocks.py +++ b/test/python/dagcircuit/test_collect_blocks.py @@ -14,6 +14,7 @@ import unittest +import ddt from qiskit import QuantumRegister, ClassicalRegister from qiskit.converters import ( @@ -28,6 +29,7 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt.ddt class TestCollectBlocks(QiskitTestCase): """Tests to verify correctness of collecting, splitting, and consolidating blocks from DAGCircuit and DAGDependency. Additional tests appear as a part of @@ -878,6 +880,64 @@ def test_collect_blocks_backwards_dagdependency(self): self.assertEqual(len(blocks[0]), 1) self.assertEqual(len(blocks[1]), 7) + @ddt.data(circuit_to_dag, circuit_to_dagdependency) + def test_max_block_width_default(self, converter): + """Test that not explicitly specifying ``max_block_width`` works as expected.""" + + # original circuit + circuit = QuantumCircuit(6) + circuit.h(0) + circuit.cx(0, 1) + circuit.cx(1, 2) + circuit.cx(2, 3) + circuit.cx(3, 4) + circuit.cx(4, 5) + + block_collector = BlockCollector(converter(circuit)) + + # When max_block_width is not specified, we should obtain 1 block + blocks = block_collector.collect_all_matching_blocks( + lambda node: True, + min_block_size=1, + ) + self.assertEqual(len(blocks), 1) + + @ddt.data( + (circuit_to_dag, None, 1), + (circuit_to_dag, 2, 5), + (circuit_to_dag, 3, 3), + (circuit_to_dag, 4, 2), + (circuit_to_dag, 6, 1), + (circuit_to_dag, 10, 1), + (circuit_to_dagdependency, None, 1), + (circuit_to_dagdependency, 2, 5), + (circuit_to_dagdependency, 3, 3), + (circuit_to_dagdependency, 4, 2), + (circuit_to_dagdependency, 6, 1), + (circuit_to_dagdependency, 10, 1), + ) + @ddt.unpack + def test_max_block_width(self, converter, max_block_width, num_expected_blocks): + """Test that the option ``max_block_width`` for collecting blocks works correctly.""" + + # original circuit + circuit = QuantumCircuit(6) + circuit.h(0) + circuit.cx(0, 1) + circuit.cx(1, 2) + circuit.cx(2, 3) + circuit.cx(3, 4) + circuit.cx(4, 5) + + block_collector = BlockCollector(converter(circuit)) + + blocks = block_collector.collect_all_matching_blocks( + lambda node: True, + min_block_size=1, + max_block_width=max_block_width, + ) + self.assertEqual(len(blocks), num_expected_blocks) + def test_split_layers_dagcircuit(self): """Test that splitting blocks of nodes into layers works correctly.""" diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index fb99ab0332e4..54bcbe4a843c 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -217,6 +217,36 @@ def test_from_bool_array(self): [[[1, 0, 1, 0], [0, 0, 1, 1]], [[1, 0, 0, 0], [0, 0, 0, 1]]], order="bg" ) + def test_to_bool_array(self): + """Test the to_bool_array method.""" + + bit_array = BitArray(u_8([[[10], [3]], [[8], [1]]]), 4) + expected_array = np.array( + [[[1, 0, 1, 0], [0, 0, 1, 1]], [[1, 0, 0, 0], [0, 0, 0, 1]]], dtype=np.bool_ + ) + self.assertTrue(np.array_equal(bit_array.to_bool_array(), expected_array)) + + bit_array = BitArray(u_8([[[10], [3]], [[8], [1]]]), 4) + expected_array = np.array( + [[[0, 1, 0, 1], [1, 1, 0, 0]], [[0, 0, 0, 1], [1, 0, 0, 0]]], dtype=np.bool_ + ) + self.assertTrue(np.array_equal(bit_array.to_bool_array(order="little"), expected_array)) + + bit_array = BitArray(u_8([[7, 3, 1]]), 21) + expected_array = np.array( + [[0, 0, 1, 1, 1] + [0, 0, 0, 0, 0, 0, 1, 1] + [0, 0, 0, 0, 0, 0, 0, 1]], dtype=np.bool_ + ) + self.assertTrue(np.array_equal(bit_array.to_bool_array(), expected_array)) + + bit_array = BitArray(u_8([[7, 3, 1]]), 21) + expected_array = np.array( + [[1, 0, 0, 0, 0, 0, 0, 0] + [1, 1, 0, 0, 0, 0, 0, 0] + [1, 1, 1, 0, 0]], dtype=np.bool_ + ) + self.assertTrue(np.array_equal(bit_array.to_bool_array(order="little"), expected_array)) + + with self.assertRaisesRegex(ValueError, "Invalid value for order"): + bit_array.to_bool_array(order="invalid") + @ddt.data("counts", "int", "hex", "bit") def test_from_counts(self, counts_type): """Test the from_counts static constructor.""" diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 62845461dacf..49d5efa2d782 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -440,7 +440,7 @@ def test_layout(self, backend): estimator.set_transpile_options(seed_transpiler=15, optimization_level=1) value = estimator.run(qc, op, shots=10000).result().values[0] if optionals.HAS_AER: - ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.916 + ref_value = -0.9954 if isinstance(backend, GenericBackendV2) else -0.934 else: ref_value = -1 self.assertEqual(value, ref_value) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index acae1d7d3cd7..1b9c24ca5410 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2665,6 +2665,23 @@ def test_switch_v1_expr_target(self): test = dumps(qc, experimental=ExperimentalFeatures.SWITCH_CASE_V1) self.assertEqual(test, expected) + def test_circuit_with_unitary(self): + """Test that circuits with `unitary` gate are correctly handled""" + matrix = [[0, 1], [1, 0]] + qc = QuantumCircuit(1) + qc.unitary(matrix, [0]) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +gate unitary _gate_q_0 { + U(pi, -pi, 0) _gate_q_0; +} +qubit[1] q; +unitary q[0]; +""" + test = dumps(qc) + self.assertEqual(test, expected) + @ddt class TestQASM3ExporterFailurePaths(QiskitTestCase): diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 253bc15852b6..177f9a9ed77d 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -473,7 +473,12 @@ def test_from_circuit_with_all_types(self): # and even circuits with other clifford objects. linear_function = LinearFunction([[0, 1], [1, 1]]) pauli_gate = PauliGate("YZ") - cliff = random_clifford(2, seed=777) + + qc_cliff = QuantumCircuit(2) + qc_cliff.h(0) + qc_cliff.cx(0, 1) + cliff = Clifford(qc_cliff) + qc = QuantumCircuit(2) qc.cx(0, 1) qc.append(random_clifford(1, seed=999), [1]) @@ -493,8 +498,8 @@ def test_from_circuit_with_all_types(self): # Additionally, make sure that it produces the correct clifford. expected_clifford_dict = { - "stabilizer": ["-IZX", "+XXZ", "-YYZ"], - "destabilizer": ["-YYI", "-XZI", "-ZXY"], + "stabilizer": ["-IZX", "+ZYZ", "+XZI"], + "destabilizer": ["+XZZ", "-XII", "+IXY"], } expected_clifford = Clifford.from_dict(expected_clifford_dict) self.assertEqual(combined_clifford, expected_clifford) diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py index 91568ac32a4b..4365fdffc4f2 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli.py @@ -30,7 +30,7 @@ CYGate, CZGate, ECRGate, - EfficientSU2, + efficient_su2, HGate, IGate, SdgGate, @@ -515,7 +515,7 @@ def test_circuit_with_bit(self): def test_apply_layout_with_transpile(self): """Test the apply_layout method with a transpiler layout.""" - psi = EfficientSU2(4, reps=4, entanglement="circular") + psi = efficient_su2(4, reps=4, entanglement="circular") op = Pauli("IZZZ") backend = GenericBackendV2(num_qubits=7) transpiled_psi = transpile(psi, backend, optimization_level=3, seed_transpiler=12345) @@ -530,7 +530,7 @@ def test_apply_layout_with_transpile(self): def test_apply_layout_consistency(self): """Test that the Pauli apply_layout() is consistent with the SparsePauliOp apply_layout().""" - psi = EfficientSU2(4, reps=4, entanglement="circular") + psi = efficient_su2(4, reps=4, entanglement="circular") op = Pauli("IZZZ") sparse_op = SparsePauliOp(op) backend = GenericBackendV2(num_qubits=7) @@ -541,7 +541,7 @@ def test_apply_layout_consistency(self): def test_permute_pauli_estimator_example(self): """Test using the apply_layout method with an estimator workflow.""" - psi = EfficientSU2(4, reps=4, entanglement="circular") + psi = efficient_su2(4, reps=4, entanglement="circular") op = Pauli("XXXI") backend = GenericBackendV2(num_qubits=7, seed=0) backend.set_options(seed_simulator=123) diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py index 8c96f63c4ddf..9abc473dc333 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -1560,6 +1560,15 @@ def test_insert(self): value1 = pauli.insert(1, insert) self.assertEqual(value1, target1) + # Insert single column to length-1 PauliList: + with self.subTest(msg="length-1, single-column, single-val"): + pauli = PauliList(["X"]) + insert = PauliList(["Y"]) + target0 = PauliList(["YX"]) + value0 = pauli.insert(1, insert, qubit=True) + self.assertEqual(value0, target0) + self.assertEqual(value0.phase.shape, (1,)) + # Insert single column pauli = PauliList(["X", "Y", "Z", "-iI"]) for i in ["I", "X", "Y", "Z", "iY"]: diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 3f96cd32e15f..3602492030c4 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -23,7 +23,7 @@ from qiskit import QiskitError from qiskit.circuit import Parameter, ParameterExpression, ParameterVector -from qiskit.circuit.library import EfficientSU2 +from qiskit.circuit.library import efficient_su2 from qiskit.circuit.parametertable import ParameterView from qiskit.compiler.transpiler import transpile from qiskit.primitives import BackendEstimator @@ -140,6 +140,11 @@ def test_sparse_pauli_op_init(self): coeffs[:] = 0 self.assertEqual(spp_op, ref_op) + def test_sparse_pauli_op_init_long_ys(self): + """Test heavy-weight SparsePauliOp initialization.""" + y = SparsePauliOp("Y" * 1000) + self.assertEqual(1, y.coeffs[0]) + @ddt.ddt class TestSparsePauliOpConversions(QiskitTestCase): @@ -1148,7 +1153,7 @@ def test_paulis_setter_absorbs_phase_2(self): def test_apply_layout_with_transpile(self): """Test the apply_layout method with a transpiler layout.""" - psi = EfficientSU2(4, reps=4, entanglement="circular") + psi = efficient_su2(4, reps=4, entanglement="circular") op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) backend = GenericBackendV2(num_qubits=7) transpiled_psi = transpile(psi, backend, optimization_level=3, seed_transpiler=12345) @@ -1163,7 +1168,7 @@ def test_apply_layout_with_transpile(self): def test_permute_sparse_pauli_op_estimator_example(self): """Test using the apply_layout method with an estimator workflow.""" - psi = EfficientSU2(4, reps=4, entanglement="circular") + psi = efficient_su2(4, reps=4, entanglement="circular") op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) backend = GenericBackendV2(num_qubits=7, seed=0) backend.set_options(seed_simulator=123) diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index 7bb0db110ac1..dfeb940809d6 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -28,7 +28,7 @@ from qiskit import QiskitError from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import library -from qiskit.circuit.library import HGate, CHGate, CXGate, QFT +from qiskit.circuit.library import HGate, CHGate, CXGate, QFTGate from qiskit.transpiler import CouplingMap from qiskit.transpiler.layout import Layout, TranspileLayout from qiskit.quantum_info.operators import Operator, ScalarOp @@ -743,7 +743,7 @@ def test_equiv(self): def test_reverse_qargs(self): """Test reverse_qargs method""" - circ1 = QFT(5) + circ1 = QFTGate(5).definition circ2 = circ1.reverse_bits() state1 = Operator(circ1) @@ -752,7 +752,7 @@ def test_reverse_qargs(self): def test_drawings(self): """Test draw method""" - qc1 = QFT(5) + qc1 = QFTGate(5).definition op = Operator.from_circuit(qc1) with self.subTest(msg="str(operator)"): str(op) diff --git a/test/python/quantum_info/operators/test_random.py b/test/python/quantum_info/operators/test_random.py index cb7e85ffabe3..bb1e65ba3d7d 100644 --- a/test/python/quantum_info/operators/test_random.py +++ b/test/python/quantum_info/operators/test_random.py @@ -190,6 +190,36 @@ def test_not_global_seed(self): rng_after = np.random.randint(1000, size=test_cases) self.assertFalse(np.all(rng_before == rng_after)) + def test_cliffords_2q(self): + """Test that we get all 2-qubit Cliffords (actually symplectic + matrices) with sufficiently many trials. + """ + seen = set() + for seed in range(10000): + cliff = random_clifford(2, seed) + seen.add(cliff.symplectic_matrix.tobytes()) + self.assertEqual(len(seen), 720) + + def test_clifford_2q_decompositions(self): + """Test that we get all possible CX-counts for 2q-random cliffords + with sufficiently many trials. + """ + seen = set() + for seed in range(100): + cliff = random_clifford(2, seed) + seen.add(cliff.to_circuit().count_ops().get("cx", 0)) + self.assertEqual(seen, {0, 1, 2, 3}) + + def test_clifford_3q_decompositions(self): + """Test that we get all possible CX-counts for 3q-random cliffords + with sufficiently many trials. + """ + seen = set() + for seed in range(10000): + cliff = random_clifford(3, seed) + seen.add(cliff.to_circuit().count_ops().get("cx", 0)) + self.assertEqual(seen, {0, 1, 2, 3, 4, 5, 6}) + @ddt class TestRandomPauliList(QiskitTestCase): diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index cf6ad3c35090..e68470ccfc10 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -20,7 +20,7 @@ from numpy.testing import assert_allclose from qiskit import QiskitError, QuantumCircuit, QuantumRegister -from qiskit.circuit.library import QFT, HGate +from qiskit.circuit.library import QFTGate, HGate from qiskit.quantum_info.operators.operator import Operator from qiskit.quantum_info.operators.symplectic import Pauli, SparsePauliOp from qiskit.quantum_info.random import random_density_matrix, random_pauli, random_unitary @@ -1186,7 +1186,7 @@ def test_expval_pauli_qargs(self, qubits): def test_reverse_qargs(self): """Test reverse_qargs method""" - circ1 = QFT(5) + circ1 = QFTGate(5).definition circ2 = circ1.reverse_bits() state1 = DensityMatrix.from_instruction(circ1) @@ -1197,7 +1197,7 @@ def test_reverse_qargs(self): @unittest.skipUnless(optionals.HAS_PYLATEX, "requires pylatexenc") def test_drawings(self): """Test draw method""" - qc1 = QFT(5) + qc1 = QFTGate(5).definition dm = DensityMatrix.from_instruction(qc1) with self.subTest(msg="str(density_matrix)"): str(dm) diff --git a/test/python/quantum_info/states/test_stabilizerstate.py b/test/python/quantum_info/states/test_stabilizerstate.py index 4e1659ff6999..f76df1134e8a 100644 --- a/test/python/quantum_info/states/test_stabilizerstate.py +++ b/test/python/quantum_info/states/test_stabilizerstate.py @@ -25,7 +25,7 @@ from qiskit.quantum_info.random import random_clifford, random_pauli from qiskit.quantum_info.states import StabilizerState, Statevector from qiskit.circuit.library import IGate, XGate, HGate -from qiskit.quantum_info.operators import Clifford, Pauli, Operator +from qiskit.quantum_info.operators import Clifford, Pauli, Operator, SparsePauliOp from test import combine # pylint: disable=wrong-import-order from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -1101,6 +1101,18 @@ def test_expval_random_subsystem(self, num_qubits): target = Statevector(qc).expectation_value(op, qargs) self.assertAlmostEqual(exp_val, target) + def test_expval_sparsepauliop(self): + """Test expectation_value method of SparsePauliOp""" + cliff = random_clifford(num_qubits=3, seed=1234) + stab = StabilizerState(cliff) + labels = ["XXX", "IXI", "YYY", "III"] + coeffs = [3.0, 5.5, -1j, 23] + spp_op = SparsePauliOp.from_list(list(zip(labels, coeffs))) + expval = stab.expectation_value(spp_op) + qc = cliff.to_circuit() + target = Statevector(qc).expectation_value(spp_op) + self.assertAlmostEqual(expval, target) + def test_stabilizer_bell_equiv(self): """Test that two circuits produce the same stabilizer group.""" diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index d16b3b3453ec..a57a20124492 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -23,7 +23,7 @@ from qiskit import QiskitError from qiskit import QuantumRegister, QuantumCircuit from qiskit import transpile -from qiskit.circuit.library import HGate, QFT, GlobalPhaseGate +from qiskit.circuit.library import HGate, QFTGate, GlobalPhaseGate from qiskit.providers.basic_provider import BasicSimulator from qiskit.utils import optionals from qiskit.quantum_info.random import random_unitary, random_statevector, random_pauli @@ -1218,7 +1218,7 @@ def test_global_phase(self): def test_reverse_qargs(self): """Test reverse_qargs method""" - circ1 = QFT(5) + circ1 = QFTGate(5).definition circ2 = circ1.reverse_bits() state1 = Statevector.from_instruction(circ1) @@ -1229,7 +1229,7 @@ def test_reverse_qargs(self): @unittest.skipUnless(optionals.HAS_PYLATEX, "requires pylatexenc") def test_drawings(self): """Test draw method""" - qc1 = QFT(5) + qc1 = QFTGate(5).definition sv = Statevector.from_instruction(qc1) with self.subTest(msg="str(statevector)"): str(sv) diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index dd6fc5800fcb..38c7d859bd53 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -1172,9 +1172,9 @@ def test_add_coercion(self): base += AllowRightArithmetic() def test_add_failures(self): - with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): _ = SparseObservable.zero(4) + SparseObservable.zero(6) - with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): _ = SparseObservable.zero(6) + SparseObservable.zero(4) def test_sub_simple(self): @@ -1283,9 +1283,9 @@ def test_sub_coercion(self): base -= AllowRightArithmetic() def test_sub_failures(self): - with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): _ = SparseObservable.zero(4) - SparseObservable.zero(6) - with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): _ = SparseObservable.zero(6) - SparseObservable.zero(4) @ddt.idata(single_cases()) diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 820c4f241a1e..06762a78f1fa 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -21,6 +21,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit import transpile from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit, Measure +from qiskit.circuit.equivalence_library import StandardEquivalenceLibrary as std_eq_lib from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( HGate, @@ -481,6 +482,22 @@ def test_different_bits(self): out = BasisTranslator(std_eqlib, basis).run(circuit_to_dag(base)) self.assertEqual(set(out.count_ops(recurse=True)), basis) + def test_correct_parameter_assignment(self): + """Test correct parameter assignment from an equivalence during translation""" + rx_key = next(key for key in std_eq_lib.keys() if key.name == "rx") + + # The circuit doesn't need to be parametric. + qc = QuantumCircuit(1) + qc.rx(0.5, 0) + + BasisTranslator( + equivalence_library=std_eq_lib, + target_basis=["cx", "id", "rz", "sx", "x"], + )(qc) + + inst = std_eq_lib._get_equivalences(rx_key)[0].circuit.data[0] + self.assertEqual(inst.params, inst.operation.params) + class TestUnrollerCompatability(QiskitTestCase): """Tests backward compatability with the Unroller pass. diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 2a39c45bc48a..0d711dcabfdf 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -400,6 +400,29 @@ def test_collect_cliffords_default(self): self.assertEqual(qct.size(), 1) self.assertIn("clifford", qct.count_ops().keys()) + def test_collect_cliffords_max_block_width(self): + """Make sure that collecting Clifford gates and replacing them by Clifford + works correctly when the option ``max_block_width`` is specified.""" + + # original circuit (consisting of Clifford gates only) + qc = QuantumCircuit(3) + qc.h(0) + qc.s(1) + qc.cx(0, 1) + qc.sdg(0) + qc.x(1) + qc.swap(2, 1) + qc.h(1) + qc.swap(1, 2) + + # We should end up with two Clifford objects + qct = PassManager(CollectCliffords(max_block_width=2)).run(qc) + self.assertEqual(qct.size(), 2) + self.assertEqual(qct[0].name, "clifford") + self.assertEqual(len(qct[0].qubits), 2) + self.assertEqual(qct[1].name, "clifford") + self.assertEqual(len(qct[1].qubits), 2) + def test_collect_cliffords_multiple_blocks(self): """Make sure that when collecting Clifford gates, non-Clifford gates are not collected, and the pass correctly splits disconnected Clifford @@ -831,6 +854,17 @@ def test_collect_all_clifford_gates(self): qct = PassManager(CollectCliffords(matrix_based=True)).run(qc) self.assertEqual(qct.count_ops()["clifford"], 1) + def test_plugin_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="clifford") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("clifford", synthesized.count_ops()) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index 2d4bc8783764..0f7eb5dd9539 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -290,6 +290,46 @@ def test_larger_blocks(self): pass_manager.run(qc) + def test_collect_from_back(self): + """Test the option to collect blocks from the outputs towards + the inputs. + ┌───┐ + q_0: ┤ H ├──■────■────■─────── + └───┘┌─┴─┐ │ │ + q_1: ─────┤ X ├──┼────┼─────── + └───┘┌─┴─┐ │ + q_2: ──────────┤ X ├──┼─────── + └───┘┌─┴─┐┌───┐ + q_3: ───────────────┤ X ├┤ H ├ + └───┘└───┘ + """ + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.h(3) + + dag = circuit_to_dag(qc) + # For the circuit above, the topological order is unique + topo_ops = list(dag.topological_op_nodes()) + + # When collecting blocks of size-3 using the default direction, + # the first block should contain the H-gate and two CX-gates, + # and the second block should contain a single CX-gate and an H-gate. + pass_ = CollectMultiQBlocks(max_block_size=3, collect_from_back=False) + pass_.run(dag) + expected_blocks = [[topo_ops[0], topo_ops[1], topo_ops[2]], [topo_ops[3], topo_ops[4]]] + self.assertEqual(pass_.property_set["block_list"], expected_blocks) + + # When collecting blocks of size-3 using the opposite direction, + # the first block should contain the H-gate and a single CX-gate, + # and the second block should contain two CX-gates and an H-gate. + pass_ = CollectMultiQBlocks(max_block_size=3, collect_from_back=True) + pass_.run(dag) + expected_blocks = [[topo_ops[0], topo_ops[1]], [topo_ops[2], topo_ops[3], topo_ops[4]]] + self.assertEqual(pass_.property_set["block_list"], expected_blocks) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 64a9b97440ba..ea783f3ff304 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -19,6 +19,7 @@ from qiskit.converters import circuit_to_dag from qiskit.circuit.library import HGate, CCXGate, U2Gate from qiskit.quantum_info.operators import Operator, Clifford + from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -352,3 +353,79 @@ def test_specify_hls_object(self): expected.h(0) self.assertEqual(expected, decomposed) + + def test_cif(self): + """Test decomposition with c_if.""" + circuit = QuantumCircuit(1, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(0, 0) + + ops = circuit.decompose().count_ops() + self.assertEqual(ops.get("u3", 0), 1) + + def test_cif_no_definition(self): + """Test decomposition with c_if when the gate has no definition. + + Regression test of #13493. + """ + circuit = QuantumCircuit(1, 1) + with self.assertWarns(DeprecationWarning): + circuit.u(1, 2, 3, 0).c_if(0, 0) + + ops = circuit.decompose().count_ops() + self.assertEqual(ops.get("u", 0), 1) + + def test_control_flow_if(self): + """Test decompose with control flow.""" + qr = QuantumRegister(2) + cr = ClassicalRegister(1) + qc = QuantumCircuit(qr, cr) + + qc.p(0.2, 0) + qc.measure(0, 0) + + with qc.if_test((cr[0], 0)) as else_: + qc.cry(0.5, 0, 1) + with else_: + qc.crz(0.5, 0, 1) + + expect = qc.copy_empty_like() + expect.u(0, 0, 0.2, 0) + expect.measure(0, 0) + + with expect.if_test((cr[0], 0)) as else_: + expect.ry(0.25, 1) + expect.cx(0, 1) + expect.ry(-0.25, 1) + expect.cx(0, 1) + with else_: + expect.rz(0.25, 1) + expect.cx(0, 1) + expect.rz(-0.25, 1) + expect.cx(0, 1) + + self.assertEqual(expect, qc.decompose()) + + def test_control_flow_for(self): + """Test decompose with control flow.""" + qr = QuantumRegister(2) + cr = ClassicalRegister(1) + qc = QuantumCircuit(qr, cr) + + qc.p(0.2, 0) + qc.measure(0, 0) + + with qc.for_loop(range(3)): + qc.cry(0.5, 0, 1) + + expect = qc.copy_empty_like() + expect.u(0, 0, 0.2, 0) + expect.measure(0, 0) + + with expect.for_loop(range(3)): + expect.ry(0.25, 1) + expect.cx(0, 1) + expect.ry(-0.25, 1) + expect.cx(0, 1) + + self.assertEqual(expect, qc.decompose()) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 11aa8501afd0..b7bc31132a48 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -827,6 +827,17 @@ def test_plugin_selection_all_with_metrix(self): self.assertEqual(qct.size(), 24) self.assertEqual(qct.depth(), 13) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="linear_function") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("linear_function", synthesized.count_ops()) + class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the KMSSynthesisLinearFunction plugin for synthesizing linear functions.""" @@ -877,6 +888,17 @@ def test_invert_and_transpose(self): self.assertEqual(qct.size(), 87) self.assertEqual(qct.depth(), 32) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="linear_function") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("linear_function", synthesized.count_ops()) + class TestTokenSwapperPermutationPlugin(QiskitTestCase): """Tests for the token swapper plugin for synthesizing permutation gates.""" @@ -1059,6 +1081,17 @@ def test_concrete_synthesis_all_permutations(self): qubits = tuple(qc_transpiled.find_bit(q).index for q in inst.qubits) self.assertIn(qubits, edges) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="permutation") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("permutation", synthesized.count_ops()) + class TestHighLevelSynthesisModifiers(QiskitTestCase): """Tests for high-level-synthesis pass.""" diff --git a/test/python/transpiler/test_linear_functions_passes.py b/test/python/transpiler/test_linear_functions_passes.py index 39997ad816ef..4435bd2bf6ea 100644 --- a/test/python/transpiler/test_linear_functions_passes.py +++ b/test/python/transpiler/test_linear_functions_passes.py @@ -532,6 +532,29 @@ def test_min_block_size(self): self.assertNotIn("linear_function", circuit4.count_ops().keys()) self.assertEqual(circuit4.count_ops()["cx"], 6) + def test_max_block_width(self): + """Test that the option max_block_width for collecting linear functions works correctly.""" + circuit = QuantumCircuit(6) + circuit.cx(0, 1) + circuit.cx(1, 2) + circuit.cx(2, 3) + circuit.cx(3, 4) + circuit.cx(4, 5) + + # When max_block_width = 3, we should obtain 3 linear blocks + circuit1 = PassManager(CollectLinearFunctions(min_block_size=1, max_block_width=3)).run( + circuit + ) + self.assertEqual(circuit1.count_ops()["linear_function"], 3) + self.assertNotIn("cx", circuit1.count_ops().keys()) + + # When max_block_width = 4, we should obtain 2 linear blocks + circuit1 = PassManager(CollectLinearFunctions(min_block_size=1, max_block_width=4)).run( + circuit + ) + self.assertEqual(circuit1.count_ops()["linear_function"], 2) + self.assertNotIn("cx", circuit1.count_ops().keys()) + @combine(do_commutative_analysis=[False, True]) def test_collect_from_back_correctness(self, do_commutative_analysis): """Test that collecting from the back of the circuit works correctly.""" diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 1ecee97a5ed9..5b23e777fac4 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -21,11 +21,17 @@ import numpy as np from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp -from qiskit.circuit.library import quantum_volume +from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp, Measure from qiskit.compiler import transpile -from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError, Target -from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate +from qiskit.transpiler import ( + CouplingMap, + Layout, + PassManager, + TranspilerError, + Target, + InstructionProperties, +) +from qiskit.circuit.library import U2Gate, U3Gate, quantum_volume, CXGate, CZGate, XGate from qiskit.transpiler.passes import ( ALAPScheduleAnalysis, PadDynamicalDecoupling, @@ -33,7 +39,7 @@ ) from qiskit.providers.fake_provider import Fake5QV1, Fake20QV1, GenericBackendV2 from qiskit.converters import circuit_to_dag -from qiskit.circuit.library import GraphState +from qiskit.circuit.library import GraphStateGate from qiskit.quantum_info import random_unitary from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.preset_passmanagers import level0, level1, level2, level3 @@ -269,7 +275,7 @@ def test_unroll_only_if_not_gates_in_basis(self): basis_gates=["id", "u1", "u2", "u3", "cx"], seed=42, ) - qv_circuit = QuantumVolume(3) + qv_circuit = quantum_volume(3) gates_in_basis_true_count = 0 consolidate_blocks_count = 0 @@ -1048,7 +1054,7 @@ def test_all_levels_use_trivial_if_perfect(self, level): adjacency_matrix = np.zeros((20, 20)) adjacency_matrix[rows, cols] = 1 - qc = GraphState(adjacency_matrix) + qc = GraphStateGate(adjacency_matrix).definition qc.measure_all() expected = { 0: Qubit(QuantumRegister(20, "q"), 0), @@ -1799,3 +1805,50 @@ def test_custom_basis_gates_raise(self, optimization_level): _ = generate_preset_pass_manager( optimization_level=optimization_level, basis_gates=basis_gates, backend=backend ) + + @data(0, 1, 2, 3) + def test_custom_measurement_subclass(self, optimization_level): + """Test that a custom measurement subclass is treated appropriately.""" + backend = GenericBackendV2(num_qubits=2) + + class CustomMeasurement(Measure): + """A custom specialized measurement.""" + + def __init__(self, label=None): + super().__init__(label=label) + self.name = "custom_measure" + + backend = GenericBackendV2(num_qubits=2, coupling_map=[[0, 1]], control_flow=True) + backend.target.add_instruction( + CustomMeasurement(), + { + (0,): InstructionProperties(error=1e-2, duration=1e-8), + (1,): InstructionProperties(error=1e-2, duration=1e-8), + }, + ) + pm = generate_preset_pass_manager(backend=backend, optimization_level=optimization_level) + qc = QuantumCircuit(2, 3) + qc.h(0) + qc.cx(0, 1) + qc.append(CustomMeasurement(), [0], [0]) + qc.append(CustomMeasurement(), [1], [1]) + qc.reset(0) + with qc.if_test((qc.clbits[0], 1)): + qc.x(0) + with qc.if_test((qc.clbits[1], 1)): + qc.z(0) + qc.measure(0, 2) + res = pm.run(qc) + counts = res.count_ops() + self.assertIn("custom_measure", counts) + self.assertEqual(counts["custom_measure"], 2) + encountered = 0 + for inst in res.data: + if inst.name == "custom_measure": + encountered += 1 + self.assertIsInstance(inst.operation, CustomMeasurement) + self.assertIsInstance(inst.operation, Measure) + + self.assertEqual( + encountered, 2, "Despite count ops no custom measurements were encountered" + ) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 2942cab30cdd..f8da34636cc7 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -18,7 +18,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit.classical import expr, types -from qiskit.circuit.library import EfficientSU2, QuantumVolume +from qiskit.circuit.library import efficient_su2, quantum_volume from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap, Unroll3qOrMore from qiskit.transpiler.exceptions import TranspilerError @@ -321,7 +321,7 @@ def test_release_valve_routes_multiple(self): Regression test of #13081. """ - qv = QuantumVolume(500, seed=42) + qv = quantum_volume(500, seed=42) qv.measure_all() qc = Unroll3qOrMore()(qv) @@ -476,7 +476,7 @@ class TestSabrePreLayout(QiskitTestCase): def setUp(self): super().setUp() - circuit = EfficientSU2(16, entanglement="circular", reps=6, flatten=True) + circuit = efficient_su2(16, entanglement="circular", reps=6) circuit.assign_parameters([math.pi / 2] * len(circuit.parameters), inplace=True) circuit.measure_all() self.circuit = circuit diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 86f1953d43a4..f7aae24eff54 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1169,6 +1169,14 @@ def test_instruction_supported_no_args(self): def test_instruction_supported_no_operation(self): self.assertFalse(self.ibm_target.instruction_supported(qargs=(0,), parameters=[math.pi])) + def test_instruction_supported_no_qubits(self): + """Checks that instruction supported works when target.num_qubits is None.""" + target = Target.from_configuration(["u", "cx", "rxx"]) + self.assertTrue(target.instruction_supported("u", (0,))) + self.assertTrue(target.instruction_supported("cx", (0, 1))) + self.assertTrue(target.instruction_supported("cx", None)) + self.assertTrue(target.instruction_supported("rxx", (2, 3))) + def test_target_serialization_preserve_variadic(self): """Checks that variadics are still seen as variadic after serialization""" diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index e763a6206d78..767b126bbb3a 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -21,10 +21,10 @@ import scipy from ddt import ddt, data -from qiskit import transpile +from qiskit import transpile, generate_preset_pass_manager from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit.library import QuantumVolume +from qiskit.circuit.library import quantum_volume from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.passes import UnitarySynthesis from qiskit.quantum_info.operators import Operator @@ -57,7 +57,9 @@ RYYGate, RZZGate, RXXGate, + PauliEvolutionGate, ) +from qiskit.quantum_info import SparsePauliOp from qiskit.circuit import Measure from qiskit.circuit.controlflow import IfElseOp from qiskit.circuit import Parameter, Gate @@ -69,8 +71,6 @@ FakeMumbaiFractionalCX, ) -from ..legacy_cmaps import YORKTOWN_CMAP - class FakeBackend2QV2(GenericBackendV2): """A 2-qubit fake backend""" @@ -110,8 +110,8 @@ def __init__(self, bidirectional=True): @ddt -class TestUnitarySynthesis(QiskitTestCase): - """Test UnitarySynthesis pass.""" +class TestUnitarySynthesisBasisGates(QiskitTestCase): + """Test UnitarySynthesis pass with basis gates.""" def test_empty_basis_gates(self): """Verify when basis_gates is None, we do not synthesize unitaries.""" @@ -124,7 +124,6 @@ def test_empty_basis_gates(self): qc.unitary(op_3q.data, [0, 1, 2]) out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc) - self.assertEqual(out.count_ops(), {"unitary": 3}) @data( @@ -146,81 +145,39 @@ def test_two_qubit_synthesis_to_basis(self, basis_gates): dag = circuit_to_dag(qc) out = UnitarySynthesis(basis_gates).run(dag) - self.assertTrue(set(out.count_ops()).issubset(basis_gates)) - def test_two_qubit_synthesis_to_directional_cx_from_gate_errors(self): + @combine(gate=["unitary", "swap"], natural_direction=[True, False]) + def test_two_qubit_synthesis_to_directional_cx(self, gate, natural_direction): """Verify two qubit unitaries are synthesized to match basis gates.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - qr = QuantumRegister(2) coupling_map = CouplingMap(conf.coupling_map) triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_swap_synthesis_to_directional_cx(self): - """Verify two qubit unitaries are synthesized to match basis gates.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() qr = QuantumRegister(2) - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) - qc.swap(qr[0], qr[1]) + if gate == "unitary": + qc.unitary(random_unitary(4, seed=12), [0, 1]) + elif gate == "swap": + qc.swap(qr[0], qr[1]) + unisynth_pass = UnitarySynthesis( basis_gates=conf.basis_gates, coupling_map=None, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self): + @data(True, False) + def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self, natural_direction): """Verify two qubit unitaries are synthesized to match basis gates across multiple registers.""" # TODO: should make check more explicit e.g. explicitly set gate @@ -239,25 +196,14 @@ def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self): coupling_map=None, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self): + @data(True, False, None) + def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction): """Verify natural cx direction is used when specified in coupling map.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend @@ -274,119 +220,22 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self): coupling_map=coupling_map, backend_props=backend.properties(), pulse_optimize=True, - natural_direction=False, + natural_direction=natural_direction, ) pm = PassManager([triv_layout_pass, unisynth_pass]) qc_out = pm.run(qc) - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_none(self): - """Verify natural cx direction is used when specified in coupling map - when natural_direction is None.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=None, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) - self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) - - def test_two_qubit_synthesis_to_directional_cx_from_coupling_map_natural_false(self): - """Verify natural cx direction is used when specified in coupling map - when natural_direction is None.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - - unisynth_pass_nat = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=False, - ) - - pm_nat = PassManager([triv_layout_pass, unisynth_pass_nat]) - qc_out_nat = pm_nat.run(qc) - # the decomposer defaults to the [1, 0] direction but the coupling - # map specifies a [0, 1] direction. Check that this is respected. - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - self.assertTrue( - all(((qr[1], qr[0]) == instr.qubits for instr in qc_out_nat.get_instructions("cx"))) - ) + if natural_direction is False: + self.assertTrue( + all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) + else: + # the decomposer defaults to the [1, 0] direction but the coupling + # map specifies a [0, 1] direction. Check that this is respected. + self.assertTrue( + all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) self.assertEqual(Operator(qc), Operator(qc_out)) - self.assertEqual(Operator(qc), Operator(qc_out_nat)) def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" @@ -430,7 +279,7 @@ def test_two_qubit_pulse_optimal_true_raises(self): with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - # this assumes iswawp pulse optimal decomposition doesn't exist + # this assumes iswap pulse optimal decomposition doesn't exist conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -449,12 +298,10 @@ def test_two_qubit_pulse_optimal_true_raises(self): pm.run(qc) def test_two_qubit_natural_direction_true_duration_fallback(self): - """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - # this assumes iswawp pulse optimal decomposition doesn't exist + """Verify fallback path when pulse_optimize==True.""" with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() - # conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) @@ -474,8 +321,9 @@ def test_two_qubit_natural_direction_true_duration_fallback(self): ) def test_two_qubit_natural_direction_true_gate_length_raises(self): - """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - # this assumes iswawp pulse optimal decomposition doesn't exist + """Verify that error is raised if preferred direction cannot be inferred + from gate lenghts/errors. + """ with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() @@ -499,7 +347,6 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" - # this assumes iswawp pulse optimal decomposition doesn't exist with self.assertWarns(DeprecationWarning): backend = Fake5QV1() conf = backend.configuration() @@ -557,8 +404,8 @@ def test_two_qubit_pulse_optimal_none_no_raise(self): self.assertLessEqual(num_ops["sx"], 14) def test_qv_natural(self): - """check that quantum volume circuit compiles for natural direction""" - qv64 = QuantumVolume(5, seed=15) + """Check that quantum volume circuit compiles for natural direction""" + qv64 = quantum_volume(5, seed=15) def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_optimize): seed = 2 @@ -619,7 +466,7 @@ def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_o @data(1, 2, 3) def test_coupling_map_transpile(self, opt): - """test natural_direction works with transpile/execute""" + """test natural_direction works with transpile""" qr = QuantumRegister(2) circ = QuantumCircuit(qr) circ.append(random_unitary(4, seed=1), [0, 1]) @@ -649,6 +496,59 @@ def test_coupling_map_transpile(self, opt): ) ) + def test_if_simple(self): + """Test a simple if statement.""" + basis_gates = {"u", "cx"} + qr = QuantumRegister(2) + cr = ClassicalRegister(2) + + qc_uni = QuantumCircuit(2) + qc_uni.h(0) + qc_uni.cx(0, 1) + qc_uni_mat = Operator(qc_uni) + + qc_true_body = QuantumCircuit(2) + qc_true_body.unitary(qc_uni_mat, [0, 1]) + + qc = QuantumCircuit(qr, cr) + qc.if_test((cr, 1), qc_true_body, [0, 1], []) + dag = circuit_to_dag(qc) + cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag) + cqc = dag_to_circuit(cdag) + cbody = cqc.data[0].operation.params[0] + self.assertEqual(cbody.count_ops().keys(), basis_gates) + self.assertEqual(qc_uni_mat, Operator(cbody)) + + def test_nested_control_flow(self): + """Test unrolling nested control flow blocks.""" + qr = QuantumRegister(2) + cr = ClassicalRegister(1) + qc_uni1 = QuantumCircuit(2) + qc_uni1.swap(0, 1) + qc_uni1_mat = Operator(qc_uni1) + + qc = QuantumCircuit(qr, cr) + with qc.for_loop(range(3)): + with qc.while_loop((cr, 0)): + qc.unitary(qc_uni1_mat, [0, 1]) + dag = circuit_to_dag(qc) + cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag) + cqc = dag_to_circuit(cdag) + cbody = cqc.data[0].operation.params[2].data[0].operation.params[0] + self.assertEqual(cbody.count_ops().keys(), {"u", "cx"}) + self.assertEqual(qc_uni1_mat, Operator(cbody)) + + def test_default_does_not_fail_on_no_syntheses(self): + qc = QuantumCircuit(1) + qc.unitary(np.eye(2), [0]) + pass_ = UnitarySynthesis(["unknown", "gates"]) + self.assertEqual(qc, pass_(qc)) + + +@ddt +class TestUnitarySynthesisTarget(QiskitTestCase): + """Test UnitarySynthesis pass with target/BackendV2.""" + @combine( opt_level=[0, 1, 2, 3], bidirectional=[True, False], @@ -673,38 +573,6 @@ def test_coupling_map_transpile_with_backendv2(self, opt_level, bidirectional): (0, 1), (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]]) ) - @data(1, 2, 3) - def test_coupling_map_unequal_durations(self, opt): - """Test direction with transpile/execute with backend durations.""" - qr = QuantumRegister(2) - circ = QuantumCircuit(qr) - circ.append(random_unitary(4, seed=1), [1, 0]) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2( - num_qubits=5, - coupling_map=YORKTOWN_CMAP, - basis_gates=["id", "rz", "sx", "x", "cx", "reset"], - calibrate_instructions=True, - pulse_channels=True, - seed=42, - ) - tqc = transpile( - circ, - backend=backend, - optimization_level=opt, - translation_method="synthesis", - layout_method="trivial", - ) - tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)} - self.assertTrue( - all( - ( - (1, 0) == (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]) - for instr in tqc.get_instructions("cx") - ) - ) - ) - @combine( opt_level=[0, 1, 2, 3], bidirectional=[True, False], @@ -841,48 +709,6 @@ def test_approximation_controlled(self): self.assertGreaterEqual(dag_100.depth(), dag_99.depth()) self.assertEqual(Operator(dag_to_circuit(dag_100)), Operator(circ)) - def test_if_simple(self): - """Test a simple if statement.""" - basis_gates = {"u", "cx"} - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - - qc_uni = QuantumCircuit(2) - qc_uni.h(0) - qc_uni.cx(0, 1) - qc_uni_mat = Operator(qc_uni) - - qc_true_body = QuantumCircuit(2) - qc_true_body.unitary(qc_uni_mat, [0, 1]) - - qc = QuantumCircuit(qr, cr) - qc.if_test((cr, 1), qc_true_body, [0, 1], []) - dag = circuit_to_dag(qc) - cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag) - cqc = dag_to_circuit(cdag) - cbody = cqc.data[0].operation.params[0] - self.assertEqual(cbody.count_ops().keys(), basis_gates) - self.assertEqual(qc_uni_mat, Operator(cbody)) - - def test_nested_control_flow(self): - """Test unrolling nested control flow blocks.""" - qr = QuantumRegister(2) - cr = ClassicalRegister(1) - qc_uni1 = QuantumCircuit(2) - qc_uni1.swap(0, 1) - qc_uni1_mat = Operator(qc_uni1) - - qc = QuantumCircuit(qr, cr) - with qc.for_loop(range(3)): - with qc.while_loop((cr, 0)): - qc.unitary(qc_uni1_mat, [0, 1]) - dag = circuit_to_dag(qc) - cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag) - cqc = dag_to_circuit(cdag) - cbody = cqc.data[0].operation.params[2].data[0].operation.params[0] - self.assertEqual(cbody.count_ops().keys(), {"u", "cx"}) - self.assertEqual(qc_uni1_mat, Operator(cbody)) - def test_mapping_control_flow(self): """Test that inner dags use proper qubit mapping.""" qr = QuantumRegister(3, "q") @@ -972,12 +798,6 @@ def __init__(self): result_qc = dag_to_circuit(result_dag) self.assertEqual(result_qc, qc) - def test_default_does_not_fail_on_no_syntheses(self): - qc = QuantumCircuit(1) - qc.unitary(np.eye(2), [0]) - pass_ = UnitarySynthesis(["unknown", "gates"]) - self.assertEqual(qc, pass_(qc)) - def test_iswap_no_cx_synthesis_succeeds(self): """Test basis set with iswap but no cx can synthesize a circuit""" target = Target() @@ -1050,6 +870,88 @@ def test_qsd(self, opt): qc_transpiled = transpile(qc, target=target, optimization_level=opt) self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data)) + def test_3q_with_measure(self): + """Test 3-qubit synthesis with measurements.""" + backend = FakeBackend5QV2() + + qc = QuantumCircuit(3, 1) + qc.unitary(np.eye(2**3), range(3)) + qc.measure(0, 0) + + qc_transpiled = transpile(qc, backend) + self.assertTrue(qc_transpiled.size, 1) + self.assertTrue(qc_transpiled.count_ops().get("measure", 0), 1) + + def test_3q_series(self): + """Test a series of 3-qubit blocks.""" + backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + + x = QuantumCircuit(3) + x.x(2) + x_mat = Operator(x) + + qc = QuantumCircuit(3) + qc.unitary(x_mat, range(3)) + qc.unitary(np.eye(2**3), range(3)) + + tqc = transpile(qc, backend, optimization_level=0, initial_layout=[0, 1, 2]) + + expected = np.kron(np.eye(2**2), x_mat) + self.assertEqual(Operator(tqc), Operator(expected)) + + def test_3q_measure_all(self): + """Regression test of #13586.""" + hamiltonian = SparsePauliOp.from_list( + [("IXX", 1), ("IYY", 1), ("IZZ", 1), ("XXI", 1), ("YYI", 1), ("ZZI", 1)] + ) + + qc = QuantumCircuit(3) + qc.x([1, 2]) + op = PauliEvolutionGate(hamiltonian, time=1) + qc.append(op.power(8), [0, 1, 2]) + qc.measure_all() + + backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + tqc = transpile(qc, backend) + + ops = tqc.count_ops() + self.assertIn("u", ops) + self.assertIn("cx", ops) + self.assertIn("measure", ops) + + def test_target_with_global_gates(self): + """Test that 2q decomposition can handle a target with global gates.""" + basis_gates = ["h", "p", "cp", "rz", "cx", "ccx", "swap"] + target = Target.from_configuration(basis_gates=basis_gates) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + bell_op = Operator(bell) + qc = QuantumCircuit(2) + qc.unitary(bell_op, [0, 1]) + + tqc = transpile(qc, target=target) + self.assertTrue(set(tqc.count_ops()).issubset(basis_gates)) + + def test_determinism(self): + """Test that the decomposition is deterministic.""" + gate_counts = {"rx": 6, "rz": 12, "iswap": 2} + basis_gates = ["rx", "rz", "iswap"] + target = Target.from_configuration(basis_gates=basis_gates) + pm = generate_preset_pass_manager(target=target, optimization_level=2, seed_transpiler=42) + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + for _ in range(10): + out = pm.run(qc) + self.assertTrue(Operator(out).equiv(qc)) + self.assertTrue(set(out.count_ops()).issubset(basis_gates)) + for basis_gate in basis_gates: + self.assertLessEqual(out.count_ops()[basis_gate], gate_counts[basis_gate]) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 1f7bb1aacc98..5257a9d1cee2 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -29,7 +29,7 @@ from qiskit.converters import circuit_to_dag from qiskit.providers.fake_provider import Fake5QV1, Fake127QPulseV1, GenericBackendV2 from qiskit.circuit import Measure -from qiskit.circuit.library import GraphState, CXGate, XGate, HGate +from qiskit.circuit.library import GraphStateGate, CXGate, XGate, HGate from qiskit.transpiler import PassManager, AnalysisPass from qiskit.transpiler.target import InstructionProperties from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager @@ -293,9 +293,9 @@ class TestVF2LayoutLattice(LayoutTestCase): cmap25 = CouplingMap.from_hexagonal_lattice(25, 25, bidirectional=False) def graph_state_from_pygraph(self, graph): - """Creates a GraphState circuit from a PyGraph""" + """Creates a GraphStateGate circuit from a PyGraph""" adjacency_matrix = rustworkx.adjacency_matrix(graph) - return GraphState(adjacency_matrix).decompose() + return GraphStateGate(adjacency_matrix).definition def test_hexagonal_lattice_graph_20_in_25(self): """A 20x20 interaction map in 25x25 coupling map""" @@ -509,10 +509,11 @@ def test_perfect_fit_Manhattan(self): rows = [x[0] for x in MANHATTAN_CMAP] cols = [x[1] for x in MANHATTAN_CMAP] - adj_matrix = numpy.zeros((65, 65)) + num_qubits = 65 + adj_matrix = numpy.zeros((num_qubits, num_qubits)) adj_matrix[rows, cols] = 1 - circuit = GraphState(adj_matrix).decompose() + circuit = GraphStateGate(adj_matrix).definition circuit.measure_all() dag = circuit_to_dag(circuit) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 592987e62dbf..9aaab695197a 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -267,7 +267,9 @@ def test_2q_circuit_5q_backend_max_trials(self): f"is >= configured max trials {max_trials}", cm.output, ) - print(pass_.property_set["VF2PostLayout_stop_reason"]) + self.assertEqual( + pass_.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND + ) self.assertLayout(dag, cmap, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index 8f33f1aa3ce8..f4cce43881fb 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -30,9 +30,9 @@ CPhaseGate, HamiltonianGate, Isometry, + iqp, ) from qiskit.circuit import Parameter, Qubit, Clbit -from qiskit.circuit.library import IQP from qiskit.quantum_info.random import random_unitary from qiskit.utils import optionals from .visualization import QiskitVisualizationTestCase @@ -295,7 +295,7 @@ def test_big_gates(self): filename = self._get_resource_path("test_latex_big_gates.tex") qr = QuantumRegister(6, "q") circuit = QuantumCircuit(qr) - circuit.append(IQP([[6, 5, 3], [5, 4, 5], [3, 5, 1]]), [0, 1, 2]) + circuit.append(iqp([[6, 5, 3], [5, 4, 5], [3, 5, 1]]), [0, 1, 2]) desired_vector = [ 1 / math.sqrt(16) * complex(0, 1), diff --git a/test/qpy_compat/run_tests.sh b/test/qpy_compat/run_tests.sh index a6ff67b14a94..f1c770809c77 100755 --- a/test/qpy_compat/run_tests.sh +++ b/test/qpy_compat/run_tests.sh @@ -42,7 +42,7 @@ python -m venv "$qiskit_venv" "$qiskit_venv/bin/pip" install -c "$repo_root/constraints.txt" "$qiskit_dev_wheel" packaging # Run all of the tests of cross-Qiskit-version compatibility. -"$qiskit_python" "$our_dir/get_versions.py" | parallel --colsep=" " bash "$our_dir/process_version.sh" -p "$qiskit_python" +"$qiskit_python" "$our_dir/get_versions.py" | parallel -j 2 --colsep=" " bash "$our_dir/process_version.sh" -p "$qiskit_python" # Test dev compatibility with itself. dev_version="$("$qiskit_python" -c 'import qiskit; print(qiskit.__version__)')" diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index b51b61cb7a90..0a2aaad0e718 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -37,6 +37,7 @@ CPhaseGate, HamiltonianGate, Isometry, + iqp, ) from qiskit.circuit.library import MCXVChain from qiskit.circuit.annotated_operation import ( @@ -46,7 +47,6 @@ PowerModifier, ) from qiskit.circuit import Parameter, Qubit, Clbit, IfElseOp, SwitchCaseOp -from qiskit.circuit.library import IQP from qiskit.circuit.classical import expr, types from qiskit.quantum_info import random_clifford from qiskit.quantum_info.random import random_unitary @@ -543,7 +543,7 @@ def test_big_gates(self): """Test large gates with params""" qr = QuantumRegister(6, "q") circuit = QuantumCircuit(qr) - circuit.append(IQP([[6, 5, 3], [5, 4, 5], [3, 5, 1]]), [0, 1, 2]) + circuit.append(iqp([[6, 5, 3], [5, 4, 5], [3, 5, 1]]), [0, 1, 2]) desired_vector = [ 1 / math.sqrt(16) * complex(0, 1), diff --git a/tools/find_deprecated.py b/tools/find_deprecated.py index a651b9ef062f..2b3e4dedd315 100755 --- a/tools/find_deprecated.py +++ b/tools/find_deprecated.py @@ -14,6 +14,7 @@ """List deprecated decorators.""" from __future__ import annotations from typing import cast, Optional +from re import findall from pathlib import Path from collections import OrderedDict, defaultdict import ast @@ -50,15 +51,15 @@ class DeprecationDecorator(Deprecation): Args: filename: where is the deprecation. - decorator_node: AST node of the decorator call. + deprecation_node: AST node of the decorator call. func_node: AST node of the decorated call. """ def __init__( - self, filename: Path, decorator_node: ast.Call, func_node: ast.FunctionDef + self, filename: Path, deprecation_node: ast.Call, func_node: ast.FunctionDef ) -> None: self.filename = filename - self.decorator_node = decorator_node + self.deprecation_node = deprecation_node self.func_node = func_node self._since: str | None = None self._pending: bool | None = None @@ -67,9 +68,11 @@ def __init__( def since(self) -> str | None: """Version since the deprecation applies.""" if not self._since: - for kwarg in self.decorator_node.keywords: + for kwarg in self.deprecation_node.keywords: if kwarg.arg == "since": - self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2]) + self._since = ".".join( + str(cast(ast.Constant, kwarg.value).value).split(".")[:2] + ) return self._since @property @@ -79,7 +82,7 @@ def pending(self) -> bool | None: self._pending = next( ( kwarg.value.value - for kwarg in self.decorator_node.keywords + for kwarg in self.deprecation_node.keywords if kwarg.arg == "pending" ), False, @@ -89,7 +92,7 @@ def pending(self) -> bool | None: @property def lineno(self) -> int: """Line number of the decorator.""" - return self.decorator_node.lineno + return self.deprecation_node.lineno @property def target(self) -> str: @@ -107,8 +110,9 @@ class DeprecationCall(Deprecation): def __init__(self, filename: Path, decorator_call: ast.Call) -> None: self.filename = filename - self.decorator_node = decorator_call + self.deprecation_node = decorator_call self.lineno = decorator_call.lineno + self.pending: bool | None = None self._target: str | None = None self._since: str | None = None @@ -116,7 +120,7 @@ def __init__(self, filename: Path, decorator_call: ast.Call) -> None: def target(self) -> str | None: """what's deprecated.""" if not self._target: - arg = self.decorator_node.args.__getitem__(0) + arg = self.deprecation_node.args.__getitem__(0) if isinstance(arg, ast.Attribute): self._target = f"{arg.value.id}.{arg.attr}" if isinstance(arg, ast.Name): @@ -127,12 +131,43 @@ def target(self) -> str | None: def since(self) -> str | None: """Version since the deprecation applies.""" if not self._since: - for kwarg in self.decorator_node.func.keywords: + for kwarg in self.deprecation_node.func.keywords: if kwarg.arg == "since": self._since = ".".join(cast(ast.Constant, kwarg.value).value.split(".")[:2]) return self._since +class DeprecationWarn(DeprecationDecorator): + """ + Deprecation via manual warning + + Args: + filename: where is the deprecation. + deprecation_node: AST node of the decorator call. + func_node: AST node of the decorated call. + """ + + @property + def since(self) -> str | None: + if not self._since: + candidates = [] + for arg in self.deprecation_node.args: + if isinstance(arg, ast.Constant): + candidates += [v.strip(".") for v in findall(r"\s+([\d.]+)", arg.value)] + self._since = (min(candidates, default=0) or "n/a") + "?" + return self._since + + @property + def pending(self) -> bool | None: + """If it is a pending deprecation.""" + if self._pending is None: + self._pending = False + for arg in self.deprecation_node.args: + if hasattr(arg, "id") and arg.id == "PendingDeprecationWarning": + self._pending = True + return self._pending + + class DecoratorVisitor(ast.NodeVisitor): """ Node visitor for finding deprecation decorator @@ -153,6 +188,19 @@ def is_deprecation_decorator(node: ast.expr) -> bool: and node.func.id.startswith("deprecate_") ) + @staticmethod + def is_deprecation_warning(node: ast.expr) -> bool: + """Check if a node is a deprecation warning""" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "warn" + ): + for arg in node.args: + if hasattr(arg, "id") and "DeprecationWarning" in arg.id: + return True + return False + @staticmethod def is_deprecation_call(node: ast.expr) -> bool: """Check if a node is a deprecation call""" @@ -164,11 +212,14 @@ def is_deprecation_call(node: ast.expr) -> bool: def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # pylint: disable=invalid-name """Visitor for function declarations""" - self.deprecations += [ - DeprecationDecorator(self.filename, cast(ast.Call, d_node), node) - for d_node in node.decorator_list - if DecoratorVisitor.is_deprecation_decorator(d_node) - ] + for d_node in node.decorator_list: + if DecoratorVisitor.is_deprecation_decorator(d_node): + self.deprecations.append( + DeprecationDecorator(self.filename, cast(ast.Call, d_node), node) + ) + for stmt in ast.walk(node): + if DecoratorVisitor.is_deprecation_warning(stmt): + self.deprecations.append(DeprecationWarn(self.filename, stmt, node)) ast.NodeVisitor.generic_visit(self, node) def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name @@ -210,7 +261,7 @@ def group_by(self, attribute_idx: str) -> None: grouped = defaultdict(list) for obj in self.deprecations: grouped[getattr(obj, attribute_idx)].append(obj) - for key in sorted(grouped.keys()): + for key in sorted(grouped.keys(), key=str): self.grouped[key] = grouped[key] @staticmethod @@ -223,7 +274,7 @@ def find_deprecations(file_name: Path) -> list[Deprecation]: return decorator_visitor.deprecations -def print_main(directory: str, pending: str) -> None: +def print_main(directory: str, pending: str, format_: str) -> None: # pylint: disable=invalid-name """Prints output""" collection = DeprecationCollection(Path(directory)) @@ -231,9 +282,9 @@ def print_main(directory: str, pending: str) -> None: DATA_JSON = LAST_TIME_MINOR = DETAILS = None try: - DATA_JSON = requests.get("https://pypi.org/pypi/qiskit-terra/json", timeout=5).json() + DATA_JSON = requests.get("https://pypi.org/pypi/qiskit/json", timeout=5).json() except requests.exceptions.ConnectionError: - print("https://pypi.org/pypi/qiskit-terra/json timeout...", file=sys.stderr) + print("https://pypi.org/pypi/qiskit/json timeout...", file=sys.stderr) if DATA_JSON: LAST_MINOR = ".".join(DATA_JSON["info"]["version"].split(".")[:2]) @@ -251,9 +302,9 @@ def print_main(directory: str, pending: str) -> None: diff_days = (LAST_TIME_MINOR - release_minor_datetime).days DETAILS = f"Released in {release_minor_date}" if diff_days: - DETAILS += f" (wrt last minor release, {round(diff_days / 30.4)} month old)" + DETAILS += f" ({round(diff_days / 30.4)} month since the last minor release)" except KeyError: - DETAILS = "Future release" + DETAILS = "Future release?" lines = [] for deprecation in deprecations: if pending == "exclude" and deprecation.pending: @@ -261,7 +312,13 @@ def print_main(directory: str, pending: str) -> None: if pending == "only" and not deprecation.pending: continue pending_arg = " - PENDING" if deprecation.pending else "" - lines.append(f" - {deprecation.location_str} ({deprecation.target}){pending_arg}") + if format_ == "console": + lines.append(f" - {deprecation.location_str} ({deprecation.target}){pending_arg}") + if format_ == "md": + lines.append(f" - `{deprecation.location_str}` (`{deprecation.target}`)") + if format_ == "md": + since_version = f"**{since_version or 'n/a'}**" + DETAILS = "" if lines: print(f"\n{since_version}: {DETAILS}") print("\n".join(lines)) @@ -283,9 +340,16 @@ def create_parser() -> argparse.ArgumentParser: default="exclude", help="show pending deprecations", ) + parser.add_argument( + "-f", + "--format", + choices=["console", "md"], + default="console", + help="format the output", + ) return parser if __name__ == "__main__": args = create_parser().parse_args() - print_main(args.directory, args.pending) + print_main(args.directory, args.pending, args.format) diff --git a/tools/verify_images.py b/tools/verify_images.py index 5a33263dd7b4..5335cd3bc4eb 100755 --- a/tools/verify_images.py +++ b/tools/verify_images.py @@ -19,64 +19,7 @@ import glob # List of allowlist files that the checker will not verify -ALLOWLIST_MISSING_ALT_TEXT = [ - "qiskit/primitives/statevector_estimator.py", - "qiskit/pulse/builder.py", - "qiskit/pulse/library/symbolic_pulses.py", - "qiskit/transpiler/layout.py", - "qiskit/transpiler/__init__.py", - "qiskit/transpiler/passes/utils/filter_op_nodes.py", - "qiskit/transpiler/passes/utils/remove_barriers.py", - "qiskit/transpiler/passes/scheduling/dynamical_decoupling.py", - "qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py", - "qiskit/transpiler/passes/routing/star_prerouting.py", - "qiskit/providers/fake_provider/__init__.py", - "qiskit/quantum_info/states/statevector.py", - "qiskit/qasm3/__init__.py", - "qiskit/converters/dag_to_circuit.py", - "qiskit/circuit/controlledgate.py", - "qiskit/circuit/operation.py", - "qiskit/circuit/quantumcircuit.py", - "qiskit/circuit/__init__.py", - "qiskit/circuit/parameter.py", - "qiskit/circuit/random/utils.py", - "qiskit/circuit/library/overlap.py", - "qiskit/circuit/library/iqp.py", - "qiskit/circuit/library/graph_state.py", - "qiskit/circuit/library/phase_estimation.py", - "qiskit/circuit/library/grover_operator.py", - "qiskit/circuit/library/quantum_volume.py", - "qiskit/circuit/library/hidden_linear_function.py", - "qiskit/circuit/library/fourier_checking.py", - "qiskit/circuit/library/__init__.py", - "qiskit/circuit/library/boolean_logic/quantum_xor.py", - "qiskit/circuit/library/boolean_logic/quantum_and.py", - "qiskit/circuit/library/boolean_logic/inner_product.py", - "qiskit/circuit/library/boolean_logic/quantum_or.py", - "qiskit/circuit/library/basis_change/qft.py", - "qiskit/circuit/library/generalized_gates/gms.py", - "qiskit/circuit/library/generalized_gates/permutation.py", - "qiskit/circuit/library/generalized_gates/gr.py", - "qiskit/circuit/library/generalized_gates/mcmt.py", - "qiskit/circuit/library/arithmetic/piecewise_chebyshev.py", - "qiskit/circuit/library/n_local/real_amplitudes.py", - "qiskit/circuit/library/n_local/evolved_operator_ansatz.py", - "qiskit/circuit/library/n_local/n_local.py", - "qiskit/circuit/library/n_local/pauli_two_design.py", - "qiskit/circuit/library/n_local/qaoa_ansatz.py", - "qiskit/circuit/library/n_local/excitation_preserving.py", - "qiskit/circuit/library/n_local/efficient_su2.py", - "qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py", - "qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py", - "qiskit/visualization/dag_visualization.py", - "qiskit/visualization/gate_map.py", - "qiskit/visualization/state_visualization.py", - "qiskit/visualization/counts_visualization.py", - "qiskit/visualization/__init__.py", - "qiskit/visualization/timeline/interface.py", - "qiskit/visualization/circuit/circuit_visualization.py", - "qiskit/visualization/pulse_v2/interface.py", -] +ALLOWLIST_MISSING_ALT_TEXT = [] def is_image(line: str) -> bool: diff --git a/tox.ini b/tox.ini index b28cb8f450c5..2809057a2ed6 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,26 @@ deps = commands = stestr run {posargs} +[testenv:rust] +basepython = python3 +package_env = .pkg-rust +setenv = + PYTHONUSERBASE={envdir} +allowlist_externals = cargo +commands = + python -c '\ + import os, subprocess, sysconfig;\ + os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [sysconfig.get_config_var("LIBDIR"), os.getenv("LD_LIBRARY_PATH")]));\ + subprocess.run(["cargo", "test", "--no-default-features"])' + +# This is a special package_env used by the 'rust' env above +# to force Qiskit's Rust code to build in debug mode. We do this +# to speed up linking and save dev time. Our Cargo tests should +# still run fast enough. +[testenv:.pkg-rust] +setenv = + RUST_DEBUG=1 + [testenv:lint] basepython = python3 # `pylint` will examine the source code, not the version that would otherwise be