diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index a773c15add50..a641ae215efd 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -92,8 +92,6 @@ jobs: # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. displayName: "Install Terra directly" - env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - ${{ if eq(parameters.installOptionals, true) }}: - bash: | @@ -178,8 +176,6 @@ jobs: sudo apt-get install -y graphviz pandoc image_tests/bin/pip check displayName: 'Install dependencies' - env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - bash: | echo "##vso[task.setvariable variable=HAVE_VISUAL_TESTS_RUN;]true" diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index b167df3f1c82..9e9620e8ecbb 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -48,8 +48,6 @@ jobs: # depending on `qiskit` will resolve correctly. pip check displayName: 'Install dependencies' - env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - ${{ if eq(parameters.installOptionals, true) }}: - bash: | diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index eed1ef3eb6f8..8d86456bd72f 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -47,8 +47,6 @@ jobs: # depending on `qiskit` will resolve correctly. pip check displayName: 'Install dependencies' - env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - ${{ if eq(parameters.installOptionals, true) }}: - bash: | diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f94ee26f4498..08ca898577d7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: true contact_links: - name: Non-API docs issues url: https://github.com/Qiskit/documentation/issues/new/choose - about: Open an issue about documentation in the Start, Build, Transpile, Verify, Run, or Migration guides sections of docs.quantum.ibm.com (non-API documentation) \ No newline at end of file + about: Open an issue about documentation in the guides and additional resources sections of docs.quantum.ibm.com (non-API documentation) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 33dc654801e1..625ea9bc1f50 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.11' - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -47,7 +47,6 @@ jobs: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Cinstrument-coverage" LLVM_PROFILE_FILE: "qiskit-%p-%m.profraw" - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Generate unittest coverage report run: | diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index f807656a84ce..1cc39893c5ec 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.11' - name: Install dependencies run: | python -m pip install -U pip setuptools wheel diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 7f503b7de735..0207b1ec51f7 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -21,8 +21,6 @@ jobs: python -m pip install -U -r requirements-dev.txt -c constraints.txt python -m pip install -c constraints.txt -e . python -m pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt - env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run all tests including slow run: stestr run env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20e40dec9824..20c40dc38321 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - # Normally we test min and max version but we can't run python 3.8 or + # Normally we test min and max version but we can't run python # 3.9 on arm64 until actions/setup-python#808 is resolved python-version: ["3.10", "3.12"] steps: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7c2b8c3f783..d3cb464bf8cc 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,35 +41,6 @@ jobs: with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }} - build_wheels_macos_arm_py38: - name: Build wheels on macOS arm - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-12] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - name: Install Python - with: - python-version: '3.10' - - uses: dtolnay/rust-toolchain@stable - with: - components: llvm-tools-preview - - name: Build wheels - uses: pypa/cibuildwheel@v2.19.2 - env: - CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin - CIBW_BUILD: cp38-macosx_universal2 cp38-macosx_arm64 - CIBW_ARCHS_MACOS: arm64 universal2 - CIBW_ENVIRONMENT: >- - CARGO_BUILD_TARGET="aarch64-apple-darwin" - PYO3_CROSS_LIB_DIR="/Library/Frameworks/Python.framework/Versions/$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')/lib/python$(python -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')" - - uses: actions/upload-artifact@v4 - with: - path: ./wheelhouse/*.whl - name: wheels-${{ matrix.os }}-arm build_wheels_32bit: name: Build wheels 32bit runs-on: ${{ matrix.os }} @@ -89,7 +60,7 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' + CIBW_SKIP: 'pp* cp36-* cp37-* cp38-* *musllinux* *amd64 *x86_64' - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl @@ -100,7 +71,7 @@ jobs: environment: release permissions: id-token: write - needs: ["build_wheels", "build_wheels_32bit", "build_wheels_macos_arm_py38"] + needs: ["build_wheels", "build_wheels_32bit"] steps: - uses: actions/download-artifact@v4 with: diff --git a/Cargo.lock b/Cargo.lock index 0a590fc1fd9f..15f5a7833ca5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -41,6 +50,15 @@ dependencies = [ "log", ] +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + [[package]] name = "approx" version = "0.5.1" @@ -64,9 +82,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" @@ -76,9 +94,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -102,22 +120,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.16.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -166,24 +184,24 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cov-mark" -version = "2.0.0-pre.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d48d8f76bd9331f19fe2aaf3821a9f9fb32c3963e1e3d6ce82a8c09cef7444a" +checksum = "0570650661aa447e7335f1d5e4f499d8e58796e617bedc9267d971e51c8b49d4" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -209,9 +227,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -263,20 +281,20 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -285,7 +303,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35da53b5a021d2484a7cc49b2ac7f2d840f8236a286f84202369bd338d761ea" dependencies = [ - "equator-macro", + "equator-macro 0.2.1", +] + +[[package]] +name = "equator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5099e7b6f0b7431c7a1c49f75929e2777693da192784f167066977a2965767af" +dependencies = [ + "equator-macro 0.4.1", ] [[package]] @@ -296,7 +323,18 @@ checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", +] + +[[package]] +name = "equator-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5322a90066ddae2b705096eb9e10c465c0498ae93bf9bdd6437415327c88e3bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -307,15 +345,15 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.19.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41543c4de4bfb32efdffdd75cbcca5ef41b800e8a811ea4a41fb9393c6ef3bc0" +checksum = "0821d176d7fd17ea91d8a5ce84c56413e11410f404e418bfd205acf40184d819" dependencies = [ "bytemuck", "coe-rs", "dbgf", "dyn-stack", - "equator", + "equator 0.4.1", "faer-entity", "gemm", "libm", @@ -335,9 +373,9 @@ dependencies = [ [[package]] name = "faer-entity" -version = "0.19.0" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab968a02be27be95de0f1ad0af901b865fa0866b6a9b553a6cc9cf7f19c2ce71" +checksum = "c9c752ab2bff6f0b9597c6a1adc0112f7fd41fb343bc5a009a6274ae9d32fd03" dependencies = [ "bytemuck", "coe-rs", @@ -495,9 +533,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -542,17 +580,23 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -567,9 +611,9 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", @@ -609,11 +653,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" -version = "0.2.154" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libm" @@ -633,9 +683,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matrixcompare" @@ -655,9 +705,9 @@ checksum = "b0bdabb30db18805d5290b3da7ceaccbddba795620b86c02145d688e04900a73" [[package]] name = "matrixmultiply" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" dependencies = [ "autocfg", "rawpointer", @@ -665,9 +715,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -693,7 +743,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f563548d38f390ef9893e4883ec38c1fb312f569e98d76bededdd91a3b41a043" dependencies = [ - "equator", + "equator 0.2.2", "nano-gemm-c32", "nano-gemm-c64", "nano-gemm-codegen", @@ -763,7 +813,8 @@ version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" dependencies = [ - "approx", + "approx 0.4.0", + "approx 0.5.1", "matrixmultiply", "num-complex", "num-integer", @@ -772,6 +823,18 @@ dependencies = [ "rayon", ] +[[package]] +name = "ndarray_einsum_beta" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b3abeae3e0637740340e0e32a9bf9308380e146ea6797950f9ff16e88d88a" +dependencies = [ + "lazy_static", + "ndarray", + "num-traits", + "regex", +] + [[package]] name = "npyz" version = "0.8.3" @@ -840,9 +903,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" [[package]] name = "oq3_lexer" @@ -912,9 +975,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -930,20 +993,20 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" dependencies = [ "memchr", "thiserror", @@ -952,9 +1015,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.10" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d" dependencies = [ "pest", "pest_generator", @@ -962,22 +1025,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.10" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174" dependencies = [ "once_cell", "pest", @@ -996,21 +1059,24 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "priority-queue" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" +checksum = "560bcab673ff7f6ca9e270c17bf3affd8a05e3bd9207f123b0d45076fd8197e8" dependencies = [ "autocfg", "equivalent", @@ -1043,18 +1109,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "pulp" -version = "0.18.21" +version = "0.18.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec8d02258294f59e4e223b41ad7e81c874aa6b15bc4ced9ba3965826da0eed5" +checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" dependencies = [ "bytemuck", "libm", @@ -1071,7 +1137,7 @@ checksum = "d315b3197b780e4873bc0e11251cb56a33f65a6032a3d39b8d1405c255513766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -1139,7 +1205,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -1148,11 +1214,11 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -1160,17 +1226,19 @@ name = "qiskit-accelerate" version = "1.3.0" dependencies = [ "ahash 0.8.11", - "approx", + "approx 0.5.1", "faer", "faer-ext", "hashbrown 0.14.5", "indexmap", "itertools 0.13.0", "ndarray", + "ndarray_einsum_beta", "num-bigint", "num-complex", "num-traits", "numpy", + "once_cell", "pulp", "pyo3", "qiskit-circuit", @@ -1187,12 +1255,18 @@ dependencies = [ name = "qiskit-circuit" version = "1.3.0" dependencies = [ + "ahash 0.8.11", + "approx 0.5.1", "bytemuck", "hashbrown 0.14.5", + "indexmap", + "itertools 0.13.0", "ndarray", "num-complex", "numpy", "pyo3", + "rayon", + "rustworkx-core", "smallvec", "thiserror", ] @@ -1231,9 +1305,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1361,13 +1435,42 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ - "bitflags 2.5.0", + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rowan" version = "0.15.15" @@ -1389,9 +1492,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rustworkx-core" @@ -1436,22 +1539,22 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -1473,9 +1576,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smol_str" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ "serde", ] @@ -1493,9 +1596,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1508,7 +1611,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "enum-as-inner", "libc", @@ -1518,9 +1621,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "text-size" @@ -1545,7 +1648,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] [[package]] @@ -1568,27 +1671,27 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "unindent" @@ -1598,9 +1701,9 @@ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -1636,11 +1739,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1679,7 +1782,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1699,18 +1811,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1721,9 +1833,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1733,9 +1845,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1745,15 +1857,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1763,9 +1875,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1775,9 +1887,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1787,9 +1899,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1799,9 +1911,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "xshell" @@ -1826,20 +1938,21 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.77", ] diff --git a/Cargo.toml b/Cargo.toml index 13ea3ead5584..15f34603c61e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,23 +14,27 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] -bytemuck = "1.16" -indexmap.version = "2.4.0" -hashbrown.version = "0.14.0" +bytemuck = "1.18" +indexmap.version = "2.5.0" +hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" thiserror = "1.0" +rustworkx-core = "0.15" +approx = "0.5" +itertools = "0.13.0" ahash = "0.8.11" +rayon = "1.10" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python # 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.21.2", features = ["abi3-py38"] } +pyo3 = { version = "0.21.2", features = ["abi3-py39"] } # These are our own crates. qiskit-accelerate = { path = "crates/accelerate" } diff --git a/asv.conf.json b/asv.conf.json index 70dd3b760e2c..a75bc0c59c3a 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -17,7 +17,7 @@ "dvcs": "git", "environment_type": "virtualenv", "show_commit_url": "http://github.com/Qiskit/qiskit/commit/", - "pythons": ["3.8", "3.9", "3.10", "3.11", "3.12"], + "pythons": ["3.9", "3.10", "3.11", "3.12"], "benchmark_dir": "test/benchmarks", "env_dir": ".asv/env", "results_dir": ".asv/results" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c3154abf412c..49df19808498 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -37,12 +37,12 @@ parameters: - name: "supportedPythonVersions" displayName: "All supported versions of Python" type: object - default: ["3.8", "3.9", "3.10", "3.11", "3.12"] + default: ["3.9", "3.10", "3.11", "3.12"] - name: "minimumPythonVersion" displayName: "Minimum supported version of Python" type: string - default: "3.8" + default: "3.9" - name: "maximumPythonVersion" displayName: "Maximum supported version of Python" diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 854c1ed05706..4c570f4a5285 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -10,7 +10,7 @@ name = "qiskit_accelerate" doctest = false [dependencies] -rayon = "1.10" +rayon.workspace = true numpy.workspace = true rand = "0.8" rand_pcg = "0.3" @@ -18,12 +18,14 @@ rand_distr = "0.4.3" ahash.workspace = true num-traits = "0.2" num-complex.workspace = true +rustworkx-core.workspace = true num-bigint.workspace = true -rustworkx-core = "0.15" -faer = "0.19.1" -itertools = "0.13.0" +faer = "0.19.3" +itertools.workspace = true qiskit-circuit.workspace = true thiserror.workspace = true +ndarray_einsum_beta = "0.7" +once_cell = "1.20.0" [dependencies.smallvec] workspace = true @@ -38,7 +40,7 @@ workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] -version = "0.5" +workspace = true features = ["num-complex"] [dependencies.hashbrown] @@ -54,5 +56,5 @@ version = "0.2.0" features = ["ndarray"] [dependencies.pulp] -version = "0.18.21" +version = "0.18.22" features = ["macro"] diff --git a/crates/accelerate/src/check_map.rs b/crates/accelerate/src/check_map.rs new file mode 100644 index 000000000000..da0592093817 --- /dev/null +++ b/crates/accelerate/src/check_map.rs @@ -0,0 +1,98 @@ +// 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 hashbrown::HashSet; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::imports::CIRCUIT_TO_DAG; +use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::Qubit; + +fn recurse<'py>( + py: Python<'py>, + dag: &'py DAGCircuit, + edge_set: &'py HashSet<[u32; 2]>, + wire_map: Option<&'py [Qubit]>, +) -> PyResult> { + let check_qubits = |qubits: &[Qubit]| -> bool { + match wire_map { + Some(wire_map) => { + let mapped_bits = [ + wire_map[qubits[0].0 as usize], + wire_map[qubits[1].0 as usize], + ]; + edge_set.contains(&[mapped_bits[0].into(), mapped_bits[1].into()]) + } + 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.0 as usize], + 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], + ))); + } + } + } + Ok(None) +} + +#[pyfunction] +pub fn check_map( + py: Python, + dag: &DAGCircuit, + edge_set: HashSet<[u32; 2]>, +) -> PyResult> { + recurse(py, dag, &edge_set, None) +} + +pub fn check_map_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(check_map))?; + Ok(()) +} diff --git a/crates/accelerate/src/circuit_library/entanglement.rs b/crates/accelerate/src/circuit_library/entanglement.rs new file mode 100644 index 000000000000..fbfb5c0193f1 --- /dev/null +++ b/crates/accelerate/src/circuit_library/entanglement.rs @@ -0,0 +1,261 @@ +// 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 itertools::Itertools; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::{ + types::{PyAnyMethods, PyInt, PyList, PyListMethods, PyString, PyTuple}, + Bound, PyAny, PyResult, +}; + +use crate::QiskitError; + +/// Get all-to-all entanglement. For 4 qubits and block size 2 we have: +/// [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] +fn full(num_qubits: u32, block_size: u32) -> impl Iterator> { + (0..num_qubits).combinations(block_size as usize) +} + +/// Get a linear entanglement structure. For ``n`` qubits and block size ``m`` we have: +/// [(0..m-1), (1..m), (2..m+1), ..., (n-m..n-1)] +fn linear(num_qubits: u32, block_size: u32) -> impl DoubleEndedIterator> { + (0..num_qubits - block_size + 1) + .map(move |start_index| (start_index..start_index + block_size).collect()) +} + +/// Get a reversed linear entanglement. This is like linear entanglement but in reversed order: +/// [(n-m..n-1), ..., (1..m), (0..m-1)] +/// This is particularly interesting, as CX+"full" uses n(n-1)/2 gates, but operationally equals +/// CX+"reverse_linear", which needs only n-1 gates. +fn reverse_linear(num_qubits: u32, block_size: u32) -> impl Iterator> { + linear(num_qubits, block_size).rev() +} + +/// Return the qubit indices for circular entanglement. This is defined as tuples of length ``m`` +/// starting at each possible index ``(0..n)``. Historically, Qiskit starts with index ``n-m+1``. +/// This is probably easiest understood for a concerete example of 4 qubits and block size 3: +/// [(2,3,0), (3,0,1), (0,1,2), (1,2,3)] +fn circular(num_qubits: u32, block_size: u32) -> Box>> { + if block_size == 1 || num_qubits == block_size { + Box::new(linear(num_qubits, block_size)) + } else { + let historic_offset = num_qubits - block_size + 1; + Box::new((0..num_qubits).map(move |start_index| { + (0..block_size) + .map(|i| (historic_offset + start_index + i) % num_qubits) + .collect() + })) + } +} + +/// Get pairwise entanglement. This is typically used on 2 qubits and only has a depth of 2, as +/// first all odd pairs, and then even pairs are entangled. For example on 6 qubits: +/// [(0, 1), (2, 3), (4, 5), /* now the even pairs */ (1, 2), (3, 4)] +fn pairwise(num_qubits: u32) -> impl Iterator> { + // for Python-folks (like me): pairwise is equal to linear[::2] + linear[1::2] + linear(num_qubits, 2) + .step_by(2) + .chain(linear(num_qubits, 2).skip(1).step_by(2)) +} + +/// The shifted, circular, alternating (sca) entanglement is motivated from circuits 14/15 of +/// https://arxiv.org/abs/1905.10876. It corresponds to circular entanglement, with the difference +/// that in each layer (controlled by ``offset``) the entanglement gates are shifted by one, plus +/// in each second layer, the entanglement gate is turned upside down. +/// Offset 0 -> [(2,3,0), (3,0,1), (0,1,2), (1,2,3)] +/// Offset 1 -> [(3,2,1), (0,3,2), (1,0,3), (2,1,0)] +/// Offset 2 -> [(0,1,2), (1,2,3), (2,3,0), (3,0,1)] +/// ... +fn shift_circular_alternating( + num_qubits: u32, + block_size: u32, + offset: usize, +) -> Box>> { + // index at which we split the circular iterator -- we use Python-like indexing here, + // and define ``split`` as equivalent to a Python index of ``-offset`` + let split = (num_qubits - (offset as u32 % num_qubits)) % num_qubits; + let shifted = circular(num_qubits, block_size) + .skip(split as usize) + .chain(circular(num_qubits, block_size).take(split as usize)); + if offset % 2 == 0 { + Box::new(shifted) + } else { + // if the offset is odd, reverse the indices inside the qubit block (e.g. turn CX + // gates upside down) + Box::new(shifted.map(|indices| indices.into_iter().rev().collect())) + } +} + +/// Get an entangler map for an arbitrary number of qubits. +/// +/// Args: +/// num_qubits: The number of qubits of the circuit. +/// block_size: The number of qubits of the entangling block. +/// entanglement: The entanglement strategy as string. +/// offset: The block offset, can be used if the entanglements differ per block, +/// for example used in the "sca" mode. +/// +/// Returns: +/// The entangler map using mode ``entanglement`` to scatter a block of ``block_size`` +/// qubits on ``num_qubits`` qubits. +pub fn get_entanglement_from_str( + num_qubits: u32, + block_size: u32, + entanglement: &str, + offset: usize, +) -> PyResult>>> { + if num_qubits == 0 || block_size == 0 { + return Ok(Box::new(std::iter::empty())); + } + + if block_size > num_qubits { + return Err(QiskitError::new_err(format!( + "block_size ({}) cannot be larger than number of qubits ({})", + block_size, num_qubits + ))); + } + + match (entanglement, block_size) { + ("full", _) => Ok(Box::new(full(num_qubits, block_size))), + ("linear", _) => Ok(Box::new(linear(num_qubits, block_size))), + ("reverse_linear", _) => Ok(Box::new(reverse_linear(num_qubits, block_size))), + ("sca", _) => Ok(shift_circular_alternating(num_qubits, block_size, offset)), + ("circular", _) => Ok(circular(num_qubits, block_size)), + ("pairwise", 1) => Ok(Box::new(linear(num_qubits, 1))), + ("pairwise", 2) => Ok(Box::new(pairwise(num_qubits))), + ("pairwise", _) => Err(QiskitError::new_err(format!( + "block_size ({}) can be at most 2 for pairwise entanglement", + block_size + ))), + _ => Err(QiskitError::new_err(format!( + "Unsupported entanglement: {}", + entanglement + ))), + } +} + +/// Get an entangler map for an arbitrary number of qubits. +/// +/// Args: +/// num_qubits: The number of qubits of the circuit. +/// block_size: The number of qubits of the entangling block. +/// entanglement: The entanglement strategy. +/// offset: The block offset, can be used if the entanglements differ per block, +/// for example used in the "sca" mode. +/// +/// Returns: +/// The entangler map using mode ``entanglement`` to scatter a block of ``block_size`` +/// qubits on ``num_qubits`` qubits. +pub fn get_entanglement<'a>( + num_qubits: u32, + block_size: u32, + entanglement: &'a Bound, + offset: usize, +) -> PyResult>> + 'a>> { + // unwrap the callable, if it is one + let entanglement = if entanglement.is_callable() { + entanglement.call1((offset,))? + } else { + entanglement.to_owned() + }; + + if let Ok(strategy) = entanglement.downcast::() { + let as_str = strategy.to_string(); + return Ok(Box::new( + get_entanglement_from_str(num_qubits, block_size, as_str.as_str(), offset)?.map(Ok), + )); + } else if let Ok(dict) = entanglement.downcast::() { + if let Some(value) = dict.get_item(block_size)? { + let list = value.downcast::()?; + return _check_entanglement_list(list.to_owned(), block_size); + } else { + return Ok(Box::new(std::iter::empty())); + } + } else if let Ok(list) = entanglement.downcast::() { + return _check_entanglement_list(list.to_owned(), block_size); + } + Err(QiskitError::new_err( + "Entanglement must be a string or list of qubit indices.", + )) +} + +fn _check_entanglement_list<'a>( + list: Bound<'a, PyList>, + block_size: u32, +) -> PyResult>> + 'a>> { + let entanglement_iter = list.iter().map(move |el| { + let connections = el + .downcast::()? + // .expect("Entanglement must be list of tuples") // clearer error message than `?` + .iter() + .map(|index| index.downcast::()?.extract()) + .collect::, _>>()?; + + if connections.len() != block_size as usize { + return Err(QiskitError::new_err(format!( + "Entanglement {:?} does not match block size {}", + connections, block_size + ))); + } + + Ok(connections) + }); + Ok(Box::new(entanglement_iter)) +} + +/// Get the entanglement for given number of qubits and block size. +/// +/// Args: +/// num_qubits: The number of qubits to entangle. +/// block_size: The entanglement block size (e.g. 2 for CX or 3 for CCX). +/// entanglement: The entanglement strategy. This can be one of: +/// +/// * string: Available options are ``"full"``, ``"linear"``, ``"pairwise"`` +/// ``"reverse_linear"``, ``"circular"``, or ``"sca"``. +/// * list of tuples: A list of entanglements given as tuple, e.g. [(0, 1), (1, 2)]. +/// * callable: A callable that takes as input an offset as ``int`` (usually the layer +/// in the variational circuit) and returns a string or list of tuples to use as +/// entanglement in this layer. +/// +/// offset: An offset used by certain entanglement strategies (e.g. ``"sca"``) or if the +/// entanglement is given as callable. This is typically used to have different +/// entanglement structures in different layers of variational quantum circuits. +/// +/// Returns: +/// The entanglement as list of tuples. +/// +/// Raises: +/// QiskitError: In case the entanglement is invalid. +#[pyfunction] +#[pyo3(signature = (num_qubits, block_size, entanglement, offset=0))] +pub fn get_entangler_map<'py>( + py: Python<'py>, + num_qubits: u32, + block_size: u32, + entanglement: &Bound, + offset: usize, +) -> PyResult>> { + // The entanglement is Result>>>, so there's two + // levels of errors we must handle: the outer error is handled by the outer match statement, + // and the inner (Result>) is handled upon the PyTuple creation. + match get_entanglement(num_qubits, block_size, entanglement, offset) { + Ok(entanglement) => entanglement + .into_iter() + .map(|vec| match vec { + Ok(vec) => Ok(PyTuple::new_bound(py, vec)), + Err(e) => Err(e), + }) + .collect::, _>>(), + Err(e) => Err(e), + } +} diff --git a/crates/accelerate/src/circuit_library/mod.rs b/crates/accelerate/src/circuit_library/mod.rs new file mode 100644 index 000000000000..94db90ccd234 --- /dev/null +++ b/crates/accelerate/src/circuit_library/mod.rs @@ -0,0 +1,22 @@ +// 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::*; + +mod entanglement; +mod pauli_feature_map; + +pub fn circuit_library(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(pauli_feature_map::pauli_feature_map))?; + m.add_wrapped(wrap_pyfunction!(entanglement::get_entangler_map))?; + Ok(()) +} diff --git a/crates/accelerate/src/circuit_library/pauli_feature_map.rs b/crates/accelerate/src/circuit_library/pauli_feature_map.rs new file mode 100644 index 000000000000..53e84a197f1d --- /dev/null +++ b/crates/accelerate/src/circuit_library/pauli_feature_map.rs @@ -0,0 +1,346 @@ +// 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 itertools::Itertools; +use pyo3::prelude::*; +use pyo3::types::PySequence; +use pyo3::types::PyString; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::imports; +use qiskit_circuit::operations::PyInstruction; +use qiskit_circuit::operations::{add_param, multiply_param, multiply_params, Param, StandardGate}; +use qiskit_circuit::packed_instruction::PackedOperation; +use qiskit_circuit::{Clbit, Qubit}; +use smallvec::{smallvec, SmallVec}; +use std::f64::consts::PI; + +use crate::circuit_library::entanglement; +use crate::QiskitError; + +// custom math and types for a more readable code +const PI2: f64 = PI / 2.; +type Instruction = ( + PackedOperation, + SmallVec<[Param; 3]>, + Vec, + Vec, +); +type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>); + +/// Return instructions (using only StandardGate operations) to implement a Pauli evolution +/// of a given Pauli string over a given time (as Param). +/// +/// The Pauli evolution is implemented as a basis transformation to the Pauli-Z basis, +/// 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: ┤ Rx(pi/2) ├┤ X ├──■─────────────■──┤ X ├┤ Rx(-pi/2) ├ +/// └──────────┘└─┬─┘ └─┬─┘└───────────┘ +/// 2: ──────────────┼───────────────────────┼─────────────── +/// ┌───┐ │ │ ┌───┐ +/// 3: ─┤ H ├────────■───────────────────────■──┤ H ├──────── +/// └───┘ └───┘ +fn pauli_evolution( + pauli: &str, + indices: Vec, + time: Param, +) -> impl Iterator + '_ { + // Get pairs of (pauli, qubit) that are active, i.e. that are not the identity. Note that + // the rest of the code also works if there are only identities, in which case we will + // effectively return an empty iterator. + let qubits = indices.iter().map(|i| Qubit(*i)).collect_vec(); + let binding = pauli.to_lowercase(); // lowercase for convenience + let active_paulis = binding + .as_str() + .chars() + .rev() // reverse due to Qiskit's bit ordering convention + .zip(qubits) + .filter(|(p, _)| *p != 'i') + .collect_vec(); + + // get the basis change: x -> HGate, y -> RXGate(pi/2), z -> nothing + let basis_change = active_paulis + .clone() + .into_iter() + .filter(|(p, _)| *p != 'z') + .map(|(p, q)| match p { + 'x' => (StandardGate::HGate, smallvec![], smallvec![q]), + 'y' => ( + StandardGate::RXGate, + smallvec![Param::Float(PI2)], + smallvec![q], + ), + _ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out + }); + + // get the inverse basis change + let inverse_basis_change = basis_change.clone().map(|(gate, _, qubit)| match gate { + StandardGate::HGate => (gate, smallvec![], qubit), + StandardGate::RXGate => (gate, smallvec![Param::Float(-PI2)], qubit), + _ => unreachable!(), + }); + + // get the CX chain down to the target rotation qubit + let chain_down = active_paulis + .clone() + .into_iter() + .map(|(_, q)| q) + .tuple_windows() // iterates over (q[i], q[i+1]) windows + .map(|(ctrl, target)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); + + // get the CX chain up (cannot use chain_down.rev since tuple_windows is not double ended) + let chain_up = active_paulis + .clone() + .into_iter() + .rev() + .map(|(_, q)| q) + .tuple_windows() + .map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); + + // get the RZ gate on the last qubit + let last_qubit = active_paulis.last().unwrap().1; + let z_rotation = std::iter::once(( + StandardGate::PhaseGate, + smallvec![time], + smallvec![last_qubit], + )); + + // and finally chain everything together + basis_change + .chain(chain_down) + .chain(z_rotation) + .chain(chain_up) + .chain(inverse_basis_change) +} + +/// Build a Pauli feature map circuit. +/// +/// Args: +/// feature_dimension: The feature dimension (i.e. the number of qubits). +/// parameters: A parameter vector with ``feature_dimension`` elements. Taken as input +/// here to avoid a call to Python constructing the vector. +/// reps: The number of repetitions of Hadamard + evolution layers. +/// entanglement: The entanglement, given as Python string or None (defaults to "full"). +/// paulis: The Pauli strings as list of strings or None (default to ["z", "zz"]). +/// alpha: A float multiplier for rotation angles. +/// insert_barriers: Whether to insert barriers in between the Hadamard and evolution layers. +/// data_map_func: An accumulation function that takes as input a vector of parameters the +/// current gate acts on and returns a scalar. +/// +/// Returns: +/// The ``CircuitData`` to construct the Pauli feature map. +#[pyfunction] +#[pyo3(signature = (feature_dimension, parameters, *, reps=1, entanglement=None, paulis=None, alpha=2.0, insert_barriers=false, data_map_func=None))] +#[allow(clippy::too_many_arguments)] +pub fn pauli_feature_map( + py: Python, + feature_dimension: u32, + parameters: Bound, + reps: usize, + entanglement: Option<&Bound>, + paulis: Option<&Bound>, + alpha: f64, + insert_barriers: bool, + data_map_func: Option<&Bound>, +) -> PyResult { + // normalize the Pauli strings + let pauli_strings = _get_paulis(feature_dimension, paulis)?; + + // set the default value for entanglement + let default = PyString::new_bound(py, "full"); + let entanglement = entanglement.unwrap_or(&default); + + // extract the parameters from the input variable ``parameters`` + let parameter_vector = parameters + .iter()? + .map(|el| Param::extract_no_coerce(&el?)) + .collect::>>()?; + + // construct a Barrier object Python side to (possibly) add to the circuit + let packed_barrier = if insert_barriers { + Some(_get_barrier(py, feature_dimension)?) + } else { + None + }; + + // Main work: construct the circuit instructions as iterator. Each repetition is constituted + // by a layer of Hadamards and the Pauli evolutions of the specified Paulis. + // Note that we eagerly trigger errors, since the final CircuitData::from_packed_operations + // does not allow Result objects in the iterator. + let mut packed_insts: Vec = Vec::new(); + for rep in 0..reps { + // add H layer + packed_insts.extend(_get_h_layer(feature_dimension)); + + if insert_barriers { + packed_insts.push(packed_barrier.clone().unwrap()); + } + + // add evolutions + let evo_layer = _get_evolution_layer( + py, + feature_dimension, + rep, + alpha, + ¶meter_vector, + &pauli_strings, + entanglement, + data_map_func, + )?; + packed_insts.extend(evo_layer); + + // add barriers, if necessary + if insert_barriers && rep < reps - 1 { + packed_insts.push(packed_barrier.clone().unwrap()); + } + } + + CircuitData::from_packed_operations(py, feature_dimension, 0, packed_insts, Param::Float(0.0)) +} + +fn _get_h_layer(feature_dimension: u32) -> impl Iterator { + (0..feature_dimension).map(|i| { + ( + StandardGate::HGate.into(), + smallvec![], + vec![Qubit(i)], + vec![] as Vec, + ) + }) +} + +#[allow(clippy::too_many_arguments)] +fn _get_evolution_layer<'a>( + py: Python<'a>, + feature_dimension: u32, + rep: usize, + alpha: f64, + parameter_vector: &'a [Param], + pauli_strings: &'a [String], + entanglement: &'a Bound, + data_map_func: Option<&'a Bound>, +) -> PyResult> { + let mut insts: Vec = Vec::new(); + + for pauli in pauli_strings { + let block_size = pauli.len() as u32; + let entanglement = + entanglement::get_entanglement(feature_dimension, block_size, entanglement, rep)?; + + for indices in entanglement { + let indices = indices?; + let active_parameters: Vec = indices + .clone() + .iter() + .map(|i| parameter_vector[*i as usize].clone()) + .collect(); + + let angle = match data_map_func { + Some(fun) => fun.call1((active_parameters,))?.extract()?, + None => _default_reduce(py, active_parameters), + }; + + // Get the pauli evolution and map it into + // (PackedOperation, SmallVec<[Params; 3]>, Vec, Vec) + // to call CircuitData::from_packed_operations. This is needed since we might + // have to interject barriers, which are not a standard gate and prevents us + // from using CircuitData::from_standard_gates. + let evo = pauli_evolution(pauli, indices.clone(), multiply_param(&angle, alpha, py)) + .map(|(gate, params, qargs)| { + (gate.into(), params, qargs.to_vec(), vec![] as Vec) + }) + .collect::>(); + insts.extend(evo); + } + } + + Ok(insts) +} + +/// The default data_map_func for Pauli feature maps. For a parameter vector (x1, ..., xN), this +/// implements +/// (pi - x1) (pi - x2) ... (pi - xN) +/// unless there is only one parameter, in which case it returns just the value. +fn _default_reduce(py: Python, parameters: Vec) -> Param { + if parameters.len() == 1 { + parameters[0].clone() + } else { + let acc = parameters.iter().fold(Param::Float(1.0), |acc, param| { + multiply_params(acc, add_param(param, -PI, py), py) + }); + if parameters.len() % 2 == 0 { + acc + } else { + multiply_param(&acc, -1.0, py) // take care of parity + } + } +} + +/// Normalize the Pauli strings to a Vec. We first define the default, which is +/// ["z", "zz"], unless we only have a single qubit, in which case we default to ["z"]. +/// Then, ``pauli_strings`` is either set to the default, or we try downcasting to a +/// PyString->String, followed by a check whether the feature dimension is large enough +/// for the Pauli (e.g. we cannot implement a "zzz" Pauli on a 2 qubit circuit). +fn _get_paulis( + feature_dimension: u32, + paulis: Option<&Bound>, +) -> PyResult> { + let default_pauli: Vec = if feature_dimension == 1 { + vec!["z".to_string()] + } else { + vec!["z".to_string(), "zz".to_string()] + }; + + paulis.map_or_else( + || Ok(default_pauli), // use Ok() since we might raise an error in the other arm + |v| { + let v = PySequenceMethods::to_list(v)?; // sequence to list + v.iter() // iterate over the list of Paulis + .map(|el| { + // Get the string and check whether it fits the feature dimension + let as_string = (*el.downcast::()?).to_string(); + if as_string.len() > feature_dimension as usize { + Err(QiskitError::new_err(format!( + "feature_dimension ({}) smaller than the Pauli ({})", + feature_dimension, as_string + ))) + } else { + Ok(as_string) + } + }) + .collect::>>() + }, + ) +} + +/// Get a barrier object from Python space. +fn _get_barrier(py: Python, feature_dimension: u32) -> PyResult { + let barrier_cls = imports::BARRIER.get_bound(py); + let barrier = barrier_cls.call1((feature_dimension,))?; + let barrier_inst = PyInstruction { + qubits: feature_dimension, + clbits: 0, + params: 0, + op_name: "barrier".to_string(), + control_flow: false, + instruction: barrier.into(), + }; + Ok(( + barrier_inst.into(), + smallvec![], + (0..feature_dimension).map(Qubit).collect(), + vec![] as Vec, + )) +} diff --git a/crates/accelerate/src/commutation_analysis.rs b/crates/accelerate/src/commutation_analysis.rs new file mode 100644 index 000000000000..a29c648a5f81 --- /dev/null +++ b/crates/accelerate/src/commutation_analysis.rs @@ -0,0 +1,191 @@ +// 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::exceptions::PyValueError; +use pyo3::prelude::PyModule; +use pyo3::{pyfunction, wrap_pyfunction, Bound, PyResult, Python}; +use qiskit_circuit::Qubit; + +use crate::commutation_checker::CommutationChecker; +use hashbrown::HashMap; +use pyo3::prelude::*; + +use pyo3::types::{PyDict, PyList}; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +// Custom types to store the commutation sets and node indices, +// see the docstring below for more information. +type CommutationSet = HashMap>>; +type NodeIndices = HashMap<(NodeIndex, Wire), usize>; + +// the maximum number of qubits we check commutativity for +const MAX_NUM_QUBITS: u32 = 3; + +/// Compute the commutation sets for a given DAG. +/// +/// We return two HashMaps: +/// * {wire: commutation_sets}: For each wire, we keep a vector of index sets, where each index +/// set contains mutually commuting nodes. Note that these include the input and output nodes +/// which do not commute with anything. +/// * {(node, wire): index}: For each (node, wire) pair we store the index indicating in which +/// commutation set the node appears on a given wire. +/// +/// For example, if we have a circuit +/// +/// |0> -- X -- SX -- Z (out) +/// 0 2 3 4 1 <-- node indices including input (0) and output (1) nodes +/// +/// Then we would have +/// +/// commutation_set = {0: [[0], [2, 3], [4], [1]]} +/// node_indices = {(0, 0): 0, (1, 0): 3, (2, 0): 1, (3, 0): 1, (4, 0): 2} +/// +pub(crate) fn analyze_commutations_inner( + py: Python, + dag: &mut DAGCircuit, + commutation_checker: &mut CommutationChecker, +) -> PyResult<(CommutationSet, NodeIndices)> { + let mut commutation_set: CommutationSet = HashMap::new(); + let mut node_indices: NodeIndices = HashMap::new(); + + for qubit in 0..dag.num_qubits() { + let wire = Wire::Qubit(Qubit(qubit as u32)); + + for current_gate_idx in dag.nodes_on_wire(py, &wire, false) { + // get the commutation set associated with the current wire, or create a new + // index set containing the current gate + let commutation_entry = commutation_set + .entry(wire.clone()) + .or_insert_with(|| vec![vec![current_gate_idx]]); + + // we can unwrap as we know the commutation entry has at least one element + let last = commutation_entry.last_mut().unwrap(); + + // if the current gate index is not in the set, check whether it commutes with + // the previous nodes -- if yes, add it to the commutation set + if !last.contains(¤t_gate_idx) { + let mut all_commute = true; + + for prev_gate_idx in last.iter() { + // 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]) + { + let op1 = packed_inst0.op.view(); + let op2 = packed_inst1.op.view(); + let params1 = packed_inst0.params_view(); + let params2 = packed_inst1.params_view(); + let qargs1 = dag.get_qargs(packed_inst0.qubits); + let qargs2 = dag.get_qargs(packed_inst1.qubits); + let cargs1 = dag.get_cargs(packed_inst0.clbits); + let cargs2 = dag.get_cargs(packed_inst1.clbits); + + all_commute = commutation_checker.commute_inner( + py, + &op1, + params1, + &packed_inst0.extra_attrs, + qargs1, + cargs1, + &op2, + params2, + &packed_inst1.extra_attrs, + qargs2, + cargs2, + MAX_NUM_QUBITS, + )?; + if !all_commute { + break; + } + } else { + all_commute = false; + break; + } + } + + if all_commute { + // all commute, add to current list + last.push(current_gate_idx); + } else { + // does not commute, create new list + commutation_entry.push(vec![current_gate_idx]); + } + } + + node_indices.insert( + (current_gate_idx, wire.clone()), + commutation_entry.len() - 1, + ); + } + } + + Ok((commutation_set, node_indices)) +} + +#[pyfunction] +#[pyo3(signature = (dag, commutation_checker))] +pub(crate) fn analyze_commutations( + py: Python, + dag: &mut DAGCircuit, + commutation_checker: &mut CommutationChecker, +) -> PyResult> { + // This returns two HashMaps: + // * The commuting nodes per wire: {wire: [commuting_nodes_1, commuting_nodes_2, ...]} + // * The index in which commutation set a given node is located on a wire: {(node, wire): index} + // 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); + + // 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), + _ => return Err(PyValueError::new_err("Unexpected wire type.")), + }; + + out_dict.set_item( + py_wire, + PyList::new_bound( + py, + commutations.iter().map(|inner| { + PyList::new_bound( + py, + inner + .iter() + .map(|node_index| dag.get_node(py, *node_index).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), + _ => return Err(PyValueError::new_err("Unexpected wire type.")), + }; + out_dict.set_item((dag.get_node(py, node_index)?, py_wire), index)?; + } + + Ok(out_dict.unbind()) +} + +pub fn commutation_analysis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(analyze_commutations))?; + Ok(()) +} diff --git a/crates/accelerate/src/commutation_cancellation.rs b/crates/accelerate/src/commutation_cancellation.rs new file mode 100644 index 000000000000..8e4e9281e103 --- /dev/null +++ b/crates/accelerate/src/commutation_cancellation.rs @@ -0,0 +1,279 @@ +// 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 std::f64::consts::PI; + +use hashbrown::{HashMap, HashSet}; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; +use pyo3::{pyfunction, wrap_pyfunction, Bound, PyResult, Python}; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use smallvec::{smallvec, SmallVec}; + +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use qiskit_circuit::operations::StandardGate::{ + CXGate, CYGate, CZGate, HGate, PhaseGate, RXGate, RZGate, SGate, TGate, U1Gate, XGate, YGate, + ZGate, +}; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; +use qiskit_circuit::Qubit; + +use crate::commutation_analysis::analyze_commutations_inner; +use crate::commutation_checker::CommutationChecker; +use crate::{euler_one_qubit_decomposer, QiskitError}; + +const _CUTOFF_PRECISION: f64 = 1e-5; +static ROTATION_GATES: [&str; 4] = ["p", "u1", "rz", "rx"]; +static HALF_TURNS: [&str; 2] = ["z", "x"]; +static QUARTER_TURNS: [&str; 1] = ["s"]; +static EIGHTH_TURNS: [&str; 1] = ["t"]; + +static VAR_Z_MAP: [(&str, StandardGate); 3] = [("rz", RZGate), ("p", PhaseGate), ("u1", U1Gate)]; +static Z_ROTATIONS: [StandardGate; 6] = [PhaseGate, ZGate, U1Gate, RZGate, TGate, SGate]; +static X_ROTATIONS: [StandardGate; 2] = [XGate, RXGate]; +static SUPPORTED_GATES: [StandardGate; 5] = [CXGate, CYGate, CZGate, HGate, YGate]; + +#[derive(Hash, Eq, PartialEq, Debug)] +enum GateOrRotation { + Gate(StandardGate), + ZRotation, + XRotation, +} +#[derive(Hash, Eq, PartialEq, Debug)] +struct CancellationSetKey { + gate: GateOrRotation, + qubits: SmallVec<[Qubit; 2]>, + com_set_index: usize, + second_index: Option, +} + +#[pyfunction] +#[pyo3(signature = (dag, commutation_checker, basis_gates=None))] +pub(crate) fn cancel_commutations( + py: Python, + dag: &mut DAGCircuit, + commutation_checker: &mut CommutationChecker, + basis_gates: Option>, +) -> PyResult<()> { + let basis: HashSet = if let Some(basis) = basis_gates { + basis + } else { + HashSet::new() + }; + let z_var_gate = dag + .get_op_counts() + .keys() + .find_map(|g| { + VAR_Z_MAP + .iter() + .find(|(key, _)| *key == g.as_str()) + .map(|(_, gate)| gate) + }) + .or_else(|| { + basis.iter().find_map(|g| { + VAR_Z_MAP + .iter() + .find(|(key, _)| *key == g.as_str()) + .map(|(_, gate)| gate) + }) + }); + // Fallback to the first matching key from basis if there is no match in dag.op_names + + // Gate sets to be cancelled + /* Traverse each qubit to generate the cancel dictionaries + Cancel dictionaries: + - For 1-qubit gates the key is (gate_type, qubit_id, commutation_set_id), + the value is the list of gates that share the same gate type, qubit, commutation set. + - For 2qbit gates the key: (gate_type, first_qbit, sec_qbit, first commutation_set_id, + sec_commutation_set_id), the value is the list gates that share the same gate type, + qubits and commutation sets. + */ + let (commutation_set, node_indices) = analyze_commutations_inner(py, dag, commutation_checker)?; + let mut cancellation_sets: HashMap> = HashMap::new(); + + (0..dag.num_qubits() as u32).for_each(|qubit| { + let wire = Qubit(qubit); + 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(_)) { + continue; + } + } else { + continue; + } + for node in com_set.iter() { + let instr = match &dag.dag()[*node] { + NodeType::Operation(instr) => instr, + _ => panic!("Unexpected type in commutation set."), + }; + let num_qargs = dag.get_qargs(instr.qubits).len(); + // no support for cancellation of parameterized gates + if instr.is_parameterized() { + continue; + } + if let Some(op_gate) = instr.op.try_standard_gate() { + if num_qargs == 1 && SUPPORTED_GATES.contains(&op_gate) { + cancellation_sets + .entry(CancellationSetKey { + gate: GateOrRotation::Gate(op_gate), + qubits: smallvec![wire], + com_set_index: com_set_idx, + second_index: None, + }) + .or_insert_with(Vec::new) + .push(*node); + } + + if num_qargs == 1 && Z_ROTATIONS.contains(&op_gate) { + cancellation_sets + .entry(CancellationSetKey { + gate: GateOrRotation::ZRotation, + qubits: smallvec![wire], + com_set_index: com_set_idx, + second_index: None, + }) + .or_insert_with(Vec::new) + .push(*node); + } + if num_qargs == 1 && X_ROTATIONS.contains(&op_gate) { + cancellation_sets + .entry(CancellationSetKey { + gate: GateOrRotation::XRotation, + qubits: smallvec![wire], + com_set_index: com_set_idx, + second_index: None, + }) + .or_insert_with(Vec::new) + .push(*node); + } + // Don't deal with Y rotation, because Y rotation doesn't commute with + // CNOT, so it should be dealt with by optimized1qgate pass + if num_qargs == 2 && dag.get_qargs(instr.qubits)[0] == wire { + let second_qarg = dag.get_qargs(instr.qubits)[1]; + cancellation_sets + .entry(CancellationSetKey { + gate: GateOrRotation::Gate(op_gate), + qubits: smallvec![wire, second_qarg], + com_set_index: com_set_idx, + second_index: node_indices + .get(&(*node, Wire::Qubit(second_qarg))) + .copied(), + }) + .or_insert_with(Vec::new) + .push(*node); + } + } + } + } + } + }); + + for (cancel_key, cancel_set) in &cancellation_sets { + if cancel_set.len() > 1 { + if let GateOrRotation::Gate(g) = cancel_key.gate { + if SUPPORTED_GATES.contains(&g) { + for &c_node in &cancel_set[0..(cancel_set.len() / 2) * 2] { + dag.remove_op_node(c_node); + } + } + continue; + } + if matches!(cancel_key.gate, GateOrRotation::ZRotation) && z_var_gate.is_none() { + continue; + } + if matches!( + cancel_key.gate, + GateOrRotation::ZRotation | GateOrRotation::XRotation + ) { + 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] { + NodeType::Operation(instr) => instr, + _ => panic!("Unexpected type in commutation set run."), + }; + let node_op_name = node_op.op.name(); + + let node_angle = if ROTATION_GATES.contains(&node_op_name) { + match node_op.params_view().first() { + Some(Param::Float(f)) => Ok(*f), + _ => return Err(QiskitError::new_err(format!( + "Rotational gate with parameter expression encountered in cancellation {:?}", + node_op.op + ))) + } + } else if HALF_TURNS.contains(&node_op_name) { + Ok(PI) + } else if QUARTER_TURNS.contains(&node_op_name) { + Ok(PI / 2.0) + } else if EIGHTH_TURNS.contains(&node_op_name) { + Ok(PI / 4.0) + } else { + Err(PyRuntimeError::new_err(format!( + "Angle for operation {} is not defined", + node_op_name + ))) + }; + total_angle += node_angle?; + + let Param::Float(new_phase) = node_op + .op + .definition(node_op.params_view()) + .unwrap() + .global_phase() + .clone() + else { + unreachable!() + }; + total_phase += new_phase + } + + let new_op = match cancel_key.gate { + GateOrRotation::ZRotation => z_var_gate.unwrap(), + GateOrRotation::XRotation => &RXGate, + _ => unreachable!(), + }; + + let gate_angle = euler_one_qubit_decomposer::mod_2pi(total_angle, 0.); + + let new_op_phase: f64 = if gate_angle.abs() > _CUTOFF_PRECISION { + dag.insert_1q_on_incoming_qubit((*new_op, &[total_angle]), cancel_set[0]); + let Param::Float(new_phase) = new_op + .definition(&[Param::Float(total_angle)]) + .unwrap() + .global_phase() + .clone() + else { + unreachable!(); + }; + new_phase + } else { + 0.0 + }; + + dag.add_global_phase(py, &Param::Float(total_phase - new_op_phase))?; + + for node in cancel_set { + dag.remove_op_node(*node); + } + } + } + } + + Ok(()) +} + +pub fn commutation_cancellation(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(cancel_commutations))?; + Ok(()) +} diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs new file mode 100644 index 000000000000..b6e61fcf3f6d --- /dev/null +++ b/crates/accelerate/src/commutation_checker.rs @@ -0,0 +1,774 @@ +// 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 hashbrown::{HashMap, HashSet}; +use ndarray::linalg::kron; +use ndarray::Array2; +use num_complex::Complex64; +use once_cell::sync::Lazy; +use smallvec::SmallVec; + +use numpy::PyReadonlyArray2; +use pyo3::exceptions::PyRuntimeError; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyBool, PyDict, PySequence, PyTuple}; + +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}; +use qiskit_circuit::{BitType, Clbit, Qubit}; + +use crate::unitary_compose; +use crate::QiskitError; + +static SKIPPED_NAMES: [&str; 4] = ["measure", "reset", "delay", "initialize"]; +static NO_CACHE_NAMES: [&str; 2] = ["annotated", "linear_function"]; +static SUPPORTED_OP: Lazy> = Lazy::new(|| { + HashSet::from([ + "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx", "cy", "cz", "swap", + "iswap", "ecr", "ccx", "cswap", + ]) +}); + +fn get_bits( + py: Python, + bits1: &Bound, + bits2: &Bound, +) -> PyResult<(Vec, Vec)> +where + T: From + Copy, + BitType: From, +{ + let mut bitdata: BitData = BitData::new(py, "bits".to_string()); + + for bit in bits1.iter().chain(bits2.iter()) { + bitdata.add(py, &bit, false)?; + } + + Ok(( + bitdata.map_bits(bits1)?.collect(), + bitdata.map_bits(bits2)?.collect(), + )) +} + +/// This is the internal structure for the Python CommutationChecker class +/// It handles the actual commutation checking, cache management, and library +/// lookups. It's not meant to be a public facing Python object though and only used +/// internally by the Python class. +#[pyclass(module = "qiskit._accelerate.commutation_checker")] +pub struct CommutationChecker { + library: CommutationLibrary, + cache_max_entries: usize, + cache: HashMap<(String, String), CommutationCacheEntry>, + current_cache_entries: usize, + #[pyo3(get)] + gates: Option>, +} + +#[pymethods] +impl CommutationChecker { + #[pyo3(signature = (standard_gate_commutations=None, cache_max_entries=1_000_000, gates=None))] + #[new] + fn py_new( + standard_gate_commutations: Option>, + cache_max_entries: usize, + gates: Option>, + ) -> Self { + // Initialize sets before they are used in the commutation checker + Lazy::force(&SUPPORTED_OP); + CommutationChecker { + library: CommutationLibrary::new(standard_gate_commutations), + cache: HashMap::new(), + cache_max_entries, + current_cache_entries: 0, + gates, + } + } + + #[pyo3(signature=(op1, op2, max_num_qubits=3))] + fn commute_nodes( + &mut self, + py: Python, + op1: &DAGOpNode, + op2: &DAGOpNode, + max_num_qubits: u32, + ) -> PyResult { + let (qargs1, qargs2) = get_bits::( + py, + op1.instruction.qubits.bind(py), + op2.instruction.qubits.bind(py), + )?; + let (cargs1, cargs2) = get_bits::( + py, + op1.instruction.clbits.bind(py), + op2.instruction.clbits.bind(py), + )?; + + self.commute_inner( + py, + &op1.instruction.operation.view(), + &op1.instruction.params, + &op1.instruction.extra_attrs, + &qargs1, + &cargs1, + &op2.instruction.operation.view(), + &op2.instruction.params, + &op2.instruction.extra_attrs, + &qargs2, + &cargs2, + max_num_qubits, + ) + } + + #[pyo3(signature=(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits=3))] + #[allow(clippy::too_many_arguments)] + fn commute( + &mut self, + py: Python, + op1: OperationFromPython, + qargs1: Option<&Bound>, + cargs1: Option<&Bound>, + op2: OperationFromPython, + qargs2: Option<&Bound>, + 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, qargs2) = get_bits::(py, &qargs1, &qargs2)?; + let (cargs1, cargs2) = get_bits::(py, &cargs1, &cargs2)?; + + self.commute_inner( + py, + &op1.operation.view(), + &op1.params, + &op1.extra_attrs, + &qargs1, + &cargs1, + &op2.operation.view(), + &op2.params, + &op2.extra_attrs, + &qargs2, + &cargs2, + max_num_qubits, + ) + } + + /// Return the current number of cache entries + fn num_cached_entries(&self) -> usize { + self.current_cache_entries + } + + /// Clear the cache + fn clear_cached_commutations(&mut self) { + self.clear_cache() + } + + fn __getstate__(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(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); + 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("gates", self.gates.clone())?; + Ok(out_dict.unbind()) + } + + fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> { + let dict_state = state.downcast_bound::(py)?; + self.cache_max_entries = dict_state + .get_item("cache_max_entries")? + .unwrap() + .extract()?; + self.current_cache_entries = dict_state + .get_item("current_cache_entries")? + .unwrap() + .extract()?; + self.library = CommutationLibrary { + library: dict_state.get_item("library")?.unwrap().extract()?, + }; + let raw_cache: Bound = dict_state.get_item("cache")?.unwrap().extract()?; + self.cache = HashMap::with_capacity(raw_cache.len()); + for (key, value) in raw_cache.iter() { + let value_dict: &Bound = value.downcast()?; + self.cache.insert( + key.extract()?, + commutation_cache_entry_from_pydict(value_dict)?, + ); + } + self.gates = dict_state.get_item("gates")?.unwrap().extract()?; + Ok(()) + } +} + +impl CommutationChecker { + #[allow(clippy::too_many_arguments)] + pub fn commute_inner( + &mut self, + py: Python, + op1: &OperationRef, + params1: &[Param], + attrs1: &ExtraInstructionAttributes, + qargs1: &[Qubit], + cargs1: &[Clbit], + op2: &OperationRef, + params2: &[Param], + attrs2: &ExtraInstructionAttributes, + qargs2: &[Qubit], + cargs2: &[Clbit], + max_num_qubits: u32, + ) -> PyResult { + if let Some(gates) = &self.gates { + if !gates.is_empty() && (!gates.contains(op1.name()) || !gates.contains(op2.name())) { + return Ok(false); + } + } + + let commutation: Option = commutation_precheck( + op1, + params1, + attrs1, + qargs1, + cargs1, + op2, + params2, + attrs2, + qargs2, + cargs2, + max_num_qubits, + ); + if let Some(is_commuting) = commutation { + return Ok(is_commuting); + } + + let reversed = if op1.num_qubits() != op2.num_qubits() { + op1.num_qubits() > op2.num_qubits() + } else { + (op1.name().len(), op1.name()) >= (op2.name().len(), op2.name()) + }; + let (first_params, second_params) = if reversed { + (params2, params1) + } else { + (params1, params2) + }; + let (first_op, second_op) = if reversed { (op2, op1) } else { (op1, op2) }; + let (first_qargs, second_qargs) = if reversed { + (qargs2, qargs1) + } else { + (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(_))); + + if skip_cache { + return self.commute_matmul( + py, + first_op, + first_params, + first_qargs, + second_op, + second_params, + second_qargs, + ); + } + + // Query commutation library + let relative_placement = get_relative_placement(first_qargs, second_qargs); + if let Some(is_commuting) = + self.library + .check_commutation_entries(first_op, second_op, &relative_placement) + { + return Ok(is_commuting); + } + + // Query cache + let key1 = hashable_params(first_params)?; + let key2 = hashable_params(second_params)?; + if let Some(commutation_dict) = self + .cache + .get(&(first_op.name().to_string(), second_op.name().to_string())) + { + let hashes = (key1.clone(), key2.clone()); + if let Some(commutation) = commutation_dict.get(&(relative_placement.clone(), hashes)) { + return Ok(*commutation); + } + } + + // Perform matrix multiplication to determine commutation + let is_commuting = self.commute_matmul( + py, + first_op, + first_params, + first_qargs, + second_op, + second_params, + second_qargs, + )?; + + // TODO: implement a LRU cache for this + if self.current_cache_entries >= self.cache_max_entries { + self.clear_cache(); + } + // Cache results from is_commuting + self.cache + .entry((first_op.name().to_string(), second_op.name().to_string())) + .and_modify(|entries| { + let key = (relative_placement.clone(), (key1.clone(), key2.clone())); + entries.insert(key, is_commuting); + self.current_cache_entries += 1; + }) + .or_insert_with(|| { + let mut entries = HashMap::with_capacity(1); + let key = (relative_placement, (key1, key2)); + entries.insert(key, is_commuting); + self.current_cache_entries += 1; + entries + }); + Ok(is_commuting) + } + + #[allow(clippy::too_many_arguments)] + fn commute_matmul( + &self, + py: Python, + first_op: &OperationRef, + first_params: &[Param], + first_qargs: &[Qubit], + second_op: &OperationRef, + second_params: &[Param], + second_qargs: &[Qubit], + ) -> PyResult { + // Compute relative positioning of qargs of the second gate to the first gate. + // Since the qargs come out the same BitData, we already know there are no accidential + // bit-duplications, but this code additionally maps first_qargs to [0..n] and then + // computes second_qargs relative to that. For example, it performs the mappings + // (first_qargs, second_qargs) = ( [1, 2], [0, 2] ) --> ( [0, 1], [2, 1] ) + // (first_qargs, second_qargs) = ( [1, 2, 0], [0, 3, 4] ) --> ( [0, 1, 2], [2, 3, 4] ) + // This re-shuffling is done to compute the correct kronecker product later. + let mut qarg: HashMap<&Qubit, Qubit> = HashMap::from_iter( + first_qargs + .iter() + .enumerate() + .map(|(i, q)| (q, Qubit(i as u32))), + ); + let mut num_qubits = first_qargs.len() as u32; + for q in second_qargs { + if !qarg.contains_key(q) { + qarg.insert(q, Qubit(num_qubits)); + num_qubits += 1; + } + } + + let first_qarg: Vec = Vec::from_iter((0..first_qargs.len() as u32).map(Qubit)); + let second_qarg: Vec = second_qargs.iter().map(|q| qarg[q]).collect(); + + if first_qarg.len() > second_qarg.len() { + return Err(QiskitError::new_err( + "first instructions must have at most as many qubits as the second instruction", + )); + }; + let first_mat = match get_matrix(py, first_op, first_params)? { + Some(matrix) => matrix, + None => return Ok(false), + }; + + let second_mat = match get_matrix(py, second_op, second_params)? { + Some(matrix) => matrix, + None => return Ok(false), + }; + + let rtol = 1e-5; + let atol = 1e-8; + if first_qarg == second_qarg { + match first_qarg.len() { + 1 => Ok(unitary_compose::commute_1q( + &first_mat.view(), + &second_mat.view(), + rtol, + atol, + )), + 2 => Ok(unitary_compose::commute_2q( + &first_mat.view(), + &second_mat.view(), + &[Qubit(0), Qubit(1)], + rtol, + atol, + )), + _ => Ok(unitary_compose::allclose( + &second_mat.dot(&first_mat).view(), + &first_mat.dot(&second_mat).view(), + rtol, + atol, + )), + } + } else { + // TODO Optimize this bit to avoid unnecessary Kronecker products: + // 1. We currently sort the operations for the cache by operation size, putting the + // *smaller* operation first: (smaller op, larger op) + // 2. This code here expands the first op to match the second -- hence we always + // match the operator sizes. + // This whole extension logic could be avoided since we know the second one is larger. + let extra_qarg2 = num_qubits - first_qarg.len() as u32; + let first_mat = if extra_qarg2 > 0 { + let id_op = Array2::::eye(usize::pow(2, extra_qarg2)); + kron(&id_op, &first_mat) + } else { + first_mat + }; + + // the 1 qubit case cannot happen, since that would already have been captured + // by the previous if clause; first_qarg == second_qarg (if they overlap they must + // be the same) + if num_qubits == 2 { + return Ok(unitary_compose::commute_2q( + &first_mat.view(), + &second_mat.view(), + &second_qarg, + rtol, + atol, + )); + }; + + let op12 = match unitary_compose::compose( + &first_mat.view(), + &second_mat.view(), + &second_qarg, + false, + ) { + Ok(matrix) => matrix, + Err(e) => return Err(PyRuntimeError::new_err(e)), + }; + let op21 = match unitary_compose::compose( + &first_mat.view(), + &second_mat.view(), + &second_qarg, + true, + ) { + Ok(matrix) => matrix, + Err(e) => return Err(PyRuntimeError::new_err(e)), + }; + Ok(unitary_compose::allclose( + &op12.view(), + &op21.view(), + rtol, + atol, + )) + } + } + + fn clear_cache(&mut self) { + self.cache.clear(); + self.current_cache_entries = 0; + } +} + +#[allow(clippy::too_many_arguments)] +fn commutation_precheck( + op1: &OperationRef, + params1: &[Param], + attrs1: &ExtraInstructionAttributes, + qargs1: &[Qubit], + cargs1: &[Clbit], + op2: &OperationRef, + params2: &[Param], + attrs2: &ExtraInstructionAttributes, + qargs2: &[Qubit], + cargs2: &[Clbit], + max_num_qubits: u32, +) -> Option { + if op1.control_flow() + || op2.control_flow() + || attrs1.condition().is_some() + || attrs2.condition().is_some() + { + return Some(false); + } + + // assuming the number of involved qubits to be small, this might be faster than set operations + if !qargs1.iter().any(|e| qargs2.contains(e)) && !cargs1.iter().any(|e| cargs2.contains(e)) { + return Some(true); + } + + if qargs1.len() > max_num_qubits as usize || qargs2.len() > max_num_qubits as usize { + return Some(false); + } + + if SUPPORTED_OP.contains(op1.name()) && SUPPORTED_OP.contains(op2.name()) { + return None; + } + + if is_commutation_skipped(op1, params1) || is_commutation_skipped(op2, params2) { + return Some(false); + } + + None +} + +fn get_matrix( + py: Python, + operation: &OperationRef, + params: &[Param], +) -> PyResult>> { + match operation.matrix(params) { + Some(matrix) => Ok(Some(matrix)), + None => match operation { + PyGateType(gate) => Ok(Some(matrix_via_operator(py, &gate.gate)?)), + PyOperationType(op) => Ok(Some(matrix_via_operator(py, &op.operation)?)), + _ => Ok(None), + }, + } +} + +fn matrix_via_operator(py: Python, py_obj: &PyObject) -> PyResult> { + Ok(QI_OPERATOR + .get_bound(py) + .call1((py_obj,))? + .getattr(intern!(py, "data"))? + .extract::>()? + .as_array() + .to_owned()) +} + +fn is_commutation_skipped(op: &T, params: &[Param]) -> bool +where + T: Operation, +{ + op.directive() + || SKIPPED_NAMES.contains(&op.name()) + || params + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) +} + +fn get_relative_placement( + first_qargs: &[Qubit], + second_qargs: &[Qubit], +) -> SmallVec<[Option; 2]> { + let mut qubits_g2: HashMap<&Qubit, Qubit> = HashMap::with_capacity(second_qargs.len()); + second_qargs.iter().enumerate().for_each(|(i_g1, q_g1)| { + qubits_g2.insert_unique_unchecked(q_g1, Qubit(i_g1 as u32)); + }); + + first_qargs + .iter() + .map(|q_g0| qubits_g2.get(q_g0).copied()) + .collect() +} + +#[derive(Clone, Debug)] +#[pyclass] +pub struct CommutationLibrary { + pub library: Option>, +} + +impl CommutationLibrary { + fn check_commutation_entries( + &self, + first_op: &OperationRef, + second_op: &OperationRef, + relative_placement: &SmallVec<[Option; 2]>, + ) -> Option { + if let Some(library) = &self.library { + match library.get(&(first_op.name().to_string(), second_op.name().to_string())) { + Some(CommutationLibraryEntry::Commutes(b)) => Some(*b), + Some(CommutationLibraryEntry::QubitMapping(qm)) => { + qm.get(relative_placement).copied() + } + _ => None, + } + } else { + None + } + } +} + +#[pymethods] +impl CommutationLibrary { + #[new] + fn new(py_any: Option>) -> Self { + match py_any { + Some(pyob) => CommutationLibrary { + library: pyob + .extract::>() + .ok(), + }, + None => CommutationLibrary { + library: Some(HashMap::new()), + }, + } + } +} + +#[derive(Clone, Debug)] +pub enum CommutationLibraryEntry { + Commutes(bool), + QubitMapping(HashMap; 2]>, bool>), +} + +impl<'py> FromPyObject<'py> for CommutationLibraryEntry { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + if let Ok(b) = b.extract::() { + return Ok(CommutationLibraryEntry::Commutes(b)); + } + let dict = b.downcast::()?; + let mut ret = hashbrown::HashMap::with_capacity(dict.len()); + for (k, v) in dict { + let raw_key: SmallVec<[Option; 2]> = k.extract()?; + let v: bool = v.extract()?; + let key = raw_key.into_iter().map(|key| key.map(Qubit)).collect(); + ret.insert(key, v); + } + Ok(CommutationLibraryEntry::QubitMapping(ret)) + } +} + +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]>), +); + +type CommutationCacheEntry = HashMap; + +fn commutation_entry_to_pydict(py: Python, entry: &CommutationCacheEntry) -> PyResult> { + let out_dict = PyDict::new_bound(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)); + out_dict.set_item( + PyTuple::new_bound(py, [qubits, PyTuple::new_bound(py, [params0, params1])]), + PyBool::new_bound(py, *v), + )?; + } + Ok(out_dict.unbind()) +} + +fn commutation_cache_entry_from_pydict(dict: &Bound) -> PyResult { + let mut ret = hashbrown::HashMap::with_capacity(dict.len()); + for (k, v) in dict { + let raw_key: CacheKeyRaw = k.extract()?; + let qubits = raw_key.0.iter().map(|q| q.map(Qubit)).collect(); + let params0: SmallVec<_> = raw_key.1 .0; + let params1: SmallVec<_> = raw_key.1 .1; + let v: bool = v.extract()?; + ret.insert((qubits, (params0, params1)), v); + } + Ok(ret) +} + +type CacheKeyRaw = ( + SmallVec<[Option; 2]>, + (SmallVec<[ParameterKey; 3]>, SmallVec<[ParameterKey; 3]>), +); + +/// This newtype wraps a f64 to make it hashable so we can cache parameterized gates +/// based on the parameter value (assuming it's a float angle). However, Rust doesn't do +/// this by default and there are edge cases to track around it's usage. The biggest one +/// is this does not work with f64::NAN, f64::INFINITY, or f64::NEG_INFINITY +/// If you try to use these values with this type they will not work as expected. +/// This should only be used with the cache hashmap's keys and not used beyond that. +#[derive(Debug, Copy, Clone, PartialEq, FromPyObject)] +struct ParameterKey(f64); + +impl ParameterKey { + fn key(&self) -> u64 { + // If we get a -0 the to_bits() return is not equivalent to 0 + // because -0 has the sign bit set we'd be hashing 9223372036854775808 + // and be storing it separately from 0. So this normalizes all 0s to + // be represented by 0 + if self.0 == 0. { + 0 + } else { + self.0.to_bits() + } + } +} + +impl std::hash::Hash for ParameterKey { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + self.key().hash(state) + } +} + +impl Eq for ParameterKey {} + +fn hashable_params(params: &[Param]) -> PyResult> { + params + .iter() + .map(|x| { + if let Param::Float(x) = x { + // NaN and Infinity (negative or positive) are not valid + // parameter values and our hacks to store parameters in + // the cache HashMap don't take these into account. So return + // an error to Python if we encounter these values. + if x.is_nan() || x.is_infinite() { + Err(PyRuntimeError::new_err( + "Can't hash parameters that are infinite or NaN", + )) + } else { + Ok(ParameterKey(*x)) + } + } else { + Err(QiskitError::new_err( + "Unable to hash a non-float instruction parameter.", + )) + } + }) + .collect() +} + +pub fn commutation_checker(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index e9f6e343b6bd..dc4d0b77c4a7 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -27,7 +27,7 @@ use qiskit_circuit::circuit_instruction::CircuitInstruction; use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::operations::Operation; use crate::QiskitError; @@ -35,7 +35,7 @@ fn get_matrix_from_inst<'py>( py: Python<'py>, inst: &'py CircuitInstruction, ) -> PyResult> { - if let Some(mat) = inst.op().matrix(&inst.params) { + if let Some(mat) = inst.operation.matrix(&inst.params) { Ok(mat) } else if inst.operation.try_standard_gate().is_some() { Err(QiskitError::new_err( @@ -124,29 +124,7 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { trans_matrix } -#[pyfunction] -pub fn collect_2q_blocks_filter(node: &Bound) -> Option { - let Ok(node) = node.downcast::() else { - return None; - }; - let node = node.borrow(); - match node.instruction.op() { - gate @ (OperationRef::Standard(_) | OperationRef::Gate(_)) => Some( - gate.num_qubits() <= 2 - && node - .instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref()) - .is_none() - && !node.is_parameterized(), - ), - _ => Some(false), - } -} - pub fn convert_2q_block_matrix(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(blocks_to_matrix))?; - m.add_wrapped(wrap_pyfunction!(collect_2q_blocks_filter))?; Ok(()) } diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 7463777af624..98333cad39d2 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -13,7 +13,7 @@ #![allow(clippy::too_many_arguments)] #![allow(clippy::upper_case_acronyms)] -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use num_complex::{Complex64, ComplexFloat}; use smallvec::{smallvec, SmallVec}; use std::cmp::Ordering; @@ -29,14 +29,19 @@ use pyo3::Python; use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use rustworkx_core::petgraph::stable_graph::NodeIndex; use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; 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 crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; + pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; #[pyclass(module = "qiskit._accelerate.euler_one_qubit_decomposer")] @@ -69,6 +74,7 @@ impl OneQubitGateErrorMap { } } +#[derive(Debug)] #[pyclass(sequence)] pub struct OneQubitGateSequence { pub gates: Vec<(StandardGate, SmallVec<[f64; 3]>)>, @@ -571,21 +577,116 @@ pub fn generate_circuit( Ok(res) } -#[derive(Clone, Debug, Copy)] +const EULER_BASIS_SIZE: usize = 12; + +static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ + &["u3"], + &["u3", "u2", "u1"], + &["u"], + &["p", "sx"], + &["u1", "rx"], + &["r"], + &["rz", "ry"], + &["rz", "rx"], + &["rz", "rx"], + &["rx", "ry"], + &["rz", "sx", "x"], + &["rz", "sx"], +]; +static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [ + EulerBasis::U3, + EulerBasis::U321, + EulerBasis::U, + EulerBasis::PSX, + EulerBasis::U1X, + EulerBasis::RR, + EulerBasis::ZYZ, + EulerBasis::ZXZ, + EulerBasis::XZX, + EulerBasis::XYX, + EulerBasis::ZSXX, + EulerBasis::ZSX, +]; + +/// A structure containing a set of supported `EulerBasis` for running 1q synthesis +#[derive(Debug, Clone)] +pub struct EulerBasisSet { + basis: [bool; EULER_BASIS_SIZE], + initialized: bool, +} + +impl EulerBasisSet { + // Instantiate a new EulerBasisSet + pub fn new() -> Self { + EulerBasisSet { + basis: [false; EULER_BASIS_SIZE], + initialized: false, + } + } + + /// Return true if this has been initialized any basis is supported + pub fn initialized(&self) -> bool { + self.initialized + } + + /// Add a basis to the set + pub fn add_basis(&mut self, basis: EulerBasis) { + self.basis[basis as usize] = true; + self.initialized = true; + } + + /// Get an iterator of all the supported EulerBasis + pub fn get_bases(&self) -> impl Iterator + '_ { + self.basis + .iter() + .enumerate() + .filter_map(|(index, supported)| { + if *supported { + Some(EULER_BASIS_NAMES[index]) + } else { + None + } + }) + } + + /// Check if a basis is supported by this set + pub fn basis_supported(&self, basis: EulerBasis) -> bool { + self.basis[basis as usize] + } + + /// Modify this set to support all EulerBasis + pub fn support_all(&mut self) { + self.basis = [true; 12]; + self.initialized = true; + } + + /// Remove an basis from the set + pub fn remove(&mut self, basis: EulerBasis) { + self.basis[basis as usize] = false; + } +} + +impl Default for EulerBasisSet { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Debug, Copy, Eq, Hash, PartialEq)] #[pyclass(module = "qiskit._accelerate.euler_one_qubit_decomposer")] pub enum EulerBasis { - U321, - U3, - U, - PSX, - ZSX, - ZSXX, - U1X, - RR, - ZYZ, - ZXZ, - XYX, - XZX, + U3 = 0, + U321 = 1, + U = 2, + PSX = 3, + U1X = 4, + RR = 5, + ZYZ = 6, + ZXZ = 7, + XZX = 8, + XYX = 9, + ZSXX = 10, + ZSX = 11, } impl EulerBasis { @@ -684,24 +785,6 @@ fn compare_error_fn( } } -fn compute_error( - gates: &[(StandardGate, SmallVec<[f64; 3]>)], - error_map: Option<&OneQubitGateErrorMap>, - qubit: usize, -) -> (f64, usize) { - match error_map { - Some(err_map) => { - let num_gates = gates.len(); - let gate_fidelities: f64 = gates - .iter() - .map(|gate| 1. - err_map.error_map[qubit].get(gate.0.name()).unwrap_or(&0.)) - .product(); - (1. - gate_fidelities, num_gates) - } - None => (gates.len() as f64, gates.len()), - } -} - fn compute_error_term(gate: &str, error_map: &OneQubitGateErrorMap, qubit: usize) -> f64 { 1. - error_map.error_map[qubit].get(gate).unwrap_or(&0.) } @@ -724,15 +807,6 @@ fn compute_error_str( } } -#[pyfunction] -pub fn compute_error_one_qubit_sequence( - circuit: &OneQubitGateSequence, - qubit: usize, - error_map: Option<&OneQubitGateErrorMap>, -) -> (f64, usize) { - compute_error(&circuit.gates, error_map, qubit) -} - #[pyfunction] pub fn compute_error_list( circuit: Vec>, @@ -743,7 +817,7 @@ pub fn compute_error_list( .iter() .map(|node| { ( - node.instruction.op().name().to_string(), + node.instruction.operation.name().to_string(), smallvec![], // Params not needed in this path ) }) @@ -761,13 +835,16 @@ pub fn unitary_to_gate_sequence( simplify: bool, atol: Option, ) -> PyResult> { - let target_basis_vec: PyResult> = target_basis_list + let mut target_basis_set = EulerBasisSet::new(); + for basis in target_basis_list .iter() .map(|basis| EulerBasis::__new__(basis)) - .collect(); + { + target_basis_set.add_basis(basis?); + } Ok(unitary_to_gate_sequence_inner( unitary.as_array(), - &target_basis_vec?, + &target_basis_set, qubit, error_map, simplify, @@ -778,17 +855,17 @@ pub fn unitary_to_gate_sequence( #[inline] pub fn unitary_to_gate_sequence_inner( unitary_mat: ArrayView2, - target_basis_list: &[EulerBasis], + target_basis_list: &EulerBasisSet, qubit: usize, error_map: Option<&OneQubitGateErrorMap>, simplify: bool, atol: Option, ) -> Option { target_basis_list - .iter() + .get_bases() .map(|target_basis| { - let [theta, phi, lam, phase] = angles_from_unitary(unitary_mat, *target_basis); - generate_circuit(target_basis, theta, phi, lam, phase, simplify, atol).unwrap() + let [theta, phi, lam, phase] = angles_from_unitary(unitary_mat, target_basis); + generate_circuit(&target_basis, theta, phi, lam, phase, simplify, atol).unwrap() }) .min_by(|a, b| { let error_a = compare_error_fn(a, &error_map, qubit); @@ -808,13 +885,16 @@ pub fn unitary_to_circuit( simplify: bool, atol: Option, ) -> PyResult> { - let target_basis_vec: PyResult> = target_basis_list + let mut target_basis_set = EulerBasisSet::new(); + for basis in target_basis_list .iter() .map(|basis| EulerBasis::__new__(basis)) - .collect(); + { + target_basis_set.add_basis(basis?); + } let circuit_sequence = unitary_to_gate_sequence_inner( unitary.as_array(), - &target_basis_vec?, + &target_basis_set, qubit, error_map, simplify, @@ -844,7 +924,7 @@ pub fn det_one_qubit(mat: ArrayView2) -> Complex64 { /// Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π #[inline] -fn mod_2pi(angle: f64, atol: f64) -> f64 { +pub(crate) fn mod_2pi(angle: f64, atol: f64) -> f64 { // f64::rem_euclid() isn't exactly the same as Python's % operator, but because // the RHS here is a constant and positive it is effectively equivalent for // this case @@ -965,69 +1045,198 @@ pub fn params_zxz(unitary: PyReadonlyArray2) -> [f64; 4] { params_zxz_inner(mat) } -type OptimizeDecompositionReturn = Option<((f64, usize), (f64, usize), OneQubitGateSequence)>; +fn compute_error_term_from_target(gate: &str, target: &Target, qubit: PhysicalQubit) -> f64 { + 1. - target.get_error(gate, &[qubit]).unwrap_or(0.) +} + +fn compute_error_from_target_one_qubit_sequence( + circuit: &OneQubitGateSequence, + qubit: PhysicalQubit, + target: Option<&Target>, +) -> (f64, usize) { + match target { + Some(target) => { + let num_gates = circuit.gates.len(); + let gate_fidelities: f64 = circuit + .gates + .iter() + .map(|gate| compute_error_term_from_target(gate.0.name(), target, qubit)) + .product(); + (1. - gate_fidelities, num_gates) + } + None => (circuit.gates.len() as f64, circuit.gates.len()), + } +} #[pyfunction] -pub fn optimize_1q_gates_decomposition( - runs: Vec>>, - qubits: Vec, - bases: Vec>, - simplify: bool, - error_map: Option<&OneQubitGateErrorMap>, - atol: Option, -) -> Vec { - runs.iter() - .enumerate() - .map(|(index, raw_run)| -> OptimizeDecompositionReturn { - let mut error = match error_map { - Some(_) => 1., - None => raw_run.len() as f64, +#[pyo3(signature = (dag, *, target=None, basis_gates=None, global_decomposers=None))] +pub(crate) fn optimize_1q_gates_decomposition( + py: Python, + dag: &mut DAGCircuit, + target: Option<&Target>, + basis_gates: Option>, + global_decomposers: Option>, +) -> PyResult<()> { + let runs: Vec> = dag.collect_1q_runs().unwrap().collect(); + let dag_qubits = dag.num_qubits(); + let mut target_basis_per_qubit: Vec = vec![EulerBasisSet::new(); dag_qubits]; + let mut basis_gates_per_qubit: Vec>> = vec![None; dag_qubits]; + for raw_run in runs { + let mut error = match target { + Some(_) => 1., + None => raw_run.len() as f64, + }; + let qubit: PhysicalQubit = if let NodeType::Operation(inst) = &dag.dag()[raw_run[0]] { + PhysicalQubit::new(dag.get_qargs(inst.qubits)[0].0) + } else { + unreachable!("nodes in runs will always be op nodes") + }; + if !dag.calibrations_empty() { + let mut has_calibration = false; + for node in &raw_run { + if dag.has_calibration_for_index(py, *node)? { + has_calibration = true; + break; + } + } + if has_calibration { + continue; + } + } + if basis_gates_per_qubit[qubit.0 as usize].is_none() { + let basis_gates = match target { + Some(target) => Some( + target + .operation_names_for_qargs(Some(&smallvec![qubit])) + .unwrap(), + ), + None => { + let basis = basis_gates.as_ref(); + basis.map(|basis| basis.iter().map(|x| x.as_str()).collect()) + } }; - let qubit = qubits[index]; - let operator = &raw_run - .iter() - .map(|node| { - if let Some(err_map) = error_map { - error *= compute_error_term(node.instruction.op().name(), err_map, qubit) - } - node.instruction - .op() - .matrix(&node.instruction.params) - .expect("No matrix defined for operation") - }) - .fold( - [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - ], - |mut operator, node| { - matmul_1q(&mut operator, node); - operator + basis_gates_per_qubit[qubit.0 as usize] = basis_gates; + } + let basis_gates = &basis_gates_per_qubit[qubit.0 as usize].as_ref(); + + let target_basis_set = &mut target_basis_per_qubit[qubit.0 as usize]; + if !target_basis_set.initialized() { + match target { + Some(_target) => EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates + .iter() + .all(|gate| basis_gates.as_ref().unwrap().contains(gate)) + { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)), + None => match &global_decomposers { + Some(bases) => bases + .iter() + .map(|basis| EulerBasis::__new__(basis).unwrap()) + .for_each(|basis| target_basis_set.add_basis(basis)), + None => match basis_gates { + Some(gates) => EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, basis_gates)| { + if !gates.iter().all(|gate| basis_gates.as_ref().contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)), + None => target_basis_set.support_all(), }, - ); - let old_error = if error_map.is_some() { - (1. - error, raw_run.len()) - } else { - (error, raw_run.len()) + }, }; - let target_basis_vec: Vec = bases[index] - .iter() - .map(|basis| EulerBasis::__new__(basis).unwrap()) - .collect(); - unitary_to_gate_sequence_inner( - aview2(operator), - &target_basis_vec, - qubit, - error_map, - simplify, - atol, - ) - .map(|out_seq| { - let new_error = compute_error_one_qubit_sequence(&out_seq, qubit, error_map); - (old_error, new_error, out_seq) + if target_basis_set.basis_supported(EulerBasis::U3) + && target_basis_set.basis_supported(EulerBasis::U321) + { + target_basis_set.remove(EulerBasis::U3); + } + if target_basis_set.basis_supported(EulerBasis::ZSX) + && target_basis_set.basis_supported(EulerBasis::ZSXX) + { + target_basis_set.remove(EulerBasis::ZSX); + } + } + let target_basis_set = &target_basis_per_qubit[qubit.0 as usize]; + let operator = raw_run + .iter() + .map(|node_index| { + let node = &dag.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); + } + inst.op.matrix(inst.params_view()).unwrap() + } else { + unreachable!("Can only have op nodes here") + } }) - }) - .collect() + .fold( + [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(1., 0.)], + ], + |mut operator, node| { + matmul_1q(&mut operator, node); + operator + }, + ); + + let old_error = if target.is_some() { + (1. - error, raw_run.len()) + } else { + (error, raw_run.len()) + }; + let sequence = unitary_to_gate_sequence_inner( + aview2(&operator), + target_basis_set, + qubit.0 as usize, + None, + true, + None, + ); + let sequence = match sequence { + Some(seq) => seq, + None => continue, + }; + let new_error = compute_error_from_target_one_qubit_sequence(&sequence, qubit, target); + + 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 !basis.contains(inst.op.name()) { + outside_basis = true; + break; + } + } + } + } else { + outside_basis = false; + } + if outside_basis + || new_error < old_error + || new_error.0.abs() < 1e-9 && old_error.0.abs() >= 1e-9 + { + for gate in sequence.gates { + dag.insert_1q_on_incoming_qubit((gate.0, &gate.1), raw_run[0]); + } + dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; + dag.remove_1q_sequence(&raw_run); + } + } + Ok(()) } fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { @@ -1043,22 +1252,6 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { ]; } -#[pyfunction] -pub fn collect_1q_runs_filter(node: &Bound) -> bool { - let Ok(node) = node.downcast::() else { - return false; - }; - let node = node.borrow(); - let op = node.instruction.op(); - op.num_qubits() == 1 - && op.num_clbits() == 0 - && op.matrix(&node.instruction.params).is_some() - && match &node.instruction.extra_attrs { - None => true, - Some(attrs) => attrs.condition.is_none(), - } -} - pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; m.add_wrapped(wrap_pyfunction!(params_xyx))?; @@ -1069,10 +1262,8 @@ pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(generate_circuit))?; m.add_wrapped(wrap_pyfunction!(unitary_to_gate_sequence))?; m.add_wrapped(wrap_pyfunction!(unitary_to_circuit))?; - m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?; m.add_wrapped(wrap_pyfunction!(compute_error_list))?; m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?; - m.add_wrapped(wrap_pyfunction!(collect_1q_runs_filter))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/accelerate/src/filter_op_nodes.rs b/crates/accelerate/src/filter_op_nodes.rs new file mode 100644 index 000000000000..7c41391f3788 --- /dev/null +++ b/crates/accelerate/src/filter_op_nodes.rs @@ -0,0 +1,63 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::packed_instruction::PackedInstruction; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +#[pyfunction] +#[pyo3(name = "filter_op_nodes")] +pub fn py_filter_op_nodes( + py: Python, + dag: &mut DAGCircuit, + predicate: &Bound, +) -> PyResult<()> { + let callable = |node: NodeIndex| -> PyResult { + let dag_op_node = dag.get_node(py, node)?; + predicate.call1((dag_op_node,))?.extract() + }; + let mut remove_nodes: Vec = Vec::new(); + for node in dag.op_nodes(true) { + if !callable(node)? { + remove_nodes.push(node); + } + } + for node in remove_nodes { + dag.remove_op_node(node); + } + Ok(()) +} + +/// Remove any nodes that have the provided label set +/// +/// Args: +/// dag (DAGCircuit): The dag circuit to filter the ops from +/// label (str): The label to filter nodes on +#[pyfunction] +pub fn filter_labeled_op(dag: &mut DAGCircuit, label: String) { + let predicate = |node: &PackedInstruction| -> bool { + match node.label() { + Some(inst_label) => inst_label != label, + None => false, + } + }; + dag.filter_op_nodes(predicate); +} + +pub fn filter_op_nodes_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_filter_op_nodes))?; + m.add_wrapped(wrap_pyfunction!(filter_labeled_op))?; + Ok(()) +} diff --git a/crates/accelerate/src/gate_direction.rs b/crates/accelerate/src/gate_direction.rs new file mode 100644 index 000000000000..7c7d9d898726 --- /dev/null +++ b/crates/accelerate/src/gate_direction.rs @@ -0,0 +1,149 @@ +// 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 crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use hashbrown::HashSet; +use pyo3::prelude::*; +use qiskit_circuit::imports; +use qiskit_circuit::operations::OperationRef; +use qiskit_circuit::{ + dag_circuit::{DAGCircuit, NodeType}, + operations::Operation, + packed_instruction::PackedInstruction, + Qubit, +}; +use smallvec::smallvec; + +/// Check if the two-qubit gates follow the right direction with respect to the coupling map. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// coupling_edges: set of edge pairs representing a directed coupling map, against which gate directionality is checked +/// +/// Returns: +/// true iff all two-qubit gates comply with the coupling constraints +#[pyfunction] +#[pyo3(name = "check_gate_direction_coupling")] +fn py_check_with_coupling_map( + py: Python, + dag: &DAGCircuit, + coupling_edges: HashSet<[Qubit; 2]>, +) -> PyResult { + let coupling_map_check = + |_: &PackedInstruction, op_args: &[Qubit]| -> bool { coupling_edges.contains(op_args) }; + + check_gate_direction(py, dag, &coupling_map_check, None) +} + +/// Check if the two-qubit gates follow the right direction with respect to instructions supported in the given target. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// target: the Target against which gate directionality compliance is checked +/// +/// Returns: +/// true iff all two-qubit gates comply with the target's coupling constraints +#[pyfunction] +#[pyo3(name = "check_gate_direction_target")] +fn py_check_with_target(py: Python, dag: &DAGCircuit, target: &Target) -> PyResult { + let target_check = |inst: &PackedInstruction, op_args: &[Qubit]| -> bool { + let qargs = smallvec![ + PhysicalQubit::new(op_args[0].0), + PhysicalQubit::new(op_args[1].0) + ]; + + target.instruction_supported(inst.op.name(), Some(&qargs)) + }; + + check_gate_direction(py, dag, &target_check, None) +} + +// The main routine for checking gate directionality. +// +// gate_complies: a function returning true iff the two-qubit gate direction complies with directionality constraints +// +// qubit_mapping: used for mapping the index of a given qubit within an instruction qargs vector to the corresponding qubit index of the +// original DAGCircuit the pass was called with. This mapping is required since control flow blocks are represented by nested DAGCircuit +// objects whose instruction qubit indices are relative to the parent DAGCircuit they reside in, thus when we recurse into nested DAGs, we need +// to carry the mapping context relative to the original DAG. +// When qubit_mapping is None, the identity mapping is assumed +fn check_gate_direction( + py: Python, + dag: &DAGCircuit, + gate_complies: &T, + qubit_mapping: Option<&[Qubit]>, +) -> PyResult +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"); + }; + + let inst_qargs = dag.get_qargs(packed_inst.qubits); + + if let OperationRef::Instruction(py_inst) = packed_inst.op.view() { + if py_inst.control_flow() { + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); // TODO: Take out of the recursion + let py_inst = py_inst.instruction.bind(py); + + for block in py_inst.getattr("blocks")?.iter()? { + let inner_dag: DAGCircuit = circuit_to_dag.call1((block?,))?.extract()?; + + let block_ok = if let Some(mapping) = qubit_mapping { + let mapping = inst_qargs // Create a temp mapping for the recursive call + .iter() + .map(|q| mapping[q.0 as usize]) + .collect::>(); + + check_gate_direction(py, &inner_dag, gate_complies, Some(&mapping))? + } else { + check_gate_direction(py, &inner_dag, gate_complies, Some(inst_qargs))? + }; + + if !block_ok { + return Ok(false); + } + } + continue; + } + } + + if inst_qargs.len() == 2 + && !match qubit_mapping { + // Check gate direction based either on a given custom mapping or the identity mapping + Some(mapping) => gate_complies( + packed_inst, + &[ + mapping[inst_qargs[0].0 as usize], + mapping[inst_qargs[1].0 as usize], + ], + ), + None => gate_complies(packed_inst, inst_qargs), + } + { + return Ok(false); + } + } + + Ok(true) +} + +pub fn gate_direction(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_check_with_coupling_map))?; + m.add_wrapped(wrap_pyfunction!(py_check_with_target))?; + Ok(()) +} diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs new file mode 100644 index 000000000000..1cfcc6c83214 --- /dev/null +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -0,0 +1,191 @@ +// 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 ahash::RandomState; +use hashbrown::HashSet; +use indexmap::IndexMap; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::packed_instruction::PackedInstruction; + +fn gate_eq(py: Python, gate_a: &PackedInstruction, gate_b: &OperationFromPython) -> PyResult { + if gate_a.op.name() != gate_b.operation.name() { + return Ok(false); + } + let a_params = gate_a.params_view(); + if a_params.len() != gate_b.params.len() { + return Ok(false); + } + let mut param_eq = true; + for (a, b) in a_params.iter().zip(&gate_b.params) { + if !a.is_close(py, b, 1e-10)? { + param_eq = false; + break; + } + } + Ok(param_eq) +} + +fn run_on_self_inverse( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + self_inverse_gate_names: HashSet, + self_inverse_gates: Vec, +) -> PyResult<()> { + if !self_inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for gate in self_inverse_gates { + let gate_count = op_counts.get(gate.operation.name()).unwrap_or(&0); + if *gate_count <= 1 { + continue; + } + let mut collect_set: HashSet = HashSet::with_capacity(1); + collect_set.insert(gate.operation.name().to_string()); + let gate_runs: Vec> = dag.collect_runs(collect_set).unwrap().collect(); + for gate_cancel_run in gate_runs { + let mut partitions: Vec> = Vec::new(); + let mut chunk: Vec = 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]; + if let NodeType::Operation(inst) = node { + if gate_eq(py, inst, &gate)? { + chunk.push(*cancel_gate); + } else { + if !chunk.is_empty() { + partitions.push(std::mem::take(&mut chunk)); + } + continue; + } + 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") + }; + if inst.qubits != next_qargs { + partitions.push(std::mem::take(&mut chunk)); + } + } + } else { + panic!("Not an op node"); + } + } + for chunk in partitions { + if chunk.len() % 2 == 0 { + dag.remove_op_node(chunk[0]); + } + for node in &chunk[1..] { + dag.remove_op_node(*node); + } + } + } + } + Ok(()) +} +fn run_on_inverse_pairs( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + inverse_gate_names: HashSet, + inverse_gates: Vec<[OperationFromPython; 2]>, +) -> PyResult<()> { + if !inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for [gate_0, gate_1] in inverse_gates { + let gate_0_name = gate_0.operation.name(); + let gate_1_name = gate_1.operation.name(); + if !op_counts.contains_key(gate_0_name) || !op_counts.contains_key(gate_1_name) { + continue; + } + let names: HashSet = [&gate_0, &gate_1] + .iter() + .map(|x| x.operation.name().to_string()) + .collect(); + let runs: Vec> = dag.collect_runs(names).unwrap().collect(); + 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 inst.qubits == next_inst.qubits + && ((gate_eq(py, inst, &gate_0)? && gate_eq(py, next_inst, &gate_1)?) + || (gate_eq(py, inst, &gate_1)? + && gate_eq(py, next_inst, &gate_0)?)) + { + dag.remove_op_node(nodes[i]); + dag.remove_op_node(nodes[i + 1]); + i += 2; + } else { + i += 1; + } + } else { + panic!("Not an op node") + } + } else { + panic!("Not an op node") + } + } + } + } + Ok(()) +} + +#[pyfunction] +pub fn inverse_cancellation( + py: Python, + dag: &mut DAGCircuit, + inverse_gates: Vec<[OperationFromPython; 2]>, + self_inverse_gates: Vec, + inverse_gate_names: HashSet, + self_inverse_gate_names: HashSet, +) -> PyResult<()> { + if self_inverse_gate_names.is_empty() && inverse_gate_names.is_empty() { + return Ok(()); + } + let op_counts = dag.count_ops(py, true)?; + if !self_inverse_gate_names.is_empty() { + run_on_self_inverse( + py, + dag, + &op_counts, + self_inverse_gate_names, + self_inverse_gates, + )?; + } + if !inverse_gate_names.is_empty() { + run_on_inverse_pairs(py, dag, &op_counts, inverse_gate_names, inverse_gates)?; + } + Ok(()) +} + +pub fn inverse_cancellation_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 4e079ea84b57..9111f932e270 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -14,19 +14,29 @@ use std::env; use pyo3::import_exception; +pub mod check_map; +pub mod circuit_library; +pub mod commutation_analysis; +pub mod commutation_cancellation; +pub mod commutation_checker; pub mod convert_2q_block_matrix; pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod filter_op_nodes; +pub mod gate_direction; +pub mod inverse_cancellation; pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; +pub mod remove_diagonal_gates_before_measure; pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; +pub mod split_2q_unitaries; pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; @@ -39,6 +49,7 @@ pub mod vf2_layout; mod rayon_ext; #[cfg(test)] mod test; +mod unitary_compose; #[inline] pub fn getenv_use_multiple_threads() -> bool { diff --git a/crates/accelerate/src/nlayout.rs b/crates/accelerate/src/nlayout.rs index e0235e5c954a..93b1036b608e 100644 --- a/crates/accelerate/src/nlayout.rs +++ b/crates/accelerate/src/nlayout.rs @@ -24,7 +24,7 @@ use hashbrown::HashMap; macro_rules! qubit_newtype { ($id: ident) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct $id(u32); + pub struct $id(pub u32); impl $id { #[inline] @@ -72,6 +72,7 @@ impl PhysicalQubit { layout.phys_to_virt[self.index()] } } + qubit_newtype!(VirtualQubit); impl VirtualQubit { /// Get the physical qubit that currently corresponds to this index of virtual qubit in the diff --git a/crates/accelerate/src/remove_diagonal_gates_before_measure.rs b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs new file mode 100644 index 000000000000..5e1ba4182344 --- /dev/null +++ b/crates/accelerate/src/remove_diagonal_gates_before_measure.rs @@ -0,0 +1,108 @@ +// 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. + +/// Remove diagonal gates (including diagonal 2Q gates) before a measurement. +use pyo3::prelude::*; + +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::operations::StandardGate; + +/// Run the RemoveDiagonalGatesBeforeMeasure pass on `dag`. +/// Args: +/// dag (DAGCircuit): the DAG to be optimized. +/// Returns: +/// DAGCircuit: the optimized DAG. +#[pyfunction] +#[pyo3(name = "remove_diagonal_gates_before_measure")] +fn run_remove_diagonal_before_measure(dag: &mut DAGCircuit) -> PyResult<()> { + static DIAGONAL_1Q_GATES: [StandardGate; 8] = [ + StandardGate::RZGate, + StandardGate::ZGate, + StandardGate::TGate, + StandardGate::SGate, + StandardGate::TdgGate, + StandardGate::SdgGate, + StandardGate::U1Gate, + StandardGate::PhaseGate, + ]; + static DIAGONAL_2Q_GATES: [StandardGate; 7] = [ + StandardGate::CZGate, + StandardGate::CRZGate, + StandardGate::CU1Gate, + StandardGate::RZZGate, + StandardGate::CPhaseGate, + StandardGate::CSGate, + StandardGate::CSdgGate, + ]; + 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!() + }; + + 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] { + NodeType::Operation(pred_inst) => match pred_inst.standard_gate() { + Some(gate) => { + if DIAGONAL_1Q_GATES.contains(&gate) { + nodes_to_remove.push(predecessor); + } 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 { + nodes_to_remove.push(predecessor); + } + } + } + None => { + continue; + } + }, + _ => { + continue; + } + } + } + } + + for node_to_remove in nodes_to_remove { + if dag.dag().node_weight(node_to_remove).is_some() { + dag.remove_op_node(node_to_remove); + } + } + + Ok(()) +} + +pub fn remove_diagonal_gates_before_measure(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(run_remove_diagonal_before_measure))?; + Ok(()) +} diff --git a/crates/accelerate/src/results/marginalization.rs b/crates/accelerate/src/results/marginalization.rs index 5260287e771d..b9e7ec4ba65c 100644 --- a/crates/accelerate/src/results/marginalization.rs +++ b/crates/accelerate/src/results/marginalization.rs @@ -26,16 +26,11 @@ fn marginalize( indices: Option>, ) -> HashMap { let mut out_counts: HashMap = HashMap::with_capacity(counts.len()); - let clbit_size = counts - .keys() - .next() - .unwrap() - .replace(|c| c == '_' || c == ' ', "") - .len(); + let clbit_size = counts.keys().next().unwrap().replace(['_', ' '], "").len(); let all_indices: Vec = (0..clbit_size).collect(); counts .iter() - .map(|(k, v)| (k.replace(|c| c == '_' || c == ' ', ""), *v)) + .map(|(k, v)| (k.replace(['_', ' '], ""), *v)) .for_each(|(k, v)| match &indices { Some(indices) => { if all_indices == *indices { diff --git a/crates/accelerate/src/sabre/layer.rs b/crates/accelerate/src/sabre/layer.rs index 899321a96681..dec8253ad282 100644 --- a/crates/accelerate/src/sabre/layer.rs +++ b/crates/accelerate/src/sabre/layer.rs @@ -70,7 +70,10 @@ impl FrontLayer { pub fn remove(&mut self, index: &NodeIndex) { // The actual order in the indexmap doesn't matter as long as it's reproducible. // Swap-remove is more efficient than a full shift-remove. - let [a, b] = self.nodes.swap_remove(index).unwrap(); + let [a, b] = self + .nodes + .swap_remove(index) + .expect("Tried removing index that does not exist."); self.qubits[a.index()] = None; self.qubits[b.index()] = None; } diff --git a/crates/accelerate/src/sabre/route.rs b/crates/accelerate/src/sabre/route.rs index bef6d501b4aa..0fd594993943 100644 --- a/crates/accelerate/src/sabre/route.rs +++ b/crates/accelerate/src/sabre/route.rs @@ -27,6 +27,7 @@ use rustworkx_core::petgraph::prelude::*; use rustworkx_core::petgraph::visit::EdgeRef; use rustworkx_core::shortest_path::dijkstra; use rustworkx_core::token_swapper::token_swapper; +use smallvec::{smallvec, SmallVec}; use crate::getenv_use_multiple_threads; use crate::nlayout::{NLayout, PhysicalQubit}; @@ -286,7 +287,7 @@ impl<'a, 'b> RoutingState<'a, 'b> { fn force_enable_closest_node( &mut self, current_swaps: &mut Vec<[PhysicalQubit; 2]>, - ) -> NodeIndex { + ) -> SmallVec<[NodeIndex; 2]> { let (&closest_node, &qubits) = { let dist = &self.target.distance; self.front_layer @@ -328,7 +329,32 @@ impl<'a, 'b> RoutingState<'a, 'b> { current_swaps.push([shortest_path[end], shortest_path[end - 1]]); } current_swaps.iter().for_each(|&swap| self.apply_swap(swap)); - closest_node + + // If we apply a single swap it could be that we route 2 nodes; that is a setup like + // A - B - A - B + // and we swap the middle two qubits. This cannot happen if we apply 2 or more swaps. + if current_swaps.len() > 1 { + smallvec![closest_node] + } else { + // check if the closest node has neighbors that are now routable -- for that we get + // the other physical qubit that was swapped and check whether the node on it + // is now routable + let mut possible_other_qubit = current_swaps[0] + .iter() + // check if other nodes are in the front layer that are connected by this swap + .filter_map(|&swap_qubit| self.front_layer.qubits()[swap_qubit.index()]) + // remove the closest_node, which we know we already routed + .filter(|(node_index, _other_qubit)| *node_index != closest_node) + .map(|(_node_index, other_qubit)| other_qubit); + + // if there is indeed another candidate, check if that gate is routable + if let Some(other_qubit) = possible_other_qubit.next() { + if let Some(also_routed) = self.routable_node_on_qubit(other_qubit) { + return smallvec![closest_node, also_routed]; + } + } + smallvec![closest_node] + } } /// Return the swap of two virtual qubits that produces the best score of all possible swaps. @@ -573,14 +599,14 @@ pub fn swap_map_trial( } if routable_nodes.is_empty() { // If we exceeded the max number of heuristic-chosen swaps without making progress, - // unwind to the last progress point and greedily swap to bring a ndoe together. + // unwind to the last progress point and greedily swap to bring a node together. // Efficiency doesn't matter much; this path never gets taken unless we're unlucky. current_swaps .drain(..) .rev() .for_each(|swap| state.apply_swap(swap)); let force_routed = state.force_enable_closest_node(&mut current_swaps); - routable_nodes.push(force_routed); + routable_nodes.extend(force_routed); } state.update_route(&routable_nodes, current_swaps); if state.heuristic.decay.is_some() { diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs new file mode 100644 index 000000000000..b7bccf44232d --- /dev/null +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -0,0 +1,75 @@ +// 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 rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use qiskit_circuit::imports::UNITARY_GATE; +use qiskit_circuit::operations::{Operation, Param}; + +use crate::two_qubit_decompose::{Specialization, TwoQubitWeylDecomposition}; + +#[pyfunction] +pub fn split_2q_unitaries( + py: Python, + dag: &mut DAGCircuit, + requested_fidelity: f64, +) -> PyResult<()> { + let nodes: Vec = dag.op_nodes(false).collect(); + for node in nodes { + if let NodeType::Operation(inst) = &dag.dag()[node] { + let qubits = dag.get_qargs(inst.qubits).to_vec(); + let matrix = inst.op.matrix(inst.params_view()); + // 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 + // to the supported basis gate set. + if qubits.len() != 2 || inst.op.name() != "unitary" { + continue; + } + let decomp = TwoQubitWeylDecomposition::new_inner( + matrix.unwrap().view(), + Some(requested_fidelity), + None, + )?; + if matches!(decomp.specialization, Specialization::IdEquiv) { + let k1r_arr = decomp.K1r(py); + let k1l_arr = decomp.K1l(py); + let k1r_gate = UNITARY_GATE.get_bound(py).call1((k1r_arr,))?; + let k1l_gate = UNITARY_GATE.get_bound(py).call1((k1l_arr,))?; + let insert_fn = |edge: &Wire| -> PyResult { + if let Wire::Qubit(qubit) = edge { + if *qubit == qubits[0] { + k1r_gate.extract() + } else { + k1l_gate.extract() + } + } else { + unreachable!("This will only be called on ops with no classical wires."); + } + }; + dag.replace_node_with_1q_ops(py, node, insert_fn)?; + dag.add_global_phase(py, &Param::Float(decomp.global_phase))?; + } + // TODO: also look into splitting on Specialization::Swap and just + // swap the virtual qubits. Doing this we will need to update the + // permutation like in ElidePermutations + } + } + Ok(()) +} + +pub fn split_2q_unitaries_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(split_2q_unitaries))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/clifford/mod.rs b/crates/accelerate/src/synthesis/clifford/mod.rs index 4828a5666734..d2c1e36fab4f 100644 --- a/crates/accelerate/src/synthesis/clifford/mod.rs +++ b/crates/accelerate/src/synthesis/clifford/mod.rs @@ -12,12 +12,13 @@ mod bm_synthesis; mod greedy_synthesis; +mod random_clifford; mod utils; use crate::synthesis::clifford::bm_synthesis::synth_clifford_bm_inner; use crate::synthesis::clifford::greedy_synthesis::GreedyCliffordSynthesis; use crate::QiskitError; -use numpy::PyReadonlyArray2; +use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; use pyo3::prelude::*; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::operations::Param; @@ -43,6 +44,28 @@ fn synth_clifford_greedy(py: Python, clifford: PyReadonlyArray2) -> PyResu CircuitData::from_standard_gates(py, num_qubits as u32, clifford_gates, Param::Float(0.0)) } +/// Generate a random Clifford tableau. +/// +/// The Clifford is sampled using the method of the paper "Hadamard-free circuits +/// expose the structure of the Clifford group" by S. Bravyi and D. Maslov (2020), +/// `https://arxiv.org/abs/2003.09412`__. +/// +/// Args: +/// num_qubits: the number of qubits. +/// seed: an optional random seed. +/// Returns: +/// result: a random clifford tableau. +#[pyfunction] +#[pyo3(signature = (num_qubits, seed=None))] +fn random_clifford_tableau( + py: Python, + num_qubits: usize, + seed: Option, +) -> PyResult>> { + let tableau = random_clifford::random_clifford_tableau_inner(num_qubits, seed); + Ok(tableau.into_pyarray_bound(py).unbind()) +} + /// Create a circuit that optimally synthesizes a given Clifford operator represented as /// a tableau for Cliffords up to 3 qubits. /// @@ -60,5 +83,6 @@ fn synth_clifford_bm(py: Python, clifford: PyReadonlyArray2) -> PyResult) -> PyResult<()> { m.add_function(wrap_pyfunction!(synth_clifford_greedy, m)?)?; m.add_function(wrap_pyfunction!(synth_clifford_bm, m)?)?; + m.add_function(wrap_pyfunction!(random_clifford_tableau, m)?)?; Ok(()) } diff --git a/crates/accelerate/src/synthesis/clifford/random_clifford.rs b/crates/accelerate/src/synthesis/clifford/random_clifford.rs new file mode 100644 index 000000000000..fee3ac8e9650 --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/random_clifford.rs @@ -0,0 +1,162 @@ +// 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 crate::synthesis::linear::utils::{ + binary_matmul_inner, calc_inverse_matrix_inner, replace_row_inner, swap_rows_inner, +}; +use ndarray::{concatenate, s, Array1, Array2, ArrayView2, ArrayViewMut2, Axis}; +use rand::{Rng, SeedableRng}; +use rand_pcg::Pcg64Mcg; + +/// Sample from the quantum Mallows distribution. +fn sample_qmallows(n: usize, rng: &mut Pcg64Mcg) -> (Array1, Array1) { + // Hadamard layer + let mut had = Array1::from_elem(n, false); + + // Permutation layer + let mut perm = Array1::from_elem(n, 0); + let mut inds: Vec = (0..n).collect(); + + for i in 0..n { + let m = n - i; + let eps: f64 = 4f64.powi(-(m as i32)); + let r: f64 = rng.gen(); + let index: usize = -((r + (1f64 - r) * eps).log2().ceil() as isize) as usize; + had[i] = index < m; + let k = if index < m { index } else { 2 * m - index - 1 }; + perm[i] = inds[k]; + inds.remove(k); + } + (had, perm) +} + +/// Add symmetric random boolean value to off diagonal entries. +fn fill_tril(mut mat: ArrayViewMut2, rng: &mut Pcg64Mcg, symmetric: bool) { + let n = mat.shape()[0]; + for i in 0..n { + for j in 0..i { + mat[[i, j]] = rng.gen(); + if symmetric { + mat[[j, i]] = mat[[i, j]]; + } + } + } +} + +/// Invert a lower-triangular matrix with unit diagonal. +fn inverse_tril(mat: ArrayView2) -> Array2 { + calc_inverse_matrix_inner(mat, false).unwrap() +} + +/// Generate a random Clifford tableau. +/// +/// The Clifford is sampled using the method of the paper "Hadamard-free circuits +/// expose the structure of the Clifford group" by S. Bravyi and D. Maslov (2020), +/// `https://arxiv.org/abs/2003.09412`__. +/// +/// The function returns a random clifford tableau. +pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option) -> Array2 { + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + + let (had, perm) = sample_qmallows(num_qubits, &mut rng); + + let mut gamma1: Array2 = Array2::from_elem((num_qubits, num_qubits), false); + for i in 0..num_qubits { + gamma1[[i, i]] = rng.gen(); + } + fill_tril(gamma1.view_mut(), &mut rng, true); + + let mut gamma2: Array2 = Array2::from_elem((num_qubits, num_qubits), false); + for i in 0..num_qubits { + gamma2[[i, i]] = rng.gen(); + } + fill_tril(gamma2.view_mut(), &mut rng, true); + + let mut delta1: Array2 = Array2::from_shape_fn((num_qubits, num_qubits), |(i, j)| i == j); + fill_tril(delta1.view_mut(), &mut rng, false); + + let mut delta2: Array2 = Array2::from_shape_fn((num_qubits, num_qubits), |(i, j)| i == j); + fill_tril(delta2.view_mut(), &mut rng, false); + + // Compute stabilizer table + let zero = Array2::from_elem((num_qubits, num_qubits), false); + let prod1 = binary_matmul_inner(gamma1.view(), delta1.view()).unwrap(); + let prod2 = binary_matmul_inner(gamma2.view(), delta2.view()).unwrap(); + let inv1 = inverse_tril(delta1.view()).t().to_owned(); + let inv2 = inverse_tril(delta2.view()).t().to_owned(); + + let table1 = concatenate( + Axis(0), + &[ + concatenate(Axis(1), &[delta1.view(), zero.view()]) + .unwrap() + .view(), + concatenate(Axis(1), &[prod1.view(), inv1.view()]) + .unwrap() + .view(), + ], + ) + .unwrap(); + + let table2 = concatenate( + Axis(0), + &[ + concatenate(Axis(1), &[delta2.view(), zero.view()]) + .unwrap() + .view(), + concatenate(Axis(1), &[prod2.view(), inv2.view()]) + .unwrap() + .view(), + ], + ) + .unwrap(); + + // Compute the full stabilizer tableau + + // The code below is identical to the Python implementation, but is based on the original + // code in the 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(), + perm[i] + num_qubits, + table2.slice(s![perm[i] + num_qubits, ..]), + ); + } + + // Apply layer of Hadamards + for i in 0..num_qubits { + if had[i] { + swap_rows_inner(table.view_mut(), i, i + num_qubits); + } + } + + // Apply table + let random_symplectic_mat = binary_matmul_inner(table1.view(), table.view()).unwrap(); + + // Generate random phases + let random_phases: Array2 = Array2::from_shape_fn((2 * num_qubits, 1), |_| rng.gen()); + + let random_tableau: Array2 = concatenate( + Axis(1), + &[random_symplectic_mat.view(), random_phases.view()], + ) + .unwrap(); + random_tableau +} diff --git a/crates/accelerate/src/synthesis/linear/utils.rs b/crates/accelerate/src/synthesis/linear/utils.rs index b4dbf4993081..5f1fb99fa682 100644 --- a/crates/accelerate/src/synthesis/linear/utils.rs +++ b/crates/accelerate/src/synthesis/linear/utils.rs @@ -10,9 +10,17 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use ndarray::{concatenate, s, Array2, ArrayView2, ArrayViewMut2, Axis}; +use ndarray::{azip, concatenate, s, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis, Zip}; use rand::{Rng, SeedableRng}; use rand_pcg::Pcg64Mcg; +use rayon::iter::{IndexedParallelIterator, ParallelIterator}; +use rayon::prelude::IntoParallelIterator; + +use crate::getenv_use_multiple_threads; + +/// Specifies the minimum number of qubits in order to parallelize computations +/// (this number is chosen based on several local experiments). +const PARALLEL_THRESHOLD: usize = 10; /// Binary matrix multiplication pub fn binary_matmul_inner( @@ -30,11 +38,29 @@ pub fn binary_matmul_inner( )); } - Ok(Array2::from_shape_fn((n1_rows, n2_cols), |(i, j)| { - (0..n2_rows) - .map(|k| mat1[[i, k]] & mat2[[k, j]]) - .fold(false, |acc, v| acc ^ v) - })) + let run_in_parallel = getenv_use_multiple_threads(); + + if n1_rows < PARALLEL_THRESHOLD || !run_in_parallel { + Ok(Array2::from_shape_fn((n1_rows, n2_cols), |(i, j)| { + (0..n2_rows) + .map(|k| mat1[[i, k]] & mat2[[k, j]]) + .fold(false, |acc, v| acc ^ v) + })) + } else { + let mut result = Array2::from_elem((n1_rows, n2_cols), false); + result + .axis_iter_mut(Axis(0)) + .into_par_iter() + .enumerate() + .for_each(|(i, mut row)| { + for j in 0..n2_cols { + row[j] = (0..n2_rows) + .map(|k| mat1[[i, k]] & mat2[[k, j]]) + .fold(false, |acc, v| acc ^ v) + } + }); + Ok(result) + } } /// Gauss elimination of a matrix mat with m rows and n columns. @@ -198,3 +224,16 @@ pub fn check_invertible_binary_matrix_inner(mat: ArrayView2) -> bool { let rank = compute_rank_inner(mat); rank == mat.nrows() } + +/// Mutate matrix ``mat`` in-place by swapping the contents of rows ``i`` and ``j``. +pub fn swap_rows_inner(mut mat: ArrayViewMut2, i: usize, j: usize) { + let (mut x, mut y) = mat.multi_slice_mut((s![i, ..], s![j, ..])); + azip!((x in &mut x, y in &mut y) (*x, *y) = (*y, *x)); +} + +/// Mutate matrix ``mat`` in-place by replacing the contents of row ``i`` by ``row``. +pub fn replace_row_inner(mut mat: ArrayViewMut2, i: usize, row: ArrayView1) { + let mut x = mat.slice_mut(s![i, ..]); + let y = row.slice(s![..]); + Zip::from(&mut x).and(&y).for_each(|x, &y| *x = y); +} diff --git a/crates/accelerate/src/synthesis/linear_phase/cz_depth_lnn.rs b/crates/accelerate/src/synthesis/linear_phase/cz_depth_lnn.rs new file mode 100644 index 000000000000..df01c1ed6fa8 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear_phase/cz_depth_lnn.rs @@ -0,0 +1,171 @@ +// 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 std::iter::once; + +use hashbrown::HashMap; +use itertools::Itertools; +use ndarray::{Array1, ArrayView2}; + +use qiskit_circuit::{ + operations::{Param, StandardGate}, + Qubit, +}; +use smallvec::{smallvec, SmallVec}; + +use crate::synthesis::permutation::{_append_cx_stage1, _append_cx_stage2}; + +// A sequence of Lnn gates +// Represents the return type for Lnn Synthesis algorithms +pub(crate) type LnnGatesVec = Vec<(StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>)>; + +/// A pattern denoted by Pj in [1] for odd number of qubits: +/// [n-2, n-4, n-4, ..., 3, 3, 1, 1, 0, 0, 2, 2, ..., n-3, n-3] +fn _odd_pattern1(n: usize) -> Vec { + once(n - 2) + .chain((0..((n - 3) / 2)).flat_map(|i| [(n - 2 * i - 4); 2])) + .chain((0..((n - 1) / 2)).flat_map(|i| [2 * i; 2])) + .collect() +} + +/// A pattern denoted by Pk in [1] for odd number of qubits: +/// [2, 2, 4, 4, ..., n-1, n-1, n-2, n-2, n-4, n-4, ..., 5, 5, 3, 3, 1] +fn _odd_pattern2(n: usize) -> Vec { + (0..((n - 1) / 2)) + .flat_map(|i| [(2 * i + 2); 2]) + .chain((0..((n - 3) / 2)).flat_map(|i| [n - 2 * i - 2; 2])) + .chain(once(1)) + .collect() +} + +/// A pattern denoted by Pj in [1] for even number of qubits: +/// [n-1, n-3, n-3, n-5, n-5, ..., 1, 1, 0, 0, 2, 2, ..., n-4, n-4, n-2] +fn _even_pattern1(n: usize) -> Vec { + once(n - 1) + .chain((0..((n - 2) / 2)).flat_map(|i| [n - 2 * i - 3; 2])) + .chain((0..((n - 2) / 2)).flat_map(|i| [2 * i; 2])) + .chain(once(n - 2)) + .collect() +} + +/// A pattern denoted by Pk in [1] for even number of qubits: +/// [2, 2, 4, 4, ..., n-2, n-2, n-1, n-1, ..., 3, 3, 1, 1] +fn _even_pattern2(n: usize) -> Vec { + (0..((n - 2) / 2)) + .flat_map(|i| [2 * (i + 1); 2]) + .chain((0..(n / 2)).flat_map(|i| [(n - 2 * i - 1); 2])) + .collect() +} + +/// Creating the patterns for the phase layers. +fn _create_patterns(n: usize) -> HashMap<(usize, usize), (usize, usize)> { + let (pat1, pat2) = if n % 2 == 0 { + (_even_pattern1(n), _even_pattern2(n)) + } else { + (_odd_pattern1(n), _odd_pattern2(n)) + }; + + let ind = if n % 2 == 0 { + (2 * n - 4) / 2 + } else { + (2 * n - 4) / 2 - 1 + }; + + HashMap::from_iter((0..n).map(|i| ((0, i), (i, i))).chain( + (0..(n / 2)).cartesian_product(0..n).map(|(layer, i)| { + ( + (layer + 1, i), + (pat1[ind - (2 * layer) + i], pat2[(2 * layer) + i]), + ) + }), + )) +} + +/// Appends correct phase gate during CZ synthesis +fn _append_phase_gate(pat_val: usize, gates: &mut LnnGatesVec, qubit: usize) { + // Add phase gates: s, sdg or z + let gate_id = pat_val % 4; + if gate_id != 0 { + let gate = match gate_id { + 1 => StandardGate::SdgGate, + 2 => StandardGate::ZGate, + 3 => StandardGate::SGate, + _ => unreachable!(), // unreachable as we have modulo 4 + }; + gates.push((gate, smallvec![], smallvec![Qubit(qubit as u32)])); + } +} + +/// Synthesis of a CZ circuit for linear nearest neighbor (LNN) connectivity, +/// based on Maslov and Roetteler. +pub(super) fn synth_cz_depth_line_mr_inner(matrix: ArrayView2) -> (usize, LnnGatesVec) { + let num_qubits = matrix.raw_dim()[0]; + let pats = _create_patterns(num_qubits); + + // s_gates[i] = 0, 1, 2 or 3 for a gate id, sdg, z or s on qubit i respectively + let mut s_gates = Array1::::zeros(num_qubits); + + let mut patlist: Vec<(usize, usize)> = Vec::new(); + + let mut gates = LnnGatesVec::new(); + + for i in 0..num_qubits { + for j in (i + 1)..num_qubits { + if matrix[[i, j]] { + // CZ(i,j) gate + s_gates[[i]] += 2; // qc.z[i] + s_gates[[j]] += 2; // qc.z[j] + patlist.push((i, j - 1)); + patlist.push((i, j)); + patlist.push((i + 1, j - 1)); + patlist.push((i + 1, j)); + } + } + } + + for i in 0..((num_qubits + 1) / 2) { + for j in 0..num_qubits { + let pat_val = pats[&(i, j)]; + if patlist.contains(&pat_val) { + // patcnt should be 0 or 1, which checks if a Sdg gate should be added + let patcnt = patlist.iter().filter(|val| **val == pat_val).count(); + s_gates[[j]] += patcnt; // qc.sdg[j] + } + + _append_phase_gate(s_gates[[j]], &mut gates, j) + } + + _append_cx_stage1(&mut gates, num_qubits); + _append_cx_stage2(&mut gates, num_qubits); + s_gates = Array1::::zeros(num_qubits); + } + + if num_qubits % 2 == 0 { + let i = num_qubits / 2; + + for j in 0..num_qubits { + let pat_val = pats[&(i, j)]; + if patlist.contains(&pat_val) && pat_val.0 != pat_val.1 { + // patcnt should be 0 or 1, which checks if a Sdg gate should be added + let patcnt = patlist.iter().filter(|val| **val == pat_val).count(); + + s_gates[[j]] += patcnt; // qc.sdg[j] + } + + _append_phase_gate(s_gates[[j]], &mut gates, j) + } + + _append_cx_stage1(&mut gates, num_qubits); + } + + (num_qubits, gates) +} diff --git a/crates/accelerate/src/synthesis/linear_phase/mod.rs b/crates/accelerate/src/synthesis/linear_phase/mod.rs new file mode 100644 index 000000000000..fd95985e1025 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear_phase/mod.rs @@ -0,0 +1,46 @@ +// 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 numpy::PyReadonlyArray2; +use pyo3::{ + prelude::*, + pyfunction, + types::{PyModule, PyModuleMethods}, + wrap_pyfunction, Bound, PyResult, +}; +use qiskit_circuit::{circuit_data::CircuitData, operations::Param}; + +pub(crate) mod cz_depth_lnn; + +/// Synthesis of a CZ circuit for linear nearest neighbor (LNN) connectivity, +/// based on Maslov and Roetteler. +/// +/// Note that this method *reverts* the order of qubits in the circuit, +/// and returns a circuit containing :class:`.CXGate`\s and phase gates +/// (:class:`.SGate`, :class:`.SdgGate` or :class:`.ZGate`). +/// +/// References: +/// 1. Dmitri Maslov, Martin Roetteler, +/// *Shorter stabilizer circuits via Bruhat decomposition and quantum circuit transformations*, +/// `arXiv:1705.09176 `_. +#[pyfunction] +#[pyo3(signature = (mat))] +fn synth_cz_depth_line_mr(py: Python, mat: PyReadonlyArray2) -> PyResult { + let view = mat.as_array(); + let (num_qubits, lnn_gates) = cz_depth_lnn::synth_cz_depth_line_mr_inner(view); + CircuitData::from_standard_gates(py, num_qubits as u32, lnn_gates, Param::Float(0.0)) +} + +pub fn linear_phase(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(synth_cz_depth_line_mr))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index fae05c6739cc..6e22281e2250 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/mod.rs @@ -12,6 +12,7 @@ mod clifford; pub mod linear; +pub mod linear_phase; mod permutation; use pyo3::prelude::*; @@ -21,6 +22,10 @@ pub fn synthesis(m: &Bound) -> PyResult<()> { linear::linear(&linear_mod)?; m.add_submodule(&linear_mod)?; + let linear_phase_mod = PyModule::new_bound(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")?; permutation::permutation(&permutation_mod)?; m.add_submodule(&permutation_mod)?; diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs index 55dc3efe4a87..2f84776cb5fd 100644 --- a/crates/accelerate/src/synthesis/permutation/mod.rs +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -20,6 +20,8 @@ use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::operations::{Param, StandardGate}; use qiskit_circuit::Qubit; +use super::linear_phase::cz_depth_lnn::LnnGatesVec; + mod utils; /// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. @@ -114,11 +116,82 @@ pub fn _synth_permutation_depth_lnn_kms( ) } +/// A single layer of CX gates. +pub(crate) fn _append_cx_stage1(gates: &mut LnnGatesVec, n: usize) { + for i in 0..(n / 2) { + gates.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit((2 * i) as u32), Qubit((2 * i + 1) as u32)], + )) + } + + for i in 0..((n + 1) / 2 - 1) { + gates.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit((2 * i + 2) as u32), Qubit((2 * i + 1) as u32)], + )) + } +} + +/// A single layer of CX gates. +pub(crate) fn _append_cx_stage2(gates: &mut LnnGatesVec, n: usize) { + for i in 0..(n / 2) { + gates.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit((2 * i + 1) as u32), Qubit((2 * i) as u32)], + )) + } + + for i in 0..((n + 1) / 2 - 1) { + gates.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit((2 * i + 1) as u32), Qubit((2 * i + 2) as u32)], + )) + } +} + +/// Append reverse permutation to a QuantumCircuit for linear nearest-neighbor architectures +/// using Kutin, Moulton, Smithline method. +fn _append_reverse_permutation_lnn_kms(gates: &mut LnnGatesVec, num_qubits: usize) { + (0..(num_qubits + 1) / 2).for_each(|_| { + _append_cx_stage1(gates, num_qubits); + _append_cx_stage2(gates, num_qubits); + }); + + if num_qubits % 2 == 0 { + _append_cx_stage1(gates, num_qubits); + } +} + +/// Synthesize reverse permutation for linear nearest-neighbor architectures using +/// Kutin, Moulton, Smithline method. +/// +/// Synthesis algorithm for reverse permutation from [1], section 5. +/// This algorithm synthesizes the reverse permutation on :math:`n` qubits over +/// a linear nearest-neighbor architecture using CX gates with depth :math:`2 * n + 2`. +/// +/// References: +/// 1. Kutin, S., Moulton, D. P., Smithline, L., +/// *Computation at a distance*, Chicago J. Theor. Comput. Sci., vol. 2007, (2007), +/// `arXiv:quant-ph/0701194 `_ +#[pyfunction] +#[pyo3(signature = (num_qubits))] +fn synth_permutation_reverse_lnn_kms(py: Python, num_qubits: usize) -> PyResult { + let mut gates = LnnGatesVec::new(); + _append_reverse_permutation_lnn_kms(&mut gates, num_qubits); + CircuitData::from_standard_gates(py, num_qubits as u32, gates, Param::Float(0.0)) +} + pub fn permutation(m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?; m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?; m.add_function(wrap_pyfunction!(_synth_permutation_basic, m)?)?; m.add_function(wrap_pyfunction!(_synth_permutation_acg, m)?)?; m.add_function(wrap_pyfunction!(_synth_permutation_depth_lnn_kms, m)?)?; + m.add_function(wrap_pyfunction!(synth_permutation_reverse_lnn_kms, m)?)?; Ok(()) } diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index eef5c5a5c3b3..3728a1948f0f 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -52,7 +52,7 @@ mod exceptions { } // Custom types -type Qargs = SmallVec<[PhysicalQubit; 2]>; +pub type Qargs = SmallVec<[PhysicalQubit; 2]>; type GateMap = IndexMap; type PropsMap = NullableIndexMap>; type GateMapState = Vec<(u16, Vec<(Option, Option)>)>; @@ -688,7 +688,7 @@ impl Target { Ok(false) } else if let Some(operation_name) = operation_name { let Some(operation_index) = self.gate_interner.get_item(&operation_name) else { - return Ok(self.instruction_supported(&operation_name, qargs.as_ref())) + return Ok(self.instruction_supported(&operation_name, qargs.as_ref())); }; if let Some(parameters) = parameters { if let Some(obj) = self.gate_name_map.get(&operation_index) { @@ -1025,6 +1025,19 @@ impl Target { }); } + /// Get the error rate of a given instruction in the target + pub fn get_error(&self, name: &str, qargs: &[PhysicalQubit]) -> Option { + self.gate_interner.get_item(name).and_then(|index| { + self.gate_map.get(&index).and_then(|gate_props| { + let qargs_key: Qargs = qargs.iter().cloned().collect(); + match gate_props.get(Some(&qargs_key)) { + Some(props) => props.as_ref().and_then(|inst_props| inst_props.error), + None => None, + } + }) + }) + } + /// Get an iterator over the indices of all physical qubits of the target pub fn physical_qubits(&self) -> impl ExactSizeIterator { 0..self.num_qubits.unwrap_or_default() @@ -1210,9 +1223,11 @@ impl Target { &self, operation: &str, ) -> Result>, TargetKeyError> { - let Some(gate_index) = self.gate_interner.get_item(operation) else { return Err(TargetKeyError::new_err(format!( - "Operation: {operation} not in Target." - )))}; + let Some(gate_index) = self.gate_interner.get_item(operation) else { + return Err(TargetKeyError::new_err(format!( + "Operation: {operation} not in Target." + ))); + }; let gate_map_oper = &self.gate_map[&gate_index]; if gate_map_oper.contains_key(None) { return Ok(None); diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 2072c5034ff7..76b92acd3faf 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -32,7 +32,7 @@ use ndarray::linalg::kron; use ndarray::prelude::*; use ndarray::Zip; use numpy::{IntoPyArray, ToPyArray}; -use numpy::{PyReadonlyArray1, PyReadonlyArray2}; +use numpy::{PyArray2, PyArrayLike2, PyReadonlyArray1, PyReadonlyArray2}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -41,7 +41,7 @@ use pyo3::types::PyList; 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, + angles_from_unitary, det_one_qubit, unitary_to_gate_sequence_inner, EulerBasis, EulerBasisSet, OneQubitGateSequence, ANGLE_ZERO_EPSILON, }; use crate::utils; @@ -55,6 +55,7 @@ use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; use qiskit_circuit::operations::{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; @@ -153,6 +154,15 @@ impl TraceToFidelity for c64 { } } +#[pyfunction] +#[pyo3(name = "trace_to_fid")] +/// Average gate fidelity is :math:`Fbar = (d + |Tr (Utarget \\cdot U^dag)|^2) / d(d+1)` +/// M. Horodecki, P. Horodecki and R. Horodecki, PRA 60, 1888 (1999) +fn py_trace_to_fid(trace: Complex64) -> PyResult { + let fid = trace.trace_to_fid(); + Ok(fid) +} + fn decompose_two_qubit_product_gate( special_unitary: ArrayView2, ) -> PyResult<(Array2, Array2, f64)> { @@ -181,9 +191,31 @@ fn decompose_two_qubit_product_gate( } l.mapv_inplace(|x| x / det_l.sqrt()); let phase = det_l.arg() / 2.; + Ok((l, r, phase)) } +#[pyfunction] +#[pyo3(name = "decompose_two_qubit_product_gate")] +/// Decompose :math:`U = U_l \otimes U_r` where :math:`U \in SU(4)`, +/// and :math:`U_l,~U_r \in SU(2)`. +/// Args: +/// special_unitary_matrix: special unitary matrix to decompose +/// Raises: +/// QiskitError: if decomposition isn't possible. +fn py_decompose_two_qubit_product_gate( + py: Python, + special_unitary: PyArrayLike2, +) -> PyResult<(PyObject, PyObject, f64)> { + 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(), + phase, + )) +} + fn __weyl_coordinates(unitary: MatRef) -> [f64; 3] { let uscaled = scale(C1 / unitary.determinant().powf(0.25)) * unitary; let uup = transform_from_magic_basis(uscaled); @@ -300,6 +332,43 @@ fn rz_matrix(theta: f64) -> Array2 { array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } +/// Generates the array :math:`e^{(i a XX + i b YY + i c ZZ)}` +fn ud(a: f64, b: f64, c: f64) -> Array2 { + array![ + [ + (IM * c).exp() * (a - b).cos(), + C_ZERO, + C_ZERO, + IM * (IM * c).exp() * (a - b).sin() + ], + [ + C_ZERO, + (M_IM * c).exp() * (a + b).cos(), + IM * (M_IM * c).exp() * (a + b).sin(), + C_ZERO + ], + [ + C_ZERO, + IM * (M_IM * c).exp() * (a + b).sin(), + (M_IM * c).exp() * (a + b).cos(), + C_ZERO + ], + [ + IM * (IM * c).exp() * (a - b).sin(), + C_ZERO, + C_ZERO, + (IM * c).exp() * (a - b).cos() + ] + ] +} + +#[pyfunction] +#[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() +} + fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); let phase = c64(0., global_phase).exp(); @@ -340,7 +409,7 @@ const DEFAULT_FIDELITY: f64 = 1.0 - 1.0e-9; #[derive(Clone, Debug, Copy)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose")] -enum Specialization { +pub enum Specialization { General, IdEquiv, SWAPEquiv, @@ -409,13 +478,13 @@ pub struct TwoQubitWeylDecomposition { #[pyo3(get)] c: f64, #[pyo3(get)] - global_phase: f64, + pub global_phase: f64, K1l: Array2, K2l: Array2, K1r: Array2, K2r: Array2, #[pyo3(get)] - specialization: Specialization, + pub specialization: Specialization, default_euler_basis: EulerBasis, #[pyo3(get)] requested_fidelity: Option, @@ -475,7 +544,7 @@ impl TwoQubitWeylDecomposition { /// Instantiate a new TwoQubitWeylDecomposition with rust native /// data structures - fn new_inner( + pub fn new_inner( unitary_matrix: ArrayView2, fidelity: Option, @@ -1020,13 +1089,13 @@ impl TwoQubitWeylDecomposition { #[allow(non_snake_case)] #[getter] - fn K1l(&self, py: Python) -> PyObject { + pub fn K1l(&self, py: Python) -> PyObject { self.K1l.to_pyarray_bound(py).into() } #[allow(non_snake_case)] #[getter] - fn K1r(&self, py: Python) -> PyObject { + pub fn K1r(&self, py: Python) -> PyObject { self.K1r.to_pyarray_bound(py).into() } @@ -1059,7 +1128,8 @@ impl TwoQubitWeylDecomposition { Some(basis) => EulerBasis::__new__(basis.deref())?, None => self.default_euler_basis, }; - let target_1q_basis_list: Vec = vec![euler_basis]; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(euler_basis); let mut gate_sequence = CircuitData::with_capacity(py, 2, 0, 21, Param::Float(0.))?; let mut global_phase: f64 = self.global_phase; @@ -1506,7 +1576,8 @@ impl TwoQubitBasisDecomposer { unitary: ArrayView2, qubit: u8, ) { - let target_1q_basis_list = vec![self.euler_basis]; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); let sequence = unitary_to_gate_sequence_inner( unitary, &target_1q_basis_list, @@ -1750,7 +1821,8 @@ impl TwoQubitBasisDecomposer { if let Some(seq) = sequence { return Ok(seq); } - let target_1q_basis_list = vec![self.euler_basis]; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); let euler_decompositions: SmallVec<[Option; 8]> = decomposition .iter() .map(|decomp| { @@ -1992,7 +2064,8 @@ impl TwoQubitBasisDecomposer { if let Some(seq) = sequence { return Ok(seq); } - let target_1q_basis_list = vec![self.euler_basis]; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); let euler_decompositions: SmallVec<[Option; 8]> = decomposition .iter() .map(|decomp| { @@ -2063,26 +2136,51 @@ impl TwoQubitBasisDecomposer { ) -> PyResult { let kak_gate = kak_gate.extract::(py)?; let sequence = self.__call__(unitary, basis_fidelity, approximate, _num_basis_uses)?; - CircuitData::from_standard_gates( - py, - 2, - sequence - .gates - .into_iter() - .map(|(gate, params, qubits)| match gate { - Some(gate) => ( - gate, - params.into_iter().map(Param::Float).collect(), - qubits.into_iter().map(|x| Qubit(x.into())).collect(), - ), - None => ( - kak_gate.operation.standard_gate(), - kak_gate.params.clone(), - qubits.into_iter().map(|x| Qubit(x.into())).collect(), - ), - }), - Param::Float(sequence.global_phase), - ) + match kak_gate.operation.try_standard_gate() { + Some(std_kak_gate) => CircuitData::from_standard_gates( + py, + 2, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => ( + gate, + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + None => ( + std_kak_gate, + kak_gate.params.clone(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + }), + Param::Float(sequence.global_phase), + ), + None => CircuitData::from_packed_operations( + py, + 2, + 0, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => ( + PackedOperation::from_standard(gate), + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + ), + None => ( + kak_gate.operation.clone(), + kak_gate.params.clone(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + ), + }), + Param::Float(sequence.global_phase), + ), + } } fn num_basis_gates(&self, unitary: PyReadonlyArray2) -> usize { @@ -2248,9 +2346,12 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(_num_basis_gates))?; + m.add_wrapped(wrap_pyfunction!(py_decompose_two_qubit_product_gate))?; m.add_wrapped(wrap_pyfunction!(two_qubit_decompose_up_to_diagonal))?; m.add_wrapped(wrap_pyfunction!(two_qubit_local_invariants))?; m.add_wrapped(wrap_pyfunction!(local_equivalence))?; + m.add_wrapped(wrap_pyfunction!(py_trace_to_fid))?; + m.add_wrapped(wrap_pyfunction!(py_ud))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/accelerate/src/unitary_compose.rs b/crates/accelerate/src/unitary_compose.rs new file mode 100644 index 000000000000..b40f6c3eb564 --- /dev/null +++ b/crates/accelerate/src/unitary_compose.rs @@ -0,0 +1,240 @@ +// 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 ndarray::{Array, Array2, ArrayView, ArrayView2, IxDyn}; +use ndarray_einsum_beta::*; +use num_complex::{Complex, Complex64, ComplexFloat}; +use num_traits::Zero; +use qiskit_circuit::Qubit; + +static LOWERCASE: [u8; 26] = [ + b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', + b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', +]; + +static _UPPERCASE: [u8; 26] = [ + b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', + b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', +]; + +// Compose the operators given by `gate_unitary` and `overall_unitary`, i.e. apply one to the other +// as specified by the involved qubits given in `qubits` and the `front` parameter +pub fn compose( + gate_unitary: &ArrayView2, + overall_unitary: &ArrayView2, + qubits: &[Qubit], + front: bool, +) -> Result, &'static str> { + let gate_qubits = gate_unitary.shape()[0].ilog2() as usize; + + // Full composition of operators + if qubits.is_empty() { + if front { + return Ok(gate_unitary.dot(overall_unitary)); + } else { + return Ok(overall_unitary.dot(gate_unitary)); + } + } + // Compose with other on subsystem + let num_indices = gate_qubits; + let shift = if front { gate_qubits } else { 0usize }; + let right_mul = front; + + //Reshape current matrix + //Note that we must reverse the subsystem dimension order as + //qubit 0 corresponds to the right-most position in the tensor + //product, which is the last tensor wire index. + let tensor = per_qubit_shaped(gate_unitary); + let mat = per_qubit_shaped(overall_unitary); + let indices = qubits + .iter() + .map(|q| num_indices - 1 - q.0 as usize) + .collect::>(); + let num_rows = usize::pow(2, num_indices as u32); + + let res = _einsum_matmul(&tensor, &mat, &indices, shift, right_mul)? + .as_standard_layout() + .into_shape((num_rows, num_rows)) + .unwrap() + .into_dimensionality::() + .unwrap() + .to_owned(); + Ok(res) +} + +// Reshape an input matrix to (2, 2, ..., 2) depending on its dimensionality +fn per_qubit_shaped<'a>(array: &ArrayView2<'a, Complex>) -> ArrayView<'a, Complex64, IxDyn> { + let overall_shape = (0..array.shape()[0].ilog2() as usize) + .flat_map(|_| [2, 2]) + .collect::>(); + array.into_shape(overall_shape).unwrap() +} + +// Determine einsum strings for perform a matrix multiplication on the input matrices +fn _einsum_matmul( + tensor: &ArrayView, + mat: &ArrayView, + indices: &[usize], + shift: usize, + right_mul: bool, +) -> Result, &'static str> { + let rank = tensor.ndim(); + let rank_mat = mat.ndim(); + if rank_mat % 2 != 0 { + return Err("Contracted matrix must have an even number of indices."); + } + // Get einsum indices for tensor + let mut indices_tensor = (0..rank).collect::>(); + for (j, index) in indices.iter().enumerate() { + indices_tensor[index + shift] = rank + j; + } + // Get einsum indices for mat + let mat_contract = (rank..rank + indices.len()).rev().collect::>(); + let mat_free = indices + .iter() + .rev() + .map(|index| index + shift) + .collect::>(); + let indices_mat = if right_mul { + [mat_contract, mat_free].concat() + } else { + [mat_free, mat_contract].concat() + }; + + let tensor_einsum = unsafe { + String::from_utf8_unchecked(indices_tensor.iter().map(|c| LOWERCASE[*c]).collect()) + }; + let mat_einsum = + unsafe { String::from_utf8_unchecked(indices_mat.iter().map(|c| LOWERCASE[*c]).collect()) }; + + einsum( + format!("{},{}", tensor_einsum, mat_einsum).as_str(), + &[tensor, mat], + ) +} + +fn _einsum_matmul_helper(qubits: &[u32], num_qubits: usize) -> [String; 4] { + let tens_in: Vec = LOWERCASE[..num_qubits].to_vec(); + let mut tens_out: Vec = tens_in.clone(); + let mut mat_l: Vec = Vec::with_capacity(num_qubits); + let mut mat_r: Vec = Vec::with_capacity(num_qubits); + qubits.iter().rev().enumerate().for_each(|(pos, idx)| { + mat_r.push(tens_in[num_qubits - 1 - pos]); + mat_l.push(LOWERCASE[25 - pos]); + tens_out[num_qubits - 1 - *idx as usize] = LOWERCASE[25 - pos]; + }); + unsafe { + [ + String::from_utf8_unchecked(mat_l), + String::from_utf8_unchecked(mat_r), + String::from_utf8_unchecked(tens_in), + String::from_utf8_unchecked(tens_out), + ] + } +} + +fn _einsum_matmul_index(qubits: &[u32], num_qubits: usize) -> String { + assert!(num_qubits > 26, "Can't compute unitary of > 26 qubits"); + + let tens_r = unsafe { String::from_utf8_unchecked(_UPPERCASE[..num_qubits].to_vec()) }; + let [mat_l, mat_r, tens_lin, tens_lout] = _einsum_matmul_helper(qubits, num_qubits); + format!( + "{}{}, {}{}->{}{}", + mat_l, mat_r, tens_lin, tens_r, tens_lout, tens_r + ) +} + +pub fn commute_1q( + left: &ArrayView2, + right: &ArrayView2, + rtol: f64, + atol: f64, +) -> bool { + // This could allow for explicit hardcoded formulas, using less FLOPS, if we only + // consider an absolute tolerance. But for backward compatibility we now implement the full + // formula including relative tolerance handling. + for i in 0..2usize { + for j in 0..2usize { + let mut ab = Complex64::zero(); + let mut ba = Complex64::zero(); + for k in 0..2usize { + ab += left[[i, k]] * right[[k, j]]; + ba += right[[i, k]] * left[[k, j]]; + } + let sum = ab - ba; + if sum.abs() > atol + ba.abs() * rtol { + return false; + } + } + } + true +} + +pub fn commute_2q( + left: &ArrayView2, + right: &ArrayView2, + qargs: &[Qubit], + rtol: f64, + atol: f64, +) -> bool { + let rev = qargs[0].0 == 1; + for i in 0..4usize { + for j in 0..4usize { + // We compute AB and BA separately, to enable checking the relative difference + // (AB - BA)_ij > atol + rtol * BA_ij. This is due to backward compatibility and could + // maybe be changed in the future to save one complex number allocation. + let mut ab = Complex64::zero(); + let mut ba = Complex64::zero(); + for k in 0..4usize { + ab += left[[_ind(i, rev), _ind(k, rev)]] * right[[k, j]]; + ba += right[[i, k]] * left[[_ind(k, rev), _ind(j, rev)]]; + } + let sum = ab - ba; + if sum.abs() > atol + ba.abs() * rtol { + return false; + } + } + } + true +} + +#[inline] +fn _ind(i: usize, reversed: bool) -> usize { + if reversed { + // reverse the first two bits + ((i & 1) << 1) + ((i & 2) >> 1) + } else { + i + } +} + +/// For equally sized matrices, ``left`` and ``right``, check whether all entries are close +/// by the criterion +/// +/// |left_ij - right_ij| <= atol + rtol * right_ij +/// +/// This is analogous to NumPy's ``allclose`` function. +pub fn allclose( + left: &ArrayView2, + right: &ArrayView2, + rtol: f64, + atol: f64, +) -> bool { + for i in 0..left.nrows() { + for j in 0..left.ncols() { + if (left[(i, j)] - right[(i, j)]).abs() > atol + rtol * right[(i, j)].abs() { + return false; + } + } + } + true +} diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 3eb430515fcf..ed1f849bbf62 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -10,17 +10,29 @@ name = "qiskit_circuit" doctest = false [dependencies] +rayon.workspace = true +ahash.workspace = true +rustworkx-core.workspace = true bytemuck.workspace = true -hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true thiserror.workspace = true +approx.workspace = true +itertools.workspace = true [dependencies.pyo3] workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] +[dependencies.hashbrown] +workspace = true +features = ["rayon"] + +[dependencies.indexmap] +workspace = true +features = ["rayon"] + [dependencies.smallvec] workspace = true features = ["union"] diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 977d1b34e496..6a8e4af6b920 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -81,17 +81,6 @@ pub struct BitData { cached: Py, } -pub struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); - -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) - } -} - impl BitData where T: From + Copy, @@ -106,6 +95,15 @@ where } } + pub fn with_capacity(py: Python<'_>, description: String, capacity: usize) -> Self { + BitData { + description, + bits: Vec::with_capacity(capacity), + indices: HashMap::with_capacity(capacity), + cached: PyList::empty_bound(py).unbind(), + } + } + /// Gets the number of bits. pub fn len(&self) -> usize { self.bits.len() @@ -139,14 +137,19 @@ where pub fn map_bits<'py>( &self, bits: impl IntoIterator>, - ) -> Result, BitNotFoundError<'py>> { + ) -> PyResult> { let v: Result, _> = bits .into_iter() .map(|b| { self.indices .get(&BitAsKey::new(&b)) .copied() - .ok_or_else(|| BitNotFoundError(b)) + .ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) }) .collect(); v.map(|x| x.into_iter()) @@ -168,7 +171,7 @@ where } /// Adds a new Python bit. - pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { + pub fn add(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult { if self.bits.len() != self.cached.bind(bit.py()).len() { return Err(PyRuntimeError::new_err( format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description) @@ -193,6 +196,29 @@ where bit ))); } + Ok(idx.into()) + } + + pub fn remove_indices(&mut self, py: Python, indices: I) -> PyResult<()> + where + I: IntoIterator, + { + let mut indices_sorted: Vec = indices + .into_iter() + .map(|i| >::from(i) as usize) + .collect(); + indices_sorted.sort(); + + for index in indices_sorted.into_iter().rev() { + self.cached.bind(py).del_item(index)?; + let bit = self.bits.remove(index); + self.indices.remove(&BitAsKey::new(bit.bind(py))); + } + // Update indices. + for (i, bit) in self.bits.iter().enumerate() { + self.indices + .insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()); + } Ok(()) } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index d29455c5363b..0308bd8dfb8b 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -14,9 +14,11 @@ use std::cell::OnceCell; use crate::bit_data::BitData; -use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; +use crate::circuit_instruction::{ + CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, +}; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; -use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::interner::{Interned, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; @@ -31,6 +33,7 @@ use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyTuple, PyType}; use pyo3::{import_exception, intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; use smallvec::SmallVec; import_exception!(qiskit.circuit.exceptions, CircuitError); @@ -91,9 +94,9 @@ pub struct CircuitData { /// The packed instruction listing. data: Vec, /// The cache used to intern instruction bits. - qargs_interner: IndexedInterner>, + qargs_interner: Interner<[Qubit]>, /// The cache used to intern instruction bits. - cargs_interner: IndexedInterner>, + cargs_interner: Interner<[Clbit]>, /// Qubits registered in the circuit. qubits: BitData, /// Clbits registered in the circuit. @@ -148,19 +151,15 @@ impl CircuitData { global_phase, )?; for (operation, params, qargs, cargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs))? - .index; - let clbits = (&mut res.cargs_interner) - .intern(InternerKey::Value(cargs))? - .index; + let qubits = res.qargs_interner.insert_owned(qargs); + let clbits = res.cargs_interner.insert_owned(cargs); let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation, qubits, clbits, params, - extra_attrs: None, + extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }); @@ -169,6 +168,63 @@ impl CircuitData { Ok(res) } + /// A constructor for CircuitData from an iterator of PackedInstruction objects + /// + /// This is tpically useful when iterating over a CircuitData or DAGCircuit + /// to construct a new CircuitData from the iterator of PackedInstructions. As + /// such it requires that you have `BitData` and `Interner` objects to run. If + /// you just wish to build a circuit data from an iterator of instructions + /// the `from_packed_operations` or `from_standard_gates` constructor methods + /// are a better choice + /// + /// # Args + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * qubits: The BitData to use for the new circuit's qubits + /// * clbits: The BitData to use for the new circuit's clbits + /// * qargs_interner: The interner for Qubit objects in the circuit. This must + /// contain all the Interned indices stored in the + /// PackedInstructions from `instructions` + /// * cargs_interner: The interner for Clbit objects in the circuit. This must + /// contain all the Interned indices stored in the + /// PackedInstructions from `instructions` + /// * Instructions: An iterator with items of type: `PyResult` + /// that contais the instructions to insert in iterator order to the new + /// CircuitData. This returns a `PyResult` to facilitate the case where + /// you need to make a python copy (such as with `PackedOperation::py_deepcopy()`) + /// of the operation while iterating for constructing the new `CircuitData`. An + /// example of this use case is in `qiskit_circuit::converters::dag_to_circuit`. + /// * global_phase: The global phase value to use for the new circuit. + pub fn from_packed_instructions( + py: Python, + qubits: BitData, + clbits: BitData, + qargs_interner: Interner<[Qubit]>, + cargs_interner: Interner<[Clbit]>, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator>, + { + let instruction_iter = instructions.into_iter(); + let mut res = CircuitData { + data: Vec::with_capacity(instruction_iter.size_hint().0), + qargs_interner, + cargs_interner, + qubits, + clbits, + param_table: ParameterTable::new(), + global_phase, + }; + + for inst in instruction_iter { + res.data.push(inst?); + res.track_instruction_parameters(py, res.data.len() - 1)?; + } + Ok(res) + } + /// An alternate constructor to build a new `CircuitData` from an iterator /// of standard gates. This can be used to build a circuit from a sequence /// of standard gates, such as for a `StandardGate` definition or circuit @@ -203,20 +259,16 @@ impl CircuitData { instruction_iter.size_hint().0, global_phase, )?; - let no_clbit_index = (&mut res.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = res.cargs_interner.get_default(); for (operation, params, qargs) in instruction_iter { - let qubits = (&mut res.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = res.qargs_interner.insert(&qargs); let params = (!params.is_empty()).then(|| Box::new(params)); res.data.push(PackedInstruction { op: operation.into(), qubits, clbits: no_clbit_index, params, - extra_attrs: None, + extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }); @@ -235,8 +287,8 @@ impl CircuitData { ) -> PyResult { let mut res = CircuitData { data: Vec::with_capacity(instruction_capacity), - qargs_interner: IndexedInterner::new(), - cargs_interner: IndexedInterner::new(), + qargs_interner: Interner::new(), + cargs_interner: Interner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), param_table: ParameterTable::new(), @@ -266,19 +318,15 @@ impl CircuitData { params: &[Param], qargs: &[Qubit], ) -> PyResult<()> { - let no_clbit_index = (&mut self.cargs_interner) - .intern(InternerKey::Value(Vec::new()))? - .index; + let no_clbit_index = self.cargs_interner.get_default(); let params = (!params.is_empty()).then(|| Box::new(params.iter().cloned().collect())); - let qubits = (&mut self.qargs_interner) - .intern(InternerKey::Value(qargs.to_vec()))? - .index; + let qubits = self.qargs_interner.insert(qargs); self.data.push(PackedInstruction { op: operation.into(), qubits, clbits: no_clbit_index, params, - extra_attrs: None, + extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }); @@ -363,8 +411,8 @@ impl CircuitData { ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), - qargs_interner: IndexedInterner::new(), - cargs_interner: IndexedInterner::new(), + qargs_interner: Interner::new(), + cargs_interner: Interner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), param_table: ParameterTable::new(), @@ -411,8 +459,8 @@ impl CircuitData { /// /// Returns: /// list(:class:`.Qubit`): The current sequence of registered qubits. - #[getter] - pub fn qubits(&self, py: Python<'_>) -> Py { + #[getter("qubits")] + pub fn py_qubits(&self, py: Python<'_>) -> Py { self.qubits.cached().clone_ref(py) } @@ -436,8 +484,8 @@ impl CircuitData { /// /// Returns: /// list(:class:`.Clbit`): The current sequence of registered clbits. - #[getter] - pub fn clbits(&self, py: Python<'_>) -> Py { + #[getter("clbits")] + pub fn py_clbits(&self, py: Python<'_>) -> Py { self.clbits.cached().clone_ref(py) } @@ -497,7 +545,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.qubits.add(py, bit, strict) + self.qubits.add(py, bit, strict)?; + Ok(()) } /// Registers a :class:`.Clbit` instance. @@ -511,7 +560,8 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - self.clbits.add(py, bit, strict) + self.clbits.add(py, bit, strict)?; + Ok(()) } /// Performs a shallow copy. @@ -582,10 +632,10 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.qargs_interner.intern(inst.qubits).value.iter() { + for b in self.qargs_interner.get(inst.qubits) { qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.cargs_interner.intern(inst.clbits).value.iter() { + for b in self.cargs_interner.get(inst.clbits) { clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -635,12 +685,7 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn map_nonstandard_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - if inst.op.try_standard_gate().is_some() - && !inst - .extra_attrs - .as_ref() - .is_some_and(|attrs| attrs.condition.is_some()) - { + if inst.op.try_standard_gate().is_some() && inst.extra_attrs.condition().is_none() { continue; } let py_op = func.call1((inst.unpack_py_op(py)?,))?; @@ -747,12 +792,12 @@ impl CircuitData { // Get a single item, assuming the index is validated as in bounds. let get_single = |index: usize| { let inst = &self.data[index]; - let qubits = self.qargs_interner.intern(inst.qubits); - let clbits = self.cargs_interner.intern(inst.clbits); + let qubits = self.qargs_interner.get(inst.qubits); + let clbits = self.cargs_interner.get(inst.clbits); CircuitInstruction { operation: inst.op.clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits.value)).unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits.value)).unbind(), + 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().cloned().collect(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -904,8 +949,7 @@ impl CircuitData { for inst in other.data.iter() { let qubits = other .qargs_interner - .intern(inst.qubits) - .value + .get(inst.qubits) .iter() .map(|b| { Ok(self @@ -916,8 +960,7 @@ impl CircuitData { .collect::>>()?; let clbits = other .cargs_interner - .intern(inst.clbits) - .value + .get(inst.clbits) .iter() .map(|b| { Ok(self @@ -927,14 +970,12 @@ impl CircuitData { }) .collect::>>()?; let new_index = self.data.len(); - let qubits_id = - Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; - let clbits_id = - Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; + let qubits_id = self.qargs_interner.insert_owned(qubits); + let clbits_id = self.cargs_interner.insert_owned(clbits); self.data.push(PackedInstruction { op: inst.op.clone(), - qubits: qubits_id.index, - clbits: clbits_id.index, + qubits: qubits_id, + clbits: clbits_id, params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -967,28 +1008,16 @@ impl CircuitData { sequence.py(), array .iter() + .map(|value| Param::Float(*value)) .zip(old_table.drain_ordered()) - .map(|(value, (param_ob, uses))| (param_ob, Param::Float(*value), uses)), + .map(|(value, (obj, uses))| (obj, value, uses)), ) } else { let values = sequence .iter()? .map(|ob| Param::extract_no_coerce(&ob?)) .collect::>>()?; - if values.len() != self.param_table.num_parameters() { - return Err(PyValueError::new_err(concat!( - "Mismatching number of values and parameters. For partial binding ", - "please pass a dictionary of {parameter: value} pairs." - ))); - } - let mut old_table = std::mem::take(&mut self.param_table); - self.assign_parameters_inner( - sequence.py(), - values - .into_iter() - .zip(old_table.drain_ordered()) - .map(|(value, (param_ob, uses))| (param_ob, value, uses)), - ) + self.assign_parameters_from_slice(sequence.py(), &values) } } @@ -1009,6 +1038,22 @@ impl CircuitData { self.param_table.clear(); } + /// Counts the number of times each operation is used in the circuit. + /// + /// # Parameters + /// - `self` - A mutable reference to the CircuitData struct. + /// + /// # Returns + /// An IndexMap containing the operation names as keys and their respective counts as values. + pub fn count_ops(&self) -> IndexMap<&str, usize, ::ahash::RandomState> { + let mut ops_count: IndexMap<&str, usize, ::ahash::RandomState> = IndexMap::default(); + for instruction in &self.data { + *ops_count.entry(instruction.op.name()).or_insert(0) += 1; + } + ops_count.par_sort_by(|_k1, v1, _k2, v2| v2.cmp(v1)); + ops_count + } + // Marks this pyclass as NOT hashable. #[classattr] const __hash__: Option> = None; @@ -1106,7 +1151,7 @@ impl CircuitData { pub fn num_nonlocal_gates(&self) -> usize { self.data .iter() - .filter(|inst| inst.op().num_qubits() > 1 && !inst.op().directive()) + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) .count() } } @@ -1127,18 +1172,16 @@ impl CircuitData { } fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { - let qubits = Interner::intern( - &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), - )?; - let clbits = Interner::intern( - &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), - )?; + let qubits = self + .qargs_interner + .insert_owned(self.qubits.map_bits(inst.qubits.bind(py))?.collect()); + let clbits = self + .cargs_interner + .insert_owned(self.clbits.map_bits(inst.clbits.bind(py))?.collect()); Ok(PackedInstruction { op: inst.operation.clone(), - qubits: qubits.index, - clbits: clbits.index, + qubits, + clbits, params: (!inst.params.is_empty()).then(|| Box::new(inst.params.clone())), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -1151,9 +1194,89 @@ impl CircuitData { self.data.iter() } - fn assign_parameters_inner(&mut self, py: Python, iter: I) -> PyResult<()> + /// Assigns parameters to circuit data based on a slice of `Param`. + pub fn assign_parameters_from_slice(&mut self, py: Python, slice: &[Param]) -> PyResult<()> { + if slice.len() != self.param_table.num_parameters() { + return Err(PyValueError::new_err(concat!( + "Mismatching number of values and parameters. For partial binding ", + "please pass a mapping of {parameter: value} pairs." + ))); + } + let mut old_table = std::mem::take(&mut self.param_table); + self.assign_parameters_inner( + py, + slice + .iter() + .zip(old_table.drain_ordered()) + .map(|(value, (param_ob, uses))| (param_ob, value.clone_ref(py), uses)), + ) + } + + /// Assigns parameters to circuit data based on a mapping of `ParameterUuid` : `Param`. + /// This mapping assumes that the provided `ParameterUuid` keys are instances + /// of `ParameterExpression`. + pub fn assign_parameters_from_mapping(&mut self, py: Python, iter: I) -> PyResult<()> + where + I: IntoIterator, + T: AsRef, + { + let mut items = Vec::new(); + for (param_uuid, value) in iter { + // Assume all the Parameters are already in the circuit + let param_obj = self.get_parameter_by_uuid(param_uuid); + if let Some(param_obj) = param_obj { + // Copy or increase ref_count for Parameter, avoid acquiring the GIL. + items.push(( + param_obj.clone_ref(py), + value.as_ref().clone_ref(py), + self.param_table.pop(param_uuid)?, + )); + } else { + return Err(PyValueError::new_err("An invalid parameter was provided.")); + } + } + self.assign_parameters_inner(py, items) + } + + /// Returns an immutable view of the Interner used for Qargs + pub fn qargs_interner(&self) -> &Interner<[Qubit]> { + &self.qargs_interner + } + + /// Returns an immutable view of the Interner used for Cargs + pub fn cargs_interner(&self) -> &Interner<[Clbit]> { + &self.cargs_interner + } + + /// Returns an immutable view of the Global Phase `Param` of the circuit + pub fn global_phase(&self) -> &Param { + &self.global_phase + } + + /// Returns an immutable view of the Qubits registered in the circuit + pub fn qubits(&self) -> &BitData { + &self.qubits + } + + /// Returns an immutable view of the Classical bits registered in the circuit + pub fn clbits(&self) -> &BitData { + &self.clbits + } + + /// Unpacks from interned value to `[Qubit]` + pub fn get_qargs(&self, index: Interned<[Qubit]>) -> &[Qubit] { + self.qargs_interner().get(index) + } + + /// Unpacks from InternerIndex to `[Clbit]` + pub fn get_cargs(&self, index: Interned<[Clbit]>) -> &[Clbit] { + self.cargs_interner().get(index) + } + + fn assign_parameters_inner(&mut self, py: Python, iter: I) -> PyResult<()> where - I: IntoIterator, Param, HashSet)>, + I: IntoIterator, T, HashSet)>, + T: AsRef + Clone, { let inconsistent = || PyRuntimeError::new_err("internal error: circuit parameter table is inconsistent"); @@ -1190,7 +1313,7 @@ impl CircuitData { for (param_ob, value, uses) in iter { debug_assert!(!uses.is_empty()); uuids.clear(); - for inner_param_ob in value.iter_parameters(py)? { + for inner_param_ob in value.as_ref().iter_parameters(py)? { uuids.push(self.param_table.track(&inner_param_ob?, None)?) } for usage in uses { @@ -1201,7 +1324,7 @@ impl CircuitData { }; self.set_global_phase( py, - bind_expr(expr.bind_borrowed(py), ¶m_ob, &value, true)?, + bind_expr(expr.bind_borrowed(py), ¶m_ob, value.as_ref(), true)?, )?; } ParameterUse::Index { @@ -1215,17 +1338,21 @@ impl CircuitData { let Param::ParameterExpression(expr) = ¶ms[parameter] else { return Err(inconsistent()); }; - params[parameter] = - match bind_expr(expr.bind_borrowed(py), ¶m_ob, &value, true)? { - Param::Obj(obj) => { - return Err(CircuitError::new_err(format!( - "bad type after binding for gate '{}': '{}'", - standard.name(), - obj.bind(py).repr()?, - ))) - } - param => param, - }; + params[parameter] = match bind_expr( + expr.bind_borrowed(py), + ¶m_ob, + value.as_ref(), + true, + )? { + Param::Obj(obj) => { + return Err(CircuitError::new_err(format!( + "bad type after binding for gate '{}': '{}'", + standard.name(), + obj.bind(py).repr()?, + ))) + } + param => param, + }; for uuid in uuids.iter() { self.param_table.add_use(*uuid, usage)? } @@ -1245,7 +1372,7 @@ impl CircuitData { user_operations .entry(instruction) .or_insert_with(Vec::new) - .push((param_ob.clone_ref(py), value.clone())); + .push((param_ob.clone_ref(py), value.as_ref().clone_ref(py))); let op = previous.unpack_py_op(py)?.into_bound(py); let previous_param = &previous.params_view()[parameter]; @@ -1257,7 +1384,7 @@ impl CircuitData { let new_param = bind_expr( expr.bind_borrowed(py), ¶m_ob, - &value, + value.as_ref(), false, )?; // Historically, `assign_parameters` called `validate_parameter` @@ -1286,7 +1413,7 @@ impl CircuitData { Param::extract_no_coerce( &obj.call_method( assign_parameters_attr, - ([(¶m_ob, &value)].into_py_dict_bound(py),), + ([(¶m_ob, value.as_ref())].into_py_dict_bound(py),), Some( &[("inplace", false), ("flat_input", true)] .into_py_dict_bound(py), diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ae649b5a8110..a56aee2c6137 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -17,7 +17,7 @@ use numpy::IntoPyArray; use pyo3::basic::CompareOp; use pyo3::exceptions::{PyDeprecationWarning, PyTypeError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PyTuple, PyType}; +use pyo3::types::{PyList, PyString, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; use smallvec::SmallVec; @@ -30,18 +30,23 @@ use crate::operations::{ }; use crate::packed_instruction::PackedOperation; -/// These are extra mutable attributes for a circuit instruction's state. In general we don't -/// typically deal with this in rust space and the majority of the time they're not used in Python -/// space either. To save memory these are put in a separate struct and are stored inside a -/// `Box` on `CircuitInstruction` and `PackedInstruction`. +/// This is a private struct used to hold the actual attributes, which we store +/// on the heap using the [Box] within [ExtraInstructionAttributes]. #[derive(Debug, Clone)] -pub struct ExtraInstructionAttributes { - pub label: Option, - pub duration: Option, - pub unit: Option, - pub condition: Option, +struct ExtraAttributes { + label: Option, + duration: Option, + unit: Option, + condition: Option, } +/// Extra mutable attributes for a circuit instruction's state. In general we don't +/// typically deal with this in rust space and the majority of the time they're not used in Python +/// space either. To save memory, the attributes are stored inside a `Box` internally, so this +/// struct is no larger than that. +#[derive(Default, Debug, Clone)] +pub struct ExtraInstructionAttributes(Option>); + impl ExtraInstructionAttributes { /// Construct a new set of the extra attributes if any of the elements are not `None`, or return /// `None` if there is no need for an object. @@ -51,16 +56,137 @@ impl ExtraInstructionAttributes { duration: Option>, unit: Option, condition: Option>, - ) -> Option { - if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { - Some(Self { + ) -> Self { + ExtraInstructionAttributes( + if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { + Some(Box::new(ExtraAttributes { + label, + duration, + unit, + condition, + })) + } else { + None + }, + ) + } + + /// Get the Python-space version of the stored `unit`. + /// This evaluates the Python-space default (`"dt"`) value if we're storing a `None`. + pub fn py_unit(&self, py: Python) -> Py { + self.0 + .as_deref() + .and_then(|attrs| { + attrs + .unit + .as_deref() + .map(|unit| <&str as IntoPy>>::into_py(unit, py)) + }) + .unwrap_or_else(|| Self::default_unit(py).clone().unbind()) + } + + /// Get the Python-space default value for the `unit` field. + pub fn default_unit(py: Python) -> &Bound { + intern!(py, "dt") + } + + /// Get the stored label attribute. + pub fn label(&self) -> Option<&str> { + self.0.as_deref().and_then(|attrs| attrs.label.as_deref()) + } + + /// Set the stored label attribute, or clear it if `label` is `None`. + pub fn set_label(&mut self, label: Option) { + if let Some(attrs) = &mut self.0 { + attrs.label = label; + self.shrink_if_empty(); + return; + } + if label.is_some() { + self.0 = Some(Box::new(ExtraAttributes { label, + duration: None, + unit: None, + condition: None, + })) + } + } + + /// Get the stored duration attribute. + pub fn duration(&self) -> Option<&PyObject> { + self.0.as_deref().and_then(|attrs| attrs.duration.as_ref()) + } + + /// Set the stored duration attribute, or clear it if `duration` is `None`. + pub fn set_duration(&mut self, duration: Option) { + if let Some(attrs) = &mut self.0 { + attrs.duration = duration; + self.shrink_if_empty(); + return; + } + if duration.is_some() { + self.0 = Some(Box::new(ExtraAttributes { + label: None, duration, + unit: None, + condition: None, + })) + } + } + + /// Get the unit attribute. + pub fn unit(&self) -> Option<&str> { + self.0.as_deref().and_then(|attrs| attrs.unit.as_deref()) + } + + /// Set the stored unit attribute, or clear it if `unit` is `None`. + pub fn set_unit(&mut self, unit: Option) { + if let Some(attrs) = &mut self.0 { + attrs.unit = unit; + self.shrink_if_empty(); + return; + } + if unit.is_some() { + self.0 = Some(Box::new(ExtraAttributes { + label: None, + duration: None, unit, + condition: None, + })) + } + } + + /// Get the condition attribute. + pub fn condition(&self) -> Option<&PyObject> { + self.0.as_deref().and_then(|attrs| attrs.condition.as_ref()) + } + + /// Set the stored condition attribute, or clear it if `condition` is `None`. + pub fn set_condition(&mut self, condition: Option) { + if let Some(attrs) = &mut self.0 { + attrs.condition = condition; + self.shrink_if_empty(); + return; + } + if condition.is_some() { + self.0 = Some(Box::new(ExtraAttributes { + label: None, + duration: None, + unit: None, condition, - }) - } else { - None + })) + } + } + + fn shrink_if_empty(&mut self) { + if let Some(attrs) = &self.0 { + if attrs.label.is_none() + && attrs.duration.is_none() + && attrs.unit.is_none() + && attrs.condition.is_none() + { + self.0 = None; + } } } } @@ -108,17 +234,12 @@ pub struct CircuitInstruction { #[pyo3(get)] pub clbits: Py, pub params: SmallVec<[Param; 3]>, - pub extra_attrs: Option>, + pub extra_attrs: ExtraInstructionAttributes, #[cfg(feature = "cache_pygates")] pub py_op: OnceCell>, } impl CircuitInstruction { - /// View the operation in this `CircuitInstruction`. - pub fn op(&self) -> OperationRef { - self.operation.view() - } - /// Get the Python-space operation, ensuring that it is mutable from Python space (singleton /// gates might not necessarily satisfy this otherwise). /// @@ -135,6 +256,10 @@ impl CircuitInstruction { out.call_method0(intern!(py, "to_mutable")) } } + + pub fn condition(&self) -> Option<&PyObject> { + self.extra_attrs.condition() + } } #[pymethods] @@ -174,14 +299,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?.unbind(), clbits: PyTuple::empty_bound(py).unbind(), params, - extra_attrs: label.map(|label| { - Box::new(ExtraInstructionAttributes { - label: Some(label), - duration: None, - unit: None, - condition: None, - }) - }), + extra_attrs: ExtraInstructionAttributes::new(label, None, None, None), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }) @@ -212,7 +330,7 @@ impl CircuitInstruction { let out = match self.operation.view() { OperationRef::Standard(standard) => standard - .create_py_op(py, Some(&self.params), self.extra_attrs.as_deref())? + .create_py_op(py, Some(&self.params), &self.extra_attrs)? .into_any(), OperationRef::Gate(gate) => gate.gate.clone_ref(py), OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), @@ -230,7 +348,7 @@ impl CircuitInstruction { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> PyObject { - self.op().name().to_object(py) + self.operation.name().to_object(py) } #[getter] @@ -246,30 +364,24 @@ impl CircuitInstruction { #[getter] fn label(&self) -> Option<&str> { - self.extra_attrs - .as_ref() - .and_then(|attrs| attrs.label.as_deref()) + self.extra_attrs.label() } #[getter] - fn condition(&self, py: Python) -> Option { - self.extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + fn get_condition(&self, py: Python) -> Option { + self.extra_attrs.condition().map(|x| x.clone_ref(py)) } #[getter] fn duration(&self, py: Python) -> Option { - self.extra_attrs - .as_ref() - .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + self.extra_attrs.duration().map(|x| x.clone_ref(py)) } #[getter] - fn unit(&self) -> Option<&str> { - self.extra_attrs - .as_ref() - .and_then(|attrs| attrs.unit.as_deref()) + fn unit(&self, py: Python) -> Py { + // Python space uses `"dt"` as the default, whereas we simply don't store the extra + // attributes at all if they're none. + self.extra_attrs.py_unit(py) } /// Is the :class:`.Operation` contained in this instruction a Qiskit standard gate? @@ -292,13 +404,13 @@ impl CircuitInstruction { /// Is the :class:`.Operation` contained in this node a directive? pub fn is_directive(&self) -> bool { - self.op().directive() + self.operation.directive() } /// Is the :class:`.Operation` contained in this instruction a control-flow operation (i.e. an /// instance of :class:`.ControlFlowOp`)? pub fn is_control_flow(&self) -> bool { - self.op().control_flow() + self.operation.control_flow() } /// Does this instruction contain any :class:`.ParameterExpression` parameters? @@ -502,7 +614,7 @@ impl CircuitInstruction { pub struct OperationFromPython { pub operation: PackedOperation, pub params: SmallVec<[Param; 3]>, - pub extra_attrs: Option>, + pub extra_attrs: ExtraInstructionAttributes, } impl<'py> FromPyObject<'py> for OperationFromPython { @@ -523,13 +635,20 @@ impl<'py> FromPyObject<'py> for OperationFromPython { .map(|params| params.unwrap_or_default()) }; let extract_extra = || -> PyResult<_> { + let unit = { + // We accept Python-space `None` or `"dt"` as both meaning the default `"dt"`. + let raw_unit = ob.getattr(intern!(py, "unit"))?; + (!(raw_unit.is_none() + || raw_unit.eq(ExtraInstructionAttributes::default_unit(py))?)) + .then(|| raw_unit.extract::()) + .transpose()? + }; Ok(ExtraInstructionAttributes::new( ob.getattr(intern!(py, "label"))?.extract()?, ob.getattr(intern!(py, "duration"))?.extract()?, - ob.getattr(intern!(py, "unit"))?.extract()?, + unit, ob.getattr(intern!(py, "condition"))?.extract()?, - ) - .map(Box::from)) + )) }; 'standard: { @@ -546,12 +665,18 @@ impl<'py> FromPyObject<'py> for OperationFromPython { // mapping to avoid an `isinstance` check on `ControlledGate` - a standard gate has // nonzero `num_ctrl_qubits` iff it is a `ControlledGate`. // - // `ControlledGate` also has a `base_gate` attribute, and we don't track enough in Rust - // space to handle the case that that was mutated away from a standard gate. + // `ControlledGate` also has a `base_gate` attribute related to its historical + // implementation, which technically allows mutations from Python space. The only + // mutation of a standard gate's `base_gate` that wouldn't have already broken the + // Python-space data model is setting a label, so we just catch that case and default + // back to non-standard-gate handling in that case. if standard.num_ctrl_qubits() != 0 && ((ob.getattr(intern!(py, "ctrl_state"))?.extract::()? != (1 << standard.num_ctrl_qubits()) - 1) - || ob.getattr(intern!(py, "mutable"))?.extract()?) + || !ob + .getattr(intern!(py, "base_gate"))? + .getattr(intern!(py, "label"))? + .is_none()) { break 'standard; } @@ -604,7 +729,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { return Ok(OperationFromPython { operation: PackedOperation::from_operation(operation), params, - extra_attrs: None, + extra_attrs: ExtraInstructionAttributes::default(), }); } Err(PyTypeError::new_err(format!("invalid input: {}", ob))) diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs new file mode 100644 index 000000000000..37ba83ae5173 --- /dev/null +++ b/crates/circuit/src/converters.rs @@ -0,0 +1,140 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023, 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. + +#[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; + +use ::pyo3::prelude::*; +use hashbrown::HashMap; +use pyo3::{ + intern, + types::{PyDict, PyList}, +}; + +use crate::circuit_data::CircuitData; +use crate::dag_circuit::{DAGCircuit, NodeType}; +use crate::packed_instruction::PackedInstruction; + +/// An extractable representation of a QuantumCircuit reserved only for +/// conversion purposes. +#[derive(Debug, Clone)] +pub struct QuantumCircuitData<'py> { + pub data: CircuitData, + pub name: Option>, + pub calibrations: Option>>, + pub metadata: Option>, + pub qregs: Option>, + pub cregs: Option>, + pub input_vars: Vec>, + pub captured_vars: Vec>, + pub declared_vars: Vec>, +} + +impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let py = ob.py(); + let circuit_data = ob.getattr("_data")?; + let data_borrowed = circuit_data.extract::()?; + Ok(QuantumCircuitData { + data: data_borrowed, + name: ob.getattr(intern!(py, "name")).ok(), + calibrations: ob.getattr(intern!(py, "calibrations"))?.extract().ok(), + metadata: ob.getattr(intern!(py, "metadata")).ok(), + qregs: ob + .getattr(intern!(py, "qregs")) + .map(|ob| ob.downcast_into())? + .ok(), + cregs: ob + .getattr(intern!(py, "cregs")) + .map(|ob| ob.downcast_into())? + .ok(), + input_vars: ob + .call_method0(intern!(py, "iter_input_vars"))? + .iter()? + .collect::>>()?, + captured_vars: ob + .call_method0(intern!(py, "iter_captured_vars"))? + .iter()? + .collect::>>()?, + declared_vars: ob + .call_method0(intern!(py, "iter_declared_vars"))? + .iter()? + .collect::>>()?, + }) + } +} + +#[pyfunction(signature = (quantum_circuit, copy_operations = true, qubit_order = None, clbit_order = None))] +pub fn circuit_to_dag( + py: Python, + quantum_circuit: QuantumCircuitData, + copy_operations: bool, + qubit_order: Option>>, + clbit_order: Option>>, +) -> PyResult { + DAGCircuit::from_circuit( + py, + quantum_circuit, + copy_operations, + qubit_order, + clbit_order, + ) +} + +#[pyfunction(signature = (dag, copy_operations = true))] +pub fn dag_to_circuit( + py: Python, + dag: &DAGCircuit, + copy_operations: bool, +) -> PyResult { + CircuitData::from_packed_instructions( + py, + dag.qubits().clone(), + dag.clbits().clone(), + 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 { + unreachable!( + "The received node from topological_op_nodes() is not an Operation node." + ) + }; + if copy_operations { + let op = instr.op.py_deepcopy(py, None)?; + Ok(PackedInstruction { + op, + qubits: instr.qubits, + clbits: instr.clbits, + params: Some(Box::new( + instr + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + )), + extra_attrs: instr.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + }) + } else { + Ok(instr.clone()) + } + }), + dag.get_global_phase(), + ) +} + +pub fn converters(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(circuit_to_dag, m)?)?; + m.add_function(wrap_pyfunction!(dag_to_circuit, m)?)?; + Ok(()) +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs new file mode 100644 index 000000000000..54f270b30558 --- /dev/null +++ b/crates/circuit/src/dag_circuit.rs @@ -0,0 +1,6911 @@ +// 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 std::hash::{Hash, Hasher}; + +use ahash::RandomState; +use smallvec::SmallVec; + +use crate::bit_data::BitData; +use crate::circuit_data::CircuitData; +use crate::circuit_instruction::{ + CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, +}; +use crate::converters::QuantumCircuitData; +use crate::dag_node::{DAGInNode, DAGNode, DAGOpNode, DAGOutNode}; +use crate::dot_utils::build_dot; +use crate::error::DAGCircuitError; +use crate::imports; +use crate::interner::{Interned, Interner}; +use crate::operations::{Operation, OperationRef, Param, PyInstruction, StandardGate}; +use crate::packed_instruction::{PackedInstruction, PackedOperation}; +use crate::rustworkx_core_vnext::isomorphism; +use crate::{BitType, Clbit, Qubit, TupleLikeArg}; + +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use itertools::Itertools; + +use pyo3::exceptions::{PyIndexError, PyRuntimeError, PyTypeError, PyValueError}; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{ + IntoPyDict, PyDict, PyInt, PyIterator, PyList, PySequence, PySet, PyString, PyTuple, PyType, +}; + +use rustworkx_core::dag_algo::layers; +use rustworkx_core::err::ContractError; +use rustworkx_core::graph_ext::ContractNodesDirected; +use rustworkx_core::petgraph; +use rustworkx_core::petgraph::prelude::StableDiGraph; +use rustworkx_core::petgraph::prelude::*; +use rustworkx_core::petgraph::stable_graph::{EdgeReference, NodeIndex}; +use rustworkx_core::petgraph::unionfind::UnionFind; +use rustworkx_core::petgraph::visit::{ + EdgeIndexable, IntoEdgeReferences, IntoNodeReferences, NodeFiltered, NodeIndexable, +}; +use rustworkx_core::petgraph::Incoming; +use rustworkx_core::traversal::{ + ancestors as core_ancestors, bfs_successors as core_bfs_successors, + descendants as core_descendants, +}; + +use std::cmp::Ordering; +use std::collections::{BTreeMap, VecDeque}; +use std::convert::Infallible; +use std::f64::consts::PI; + +#[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; + +static CONTROL_FLOW_OP_NAMES: [&str; 4] = ["for_loop", "while_loop", "if_else", "switch_case"]; +static SEMANTIC_EQ_SYMMETRIC: [&str; 4] = ["barrier", "swap", "break_loop", "continue_loop"]; + +#[derive(Clone, Debug)] +pub enum NodeType { + QubitIn(Qubit), + QubitOut(Qubit), + ClbitIn(Clbit), + ClbitOut(Clbit), + VarIn(PyObject), + VarOut(PyObject), + Operation(PackedInstruction), +} + +#[derive(Clone, Debug)] +pub enum Wire { + Qubit(Qubit), + Clbit(Clbit), + Var(PyObject), +} + +impl PartialEq for Wire { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Wire::Qubit(q1), Wire::Qubit(q2)) => q1 == q2, + (Wire::Clbit(c1), Wire::Clbit(c2)) => c1 == c2, + (Wire::Var(v1), Wire::Var(v2)) => { + v1.is(v2) || Python::with_gil(|py| v1.bind(py).eq(v2).unwrap()) + } + _ => false, + } + } +} + +impl Eq for Wire {} + +impl Hash for Wire { + fn hash(&self, state: &mut H) { + match self { + Self::Qubit(qubit) => qubit.hash(state), + Self::Clbit(clbit) => clbit.hash(state), + Self::Var(var) => Python::with_gil(|py| var.bind(py).hash().unwrap().hash(state)), + } + } +} + +impl Wire { + fn to_pickle(&self, py: Python) -> PyObject { + match self { + Self::Qubit(bit) => (0, bit.0.into_py(py)).into_py(py), + Self::Clbit(bit) => (1, bit.0.into_py(py)).into_py(py), + Self::Var(var) => (2, var.clone_ref(py)).into_py(py), + } + } + + fn from_pickle(b: &Bound) -> PyResult { + let tuple: Bound = b.extract()?; + let wire_type: usize = tuple.get_item(0)?.extract()?; + if wire_type == 0 { + Ok(Self::Qubit(Qubit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 1 { + Ok(Self::Clbit(Clbit(tuple.get_item(1)?.extract()?))) + } else if wire_type == 2 { + Ok(Self::Var(tuple.get_item(1)?.unbind())) + } else { + Err(PyTypeError::new_err("Invalid wire type")) + } + } +} + +// TODO: Remove me. +// This is a temporary map type used to store a mapping of +// Var to NodeIndex to hold us over until Var is ported to +// Rust. Currently, we need this because PyObject cannot be +// used as the key to an IndexMap. +// +// Once we've got Var ported, Wire should also become Hash + Eq +// and we can consider combining input/output nodes maps. +#[derive(Clone, Debug)] +struct _VarIndexMap { + dict: Py, +} + +impl _VarIndexMap { + pub fn new(py: Python) -> Self { + Self { + dict: PyDict::new_bound(py).unbind(), + } + } + + pub fn keys(&self, py: Python) -> impl Iterator { + self.dict + .bind(py) + .keys() + .into_iter() + .map(|k| k.unbind()) + .collect::>() + .into_iter() + } + + pub fn contains_key(&self, py: Python, key: &PyObject) -> bool { + self.dict.bind(py).contains(key).unwrap() + } + + pub fn get(&self, py: Python, key: &PyObject) -> Option { + self.dict + .bind(py) + .get_item(key) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())) + } + + pub fn insert(&mut self, py: Python, key: PyObject, value: NodeIndex) { + self.dict + .bind(py) + .set_item(key, value.index().into_py(py)) + .unwrap() + } + + pub fn remove(&mut self, py: Python, key: &PyObject) -> Option { + let bound_dict = self.dict.bind(py); + let res = bound_dict + .get_item(key.clone_ref(py)) + .unwrap() + .map(|v| NodeIndex::new(v.extract().unwrap())); + let _del_result = bound_dict.del_item(key); + res + } + pub fn values<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + let values = self.dict.bind(py).values(); + values.iter().map(|x| NodeIndex::new(x.extract().unwrap())) + } + + pub fn iter<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { + self.dict + .bind(py) + .iter() + .map(|(var, index)| (var.unbind(), NodeIndex::new(index.extract().unwrap()))) + } +} + +/// Quantum circuit as a directed acyclic graph. +/// +/// There are 3 types of nodes in the graph: inputs, outputs, and operations. +/// The nodes are connected by directed edges that correspond to qubits and +/// bits. +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +pub struct DAGCircuit { + /// Circuit name. Generally, this corresponds to the name + /// of the QuantumCircuit from which the DAG was generated. + #[pyo3(get, set)] + name: Option, + /// Circuit metadata + #[pyo3(get, set)] + metadata: Option, + + calibrations: HashMap>, + + dag: StableDiGraph, + + #[pyo3(get)] + qregs: Py, + #[pyo3(get)] + cregs: Py, + + /// The cache used to intern instruction qargs. + qargs_interner: Interner<[Qubit]>, + /// The cache used to intern instruction cargs. + cargs_interner: Interner<[Clbit]>, + /// Qubits registered in the circuit. + qubits: BitData, + /// Clbits registered in the circuit. + clbits: BitData, + /// Global phase. + global_phase: Param, + /// Duration. + #[pyo3(get, set)] + duration: Option, + /// Unit of duration. + #[pyo3(get, set)] + unit: String, + + // Note: these are tracked separately from `qubits` and `clbits` + // because it's not yet clear if the Rust concept of a native Qubit + // and Clbit should correspond directly to the numerical Python + // index that users see in the Python API. + /// The index locations of bits, and their positions within + /// registers. + qubit_locations: Py, + clbit_locations: Py, + + /// Map from qubit to input and output nodes of the graph. + qubit_io_map: Vec<[NodeIndex; 2]>, + + /// Map from clbit to input and output nodes of the graph. + clbit_io_map: Vec<[NodeIndex; 2]>, + + // TODO: use IndexMap once Var is ported to Rust + /// Map from var to input nodes of the graph. + var_input_map: _VarIndexMap, + /// Map from var to output nodes of the graph. + var_output_map: _VarIndexMap, + + /// Operation kind to count + op_names: IndexMap, + + // Python modules we need to frequently access (for now). + control_flow_module: PyControlFlowModule, + vars_info: HashMap, + vars_by_type: [Py; 3], +} + +#[derive(Clone, Debug)] +struct PyControlFlowModule { + condition_resources: Py, + node_resources: Py, +} + +#[derive(Clone, Debug)] +struct PyLegacyResources { + clbits: Py, + cregs: Py, +} + +impl PyControlFlowModule { + fn new(py: Python) -> PyResult { + let module = PyModule::import_bound(py, "qiskit.circuit.controlflow")?; + Ok(PyControlFlowModule { + condition_resources: module.getattr("condition_resources")?.unbind(), + node_resources: module.getattr("node_resources")?.unbind(), + }) + } + + fn condition_resources(&self, condition: &Bound) -> PyResult { + let res = self + .condition_resources + .bind(condition.py()) + .call1((condition,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } + + fn node_resources(&self, node: &Bound) -> PyResult { + let res = self.node_resources.bind(node.py()).call1((node,))?; + Ok(PyLegacyResources { + clbits: res.getattr("clbits")?.downcast_into_exact()?.unbind(), + cregs: res.getattr("cregs")?.downcast_into_exact()?.unbind(), + }) + } +} + +struct PyVariableMapper { + mapper: Py, +} + +impl PyVariableMapper { + fn new( + py: Python, + target_cregs: Bound, + bit_map: Option>, + var_map: Option>, + add_register: Option>, + ) -> PyResult { + let kwargs: HashMap<&str, Option>> = + HashMap::from_iter([("add_register", add_register)]); + Ok(PyVariableMapper { + mapper: imports::VARIABLE_MAPPER + .get_bound(py) + .call( + (target_cregs, bit_map, var_map), + Some(&kwargs.into_py_dict_bound(py)), + )? + .unbind(), + }) + } + + fn map_condition<'py>( + &self, + condition: &Bound<'py, PyAny>, + allow_reorder: bool, + ) -> PyResult> { + let py = condition.py(); + let kwargs: HashMap<&str, Py> = + HashMap::from_iter([("allow_reorder", allow_reorder.into_py(py))]); + self.mapper.bind(py).call_method( + intern!(py, "map_condition"), + (condition,), + Some(&kwargs.into_py_dict_bound(py)), + ) + } + + fn map_target<'py>(&self, target: &Bound<'py, PyAny>) -> PyResult> { + let py = target.py(); + self.mapper + .bind(py) + .call_method1(intern!(py, "map_target"), (target,)) + } +} + +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!( + "No register with '{:?}' to map this expression onto.", + reg.getattr("bits")? + ))) +} + +#[pyclass(module = "qiskit._accelerate.circuit")] +#[derive(Clone, Debug)] +struct BitLocations { + #[pyo3(get)] + index: usize, + #[pyo3(get)] + registers: Py, +} + +#[derive(Copy, Clone, Debug)] +enum DAGVarType { + Input = 0, + Capture = 1, + Declare = 2, +} + +#[derive(Clone, Debug)] +struct DAGVarInfo { + var: PyObject, + type_: DAGVarType, + in_node: NodeIndex, + out_node: NodeIndex, +} + +#[pymethods] +impl DAGCircuit { + #[new] + pub fn new(py: Python<'_>) -> PyResult { + Ok(DAGCircuit { + name: None, + metadata: Some(PyDict::new_bound(py).unbind().into()), + calibrations: HashMap::new(), + dag: StableDiGraph::default(), + qregs: PyDict::new_bound(py).unbind(), + cregs: PyDict::new_bound(py).unbind(), + qargs_interner: Interner::new(), + cargs_interner: Interner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + 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_io_map: Vec::new(), + clbit_io_map: Vec::new(), + var_input_map: _VarIndexMap::new(py), + var_output_map: _VarIndexMap::new(py), + op_names: IndexMap::default(), + 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(), + ], + }) + } + + #[getter] + fn input_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, + )?; + } + for (var, index) in self.var_input_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + #[getter] + fn output_map(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(py); + for (qubit, indices) in self + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + out_dict.set_item( + self.qubits.get(qubit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (clbit, indices) in self + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + out_dict.set_item( + self.clbits.get(clbit).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, + )?; + } + for (var, index) in self.var_output_map.dict.bind(py).iter() { + out_dict.set_item( + var, + self.get_node(py, NodeIndex::new(index.extract::()?))?, + )?; + } + Ok(out_dict.unbind()) + } + + fn __getstate__(&self, py: Python) -> PyResult> { + let out_dict = PyDict::new_bound(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", self.calibrations.clone())?; + out_dict.set_item("qregs", self.qregs.clone_ref(py))?; + out_dict.set_item("cregs", self.cregs.clone_ref(py))?; + out_dict.set_item("global_phase", self.global_phase.clone())?; + out_dict.set_item( + "qubit_io_map", + self.qubit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item( + "clbit_io_map", + self.clbit_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .collect::>(), + )?; + out_dict.set_item("var_input_map", self.var_input_map.dict.clone_ref(py))?; + out_dict.set_item("var_output_map", self.var_output_map.dict.clone_ref(py))?; + out_dict.set_item("op_name", self.op_names.clone())?; + out_dict.set_item( + "vars_info", + self.vars_info + .iter() + .map(|(k, v)| { + ( + k, + ( + v.var.clone_ref(py), + v.type_ as u8, + v.in_node.index(), + v.out_node.index(), + ), + ) + }) + .collect::>(), + )?; + out_dict.set_item("vars_by_type", self.vars_by_type.clone())?; + out_dict.set_item("qubits", self.qubits.bits())?; + out_dict.set_item("clbits", self.clbits.bits())?; + 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)); + } + out_dict.set_item("nodes", nodes)?; + out_dict.set_item( + "nodes_removed", + self.dag.node_count() != self.dag.node_bound(), + )?; + let mut edges: Vec = Vec::with_capacity(self.dag.edge_bound()); + // edges are saved with none (deleted edges) instead of their index to save space + for i in 0..self.dag.edge_bound() { + let idx = EdgeIndex::new(i); + let edge = match self.dag.edge_weight(idx) { + Some(edge_w) => { + let endpoints = self.dag.edge_endpoints(idx).unwrap(); + ( + endpoints.0.index(), + endpoints.1.index(), + edge_w.clone().to_pickle(py), + ) + .to_object(py) + } + None => py.None(), + }; + edges.push(edge); + } + out_dict.set_item("edges", edges)?; + Ok(out_dict.unbind()) + } + + fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> { + let dict_state = state.downcast_bound::(py)?; + self.name = dict_state.get_item("name")?.unwrap().extract()?; + self.metadata = dict_state.get_item("metadata")?.unwrap().extract()?; + self.calibrations = dict_state.get_item("calibrations")?.unwrap().extract()?; + self.qregs = dict_state.get_item("qregs")?.unwrap().extract()?; + self.cregs = dict_state.get_item("cregs")?.unwrap().extract()?; + self.global_phase = dict_state.get_item("global_phase")?.unwrap().extract()?; + self.op_names = dict_state.get_item("op_name")?.unwrap().extract()?; + self.var_input_map = _VarIndexMap { + dict: dict_state.get_item("var_input_map")?.unwrap().extract()?, + }; + self.var_output_map = _VarIndexMap { + dict: dict_state.get_item("var_output_map")?.unwrap().extract()?, + }; + self.vars_by_type = dict_state.get_item("vars_by_type")?.unwrap().extract()?; + let binding = dict_state.get_item("vars_info")?.unwrap(); + let vars_info_raw = binding.downcast::().unwrap(); + self.vars_info = HashMap::with_capacity(vars_info_raw.len()); + for (key, value) in vars_info_raw.iter() { + let val_tuple = value.downcast::()?; + let info = DAGVarInfo { + var: val_tuple.get_item(0)?.unbind(), + type_: match val_tuple.get_item(1)?.extract::()? { + 0 => DAGVarType::Input, + 1 => DAGVarType::Capture, + 2 => DAGVarType::Declare, + _ => return Err(PyValueError::new_err("Invalid var type")), + }, + in_node: NodeIndex::new(val_tuple.get_item(2)?.extract()?), + out_node: NodeIndex::new(val_tuple.get_item(3)?.extract()?), + }; + self.vars_info.insert(key.extract()?, info); + } + + let binding = dict_state.get_item("qubits")?.unwrap(); + let qubits_raw = binding.downcast::().unwrap(); + for bit in qubits_raw.iter() { + self.qubits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("clbits")?.unwrap(); + let clbits_raw = binding.downcast::().unwrap(); + for bit in clbits_raw.iter() { + self.clbits.add(py, &bit, false)?; + } + let binding = dict_state.get_item("qubit_io_map")?.unwrap(); + let qubit_index_map_raw = binding.downcast::().unwrap(); + self.qubit_io_map = Vec::with_capacity(qubit_index_map_raw.len()); + for (_k, v) in qubit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.qubit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + let binding = dict_state.get_item("clbit_io_map")?.unwrap(); + let clbit_index_map_raw = binding.downcast::().unwrap(); + self.clbit_io_map = Vec::with_capacity(clbit_index_map_raw.len()); + + for (_k, v) in clbit_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.clbit_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } + // Rebuild Graph preserving index holes: + let binding = dict_state.get_item("nodes")?.unwrap(); + let nodes_lst = binding.downcast::()?; + let binding = dict_state.get_item("edges")?.unwrap(); + let edges_lst = binding.downcast::()?; + let node_removed: bool = dict_state.get_item("nodes_removed")?.unwrap().extract()?; + self.dag = StableDiGraph::default(); + if !node_removed { + for item in nodes_lst.iter() { + let node_w = item.downcast::().unwrap().get_item(1).unwrap(); + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + } + } else if nodes_lst.len() == 1 { + // graph has only one node, handle logic here to save one if in the loop later + let binding = nodes_lst.get_item(0).unwrap(); + let item = binding.downcast::().unwrap(); + let node_idx: usize = item.get_item(0).unwrap().extract().unwrap(); + let node_w = item.get_item(1).unwrap(); + + for _i in 0..node_idx { + self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + } + let weight = self.pack_into(py, &node_w)?; + self.dag.add_node(weight); + for i in 0..node_idx { + self.dag.remove_node(NodeIndex::new(i)); + } + } else { + let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap(); + let last_item = binding.downcast::().unwrap(); + + // list of temporary nodes that will be removed later to re-create holes + let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap(); + let mut tmp_nodes: Vec = + Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len()); + + for item in nodes_lst { + let item = item.downcast::().unwrap(); + let next_index: usize = item.get_item(0).unwrap().extract().unwrap(); + let weight: PyObject = item.get_item(1).unwrap().extract().unwrap(); + while next_index > self.dag.node_bound() { + // node does not exist + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + tmp_nodes.push(tmp_node); + } + // add node to the graph, and update the next available node index + let weight = self.pack_into(py, weight.bind(py))?; + self.dag.add_node(weight); + } + // Remove any temporary nodes we added + for tmp_node in tmp_nodes { + self.dag.remove_node(tmp_node); + } + } + + // to ensure O(1) on edge deletion, use a temporary node to store missing edges + let tmp_node = self.dag.add_node(NodeType::QubitIn(Qubit(u32::MAX))); + + for item in edges_lst { + if item.is_none() { + // add a temporary edge that will be deleted later to re-create the hole + self.dag + .add_edge(tmp_node, tmp_node, Wire::Qubit(Qubit(u32::MAX))); + } else { + let triple = item.downcast::().unwrap(); + let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap(); + let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap(); + let edge_w = Wire::from_pickle(&triple.get_item(2).unwrap())?; + self.dag + .add_edge(NodeIndex::new(edge_p), NodeIndex::new(edge_c), edge_w); + } + } + self.dag.remove_node(tmp_node); + Ok(()) + } + + /// Returns the current sequence of registered :class:`.Qubit` instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Qubit`): The current sequence of registered qubits. + #[getter(qubits)] + pub fn py_qubits(&self, py: Python<'_>) -> Py { + self.qubits.cached().clone_ref(py) + } + + /// Returns the current sequence of registered :class:`.Clbit` + /// instances as a list. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`DAGCircuit` data + /// structures. + /// + /// Returns: + /// list(:class:`.Clbit`): The current sequence of registered clbits. + #[getter(clbits)] + pub fn py_clbits(&self, py: Python<'_>) -> Py { + self.clbits.cached().clone_ref(py) + } + + /// Return a list of the wires in order. + #[getter] + fn get_wires(&self, py: Python<'_>) -> PyResult> { + let wires: Vec<&PyObject> = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits().iter()) + .collect(); + let out_list = PyList::new_bound(py, wires); + for var_type_set in &self.vars_by_type { + for var in var_type_set.bind(py).iter() { + out_list.append(var)?; + } + } + Ok(out_list.unbind()) + } + + /// Returns the number of nodes in the dag. + #[getter] + fn get_node_counter(&self) -> usize { + self.dag.node_count() + } + + /// Return the global phase of the circuit. + #[getter] + pub fn get_global_phase(&self) -> Param { + self.global_phase.clone() + } + + /// Set the global phase of the circuit. + /// + /// Args: + /// angle (float, :class:`.ParameterExpression`): The phase angle. + #[setter] + fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * PI)); + } + Param::ParameterExpression(angle) => { + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyTypeError::new_err("Invalid type for global phase")), + } + Ok(()) + } + + /// Return calibration dictionary. + /// + /// The custom pulse definition of a given gate is of the form + /// {'gate_name': {(qubits, params): schedule}} + #[getter] + fn get_calibrations(&self) -> HashMap> { + self.calibrations.clone() + } + + /// Set the circuit calibration data from a dictionary of calibration definition. + /// + /// Args: + /// calibrations (dict): A dictionary of input in the format + /// {'gate_name': {(qubits, gate_params): schedule}} + #[setter] + fn set_calibrations(&mut self, calibrations: HashMap>) { + self.calibrations = calibrations; + } + + /// Register a low-level, custom pulse definition for the given gate. + /// + /// Args: + /// gate (Union[Gate, str]): Gate information. + /// qubits (Union[int, Tuple[int]]): List of qubits to be measured. + /// schedule (Schedule): Schedule information. + /// params (Optional[List[Union[float, Parameter]]]): A list of parameters. + /// + /// Raises: + /// Exception: if the gate is of type string and params is None. + fn add_calibration<'py>( + &mut self, + py: Python<'py>, + mut gate: Bound<'py, PyAny>, + qubits: Bound<'py, PyAny>, + schedule: Py, + mut params: Option>, + ) -> PyResult<()> { + if gate.is_instance(imports::GATE.get_bound(py))? { + params = Some(gate.getattr(intern!(py, "params"))?); + gate = gate.getattr(intern!(py, "name"))?; + } + + let params_tuple = if let Some(operands) = params { + let add_calibration = PyModule::from_code_bound( + py, + r#" +import numpy as np + +def _format(operand): + try: + # Using float/complex value as a dict key is not good idea. + # This makes the mapping quite sensitive to the rounding error. + # However, the mechanism is already tied to the execution model (i.e. pulse gate) + # and we cannot easily update this rule. + # The same logic exists in QuantumCircuit.add_calibration. + evaluated = complex(operand) + if np.isreal(evaluated): + evaluated = float(evaluated.real) + if evaluated.is_integer(): + evaluated = int(evaluated) + return evaluated + except TypeError: + # Unassigned parameter + return operand + "#, + "add_calibration.py", + "add_calibration", + )?; + + let format = add_calibration.getattr("_format")?; + let mapped: PyResult> = operands.iter()?.map(|p| format.call1((p?,))).collect(); + PyTuple::new_bound(py, mapped?).into_any() + } else { + PyTuple::empty_bound(py).into_any() + }; + + let calibrations = self + .calibrations + .entry(gate.extract()?) + .or_insert_with(|| PyDict::new_bound(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() + }; + let key = PyTuple::new_bound(py, &[qubits.unbind(), params_tuple.into_any().unbind()]); + calibrations.set_item(key, schedule)?; + Ok(()) + } + + /// Return True if the dag has a calibration defined for the node operation. In this + /// case, the operation does not need to be translated to the device basis. + fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { + if !self + .calibrations + .contains_key(node.instruction.operation.name()) + { + return Ok(false); + } + let mut params = Vec::new(); + for p in &node.instruction.params { + if let Param::ParameterExpression(exp) = p { + let exp = exp.bind(py); + if !exp.getattr(intern!(py, "parameters"))?.is_truthy()? { + let as_py_float = exp.call_method0(intern!(py, "__float__"))?; + params.push(as_py_float.unbind()); + continue; + } + } + params.push(p.to_object(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); + self.calibrations[node.instruction.operation.name()] + .bind(py) + .contains((qubits, params).to_object(py)) + } + + /// Remove all operation nodes with the given name. + fn remove_all_ops_named(&mut self, opname: &str) { + let mut to_remove = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if opname == packed.op.name() { + to_remove.push(id); + } + } + } + for node in to_remove { + self.remove_op_node(node); + } + } + + /// Add individual qubit wires. + fn add_qubits(&mut self, py: Python, qubits: Vec>) -> PyResult<()> { + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Qubit instance.")); + } + + if self.qubits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate qubits {}", + bit + ))); + } + } + + for bit in qubits.iter() { + self.add_qubit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add individual qubit wires. + fn add_clbits(&mut self, py: Python, clbits: Vec>) -> PyResult<()> { + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a Clbit instance.")); + } + + if self.clbits.find(bit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate clbits {}", + bit + ))); + } + } + + for bit in clbits.iter() { + self.add_clbit_unchecked(py, bit)?; + } + Ok(()) + } + + /// Add all wires in a quantum register. + fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); + } + + let register_name = qreg.getattr(intern!(py, "name"))?; + if self.qregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.qregs.bind(py).set_item(®ister_name, qreg)?; + + for (index, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + if self.qubits.find(&bit).is_none() { + self.add_qubit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .qubit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((qreg, index))?; + } + Ok(()) + } + + /// Add all wires in a classical register. + fn add_creg(&mut self, py: Python, creg: &Bound) -> PyResult<()> { + if !creg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + return Err(DAGCircuitError::new_err( + "not a ClassicalRegister instance.", + )); + } + + let register_name = creg.getattr(intern!(py, "name"))?; + if self.cregs.bind(py).contains(®ister_name)? { + return Err(DAGCircuitError::new_err(format!( + "duplicate register {}", + register_name + ))); + } + self.cregs.bind(py).set_item(register_name, creg)?; + + for (index, bit) in creg.iter()?.enumerate() { + let bit = bit?; + if self.clbits.find(&bit).is_none() { + self.add_clbit_unchecked(py, &bit)?; + } + let locations: PyRef = self + .clbit_locations + .bind(py) + .get_item(&bit)? + .unwrap() + .extract()?; + locations.registers.bind(py).append((creg, index))?; + } + Ok(()) + } + + /// Finds locations in the circuit, by mapping the Qubit and Clbit to positional index + /// BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) + /// + /// Args: + /// bit (Bit): The bit to locate. + /// + /// Returns: + /// namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) + /// contains the index at which the ``Bit`` can be found (in either + /// :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its + /// type). The second element (``registers``) is a list of ``(register, index)`` + /// pairs with an entry for each :obj:`~Register` in the circuit which contains the + /// :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). + /// + /// 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> { + 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!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + if bit.is_instance(imports::CLBIT.get_bound(py))? { + return self.clbit_locations.bind(py).get_item(bit)?.ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Could not locate provided bit: {}. Has it been added to the DAGCircuit?", + bit + )) + }); + } + + Err(DAGCircuitError::new_err(format!( + "Could not locate bit of unknown type: {}", + bit.get_type() + ))) + } + + /// Remove classical bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// clbits (List[Clbit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*clbits))] + fn remove_clbits(&mut self, py: Python, clbits: &Bound) -> PyResult<()> { + let mut non_bits = Vec::new(); + for bit in clbits.iter() { + if !bit.is_instance(imports::CLBIT.get_bound(py))? { + non_bits.push(bit); + } + } + if !non_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not of type Clbit: {:?}", + non_bits + ))); + } + + let bit_iter = match self.clbits.map_bits(clbits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "clbits not in circuit: {:?}", + clbits + ))) + } + }; + let clbits: HashSet = bit_iter.collect(); + let mut busy_bits = Vec::new(); + for bit in clbits.iter() { + if !self.is_wire_idle(py, &Wire::Clbit(*bit))? { + busy_bits.push(self.clbits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "clbits not idle: {:?}", + busy_bits + ))); + } + + // 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()? { + let bit = bit?; + if clbits.contains(&self.clbits.find(&bit).unwrap()) { + cregs_to_remove.push(creg); + break; + } + } + } + self.remove_cregs(py, &PyTuple::new_bound(py, cregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in clbits.iter() { + self.remove_idle_wire(py, Wire::Clbit(*bit))?; + } + + // Copy the current clbit mapping so we can use it while remapping + // wires used on edges and in operation cargs. + let old_clbits = self.clbits.clone(); + + // Remove the clbit indices, which will invalidate our mapping of Clbit to + // Python bits throughout the entire DAG. + self.clbits.remove_indices(py, clbits.clone())?; + + // Update input/output maps to use new Clbits. + let io_mapping: HashMap = self + .clbit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let clbit = Clbit(k as u32); + if clbits.contains(&clbit) { + None + } else { + Some(( + self.clbits + .find(old_clbits.get(Clbit(k as u32)).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.clbit_io_map = (0..io_mapping.len()) + .map(|idx| { + let clbit = Clbit(idx as u32); + io_mapping[&clbit] + }) + .collect(); + + // Update edges to use the new Clbits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Clbit(c) = edge_weight { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation cargs to use the new Clbits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let cargs = self.cargs_interner.get(op.clbits); + let carg_bits = old_clbits.map_indices(cargs).map(|b| b.bind(py).clone()); + op.clbits = self + .cargs_interner + .insert_owned(self.clbits.map_bits(carg_bits)?.collect()); + } + NodeType::ClbitIn(c) | NodeType::ClbitOut(c) => { + *c = self + .clbits + .find(old_clbits.get(*c).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.clbit_locations.bind(py); + for (i, bit) in self.clbits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove classical registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a creg is not a ClassicalRegister, or is not in + /// the circuit. + #[pyo3(signature = (*cregs))] + fn remove_cregs(&mut self, py: Python, cregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_cregs = self.cregs.bind(py); + for reg in cregs.iter() { + if !reg.is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_creg) = + self_bound_cregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_creg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not of type ClassicalRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "cregs not in circuit: {:?}", + unknown_regs + ))); + } + + for creg in cregs { + self.cregs + .bind(py) + .del_item(creg.getattr(intern!(py, "name"))?)?; + for (i, bit) in creg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .clbit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&creg, i),))?; + } + } + Ok(()) + } + + /// Remove quantum bits from the circuit. All bits MUST be idle. + /// Any registers with references to at least one of the specified bits will + /// also be removed. + /// + /// .. warning:: + /// This method is rather slow, since it must iterate over the entire + /// DAG to fix-up bit indices. + /// + /// Args: + /// qubits (List[~qiskit.circuit.Qubit]): The bits to remove. + /// + /// Raises: + /// DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, + /// or is not idle. + #[pyo3(signature = (*qubits))] + fn remove_qubits(&mut self, py: Python, qubits: &Bound) -> PyResult<()> { + let mut non_qbits = Vec::new(); + for bit in qubits.iter() { + if !bit.is_instance(imports::QUBIT.get_bound(py))? { + non_qbits.push(bit); + } + } + if !non_qbits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not of type Qubit: {:?}", + non_qbits + ))); + } + + let bit_iter = match self.qubits.map_bits(qubits.iter()) { + Ok(bit_iter) => bit_iter, + Err(_) => { + return Err(DAGCircuitError::new_err(format!( + "qubits not in circuit: {:?}", + qubits + ))) + } + }; + let qubits: HashSet = bit_iter.collect(); + + let mut busy_bits = Vec::new(); + for bit in qubits.iter() { + if !self.is_wire_idle(py, &Wire::Qubit(*bit))? { + busy_bits.push(self.qubits.get(*bit).unwrap()); + } + } + + if !busy_bits.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qubits not idle: {:?}", + busy_bits + ))); + } + + // 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()? { + let bit = bit?; + if qubits.contains(&self.qubits.find(&bit).unwrap()) { + qregs_to_remove.push(qreg); + break; + } + } + } + self.remove_qregs(py, &PyTuple::new_bound(py, qregs_to_remove))?; + + // Remove DAG in/out nodes etc. + for bit in qubits.iter() { + self.remove_idle_wire(py, Wire::Qubit(*bit))?; + } + + // Copy the current qubit mapping so we can use it while remapping + // wires used on edges and in operation qargs. + let old_qubits = self.qubits.clone(); + + // Remove the qubit indices, which will invalidate our mapping of Qubit to + // Python bits throughout the entire DAG. + self.qubits.remove_indices(py, qubits.clone())?; + + // Update input/output maps to use new Qubits. + let io_mapping: HashMap = self + .qubit_io_map + .drain(..) + .enumerate() + .filter_map(|(k, v)| { + let qubit = Qubit(k as u32); + if qubits.contains(&qubit) { + None + } else { + Some(( + self.qubits + .find(old_qubits.get(qubit).unwrap().bind(py)) + .unwrap(), + v, + )) + } + }) + .collect(); + + self.qubit_io_map = (0..io_mapping.len()) + .map(|idx| { + let qubit = Qubit(idx as u32); + io_mapping[&qubit] + }) + .collect(); + + // Update edges to use the new Qubits. + for edge_weight in self.dag.edge_weights_mut() { + if let Wire::Qubit(b) = edge_weight { + *b = self + .qubits + .find(old_qubits.get(*b).unwrap().bind(py)) + .unwrap(); + } + } + + // Update operation qargs to use the new Qubits. + for node_weight in self.dag.node_weights_mut() { + match node_weight { + NodeType::Operation(op) => { + let qargs = self.qargs_interner.get(op.qubits); + let qarg_bits = old_qubits.map_indices(qargs).map(|b| b.bind(py).clone()); + op.qubits = self + .qargs_interner + .insert_owned(self.qubits.map_bits(qarg_bits)?.collect()); + } + NodeType::QubitIn(q) | NodeType::QubitOut(q) => { + *q = self + .qubits + .find(old_qubits.get(*q).unwrap().bind(py)) + .unwrap(); + } + _ => (), + } + } + + // Update bit locations. + let bit_locations = self.qubit_locations.bind(py); + for (i, bit) in self.qubits.bits().iter().enumerate() { + let raw_loc = bit_locations.get_item(bit)?.unwrap(); + let loc = raw_loc.downcast::().unwrap(); + loc.borrow_mut().index = i; + bit_locations.set_item(bit, loc)?; + } + Ok(()) + } + + /// Remove quantum registers from the circuit, leaving underlying bits + /// in place. + /// + /// Raises: + /// DAGCircuitError: a qreg is not a QuantumRegister, or is not in + /// the circuit. + #[pyo3(signature = (*qregs))] + fn remove_qregs(&mut self, py: Python, qregs: &Bound) -> PyResult<()> { + let mut non_regs = Vec::new(); + let mut unknown_regs = Vec::new(); + let self_bound_qregs = self.qregs.bind(py); + for reg in qregs.iter() { + if !reg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { + non_regs.push(reg); + } else if let Some(existing_qreg) = + self_bound_qregs.get_item(®.getattr(intern!(py, "name"))?)? + { + if !existing_qreg.eq(®)? { + unknown_regs.push(reg); + } + } else { + unknown_regs.push(reg); + } + } + if !non_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not of type QuantumRegister: {:?}", + non_regs + ))); + } + if !unknown_regs.is_empty() { + return Err(DAGCircuitError::new_err(format!( + "qregs not in circuit: {:?}", + unknown_regs + ))); + } + + for qreg in qregs { + self.qregs + .bind(py) + .del_item(qreg.getattr(intern!(py, "name"))?)?; + for (i, bit) in qreg.iter()?.enumerate() { + let bit = bit?; + let bit_position = self + .qubit_locations + .bind(py) + .get_item(bit)? + .unwrap() + .downcast_into_exact::()?; + bit_position + .borrow() + .registers + .bind(py) + .as_any() + .call_method1(intern!(py, "remove"), ((&qreg, i),))?; + } + } + Ok(()) + } + + /// Verify that the condition is valid. + /// + /// Args: + /// name (string): used for error reporting + /// condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) + /// + /// Raises: + /// DAGCircuitError: if conditioning on an invalid register + fn _check_condition(&self, py: Python, name: &str, condition: &Bound) -> PyResult<()> { + if condition.is_none() { + return Ok(()); + } + + let resources = self.control_flow_module.condition_resources(condition)?; + for reg in resources.cregs.bind(py) { + if !self + .cregs + .bind(py) + .contains(reg.getattr(intern!(py, "name"))?)? + { + return Err(DAGCircuitError::new_err(format!( + "invalid creg in condition for {}", + name + ))); + } + } + + for bit in resources.clbits.bind(py) { + if self.clbits.find(&bit).is_none() { + return Err(DAGCircuitError::new_err(format!( + "invalid clbits in condition for {}", + name + ))); + } + } + + Ok(()) + } + + /// Return a copy of self with the same structure but empty. + /// + /// That structure includes: + /// * name and other metadata + /// * global phase + /// * duration + /// * all the qubits and clbits, including the registers. + /// + /// Returns: + /// DAGCircuit: An empty copy of self. + #[pyo3(signature = (*, vars_mode="alike"))] + fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { + let mut target_dag = DAGCircuit::with_capacity( + py, + self.num_qubits(), + self.num_clbits(), + Some(self.num_vars()), + None, + None, + )?; + target_dag.name = self.name.as_ref().map(|n| n.clone_ref(py)); + target_dag.global_phase = self.global_phase.clone(); + target_dag.duration = self.duration.as_ref().map(|d| d.clone_ref(py)); + target_dag.unit.clone_from(&self.unit); + target_dag.metadata = self.metadata.as_ref().map(|m| m.clone_ref(py)); + target_dag.qargs_interner = self.qargs_interner.clone(); + target_dag.cargs_interner = self.cargs_interner.clone(); + + for bit in self.qubits.bits() { + target_dag.add_qubit_unchecked(py, bit.bind(py))?; + } + for bit in self.clbits.bits() { + target_dag.add_clbit_unchecked(py, bit.bind(py))?; + } + for reg in self.qregs.bind(py).values() { + target_dag.add_qreg(py, ®)?; + } + for reg in self.cregs.bind(py).values() { + target_dag.add_creg(py, ®)?; + } + if vars_mode == "alike" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Input)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Declare)?; + } + } else if vars_mode == "captures" { + for var in self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + for var in self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .iter() + { + target_dag.add_var(py, &var, DAGVarType::Capture)?; + } + } else if vars_mode != "drop" { + return Err(PyValueError::new_err(format!( + "unknown vars_mode: '{}'", + vars_mode + ))); + } + + Ok(target_dag) + } + + #[pyo3(signature=(node, check=false))] + fn _apply_op_node_back( + &mut self, + py: Python, + node: &Bound, + check: bool, + ) -> PyResult<()> { + if let NodeType::Operation(inst) = self.pack_into(py, node)? { + if check { + self.check_op_addition(py, &inst)?; + } + + self.push_back(py, inst)?; + Ok(()) + } else { + Err(PyTypeError::new_err("Invalid node type input")) + } + } + + /// Apply an operation to the output of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if a leaf node is connected to multiple outputs + #[pyo3(name = "apply_operation_back", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_back( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = self + .qargs_interner + .insert_owned(self.qubits.map_bits(qargs.iter().flatten())?.collect()); + let clbits_id = self + .cargs_interner + .insert_owned(self.clbits.map_bits(cargs.iter().flatten())?.collect()); + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_back(py, instr)? + }; + + self.get_node(py, node) + } + + /// Apply an operation to the input of the circuit. + /// + /// Args: + /// op (qiskit.circuit.Operation): the operation associated with the DAG node + /// qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to + /// cargs (tuple[Clbit]): cbits that op will be applied to + /// check (bool): If ``True`` (default), this function will enforce that the + /// :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are + /// :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* + /// uphold these invariants itself, but the cost of several checks will be skipped. + /// This is most useful when building a new DAG from a source of known-good nodes. + /// Returns: + /// DAGOpNode: the node for the op that was added to the dag + /// + /// Raises: + /// DAGCircuitError: if initial nodes connected to multiple out edges + #[pyo3(name = "apply_operation_front", signature = (op, qargs=None, cargs=None, *, check=true))] + fn py_apply_operation_front( + &mut self, + py: Python, + op: Bound, + qargs: Option, + cargs: Option, + check: bool, + ) -> PyResult> { + let py_op = op.extract::()?; + let qargs = qargs.map(|q| q.value); + let cargs = cargs.map(|c| c.value); + let node = { + let qubits_id = self + .qargs_interner + .insert_owned(self.qubits.map_bits(qargs.iter().flatten())?.collect()); + let clbits_id = self + .cargs_interner + .insert_owned(self.clbits.map_bits(cargs.iter().flatten())?.collect()); + let instr = PackedInstruction { + op: py_op.operation, + qubits: qubits_id, + clbits: clbits_id, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + if check { + self.check_op_addition(py, &instr)?; + } + self.push_front(py, instr)? + }; + + self.get_node(py, node) + } + + /// Compose the ``other`` circuit onto the output of this circuit. + /// + /// A subset of input wires of ``other`` are mapped + /// to a subset of output wires of this circuit. + /// + /// ``other`` can be narrower or of equal width to ``self``. + /// + /// Args: + /// other (DAGCircuit): circuit to compose with self + /// qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. + /// clbits (list[Clbit|int]): clbits of self to compose onto. + /// front (bool): If True, front composition will be performed (not implemented yet) + /// inplace (bool): If True, modify the object. Otherwise return composed circuit. + /// inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG + /// will be inlined onto existing uses of those same variables in ``self``. If ``False``, + /// all variables in ``other`` are required to be distinct from ``self``, and they will + /// be added to ``self``. + /// + /// .. + /// Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's + /// because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, + /// and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within + /// itself than to join unrelated circuits. While there's no strong motivating use-case + /// (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide + /// the option. + /// + /// Returns: + /// DAGCircuit: the composed dag (returns None if inplace==True). + /// + /// Raises: + /// DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (other, qubits=None, clbits=None, front=false, inplace=true, *, inline_captures=false))] + fn compose( + slf: PyRefMut, + py: Python, + other: &DAGCircuit, + qubits: Option>, + clbits: Option>, + front: bool, + inplace: bool, + inline_captures: bool, + ) -> PyResult> { + if front { + return Err(DAGCircuitError::new_err( + "Front composition not supported yet.", + )); + } + + if other.qubits.len() > slf.qubits.len() || other.clbits.len() > slf.clbits.len() { + return Err(DAGCircuitError::new_err( + "Trying to compose with another DAGCircuit which has more 'in' edges.", + )); + } + + // Number of qubits and clbits must match number in circuit or None + let identity_qubit_map = other + .qubits + .bits() + .iter() + .zip(slf.qubits.bits()) + .into_py_dict_bound(py); + let identity_clbit_map = other + .clbits + .bits() + .iter() + .zip(slf.clbits.bits()) + .into_py_dict_bound(py); + + let qubit_map: Bound = match qubits { + None => identity_qubit_map.clone(), + Some(qubits) => { + if qubits.len() != other.qubits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in qubits parameter does not", + " match number of qubits in the circuit." + ))); + } + + let self_qubits = slf.qubits.cached().bind(py); + let other_qubits = other.qubits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in qubits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_qubits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_qubits.get_item(i)?, q)?; + } + dict + } + }; + + let clbit_map: Bound = match clbits { + None => identity_clbit_map.clone(), + Some(clbits) => { + if clbits.len() != other.clbits.len() { + return Err(DAGCircuitError::new_err(concat!( + "Number of items in clbits parameter does not", + " match number of clbits in the circuit." + ))); + } + + let self_clbits = slf.clbits.cached().bind(py); + let other_clbits = other.clbits.cached().bind(py); + let dict = PyDict::new_bound(py); + for (i, q) in clbits.iter().enumerate() { + let q = if q.is_instance_of::() { + self_clbits.get_item(q.extract()?)? + } else { + q + }; + + dict.set_item(other_clbits.get_item(i)?, q)?; + } + dict + } + }; + + let edge_map = if qubit_map.is_empty() && clbit_map.is_empty() { + // try to do a 1-1 mapping in order + identity_qubit_map + .iter() + .chain(identity_clbit_map.iter()) + .into_py_dict_bound(py) + } else { + qubit_map + .iter() + .chain(clbit_map.iter()) + .into_py_dict_bound(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() { + return Err(DAGCircuitError::new_err("duplicates in wire_map")); + } + } + + // Compose + let mut dag: PyRefMut = if inplace { + slf + } else { + Py::new(py, slf.clone())?.into_bound(py).borrow_mut() + }; + + dag.global_phase = add_global_phase(py, &dag.global_phase, &other.global_phase)?; + + for (gate, cals) in other.calibrations.iter() { + let calibrations = match dag.calibrations.get(gate) { + Some(calibrations) => calibrations, + None => { + dag.calibrations + .insert(gate.clone(), PyDict::new_bound(py).unbind()); + &dag.calibrations[gate] + } + }; + calibrations.bind(py).update(cals.bind(py).as_mapping())?; + } + + // This is all the handling we need for realtime variables, if there's no remapping. They: + // + // * get added to the DAG and then operations involving them get appended on normally. + // * get inlined onto an existing variable, then operations get appended normally. + // * there's a clash or a failed inlining, and we just raise an error. + // + // Notably if there's no remapping, there's no need to recurse into control-flow or to do any + // Var rewriting during the Expr visits. + for var in other.iter_input_vars(py)?.bind(py) { + dag.add_input_var(py, &var?)?; + } + if inline_captures { + for var in other.iter_captured_vars(py)?.bind(py) { + let var = var?; + if !dag.has_var(&var)? { + return Err(DAGCircuitError::new_err(format!("Variable '{}' to be inlined is not in the base DAG. If you wanted it to be automatically added, use `inline_captures=False`.", var))); + } + } + } else { + for var in other.iter_captured_vars(py)?.bind(py) { + dag.add_captured_var(py, &var?)?; + } + } + for var in other.iter_declared_vars(py)?.bind(py) { + dag.add_declared_var(py, &var?)?; + } + + let variable_mapper = PyVariableMapper::new( + py, + dag.cregs.bind(py).values().into_any(), + Some(edge_map.clone()), + None, + Some(wrap_pyfunction_bound!(reject_new_register, py)?.to_object(py)), + )?; + + for node in other.topological_nodes()? { + match &other.dag[node] { + NodeType::QubitIn(q) => { + let bit = other.qubits.get(*q).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.qubits.find(&m_wire); + + if wire_in_dag.is_none() + || (dag.qubit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize) + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::ClbitIn(c) => { + let bit = other.clbits.get(*c).unwrap().bind(py); + let m_wire = edge_map.get_item(bit)?.unwrap_or_else(|| bit.clone()); + let wire_in_dag = dag.clbits.find(&m_wire); + if wire_in_dag.is_none() + || dag.clbit_io_map.len() - 1 < wire_in_dag.unwrap().0 as usize + { + return Err(DAGCircuitError::new_err(format!( + "wire {} not in self", + m_wire, + ))); + } + // TODO: Python code has check here if node.wire is in other._wires. Why? + } + NodeType::Operation(op) => { + let m_qargs = { + let qubits = other + .qubits + .map_indices(other.qargs_interner.get(op.qubits)); + let mut mapped = Vec::with_capacity(qubits.len()); + for bit in qubits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + let m_cargs = { + let clbits = other + .clbits + .map_indices(other.cargs_interner.get(op.clbits)); + let mut mapped = Vec::with_capacity(clbits.len()); + for bit in clbits { + mapped.push( + edge_map + .get_item(bit)? + .unwrap_or_else(|| bit.bind(py).clone()), + ); + } + PyTuple::new_bound(py, mapped) + }; + + // We explicitly create a mutable py_op here since we might + // update the condition. + let mut py_op = op.unpack_py_op(py)?.into_bound(py); + if py_op.getattr(intern!(py, "mutable"))?.extract::()? { + py_op = py_op.call_method0(intern!(py, "to_mutable"))?; + } + + if let Some(condition) = op.condition() { + // TODO: do we need to check for condition.is_none()? + let condition = variable_mapper.map_condition(condition.bind(py), true)?; + if !op.op.control_flow() { + py_op = py_op.call_method1( + intern!(py, "c_if"), + condition.downcast::()?, + )?; + } else { + py_op.setattr(intern!(py, "condition"), condition)?; + } + } else if py_op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + py_op.setattr( + intern!(py, "target"), + variable_mapper.map_target(&py_op.getattr(intern!(py, "target"))?)?, + )?; + }; + + dag.py_apply_operation_back( + py, + py_op, + Some(TupleLikeArg { value: m_qargs }), + Some(TupleLikeArg { value: m_cargs }), + false, + )?; + } + // If its a Var wire, we already checked that it exists in the destination. + NodeType::VarIn(_) + | NodeType::VarOut(_) + | NodeType::QubitOut(_) + | NodeType::ClbitOut(_) => (), + } + } + + if !inplace { + Ok(Some(dag.into_py(py))) + } else { + Ok(None) + } + } + + /// Reverse the operations in the ``self`` circuit. + /// + /// Returns: + /// DAGCircuit: the reversed dag. + fn reverse_ops<'py>(slf: PyRef, 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,)) + } + + /// Return idle wires. + /// + /// Args: + /// ignore (list(str)): List of node names to ignore. Default: [] + /// + /// Yields: + /// Bit: Bit in idle wire. + /// + /// Raises: + /// DAGCircuitError: If the DAG is invalid + fn idle_wires(&self, py: Python, ignore: Option<&Bound>) -> PyResult> { + let mut result: Vec = Vec::new(); + let wires = (0..self.qubit_io_map.len()) + .map(|idx| Wire::Qubit(Qubit(idx as u32))) + .chain((0..self.clbit_io_map.len()).map(|idx| Wire::Clbit(Clbit(idx as u32)))) + .chain(self.var_input_map.keys(py).map(Wire::Var)); + match ignore { + Some(ignore) => { + // Convert the list to a Rust set. + let ignore_set = ignore + .into_iter() + .map(|s| s.extract()) + .collect::>>()?; + for wire in wires { + let nodes_found = self.nodes_on_wire(py, &wire, true).into_iter().any(|node| { + let weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(packed) = weight { + !ignore_set.contains(packed.op.name()) + } else { + false + } + }); + + if !nodes_found { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + None => { + for wire in wires { + if self.is_wire_idle(py, &wire)? { + result.push(match wire { + Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), + Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), + Wire::Var(var) => var, + }); + } + } + } + } + Ok(PyTuple::new_bound(py, result).into_any().iter()?.unbind()) + } + + /// Return the number of operations. If there is control flow present, this count may only + /// be an estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops with + /// known-length iterators are counted unrolled. If-else blocks sum both of the two + /// branches. While loops are counted as if the loop body runs once only. Defaults to + /// ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to + /// avoid silently returning a mostly meaningless number. + /// + /// Returns: + /// int: the circuit size + /// + /// Raises: + /// DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with + /// ``recurse=True``, or any control flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn size(&self, py: Python, recurse: bool) -> PyResult { + let mut length = self.dag.node_count() - (self.width() * 2); + if !self.has_control_flow() { + return Ok(length); + } + if !recurse { + return Err(DAGCircuitError::new_err(concat!( + "Size with control flow is ambiguous.", + " You may use `recurse=True` to get a result", + " but see this method's documentation for the meaning of this." + ))); + } + + // Handle recursively. + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node in self.dag.node_weights() { + let NodeType::Operation(node) = node else { + continue; + }; + if !node.op.control_flow() { + continue; + } + let OperationRef::Instruction(inst) = node.op.view() else { + panic!("control flow op must be an instruction"); + }; + let inst_bound = inst.instruction.bind(py); + if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + let blocks = inst_bound.getattr("blocks")?; + let block_zero = blocks.get_item(0)?; + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block_zero,))?.extract()?; + length += node.params_view().len() * inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::WHILE_LOOP_OP.get_bound(py))? { + let blocks = inst_bound.getattr("blocks")?; + let block_zero = blocks.get_item(0)?; + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block_zero,))?.extract()?; + length += inner_dag.size(py, true)? + } else if inst_bound.is_instance(imports::IF_ELSE_OP.get_bound(py))? + || inst_bound.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? + { + let blocks = inst_bound.getattr("blocks")?; + for block in blocks.iter()? { + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; + length += inner_dag.size(py, true)?; + } + } else { + continue; + } + // We don't count a control-flow node itself! + length -= 1; + } + Ok(length) + } + + /// Return the circuit depth. If there is control flow present, this count may only be an + /// estimate, as the complete control-flow path cannot be statically known. + /// + /// Args: + /// recurse: if ``True``, then recurse into control-flow operations. For loops + /// with known-length iterators are counted as if the loop had been manually unrolled + /// (*i.e.* with each iteration of the loop body written out explicitly). + /// If-else blocks take the longer case of the two branches. While loops are counted as + /// if the loop body runs once only. Defaults to ``False`` and raises + /// :class:`.DAGCircuitError` if any control flow is present, to avoid silently + /// returning a nonsensical number. + /// + /// Returns: + /// int: the circuit depth + /// + /// Raises: + /// DAGCircuitError: if not a directed acyclic graph + /// DAGCircuitError: if unknown control flow is present in a recursive call, or any control + /// flow is present in a non-recursive call. + #[pyo3(signature= (*, recurse=false))] + fn depth(&self, py: Python, recurse: bool) -> PyResult { + if self.qubits.is_empty() && self.clbits.is_empty() && self.vars_info.is_empty() { + return Ok(0); + } + if !self.has_control_flow() { + let weight_fn = |_| -> Result { Ok(1) }; + return match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => Ok(res.1 - 1), + None => Err(DAGCircuitError::new_err("not a DAG")), + }; + } + if !recurse { + return Err(DAGCircuitError::new_err(concat!( + "Depth with control flow is ambiguous.", + " You may use `recurse=True` to get a result", + " but see this method's documentation for the meaning of this." + ))); + } + + // Handle recursively. + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + let mut node_lookup: HashMap = HashMap::new(); + for (node_index, node) in self.dag.node_references() { + let NodeType::Operation(node) = node else { + continue; + }; + if !node.op.control_flow() { + continue; + } + let OperationRef::Instruction(inst) = node.op.view() else { + panic!("control flow op must be an instruction") + }; + let inst_bound = inst.instruction.bind(py); + let weight = if inst_bound.is_instance(imports::FOR_LOOP_OP.get_bound(py))? { + node.params_view().len() + } else { + 1 + }; + if weight == 0 { + node_lookup.insert(node_index, 0); + } else { + let blocks = inst_bound.getattr("blocks")?; + let mut block_weights: Vec = Vec::with_capacity(blocks.len()?); + for block in blocks.iter()? { + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; + block_weights.push(inner_dag.depth(py, true)?); + } + node_lookup.insert(node_index, weight * block_weights.iter().max().unwrap()); + } + } + + let weight_fn = |edge: EdgeReference<'_, Wire>| -> Result { + Ok(*node_lookup.get(&edge.target()).unwrap_or(&1)) + }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => Ok(res.1 - 1), + None => Err(DAGCircuitError::new_err("not a DAG")), + } + } + + /// Return the total number of qubits + clbits used by the circuit. + /// This function formerly returned the number of qubits by the calculation + /// return len(self._wires) - self.num_clbits() + /// but was changed by issue #2564 to return number of qubits + clbits + /// with the new function DAGCircuit.num_qubits replacing the former + /// semantic of DAGCircuit.width(). + fn width(&self) -> usize { + self.qubits.len() + self.clbits.len() + self.vars_info.len() + } + + /// Return the total number of qubits used by the circuit. + /// num_qubits() replaces former use of width(). + /// DAGCircuit.width() now returns qubits + clbits for + /// consistency with Circuit.width() [qiskit-terra #2564]. + pub fn num_qubits(&self) -> usize { + self.qubits.len() + } + + /// Return the total number of classical bits used by the circuit. + pub fn num_clbits(&self) -> usize { + self.clbits.len() + } + + /// Compute how many components the circuit can decompose into. + fn num_tensor_factors(&self) -> usize { + // This function was forked from rustworkx's + // number_weekly_connected_components() function as of 0.15.0: + // https://github.com/Qiskit/rustworkx/blob/0.15.0/src/connectivity/mod.rs#L215-L235 + + let mut weak_components = self.dag.node_count(); + let mut vertex_sets = UnionFind::new(self.dag.node_bound()); + for edge in self.dag.edge_references() { + let (a, b) = (edge.source(), edge.target()); + // union the two vertices of the edge + if vertex_sets.union(a.index(), b.index()) { + weak_components -= 1 + }; + } + weak_components + } + + fn __eq__(&self, py: Python, other: &DAGCircuit) -> PyResult { + // Try to convert to float, but in case of unbound ParameterExpressions + // a TypeError will be raise, fallback to normal equality in those + // cases. + let phase_is_close = |self_phase: f64, other_phase: f64| -> bool { + ((self_phase - other_phase + PI).rem_euclid(2. * PI) - PI).abs() <= 1.0e-10 + }; + let normalize_param = |param: &Param| { + if let Param::ParameterExpression(ob) = param { + ob.bind(py) + .call_method0(intern!(py, "numeric")) + .ok() + .map(|ob| ob.extract::()) + .unwrap_or_else(|| Ok(param.clone())) + } else { + Ok(param.clone()) + } + }; + + let phase_eq = match [ + normalize_param(&self.global_phase)?, + normalize_param(&other.global_phase)?, + ] { + [Param::Float(self_phase), Param::Float(other_phase)] => { + Ok(phase_is_close(self_phase, other_phase)) + } + _ => self.global_phase.eq(py, &other.global_phase), + }?; + if !phase_eq { + return Ok(false); + } + if self.calibrations.len() != other.calibrations.len() { + return Ok(false); + } + + for (k, v1) in &self.calibrations { + match other.calibrations.get(k) { + Some(v2) => { + if !v1.bind(py).eq(v2.bind(py))? { + return Ok(false); + } + } + None => { + return Ok(false); + } + } + } + + // We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be + // equal in our mind if they use the exact same UUID vars. + for (our_vars, their_vars) in self.vars_by_type.iter().zip(&other.vars_by_type) { + if !our_vars.bind(py).eq(their_vars)? { + return Ok(false); + } + } + + let self_bit_indices = { + let indices = self + .qubits + .bits() + .iter() + .chain(self.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + let other_bit_indices = { + let indices = other + .qubits + .bits() + .iter() + .chain(other.clbits.bits()) + .enumerate() + .map(|(idx, bit)| (bit, idx)); + indices.into_py_dict_bound(py) + }; + + // Check if qregs are the same. + let self_qregs = self.qregs.bind(py); + let other_qregs = other.qregs.bind(py); + if self_qregs.len() != other_qregs.len() { + return Ok(false); + } + for (regname, self_bits) in self_qregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_qregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .qubits + .map_bits(self_bits)? + .eq(other.qubits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check if cregs are the same. + let self_cregs = self.cregs.bind(py); + let other_cregs = other.cregs.bind(py); + if self_cregs.len() != other_cregs.len() { + return Ok(false); + } + + for (regname, self_bits) in self_cregs { + let self_bits = self_bits + .getattr("_bits")? + .downcast_into_exact::()?; + let other_bits = match other_cregs.get_item(regname)? { + Some(bits) => bits.getattr("_bits")?.downcast_into_exact::()?, + None => return Ok(false), + }; + if !self + .clbits + .map_bits(self_bits)? + .eq(other.clbits.map_bits(other_bits)?) + { + return Ok(false); + } + } + + // Check for VF2 isomorphic match. + let legacy_condition_eq = imports::LEGACY_CONDITION_CHECK.get_bound(py); + let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py); + let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py); + let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py); + let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult { + match [n1, n2] { + [NodeType::Operation(inst1), NodeType::Operation(inst2)] => { + if inst1.op.name() != inst2.op.name() { + return Ok(false); + } + let check_args = || -> bool { + let node1_qargs = self.qargs_interner.get(inst1.qubits); + let node2_qargs = other.qargs_interner.get(inst2.qubits); + let node1_cargs = self.cargs_interner.get(inst1.clbits); + let node2_cargs = other.cargs_interner.get(inst2.clbits); + if SEMANTIC_EQ_SYMMETRIC.contains(&inst1.op.name()) { + let node1_qargs = + node1_qargs.iter().copied().collect::>(); + let node2_qargs = + node2_qargs.iter().copied().collect::>(); + let node1_cargs = + node1_cargs.iter().copied().collect::>(); + let node2_cargs = + node2_cargs.iter().copied().collect::>(); + if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + } else if node1_qargs != node2_qargs || node1_cargs != node2_cargs { + return false; + } + true + }; + let check_conditions = || -> PyResult { + if let Some(cond1) = inst1.extra_attrs.condition() { + if let Some(cond2) = inst2.extra_attrs.condition() { + legacy_condition_eq + .call1((cond1, cond2, &self_bit_indices, &other_bit_indices))? + .extract::() + } else { + Ok(false) + } + } else { + Ok(inst2.extra_attrs.condition().is_none()) + } + }; + + match [inst1.op.view(), inst2.op.view()] { + [OperationRef::Standard(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()? + && inst1 + .params_view() + .iter() + .zip(inst2.params_view().iter()) + .all(|(a, b)| a.is_close(py, b, 1e-10).unwrap())) + } + [OperationRef::Instruction(op1), OperationRef::Instruction(op2)] => { + if op1.control_flow() && op2.control_flow() { + let n1 = self.unpack_into(py, NodeIndex::new(0), n1)?; + let n2 = other.unpack_into(py, NodeIndex::new(0), n2)?; + let name = op1.name(); + if name == "if_else" || name == "while_loop" { + condition_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "switch_case" { + switch_case_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else if name == "for_loop" { + for_loop_op_check + .call1((n1, n2, &self_bit_indices, &other_bit_indices))? + .extract() + } else { + Err(PyRuntimeError::new_err(format!( + "unhandled control-flow operation: {}", + name + ))) + } + } else { + Ok(inst1.py_op_eq(py, inst2)? + && check_args() + && check_conditions()?) + } + } + [OperationRef::Gate(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Operation(_op1), OperationRef::Operation(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args()) + } + // Handle the case we end up with a pygate for a standardgate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + [OperationRef::Standard(_op1), OperationRef::Gate(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + [OperationRef::Gate(_op1), OperationRef::Standard(_op2)] => { + Ok(inst1.py_op_eq(py, inst2)? && check_args() && check_conditions()?) + } + _ => Ok(false), + } + } + [NodeType::QubitIn(bit1), NodeType::QubitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitIn(bit1), NodeType::ClbitIn(bit2)] => Ok(bit1 == bit2), + [NodeType::QubitOut(bit1), NodeType::QubitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::ClbitOut(bit1), NodeType::ClbitOut(bit2)] => Ok(bit1 == bit2), + [NodeType::VarIn(var1), NodeType::VarIn(var2)] => var1.bind(py).eq(var2), + [NodeType::VarOut(var1), NodeType::VarOut(var2)] => var1.bind(py).eq(var2), + _ => Ok(false), + } + }; + + isomorphism::vf2::is_isomorphic( + &self.dag, + &other.dag, + node_match, + isomorphism::vf2::NoSemanticMatch, + true, + Ordering::Equal, + true, + None, + ) + .map_err(|e| match e { + isomorphism::vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + _ => { + unreachable!() + } + }) + } + + /// Yield nodes in topological order. + /// + /// 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. + /// + /// Returns: + /// generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order + #[pyo3(name = "topological_nodes")] + fn py_topological_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .map(|node| self.get_node(py, node)) + .collect() + } else { + // Good path, using interner IDs. + self.topological_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Yield op nodes in topological order. + /// + /// Allowed to pass in specific key to break ties in top order + /// + /// 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. + /// + /// Returns: + /// generator(DAGOpNode): op node in topological order + #[pyo3(name = "topological_op_nodes")] + fn py_topological_op_nodes( + &self, + py: Python, + key: Option>, + ) -> PyResult> { + let nodes: PyResult> = if let Some(key) = key { + self.topological_key_sort(py, &key)? + .filter_map(|node| match self.dag.node_weight(node) { + Some(NodeType::Operation(_)) => Some(self.get_node(py, node)), + _ => None, + }) + .collect() + } else { + // Good path, using interner IDs. + self.topological_op_nodes()? + .map(|n| self.get_node(py, n)) + .collect() + }; + + Ok(PyTuple::new_bound(py, nodes?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Replace a block of nodes with a single node. + /// + /// This is used to consolidate a block of DAGOpNodes into a single + /// operation. A typical example is a block of gates being consolidated + /// into a single ``UnitaryGate`` representing the unitary matrix of the + /// block. + /// + /// Args: + /// node_block (List[DAGNode]): A list of dag nodes that represents the + /// node block to be replaced + /// op (qiskit.circuit.Operation): The operation to replace the + /// block with + /// wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the + /// output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over + /// multiple gates in the combined single op node. If a :class:`.Bit` is not in the + /// dictionary, it will not be added to the args; this can be useful when dealing with + /// control-flow operations that have inherent bits in their ``condition`` or ``target`` + /// fields. + /// cycle_check (bool): When set to True this method will check that + /// replacing the provided ``node_block`` with a single node + /// would introduce a cycle (which would invalidate the + /// ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle + /// would be introduced. This checking comes with a run time + /// penalty. If you can guarantee that your input ``node_block`` is + /// a contiguous block and won't introduce a cycle when it's + /// contracted to a single node, this can be set to ``False`` to + /// improve the runtime performance of this method. + /// + /// Raises: + /// DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing + /// the specified block introduces a cycle or if ``node_block`` is + /// empty. + /// + /// Returns: + /// DAGOpNode: The op node that replaces the block. + #[pyo3(signature = (node_block, op, wire_pos_map, cycle_check=true))] + fn replace_block_with_op( + &mut self, + py: Python, + node_block: Vec>, + op: Bound, + wire_pos_map: &Bound, + cycle_check: bool, + ) -> PyResult> { + // If node block is empty return early + if node_block.is_empty() { + return Err(DAGCircuitError::new_err( + "Can't replace an empty 'node_block'", + )); + } + + let mut qubit_pos_map: HashMap = HashMap::new(); + let mut clbit_pos_map: HashMap = HashMap::new(); + for (bit, index) in wire_pos_map.iter() { + if bit.is_instance(imports::QUBIT.get_bound(py))? { + qubit_pos_map.insert(self.qubits.find(&bit).unwrap(), index.extract()?); + } else if bit.is_instance(imports::CLBIT.get_bound(py))? { + clbit_pos_map.insert(self.clbits.find(&bit).unwrap(), index.extract()?); + } else { + return Err(DAGCircuitError::new_err( + "Wire map keys must be Qubit or Clbit instances.", + )); + } + } + + let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); + + let mut block_op_names = Vec::new(); + let mut block_qargs: HashSet = HashSet::new(); + let mut block_cargs: HashSet = HashSet::new(); + for nd in &block_ids { + let weight = self.dag.node_weight(*nd); + match weight { + Some(NodeType::Operation(packed)) => { + block_op_names.push(packed.op.name().to_string()); + block_qargs.extend(self.qargs_interner.get(packed.qubits)); + block_cargs.extend(self.cargs_interner.get(packed.clbits)); + + if let Some(condition) = packed.condition() { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + continue; + } + + // Add classical bits from SwitchCaseOp, if applicable. + if let OperationRef::Instruction(op) = packed.op.view() { + if op.name() == "switch_case" { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + } + } + } + Some(_) => { + return Err(DAGCircuitError::new_err( + "Nodes in 'node_block' must be of type 'DAGOpNode'.", + )) + } + None => { + return Err(DAGCircuitError::new_err( + "Node in 'node_block' not found in DAG.", + )) + } + } + } + + let mut block_qargs: Vec = block_qargs + .into_iter() + .filter(|q| qubit_pos_map.contains_key(q)) + .collect(); + block_qargs.sort_by_key(|q| qubit_pos_map[q]); + + let mut block_cargs: Vec = block_cargs + .into_iter() + .filter(|c| clbit_pos_map.contains_key(c)) + .collect(); + block_cargs.sort_by_key(|c| clbit_pos_map[c]); + + let py_op = op.extract::()?; + + if py_op.operation.num_qubits() as usize != block_qargs.len() { + return Err(DAGCircuitError::new_err(format!( + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + ))); + } + + let op_name = py_op.operation.name().to_string(); + let qubits = self.qargs_interner.insert_owned(block_qargs); + let clbits = self.cargs_interner.insert_owned(block_cargs); + let weight = NodeType::Operation(PackedInstruction { + op: py_op.operation, + qubits, + clbits, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }); + + let new_node = self + .dag + .contract_nodes(block_ids, weight, cycle_check) + .map_err(|e| match e { + ContractError::DAGWouldCycle => DAGCircuitError::new_err( + "Replacing the specified node block would introduce a cycle", + ), + })?; + + self.increment_op(op_name.as_str()); + for name in block_op_names { + self.decrement_op(name.as_str()); + } + + self.get_node(py, new_node) + } + + /// Replace one node with dag. + /// + /// Args: + /// node (DAGOpNode): node to substitute + /// input_dag (DAGCircuit): circuit that will substitute the node + /// wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits + /// in the input circuit. If a list, then the bits refer to those in the ``input_dag``, + /// and the order gets matched to the node wires by qargs first, then cargs, then + /// conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those + /// that the ``node`` acts on. + /// propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on + /// the operation within ``node`` is propagated to each node in the ``input_dag``. If + /// ``False``, then the ``input_dag`` is assumed to faithfully implement suitable + /// conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. + /// treated as if it is ``False``); replacements of those must already fulfill the same + /// conditional logic or this function would be close to useless for them. + /// + /// Returns: + /// dict: maps node IDs from `input_dag` to their new node incarnations in `self`. + /// + /// Raises: + /// DAGCircuitError: if met with unexpected predecessor/successors + #[pyo3(signature = (node, input_dag, wires=None, propagate_condition=true))] + fn substitute_node_with_dag( + &mut self, + py: Python, + node: &Bound, + input_dag: &DAGCircuit, + wires: Option>, + propagate_condition: bool, + ) -> PyResult> { + let (node_index, bound_node) = match node.downcast::() { + Ok(bound_node) => (bound_node.borrow().as_ref().node.unwrap(), bound_node), + Err(_) => return Err(DAGCircuitError::new_err("expected node DAGOpNode")), + }; + + let node = match &self.dag[node_index] { + NodeType::Operation(op) => op.clone(), + _ => return Err(DAGCircuitError::new_err("expected node")), + }; + + type WireMapsTuple = (HashMap, HashMap, Py); + + let build_wire_map = |wires: &Bound| -> PyResult { + let qargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_qargs(py),))?; + let qargs_list = qargs_list.downcast::().unwrap(); + let cargs_list = imports::BUILTIN_LIST + .get_bound(py) + .call1((bound_node.borrow().get_cargs(py),))?; + let cargs_list = cargs_list.downcast::().unwrap(); + let cargs_set = imports::BUILTIN_SET.get_bound(py).call1((cargs_list,))?; + let cargs_set = cargs_set.downcast::().unwrap(); + if !propagate_condition && self.may_have_additional_wires(py, &node) { + let (add_cargs, _add_vars) = + self.additional_wires(py, node.op.view(), node.condition())?; + for wire in add_cargs.iter() { + let clbit = &self.clbits.get(*wire).unwrap(); + if !cargs_set.contains(clbit.clone_ref(py))? { + cargs_list.append(clbit)?; + } + } + } + let qargs_len = qargs_list.len(); + let cargs_len = cargs_list.len(); + + if qargs_len + cargs_len != wires.len() { + return Err(DAGCircuitError::new_err(format!( + "bit mapping invalid: expected {}, got {}", + qargs_len + cargs_len, + wires.len() + ))); + } + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (index, wire) in wires.iter().enumerate() { + if wire.is_instance(imports::QUBIT.get_bound(py))? { + if index >= qargs_len { + unreachable!() + } + let input_qubit: Qubit = input_dag.qubits.find(&wire).unwrap(); + let self_qubit: Qubit = self.qubits.find(&qargs_list.get_item(index)?).unwrap(); + qubit_wire_map.insert(input_qubit, self_qubit); + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + if index < qargs_len { + unreachable!() + } + clbit_wire_map.insert( + input_dag.clbits.find(&wire).unwrap(), + self.clbits + .find(&cargs_list.get_item(index - qargs_len)?) + .unwrap(), + ); + } else { + return Err(DAGCircuitError::new_err( + "`Var` nodes cannot be remapped during substitution", + )); + } + } + Ok((qubit_wire_map, clbit_wire_map, var_map.unbind())) + }; + + let (mut qubit_wire_map, mut clbit_wire_map, var_map): ( + HashMap, + HashMap, + Py, + ) = match wires { + Some(wires) => match wires.downcast::() { + Ok(bound_wires) => { + let mut qubit_wire_map = HashMap::new(); + let mut clbit_wire_map = HashMap::new(); + let var_map = PyDict::new_bound(py); + for (source_wire, target_wire) in bound_wires.iter() { + if source_wire.is_instance(imports::QUBIT.get_bound(py))? { + qubit_wire_map.insert( + input_dag.qubits.find(&source_wire).unwrap(), + self.qubits.find(&target_wire).unwrap(), + ); + } else if source_wire.is_instance(imports::CLBIT.get_bound(py))? { + clbit_wire_map.insert( + input_dag.clbits.find(&source_wire).unwrap(), + self.clbits.find(&target_wire).unwrap(), + ); + } else { + var_map.set_item(source_wire, target_wire)?; + } + } + (qubit_wire_map, clbit_wire_map, var_map.unbind()) + } + Err(_) => { + let wires: Bound = match wires.downcast::() { + Ok(bound_list) => bound_list.clone(), + // If someone passes a sequence instead of an exact list (tuple is + // occasionally used) cast that to a list and then use it. + Err(_) => { + let raw_wires = imports::BUILTIN_LIST.get_bound(py).call1((wires,))?; + raw_wires.extract()? + } + }; + build_wire_map(&wires)? + } + }, + None => { + let raw_wires = input_dag.get_wires(py); + let binding = raw_wires?; + let wires = binding.bind(py); + build_wire_map(wires)? + } + }; + + let var_iter = input_dag.iter_vars(py)?; + let raw_set = imports::BUILTIN_SET.get_bound(py).call1((var_iter,))?; + let input_dag_var_set: &Bound = raw_set.downcast()?; + + 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)?; + if input_dag_var_set + .call_method1(intern!(py, "difference"), (var_set.clone(),))? + .is_truthy()? + { + return Err(DAGCircuitError::new_err(format!( + "Cannot replace a node with a DAG with more variables. Variables in node: {:?}. Variables in dag: {:?}", + var_set.str(), input_dag_var_set.str(), + ))); + } + var_set + } else { + PySet::empty_bound(py)? + }; + let bound_var_map = var_map.bind(py); + for var in input_dag_var_set.iter() { + bound_var_map.set_item(var.clone(), var)?; + } + + for contracted_var in node_vars + .call_method1(intern!(py, "difference"), (input_dag_var_set,))? + .downcast::()? + .iter() + { + let pred = self + .dag + .edges_directed(node_index, Incoming) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node_index, Outgoing) + .find(|edge| { + if let Wire::Var(var) = edge.weight() { + contracted_var.eq(var).unwrap() + } else { + false + } + }) + .unwrap(); + self.dag.add_edge( + pred.source(), + succ.target(), + Wire::Var(contracted_var.unbind()), + ); + } + + let mut new_input_dag: Option = None; + // It doesn't make sense to try and propagate a condition from a control-flow op; a + // replacement for the control-flow op should implement the operation completely. + let node_map = if propagate_condition && !node.op.control_flow() { + // Nested until https://github.com/rust-lang/rust/issues/53667 is fixed in a stable + // release + if let Some(condition) = node.extra_attrs.condition() { + let mut in_dag = input_dag.copy_empty_like(py, "alike")?; + // The remapping of `condition` below is still using the old code that assumes a 2-tuple. + // This is because this remapping code only makes sense in the case of non-control-flow + // operations being replaced. These can only have the 2-tuple conditions, and the + // ability to set a condition at an individual node level will be deprecated and removed + // 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); + for (source_qubit, target_qubit) in &qubit_wire_map { + wire_map.set_item( + in_dag.qubits.get(*source_qubit).unwrap().clone_ref(py), + self.qubits.get(*target_qubit).unwrap().clone_ref(py), + )? + } + for (source_clbit, target_clbit) in &clbit_wire_map { + wire_map.set_item( + in_dag.clbits.get(*source_clbit).unwrap().clone_ref(py), + self.clbits.get(*target_clbit).unwrap().clone_ref(py), + )? + } + 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 (py_target, py_value): (Bound, Bound) = + condition.bind(py).extract()?; + let (py_new_target, target_cargs) = + if py_target.is_instance(imports::CLBIT.get_bound(py))? { + let new_target = reverse_wire_map + .get_item(&py_target)? + .map(Ok::<_, PyErr>) + .unwrap_or_else(|| { + // Target was not in node's wires, so we need a dummy. + let new_target = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &new_target)?; + wire_map.set_item(&new_target, &py_target)?; + reverse_wire_map.set_item(&py_target, &new_target)?; + Ok(new_target) + })?; + (new_target.clone(), PySet::new_bound(py, &[new_target])?) + } else { + // ClassicalRegister + let target_bits: Vec> = + py_target.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)?; + for (ours, theirs) in target_bits.into_iter().zip(mapped_bits) { + if let Some(theirs) = theirs { + // Target bit was in node's wires. + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } else { + // Target bit was not in node's wires, so we need a dummy. + let theirs = imports::CLBIT.get_bound(py).call0()?; + in_dag.add_clbit_unchecked(py, &theirs)?; + wire_map.set_item(&theirs, &ours)?; + reverse_wire_map.set_item(&ours, &theirs)?; + new_target.push(theirs.clone()); + target_cargs.add(theirs)?; + } + } + let kwargs = [("bits", new_target.into_py(py))].into_py_dict_bound(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]); + + qubit_wire_map.clear(); + clbit_wire_map.clear(); + for item in wire_map.items().iter() { + let (in_bit, self_bit): (Bound, Bound) = item.extract()?; + if in_bit.is_instance(imports::QUBIT.get_bound(py))? { + let in_index = in_dag.qubits.find(&in_bit).unwrap(); + let self_index = self.qubits.find(&self_bit).unwrap(); + qubit_wire_map.insert(in_index, self_index); + } else { + let in_index = in_dag.clbits.find(&in_bit).unwrap(); + let self_index = self.clbits.find(&self_bit).unwrap(); + clbit_wire_map.insert(in_index, self_index); + } + } + for in_node_index in input_dag.topological_op_nodes()? { + let in_node = &input_dag.dag[in_node_index]; + if let NodeType::Operation(inst) = in_node { + if inst.extra_attrs.condition().is_some() { + return Err(DAGCircuitError::new_err( + "cannot propagate a condition to an element that already has one", + )); + } + let cargs = input_dag.cargs_interner.get(inst.clbits); + let cargs_bits: Vec = input_dag + .clbits + .map_indices(cargs) + .map(|x| x.clone_ref(py)) + .collect(); + if !target_cargs + .call_method1(intern!(py, "intersection"), (cargs_bits,))? + .downcast::()? + .is_empty() + { + return Err(DAGCircuitError::new_err("cannot propagate a condition to an element that acts on those bits")); + } + let mut new_inst = inst.clone(); + if new_condition.is_truthy()? { + new_inst + .extra_attrs + .set_condition(Some(new_condition.as_any().clone().unbind())); + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + } + in_dag.push_back(py, new_inst)?; + } + } + let node_map = self.substitute_node_with_subgraph( + py, + node_index, + &in_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )?; + new_input_dag = Some(in_dag); + node_map + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + } + } else { + self.substitute_node_with_subgraph( + py, + node_index, + input_dag, + &qubit_wire_map, + &clbit_wire_map, + &var_map, + )? + }; + self.global_phase = add_global_phase(py, &self.global_phase, &input_dag.global_phase)?; + + let wire_map_dict = PyDict::new_bound(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), + None => input_dag.clbits.get(*source), + }; + let target_bit = self.clbits.get(*target); + wire_map_dict.set_item(source_bit, target_bit)?; + } + let bound_var_map = var_map.bind(py); + + // Note: creating this list to hold new registers created by the mapper is a temporary + // 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 add_new_register = new_registers.getattr("append")?.unbind(); + let flush_new_registers = |dag: &mut DAGCircuit| -> PyResult<()> { + for reg in &new_registers { + dag.add_creg(py, ®)?; + } + new_registers.del_slice(0, new_registers.len())?; + Ok(()) + }; + + let variable_mapper = PyVariableMapper::new( + py, + self.cregs.bind(py).values().into_any(), + Some(wire_map_dict), + Some(bound_var_map.clone()), + Some(add_new_register), + )?; + + for (old_node_index, new_node_index) in node_map.iter() { + let old_node = match new_input_dag { + Some(ref in_dag) => &in_dag.dag[*old_node_index], + None => &input_dag.dag[*old_node_index], + }; + if let NodeType::Operation(old_inst) = old_node { + if let OperationRef::Instruction(old_op) = old_inst.op.view() { + 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); + kwargs.set_item("label", old_inst.extra_attrs.label())?; + let new_op = imports::SWITCH_CASE_OP.get_bound(py).call( + ( + variable_mapper.map_target(target)?, + old_op.instruction.call_method0(py, "cases_specifier")?, + ), + Some(&kwargs), + )?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + new_inst.op = PyInstruction { + qubits: old_op.num_qubits(), + clbits: old_op.num_clbits(), + params: old_op.num_params(), + control_flow: old_op.control_flow(), + op_name: old_op.name().to_string(), + instruction: new_op.clone().unbind(), + } + .into(); + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op = new_op.unbind().into(); + } + } + } + } + if let Some(condition) = old_inst.extra_attrs.condition() { + if old_inst.op.name() != "switch_case" { + let new_condition: Option = variable_mapper + .map_condition(condition.bind(py), false)? + .extract()?; + flush_new_registers(self)?; + + if let NodeType::Operation(ref mut new_inst) = + &mut self.dag[*new_node_index] + { + new_inst.extra_attrs.set_condition(new_condition.clone()); + #[cfg(feature = "cache_pygates")] + { + new_inst.py_op.take(); + } + match new_inst.op.view() { + OperationRef::Instruction(py_inst) => { + py_inst + .instruction + .setattr(py, "condition", new_condition)?; + } + OperationRef::Gate(py_gate) => { + py_gate.gate.setattr(py, "condition", new_condition)?; + } + OperationRef::Operation(py_op) => { + py_op.operation.setattr(py, "condition", new_condition)?; + } + OperationRef::Standard(_) => {} + } + } + } + } + } + } + let out_dict = PyDict::new_bound(py); + for (old_index, new_index) in node_map { + out_dict.set_item(old_index.index(), self.get_node(py, new_index)?)?; + } + Ok(out_dict.unbind()) + } + + /// Replace a DAGOpNode with a single operation. qargs, cargs and + /// conditions for the new operation will be inferred from the node to be + /// replaced. The new operation will be checked to match the shape of the + /// replaced operation. + /// + /// Args: + /// node (DAGOpNode): Node to be replaced + /// op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` + /// instance to be added to the DAG + /// inplace (bool): Optional, default False. If True, existing DAG node + /// will be modified to include op. Otherwise, a new DAG node will + /// be used. + /// propagate_condition (bool): Optional, default True. If True, a condition on the + /// ``node`` to be replaced will be applied to the new ``op``. This is the legacy + /// behaviour. If either node is a control-flow operation, this will be ignored. If + /// the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. + /// + /// Returns: + /// DAGOpNode: the new node containing the added operation. + /// + /// Raises: + /// DAGCircuitError: If replacement operation was incompatible with + /// location of target node. + #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] + fn substitute_node( + &mut self, + node: &Bound, + op: &Bound, + inplace: bool, + propagate_condition: bool, + ) -> PyResult> { + let mut node: PyRefMut = match node.downcast() { + Ok(node) => node.borrow_mut(), + Err(_) => return Err(DAGCircuitError::new_err("Only DAGOpNodes can be replaced.")), + }; + let py = op.py(); + let node_index = node.as_ref().node.unwrap(); + // Extract information from node that is going to be replaced + let old_packed = match self.dag.node_weight(node_index) { + Some(NodeType::Operation(old_packed)) => old_packed.clone(), + Some(_) => { + return Err(DAGCircuitError::new_err( + "'node' must be of type 'DAGOpNode'.", + )) + } + None => return Err(DAGCircuitError::new_err("'node' not found in DAG.")), + }; + // Extract information from new op + let new_op = op.extract::()?; + let current_wires: HashSet = self + .dag + .edges(node_index) + .map(|e| e.weight().clone()) + .collect(); + let mut new_wires: HashSet = self + .qargs_interner + .get(old_packed.qubits) + .iter() + .map(|x| Wire::Qubit(*x)) + .chain( + self.cargs_interner + .get(old_packed.clbits) + .iter() + .map(|x| Wire::Clbit(*x)), + ) + .collect(); + let (additional_clbits, additional_vars) = + self.additional_wires(py, new_op.operation.view(), new_op.extra_attrs.condition())?; + new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); + new_wires.extend(additional_vars.iter().map(|x| Wire::Var(x.clone_ref(py)))); + + if old_packed.op.num_qubits() != new_op.operation.num_qubits() + || old_packed.op.num_clbits() != new_op.operation.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() + ))); + } + + #[cfg(feature = "cache_pygates")] + let mut py_op_cache = Some(op.clone().unbind()); + + let mut extra_attrs = new_op.extra_attrs.clone(); + // If either operation is a control-flow operation, propagate_condition is ignored + if propagate_condition + && !(node.instruction.operation.control_flow() || new_op.operation.control_flow()) + { + // if new_op has a condition, the condition can't be propagated from the old node + if new_op.extra_attrs.condition().is_some() { + return Err(DAGCircuitError::new_err( + "Cannot propagate a condition to an operation that already has one.", + )); + } + if let Some(old_condition) = old_packed.condition() { + if matches!(new_op.operation.view(), OperationRef::Operation(_)) { + return Err(DAGCircuitError::new_err( + "Cannot add a condition on a generic Operation.", + )); + } + extra_attrs.set_condition(Some(old_condition.clone_ref(py))); + + let binding = self + .control_flow_module + .condition_resources(old_condition.bind(py))?; + let condition_clbits = binding.clbits.bind(py); + for bit in condition_clbits { + new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); + } + let op_ref = new_op.operation.view(); + if let OperationRef::Instruction(inst) = op_ref { + inst.instruction + .bind(py) + .setattr(intern!(py, "condition"), old_condition)?; + } else if let OperationRef::Gate(gate) = op_ref { + gate.gate.bind(py).call_method1( + intern!(py, "c_if"), + old_condition.downcast_bound::(py)?, + )?; + } + #[cfg(feature = "cache_pygates")] + { + py_op_cache = None; + } + } + }; + if new_wires != current_wires { + // The new wires must be a non-strict subset of the current wires; if they add new + // wires, we'd not know where to cut the existing wire to insert the new dependency. + return Err(DAGCircuitError::new_err(format!( + "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires + ))); + } + + if inplace { + node.instruction.operation = new_op.operation.clone(); + node.instruction.params = new_op.params.clone(); + node.instruction.extra_attrs = extra_attrs.clone(); + #[cfg(feature = "cache_pygates")] + { + node.instruction.py_op = py_op_cache + .as_ref() + .map(|ob| OnceCell::from(ob.clone_ref(py))) + .unwrap_or_default(); + } + } + // Clone op data, as it will be moved into the PackedInstruction + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op.operation.clone(), + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!new_op.params.is_empty()).then(|| new_op.params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), + }); + let node_index = node.as_ref().node.unwrap(); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(old_packed.op.name()); + self.increment_op(new_op.operation.name()); + + if inplace { + Ok(node.into_py(py)) + } else { + self.get_node(py, node_index) + } + } + + /// Decompose the circuit into sets of qubits with no gates connecting them. + /// + /// Args: + /// remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from + /// the separated circuits. If ``False``, each output circuit will contain the + /// same number of qubits as ``self``. + /// + /// Returns: + /// List[DAGCircuit]: The circuits resulting from separating ``self`` into sets + /// of disconnected qubits + /// + /// Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of + /// clbits as ``self``. The global phase information in ``self`` will not be maintained + /// in the subcircuits returned by this method. + #[pyo3(signature = (remove_idle_qubits=false, *, vars_mode="alike"))] + fn separable_circuits( + &self, + py: Python, + remove_idle_qubits: bool, + vars_mode: &str, + ) -> PyResult> { + let connected_components = rustworkx_core::connectivity::connected_components(&self.dag); + let dags = PyList::empty_bound(py); + + for comp_nodes in connected_components.iter() { + let mut new_dag = self.copy_empty_like(py, vars_mode)?; + new_dag.global_phase = Param::Float(0.); + + // A map from nodes in the this DAGCircuit to nodes in the new dag. Used for adding edges + let mut node_map: HashMap = + HashMap::with_capacity(comp_nodes.len()); + + // Adding the nodes to the new dag + let mut non_classical = false; + for node in comp_nodes { + match self.dag.node_weight(*node) { + Some(w) => match w { + NodeType::ClbitIn(b) => { + let clbit_in = new_dag.clbit_io_map[b.0 as usize][0]; + node_map.insert(*node, clbit_in); + } + NodeType::ClbitOut(b) => { + let clbit_out = new_dag.clbit_io_map[b.0 as usize][1]; + node_map.insert(*node, clbit_out); + } + NodeType::QubitIn(q) => { + let qbit_in = new_dag.qubit_io_map[q.0 as usize][0]; + node_map.insert(*node, qbit_in); + non_classical = true; + } + NodeType::QubitOut(q) => { + let qbit_out = new_dag.qubit_io_map[q.0 as usize][1]; + node_map.insert(*node, qbit_out); + non_classical = true; + } + NodeType::VarIn(v) => { + let var_in = new_dag.var_input_map.get(py, v).unwrap(); + node_map.insert(*node, var_in); + } + NodeType::VarOut(v) => { + let var_out = new_dag.var_output_map.get(py, v).unwrap(); + node_map.insert(*node, var_out); + } + NodeType::Operation(pi) => { + let new_node = new_dag.dag.add_node(NodeType::Operation(pi.clone())); + new_dag.increment_op(pi.op.name()); + node_map.insert(*node, new_node); + non_classical = true; + } + }, + None => panic!("DAG node without payload!"), + } + } + if !non_classical { + continue; + } + let node_filter = |node: NodeIndex| -> bool { node_map.contains_key(&node) }; + + let filtered = NodeFiltered(&self.dag, node_filter); + + // Remove the edges added by copy_empty_like (as idle wires) to avoid duplication + new_dag.dag.clear_edges(); + for edge in filtered.edge_references() { + let new_source = node_map[&edge.source()]; + let new_target = node_map[&edge.target()]; + new_dag + .dag + .add_edge(new_source, new_target, edge.weight().clone()); + } + // Add back any edges for idle wires + for (qubit, [in_node, out_node]) in new_dag + .qubit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Qubit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Qubit(qubit)); + } + } + for (clbit, [in_node, out_node]) in new_dag + .clbit_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Clbit(idx as u32), indices)) + { + if new_dag.dag.edges(*in_node).next().is_none() { + new_dag + .dag + .add_edge(*in_node, *out_node, Wire::Clbit(clbit)); + } + } + for (var, in_node) in new_dag.var_input_map.iter(py) { + if new_dag.dag.edges(in_node).next().is_none() { + let out_node = new_dag.var_output_map.get(py, &var).unwrap(); + new_dag + .dag + .add_edge(in_node, out_node, Wire::Var(var.clone_ref(py))); + } + } + if remove_idle_qubits { + let idle_wires: Vec> = new_dag + .idle_wires(py, None)? + .into_bound(py) + .map(|q| q.unwrap()) + .filter(|e| e.is_instance(imports::QUBIT.get_bound(py)).unwrap()) + .collect(); + + let qubits = PyTuple::new_bound(py, idle_wires); + new_dag.remove_qubits(py, &qubits)?; // TODO: this does not really work, some issue with remove_qubits itself + } + + dags.append(pyo3::Py::new(py, new_dag)?)?; + } + + Ok(dags.unbind()) + } + + /// Swap connected nodes e.g. due to commutation. + /// + /// Args: + /// node1 (OpNode): predecessor node + /// node2 (OpNode): successor node + /// + /// Raises: + /// DAGCircuitError: if either node is not an OpNode or nodes are not connected + fn swap_nodes(&mut self, node1: &DAGNode, node2: &DAGNode) -> PyResult<()> { + let node1 = node1.node.unwrap(); + let node2 = node2.node.unwrap(); + + // Check that both nodes correspond to operations + if !matches!(self.dag.node_weight(node1).unwrap(), NodeType::Operation(_)) + || !matches!(self.dag.node_weight(node2).unwrap(), NodeType::Operation(_)) + { + return Err(DAGCircuitError::new_err( + "Nodes to swap are not both DAGOpNodes", + )); + } + + // Gather all wires connecting node1 and node2. + // This functionality was extracted from rustworkx's 'get_edge_data' + let wires: Vec = self + .dag + .edges(node1) + .filter(|edge| edge.target() == node2) + .map(|edge| edge.weight().clone()) + .collect(); + + if wires.is_empty() { + return Err(DAGCircuitError::new_err( + "Attempt to swap unconnected nodes", + )); + }; + + // Closure that finds the first parent/child node connected to a reference node by given wire + // and returns relevant edge information depending on the specified direction: + // - Incoming -> parent -> outputs (parent_edge_id, parent_source_node_id) + // - Outgoing -> child -> outputs (child_edge_id, child_target_node_id) + // This functionality was inspired in rustworkx's 'find_predecessors_by_edge' and 'find_successors_by_edge'. + let directed_edge_for_wire = |node: NodeIndex, direction: Direction, wire: &Wire| { + for edge in self.dag.edges_directed(node, direction) { + if wire == edge.weight() { + match direction { + Incoming => return Some((edge.id(), edge.source())), + Outgoing => return Some((edge.id(), edge.target())), + } + } + } + None + }; + + // Vector that contains a tuple of (wire, edge_info, parent_info, child_info) per wire in wires + let relevant_edges = wires + .iter() + .rev() + .map(|wire| { + ( + wire, + directed_edge_for_wire(node1, Outgoing, wire).unwrap(), + directed_edge_for_wire(node1, Incoming, wire).unwrap(), + directed_edge_for_wire(node2, Outgoing, wire).unwrap(), + ) + }) + .collect::>(); + + // Iterate over relevant edges and modify self.dag + for (wire, (node1_to_node2, _), (parent_to_node1, parent), (node2_to_child, child)) in + relevant_edges + { + self.dag.remove_edge(parent_to_node1); + self.dag.add_edge(parent, node2, wire.clone()); + self.dag.remove_edge(node1_to_node2); + self.dag.add_edge(node2, node1, wire.clone()); + self.dag.remove_edge(node2_to_child); + self.dag.add_edge(node1, child, wire.clone()); + } + Ok(()) + } + + /// Get the node in the dag. + /// + /// Args: + /// node_id(int): Node identifier. + /// + /// Returns: + /// node: the node. + fn node(&self, py: Python, node_id: isize) -> PyResult> { + self.get_node(py, NodeIndex::new(node_id as usize)) + } + + /// Iterator for node values. + /// + /// Yield: + /// node: the node. + fn nodes(&self, py: Python) -> PyResult> { + let result: PyResult> = self + .dag + .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()) + } + + /// Iterator for edge values with source and destination node. + /// + /// This works by returning the outgoing edges from the specified nodes. If + /// no nodes are specified all edges from the graph are returned. + /// + /// Args: + /// nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): + /// Either a list of nodes or a single input node. If none is specified, + /// all edges are returned from the graph. + /// + /// Yield: + /// edge: the edge as a tuple with the format + /// (source node, destination node, edge wire) + fn edges(&self, nodes: Option>, py: Python) -> PyResult> { + let get_node_index = |obj: &Bound| -> PyResult { + Ok(obj.downcast::()?.borrow().node.unwrap()) + }; + + let actual_nodes: Vec<_> = match nodes { + None => self.dag.node_indices().collect(), + Some(nodes) => { + let mut out = Vec::new(); + if let Ok(node) = get_node_index(&nodes) { + out.push(node); + } else { + for node in nodes.iter()? { + out.push(get_node_index(&node?)?); + } + } + out + } + }; + + let mut edges = Vec::new(); + for node in actual_nodes { + for edge in self.dag.edges_directed(node, Outgoing) { + edges.push(( + self.get_node(py, edge.source())?, + self.get_node(py, edge.target())?, + match edge.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + )) + } + } + + Ok(PyTuple::new_bound(py, edges) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Get the list of "op" nodes in the dag. + /// + /// Args: + /// op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to + /// return. If None, return all op nodes. + /// include_directives (bool): include `barrier`, `snapshot` etc. + /// + /// Returns: + /// list[DAGOpNode]: the list of dag nodes containing the given op. + #[pyo3(name= "op_nodes", signature=(op=None, include_directives=true))] + fn py_op_nodes( + &self, + py: Python, + op: Option<&Bound>, + include_directives: bool, + ) -> PyResult>> { + let mut nodes = Vec::new(); + let filter_is_nonstandard = if let Some(op) = op { + op.getattr(intern!(py, "_standard_gate")).ok().is_none() + } else { + true + }; + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(packed) = &weight { + if !include_directives && packed.op.directive() { + continue; + } + if let Some(op_type) = op { + // This middle catch is to avoid Python-space operation creation for most uses of + // `op`; we're usually just looking for control-flow ops, and standard gates + // aren't control-flow ops. + if !(filter_is_nonstandard && packed.op.try_standard_gate().is_some()) + && packed.op.py_op_is_instance(op_type)? + { + nodes.push(self.unpack_into(py, node, weight)?); + } + } else { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Get a list of "op" nodes in the dag that contain control flow instructions. + /// + /// Returns: + /// list[DAGOpNode] | None: The list of dag nodes containing control flow ops. If there + /// are no control flow nodes None is returned + fn control_flow_op_nodes(&self, py: Python) -> PyResult>>> { + if self.has_control_flow() { + let result: PyResult>> = self + .dag + .node_references() + .filter_map(|(node_index, node_type)| match node_type { + NodeType::Operation(ref node) => { + if node.op.control_flow() { + Some(self.unpack_into(py, node_index, node_type)) + } else { + None + } + } + _ => None, + }) + .collect(); + Ok(Some(result?)) + } else { + Ok(None) + } + } + + /// Get the list of gate nodes in the dag. + /// + /// Returns: + /// list[DAGOpNode]: the list of DAGOpNodes that represent gates. + fn gate_nodes(&self, py: Python) -> PyResult>> { + self.dag + .node_references() + .filter_map(|(node, weight)| match weight { + NodeType::Operation(ref packed) => match packed.op.view() { + OperationRef::Gate(_) | OperationRef::Standard(_) => { + Some(self.unpack_into(py, node, weight)) + } + _ => None, + }, + _ => None, + }) + .collect() + } + + /// Get the set of "op" nodes with the given name. + #[pyo3(signature = (*names))] + fn named_nodes(&self, py: Python<'_>, names: &Bound) -> PyResult>> { + let mut names_set: HashSet = HashSet::with_capacity(names.len()); + for name_obj in names.iter() { + names_set.insert(name_obj.extract::()?); + } + let mut result: Vec> = Vec::new(); + for (id, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if names_set.contains(packed.op.name()) { + result.push(self.unpack_into(py, id, weight)?); + } + } + } + Ok(result) + } + + /// 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) + } + + /// Get list of 3+ qubit operations. Ignore directives like snapshot and barrier. + fn multi_qubit_ops(&self, py: Python) -> PyResult>> { + let mut nodes = Vec::new(); + for (node, weight) in self.dag.node_references() { + if let NodeType::Operation(ref packed) = weight { + if packed.op.directive() { + continue; + } + + let qargs = self.qargs_interner.get(packed.qubits); + if qargs.len() >= 3 { + nodes.push(self.unpack_into(py, node, weight)?); + } + } + } + Ok(nodes) + } + + /// Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes. + fn longest_path(&self, py: Python) -> PyResult> { + let weight_fn = |_| -> Result { Ok(1) }; + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + } + .into_iter() + .map(|node_index| self.get_node(py, node_index)) + .collect() + } + + /// Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" + fn successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes. + fn predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of "op" successors of a node in the dag. + fn op_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Outgoing) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns the iterator of "op" predecessors of a node in the dag. + fn op_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .dag + .neighbors_directed(node.node.unwrap(), Incoming) + .unique() + .filter_map(|i| match self.dag[i] { + NodeType::Operation(_) => Some(self.get_node(py, i)), + _ => None, + }) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Checks if a second node is in the successors of node. + fn is_successor(&self, node: &DAGNode, node_succ: &DAGNode) -> bool { + self.dag + .find_edge(node.node.unwrap(), node_succ.node.unwrap()) + .is_some() + } + + /// Checks if a second node is in the predecessors of node. + fn is_predecessor(&self, node: &DAGNode, node_pred: &DAGNode) -> bool { + self.dag + .find_edge(node_pred.node.unwrap(), node.node.unwrap()) + .is_some() + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGInNodes. + #[pyo3(name = "quantum_predecessors")] + fn py_quantum_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let predecessors: PyResult> = self + .quantum_predecessors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a quantum edge as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "quantum_successors")] + fn py_quantum_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successors: PyResult> = self + .quantum_successors(node.node.unwrap()) + .map(|i| self.get_node(py, i)) + .collect(); + Ok(PyTuple::new_bound(py, successors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns iterator of the predecessors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGInNodes. + fn classical_predecessors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Incoming); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.source()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes. + #[pyo3(name = "ancestors")] + fn py_ancestors(&self, py: Python, node: &DAGNode) -> PyResult> { + let ancestors: PyResult> = self + .ancestors(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &ancestors?)?.unbind()) + } + + /// Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes. + #[pyo3(name = "descendants")] + fn py_descendants(&self, py: Python, node: &DAGNode) -> PyResult> { + let descendants: PyResult> = self + .descendants(node.node.unwrap()) + .map(|node| self.get_node(py, node)) + .collect(); + Ok(PySet::new_bound(py, &descendants?)?.unbind()) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + #[pyo3(name = "bfs_successors")] + fn py_bfs_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let successor_index: PyResult)>> = self + .bfs_successors(node.node.unwrap()) + .map(|(node, nodes)| -> PyResult<(PyObject, Vec)> { + Ok(( + self.get_node(py, node)?, + nodes + .iter() + .map(|sub_node| self.get_node(py, *sub_node)) + .collect::>>()?, + )) + }) + .collect(); + Ok(PyList::new_bound(py, successor_index?) + .into_any() + .iter()? + .unbind()) + } + + /// Returns iterator of the successors of a node that are + /// connected by a classical edge as DAGOpNodes and DAGOutNodes. + fn classical_successors(&self, py: Python, node: &DAGNode) -> PyResult> { + let edges = self.dag.edges_directed(node.node.unwrap(), Outgoing); + let filtered = edges.filter_map(|e| match e.weight() { + Wire::Qubit(_) => None, + _ => Some(e.target()), + }); + let predecessors: PyResult> = + filtered.unique().map(|i| self.get_node(py, i)).collect(); + Ok(PyTuple::new_bound(py, predecessors?) + .into_any() + .iter() + .unwrap() + .unbind()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + #[pyo3(name = "remove_op_node")] + fn py_remove_op_node(&mut self, node: &Bound) -> PyResult<()> { + let node: PyRef = match node.downcast::() { + Ok(node) => node.borrow(), + Err(_) => return Err(DAGCircuitError::new_err("Node not an DAGOpNode")), + }; + let index = node.as_ref().node.unwrap(); + if self.dag.node_weight(index).is_none() { + return Err(DAGCircuitError::new_err("Node not in DAG")); + } + self.remove_op_node(index); + Ok(()) + } + + /// Remove all of the ancestor operation nodes of node. + fn remove_ancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: Vec<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for a in ancestors { + self.dag.remove_node(a); + } + Ok(()) + } + + /// Remove all of the descendant operation nodes of node. + fn remove_descendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: Vec<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + for d in descendants { + self.dag.remove_node(d); + } + Ok(()) + } + + /// Remove all of the non-ancestors operation nodes of node. + fn remove_nonancestors_of(&mut self, node: &DAGNode) -> PyResult<()> { + let ancestors: HashSet<_> = core_ancestors(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_ancestors: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !ancestors.contains(node_id)) + .collect(); + for na in non_ancestors { + self.dag.remove_node(na); + } + Ok(()) + } + + /// Remove all of the non-descendants operation nodes of node. + fn remove_nondescendants_of(&mut self, node: &DAGNode) -> PyResult<()> { + let descendants: HashSet<_> = core_descendants(&self.dag, node.node.unwrap()) + .filter(|next| { + next != &node.node.unwrap() + && matches!(self.dag.node_weight(*next), Some(NodeType::Operation(_))) + }) + .collect(); + let non_descendants: Vec<_> = self + .dag + .node_indices() + .filter(|node_id| !descendants.contains(node_id)) + .collect(); + for nd in non_descendants { + self.dag.remove_node(nd); + } + Ok(()) + } + + /// Return a list of op nodes in the first layer of this dag. + #[pyo3(name = "front_layer")] + fn py_front_layer(&self, py: Python) -> PyResult> { + let native_front_layer = self.front_layer(py); + let front_layer_list = PyList::empty_bound(py); + for node in native_front_layer { + front_layer_list.append(self.get_node(py, node)?)?; + } + Ok(front_layer_list.into()) + } + + /// Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. + /// + /// A layer is a circuit whose gates act on disjoint qubits, i.e., + /// a layer has depth 1. The total number of layers equals the + /// circuit depth d. The layers are indexed from 0 to d-1 with the + /// earliest layer at index 0. The layers are constructed using a + /// greedy algorithm. Each returned layer is a dict containing + /// {"graph": circuit graph, "partition": list of qubit lists}. + /// + /// The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, + /// and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent + /// via DAGNode.semantic_eq(node1, node2). + /// + /// TODO: Gates that use the same cbits will end up in different + /// layers as this is currently implemented. This may not be + /// 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 mut graph_layers = self.multigraph_layers(py); + if graph_layers.next().is_none() { + return Ok(PyIterator::from_bound_object(&layer_list)?.into()); + } + + for graph_layer in graph_layers { + let layer_dict = PyDict::new_bound(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 + // Drawing tools rely on _node_id to infer order of node creation + // so we need this to be preserved by layers() + // Get the op nodes from the layer, removing any input and output nodes. + let mut op_nodes: Vec<(&PackedInstruction, &NodeIndex)> = graph_layer + .iter() + .filter_map(|node| self.dag.node_weight(*node).map(|dag_node| (dag_node, node))) + .filter_map(|(node, index)| match node { + NodeType::Operation(oper) => Some((oper, index)), + _ => None, + }) + .collect(); + op_nodes.sort_by_key(|(_, node_index)| **node_index); + + if op_nodes.is_empty() { + return Ok(PyIterator::from_bound_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( + py, + new_layer + .qubits + .map_indices(new_layer.qargs_interner.get(node.qubits)), + ) + }); + let support_list = PyList::empty_bound(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("partition", support_list)?; + layer_list.append(layer_dict)?; + } + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield a layer for all gates of this circuit. + /// + /// A serial layer is a circuit with one gate. The layers have the + /// 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); + for next_node in self.topological_op_nodes()? { + let retrieved_node: &PackedInstruction = match self.dag.node_weight(next_node) { + Some(NodeType::Operation(node)) => node, + _ => unreachable!("A non-operation node was obtained from topological_op_nodes."), + }; + 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( + py, + self.qargs_interner + .get(retrieved_node.qubits) + .iter() + .map(|qubit| self.qubits.get(*qubit)), + ) + .unbind(); + new_layer.push_back(py, retrieved_node.clone())?; + + if !retrieved_node.op.directive() { + support_list.append(qubits)?; + } + + let layer_dict = [ + ("graph", new_layer.into_py(py)), + ("partition", support_list.into_any().unbind()), + ] + .into_py_dict_bound(py); + layer_list.append(layer_dict)?; + } + + Ok(layer_list.into_any().iter()?.into()) + } + + /// Yield layers of the multigraph. + #[pyo3(name = "multigraph_layers")] + fn py_multigraph_layers(&self, py: Python) -> PyResult> { + let graph_layers = self.multigraph_layers(py).map(|layer| -> Vec { + layer + .into_iter() + .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()) + } + + /// Return a set of non-conditional runs of "op" nodes with the given names. + /// + /// For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." + /// would produce the tuple of cx nodes as an element of the set returned + /// from a call to collect_runs(["cx"]). If instead the cx nodes were + /// "cx q[0],q[1]; cx q[1],q[0];", the method would still return the + /// pair in a tuple. The namelist can contain names that are not + /// in the circuit's basis. + /// + /// Nodes must have only one successor to continue the run. + #[pyo3(name = "collect_runs")] + fn py_collect_runs(&self, py: Python, namelist: &Bound) -> PyResult> { + let mut name_list_set = HashSet::with_capacity(namelist.len()); + for name in namelist.iter() { + name_list_set.insert(name.extract::()?); + } + match self.collect_runs(name_list_set) { + Some(runs) => { + let run_iter = runs.map(|node_indices| { + PyTuple::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_set = PySet::empty_bound(py)?; + for run_tuple in run_iter { + out_set.add(run_tuple)?; + } + Ok(out_set.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + #[pyo3(name = "collect_1q_runs")] + fn py_collect_1q_runs(&self, py: Python) -> PyResult> { + match self.collect_1q_runs() { + Some(runs) => { + let runs_iter = runs.map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + #[pyo3(name = "collect_2q_runs")] + fn py_collect_2q_runs(&self, py: Python) -> PyResult> { + match self.collect_2q_runs() { + Some(runs) => { + let runs_iter = runs.into_iter().map(|node_indices| { + PyList::new_bound( + py, + node_indices + .into_iter() + .map(|node_index| self.get_node(py, node_index).unwrap()), + ) + .unbind() + }); + let out_list = PyList::empty_bound(py); + for run_list in runs_iter { + out_list.append(run_list)?; + } + Ok(out_list.unbind()) + } + None => Err(PyRuntimeError::new_err( + "Invalid DAGCircuit, cycle encountered", + )), + } + } + + /// Iterator for nodes that affect a given wire. + /// + /// Args: + /// wire (Bit): the wire to be looked at. + /// only_ops (bool): True if only the ops nodes are wanted; + /// otherwise, all nodes are returned. + /// Yield: + /// Iterator: the successive nodes on the given wire + /// + /// Raises: + /// DAGCircuitError: if the given wire doesn't exist in the DAG + #[pyo3(name = "nodes_on_wire", signature = (wire, only_ops=false))] + fn py_nodes_on_wire( + &self, + py: Python, + wire: &Bound, + only_ops: bool, + ) -> PyResult> { + let wire = if wire.is_instance(imports::QUBIT.get_bound(py))? { + self.qubits.find(wire).map(Wire::Qubit) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + self.clbits.find(wire).map(Wire::Clbit) + } else if self.var_input_map.contains_key(py, &wire.clone().unbind()) { + Some(Wire::Var(wire.clone().unbind())) + } else { + None + } + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given wire {:?} is not present in the circuit", + wire + )) + })?; + + let nodes = self + .nodes_on_wire(py, &wire, only_ops) + .into_iter() + .map(|n| self.get_node(py, n)) + .collect::>>()?; + Ok(PyTuple::new_bound(py, nodes).into_any().iter()?.unbind()) + } + + /// Count the occurrences of operation names. + /// + /// Args: + /// recurse: if ``True`` (default), then recurse into control-flow operations. In all + /// cases, this counts only the number of times the operation appears in any possible + /// block; both branches of if-elses are counted, and for- and while-loop blocks are + /// only counted once. + /// + /// Returns: + /// 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)) + } + + /// Count the occurrences of operation names on the longest path. + /// + /// Returns a dictionary of counts keyed on the operation name. + fn count_ops_longest_path(&self) -> PyResult> { + if self.dag.node_count() == 0 { + return Ok(HashMap::new()); + } + let weight_fn = |_| -> Result { Ok(1) }; + let longest_path = + match rustworkx_core::dag_algo::longest_path(&self.dag, weight_fn).unwrap() { + Some(res) => res.0, + None => return Err(DAGCircuitError::new_err("not a DAG")), + }; + // Allocate for worst case where all operations are unique + let mut op_counts: HashMap<&str, usize> = HashMap::with_capacity(longest_path.len() - 2); + for node_index in &longest_path[1..longest_path.len() - 1] { + if let NodeType::Operation(ref packed) = self.dag[*node_index] { + let name = packed.op.name(); + op_counts + .entry(name) + .and_modify(|count| *count += 1) + .or_insert(1); + } + } + Ok(op_counts) + } + + /// Returns causal cone of a qubit. + /// + /// A qubit's causal cone is the set of qubits that can influence the output of that + /// qubit through interactions, whether through multi-qubit gates or operations. Knowing + /// the causal cone of a qubit can be useful when debugging faulty circuits, as it can + /// help identify which wire(s) may be causing the problem. + /// + /// This method does not consider any classical data dependency in the ``DAGCircuit``, + /// classical bit wires are ignored for the purposes of building the causal cone. + /// + /// Args: + /// qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. + /// + /// Returns: + /// Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. + fn quantum_causal_cone(&self, py: Python, qubit: &Bound) -> PyResult> { + // Retrieve the output node from the qubit + let output_qubit = self.qubits.find(qubit).ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in the circuit", + qubit + )) + })?; + let output_node_index = self + .qubit_io_map + .get(output_qubit.0 as usize) + .map(|x| x[1]) + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "The given qubit {:?} is not present in qubit_output_map", + qubit + )) + })?; + + let mut qubits_in_cone: HashSet<&Qubit> = HashSet::from([&output_qubit]); + let mut queue: VecDeque = self.quantum_predecessors(output_node_index).collect(); + + // The processed_non_directive_nodes stores the set of processed non-directive nodes. + // This is an optimization to avoid considering the same non-directive node multiple + // times when reached from different paths. + // The directive nodes (such as barriers or measures) are trickier since when processing + // them we only add their predecessors that intersect qubits_in_cone. Hence, directive + // nodes have to be considered multiple times. + let mut processed_non_directive_nodes: HashSet = HashSet::new(); + + while !queue.is_empty() { + let cur_index = queue.pop_front().unwrap(); + + if let NodeType::Operation(packed) = self.dag.node_weight(cur_index).unwrap() { + if !packed.op.directive() { + // If the operation is not a directive (in particular not a barrier nor a measure), + // we do not do anything if it was already processed. Otherwise, we add its qubits + // to qubits_in_cone, and append its predecessors to queue. + if processed_non_directive_nodes.contains(&cur_index) { + continue; + } + qubits_in_cone.extend(self.qargs_interner.get(packed.qubits)); + processed_non_directive_nodes.insert(cur_index); + + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(_pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + queue.push_back(pred_index); + } + } + } else { + // Directives (such as barriers and measures) may be defined over all the qubits, + // yet not all of these qubits should be considered in the causal cone. So we + // only add those predecessors that have qubits in common with qubits_in_cone. + for pred_index in self.quantum_predecessors(cur_index) { + if let NodeType::Operation(pred_packed) = + self.dag.node_weight(pred_index).unwrap() + { + if self + .qargs_interner + .get(pred_packed.qubits) + .iter() + .any(|x| qubits_in_cone.contains(x)) + { + queue.push_back(pred_index); + } + } + } + } + } + } + + 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()) + } + + /// 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)), + ("operations", self.py_count_ops(py, true)?), + ])) + } + + /// Draws the dag circuit. + /// + /// This function needs `Graphviz `_ to be + /// installed. Graphviz is not a python package and can't be pip installed + /// (the ``graphviz`` package on PyPI is a Python interface library for + /// Graphviz and does not actually install Graphviz). You can refer to + /// `the Graphviz documentation `__ on + /// how to install it. + /// + /// Args: + /// scale (float): scaling factor + /// filename (str): file path to save image to (format inferred from name) + /// style (str): + /// 'plain': B&W graph; + /// 'color' (default): color input/output/op nodes + /// + /// Returns: + /// Ipython.display.Image: if in Jupyter notebook and not saving to file, + /// otherwise None. + #[pyo3(signature=(scale=0.7, filename=None, style="color"))] + fn draw<'py>( + slf: PyRef<'py, Self>, + py: Python<'py>, + scale: f64, + filename: Option<&str>, + style: &str, + ) -> PyResult> { + let module = PyModule::import_bound(py, "qiskit.visualization.dag_visualization")?; + module.call_method1("dag_drawer", (slf, scale, filename, style)) + } + + fn _to_dot<'py>( + &self, + py: Python<'py>, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, + ) -> 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)?)) + } + + /// Add an input variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_input_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add inputs to a circuit with captures", + )); + } + self.add_var(py, var, DAGVarType::Input) + } + + /// Add a captured variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_captured_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add captures to a circuit with inputs", + )); + } + self.add_var(py, var, DAGVarType::Capture) + } + + /// Add a declared local variable to the circuit. + /// + /// Args: + /// var: the variable to add. + fn add_declared_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { + self.add_var(py, var, DAGVarType::Declare) + } + + /// Total number of classical variables tracked by the circuit. + #[getter] + fn num_vars(&self) -> usize { + self.vars_info.len() + } + + /// Number of input classical variables tracked by the circuit. + #[getter] + fn num_input_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Input as usize].bind(py).len() + } + + /// Number of captured classical variables tracked by the circuit. + #[getter] + fn num_captured_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .len() + } + + /// Number of declared local classical variables tracked by the circuit. + #[getter] + fn num_declared_vars(&self, py: Python) -> usize { + self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .len() + } + + /// Is this realtime variable in the DAG? + /// + /// Args: + /// var: the variable or name to check. + fn has_var(&self, var: &Bound) -> PyResult { + match var.extract::() { + Ok(name) => Ok(self.vars_info.contains_key(&name)), + Err(_) => { + let raw_name = var.getattr("name")?; + let var_name: String = raw_name.extract()?; + match self.vars_info.get(&var_name) { + Some(var_in_dag) => Ok(var_in_dag.var.is(var)), + None => Ok(false), + } + } + } + } + + /// Iterable over the input classical variables tracked by the circuit. + fn iter_input_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the captured classical variables tracked by the circuit. + fn iter_captured_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .clone() + .into_any() + .iter()? + .unbind()) + } + + /// Iterable over the declared classical variables tracked by the circuit. + fn iter_declared_vars(&self, py: Python) -> PyResult> { + Ok(self.vars_by_type[DAGVarType::Declare as usize] + .bind(py) + .clone() + .into_any() + .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)?; + 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()) + } + + fn _has_edge(&self, source: usize, target: usize) -> bool { + self.dag + .contains_edge(NodeIndex::new(source), NodeIndex::new(target)) + } + + fn _is_dag(&self) -> bool { + rustworkx_core::petgraph::algo::toposort(&self.dag, None).is_ok() + } + + fn _in_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _out_edges(&self, py: Python, node_index: usize) -> Vec> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| { + ( + wire.source().index(), + wire.target().index(), + match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }, + ) + .into_py(py) + }) + .collect() + } + + fn _in_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Incoming) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _out_wires(&self, node_index: usize) -> Vec<&PyObject> { + self.dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .map(|wire| match wire.weight() { + Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), + Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), + Wire::Var(var) => var, + }) + .collect() + } + + fn _find_successors_by_edge( + &self, + py: Python, + node_index: usize, + edge_checker: &Bound, + ) -> PyResult> { + let mut result = Vec::new(); + for e in self + .dag + .edges_directed(NodeIndex::new(node_index), Outgoing) + .unique_by(|e| e.id()) + { + let weight = match e.weight() { + Wire::Qubit(q) => self.qubits.get(*q).unwrap(), + Wire::Clbit(c) => self.clbits.get(*c).unwrap(), + Wire::Var(v) => v, + }; + if edge_checker.call1((weight,))?.extract::()? { + result.push(self.get_node(py, e.target())?); + } + } + Ok(result) + } + + fn _edges(&self, py: Python) -> Vec { + 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) => var.clone_ref(py), + } + }) + .collect() + } +} + +impl DAGCircuit { + /// Returns an immutable view of the inner StableGraph managed by the circuit. + #[inline(always)] + pub fn dag(&self) -> &StableDiGraph { + &self.dag + } + + /// Returns an immutable view of the Interner used for Qargs + #[inline(always)] + pub fn qargs_interner(&self) -> &Interner<[Qubit]> { + &self.qargs_interner + } + + /// Returns an immutable view of the Interner used for Cargs + #[inline(always)] + pub fn cargs_interner(&self) -> &Interner<[Clbit]> { + &self.cargs_interner + } + + /// Returns an immutable view of the Global Phase `Param` of the circuit + #[inline(always)] + pub fn global_phase(&self) -> &Param { + &self.global_phase + } + + /// Returns an immutable view of the Qubits registered in the circuit + #[inline(always)] + pub fn qubits(&self) -> &BitData { + &self.qubits + } + + /// Returns an immutable view of the Classical bits registered in the circuit + #[inline(always)] + pub fn clbits(&self) -> &BitData { + &self.clbits + } + + /// Return an iterator of gate runs with non-conditional op nodes of given names + pub fn collect_runs( + &self, + namelist: HashSet, + ) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => { + Ok(namelist.contains(inst.op.name()) && inst.extra_attrs.condition().is_none()) + } + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 1q "op" nodes. + pub fn collect_1q_runs(&self) -> Option> + '_> { + let filter_fn = move |node_index: NodeIndex| -> Result { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => Ok(inst.op.num_qubits() == 1 + && inst.op.num_clbits() == 0 + && !inst.is_parameterized() + && (inst.op.try_standard_gate().is_some() + || inst.op.matrix(inst.params_view()).is_some()) + && inst.condition().is_none()), + _ => Ok(false), + } + }; + rustworkx_core::dag_algo::collect_runs(&self.dag, filter_fn) + .map(|node_iter| node_iter.map(|x| x.unwrap())) + } + + /// Return a set of non-conditional runs of 2q "op" nodes. + pub fn collect_2q_runs(&self) -> Option>> { + let filter_fn = move |node_index: NodeIndex| -> Result, Infallible> { + let node = &self.dag[node_index]; + match node { + NodeType::Operation(inst) => match inst.op.view() { + OperationRef::Standard(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + OperationRef::Gate(gate) => Ok(Some( + gate.num_qubits() <= 2 + && inst.condition().is_none() + && !inst.is_parameterized(), + )), + _ => Ok(Some(false)), + }, + _ => Ok(None), + } + }; + + let color_fn = move |edge_index: EdgeIndex| -> Result, Infallible> { + let wire = self.dag.edge_weight(edge_index).unwrap(); + match wire { + Wire::Qubit(index) => Ok(Some(index.0 as usize)), + _ => Ok(None), + } + }; + rustworkx_core::dag_algo::collect_bicolor_runs(&self.dag, filter_fn, color_fn).unwrap() + } + + fn increment_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + *count += 1; + } + None => { + self.op_names.insert(op.to_string(), 1); + } + } + } + + fn decrement_op(&mut self, op: &str) { + match self.op_names.get_mut(op) { + Some(count) => { + if *count > 1 { + *count -= 1; + } else { + self.op_names.swap_remove(op); + } + } + None => panic!("Cannot decrement something not added!"), + } + } + + pub fn quantum_predecessors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Incoming) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.source()), + _ => None, + }) + .unique() + } + + pub fn quantum_successors(&self, node: NodeIndex) -> impl Iterator + '_ { + self.dag + .edges_directed(node, Outgoing) + .filter_map(|e| match e.weight() { + Wire::Qubit(_) => Some(e.target()), + _ => None, + }) + .unique() + } + + /// Apply a [PackedInstruction] to the back of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { + let op_name = instr.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &instr) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_interner.get(instr.clbits).iter().copied()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, instr.op.view(), instr.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_interner.get(instr.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = instr.qubits; + let new_node = self.dag.add_node(NodeType::Operation(instr)); + + // Put the new node in-between the previously "last" nodes on each wire + // and the output map. + let output_nodes: HashSet = self + .qargs_interner + .get(qubits_id) + .iter() + .map(|q| self.qubit_io_map.get(q.0 as usize).map(|x| x[1]).unwrap()) + .chain( + all_cbits + .iter() + .map(|c| self.clbit_io_map.get(c.0 as usize).map(|x| x[1]).unwrap()), + ) + .chain( + vars.iter() + .flatten() + .map(|v| self.var_output_map.get(py, v).unwrap()), + ) + .collect(); + + for output_node in output_nodes { + let last_edges: Vec<_> = self + .dag + .edges_directed(output_node, Incoming) + .map(|e| (e.source(), e.id(), e.weight().clone())) + .collect(); + for (source, old_edge, weight) in last_edges.into_iter() { + self.dag.add_edge(source, new_node, weight.clone()); + self.dag.add_edge(new_node, output_node, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + /// Apply a [PackedInstruction] to the front of the circuit. + /// + /// The provided `instr` MUST be valid for this DAG, e.g. its + /// bits, registers, vars, and interner IDs must be valid in + /// this DAG. + /// + /// This is mostly used to apply operations from one DAG to + /// another that was created from the first via + /// [DAGCircuit::copy_empty_like]. + fn push_front(&mut self, py: Python, inst: PackedInstruction) -> PyResult { + let op_name = inst.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &inst) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_interner.get(inst.clbits).iter().copied()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, inst.op.view(), inst.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_interner.get(inst.clbits).to_vec(), None) + } + }; + + self.increment_op(op_name); + + let qubits_id = inst.qubits; + let new_node = self.dag.add_node(NodeType::Operation(inst)); + + // Put the new node in-between the input map and the previously + // "first" nodes on each wire. + let mut input_nodes: Vec = self + .qargs_interner + .get(qubits_id) + .iter() + .map(|q| self.qubit_io_map[q.0 as usize][0]) + .chain(all_cbits.iter().map(|c| self.clbit_io_map[c.0 as usize][0])) + .collect(); + if let Some(vars) = vars { + for var in vars { + input_nodes.push(self.var_input_map.get(py, &var).unwrap()); + } + } + + for input_node in input_nodes { + let first_edges: Vec<_> = self + .dag + .edges_directed(input_node, Outgoing) + .map(|e| (e.target(), e.id(), e.weight().clone())) + .collect(); + for (target, old_edge, weight) in first_edges.into_iter() { + self.dag.add_edge(input_node, new_node, weight.clone()); + self.dag.add_edge(new_node, target, weight); + self.dag.remove_edge(old_edge); + } + } + + Ok(new_node) + } + + /// Apply a [PackedOperation] to the back of the circuit. + pub fn apply_operation_back( + &mut self, + py: Python, + op: PackedOperation, + qargs: &[Qubit], + cargs: &[Clbit], + params: Option>, + extra_attrs: ExtraInstructionAttributes, + #[cfg(feature = "cache_pygates")] py_op: Option, + ) -> PyResult { + self.inner_apply_op( + py, + op, + qargs, + cargs, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op, + false, + ) + } + + /// Apply a [PackedOperation] to the front of the circuit. + pub fn apply_operation_front( + &mut self, + py: Python, + op: PackedOperation, + qargs: &[Qubit], + cargs: &[Clbit], + params: Option>, + extra_attrs: ExtraInstructionAttributes, + #[cfg(feature = "cache_pygates")] py_op: Option, + ) -> PyResult { + self.inner_apply_op( + py, + op, + qargs, + cargs, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op, + true, + ) + } + + #[inline] + #[allow(clippy::too_many_arguments)] + fn inner_apply_op( + &mut self, + py: Python, + op: PackedOperation, + qargs: &[Qubit], + cargs: &[Clbit], + params: Option>, + extra_attrs: ExtraInstructionAttributes, + #[cfg(feature = "cache_pygates")] py_op: Option, + front: bool, + ) -> PyResult { + // Check that all qargs are within an acceptable range + qargs.iter().try_for_each(|qarg| { + if qarg.0 as usize >= self.num_qubits() { + return Err(PyValueError::new_err(format!( + "Qubit index {} is out of range. This DAGCircuit currently has only {} qubits.", + qarg.0, + self.num_qubits() + ))); + } + Ok(()) + })?; + + // Check that all cargs are within an acceptable range + cargs.iter().try_for_each(|carg| { + if carg.0 as usize >= self.num_clbits() { + return Err(PyValueError::new_err(format!( + "Clbit index {} is out of range. This DAGCircuit currently has only {} clbits.", + carg.0, + self.num_clbits() + ))); + } + Ok(()) + })?; + + #[cfg(feature = "cache_pygates")] + let py_op = if let Some(py_op) = py_op { + py_op.into() + } else { + OnceCell::new() + }; + let packed_instruction = PackedInstruction { + op, + qubits: self.qargs_interner.insert(qargs), + clbits: self.cargs_interner.insert(cargs), + params: params.map(Box::new), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op, + }; + + if front { + self.push_front(py, packed_instruction) + } else { + self.push_back(py, packed_instruction) + } + } + + fn sort_key(&self, node: NodeIndex) -> SortKeyType { + match &self.dag[node] { + NodeType::Operation(packed) => ( + self.qargs_interner.get(packed.qubits), + self.cargs_interner.get(packed.clbits), + ), + NodeType::QubitIn(q) => (std::slice::from_ref(q), &[Clbit(u32::MAX)]), + NodeType::QubitOut(_q) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + NodeType::ClbitIn(c) => (&[Qubit(u32::MAX)], std::slice::from_ref(c)), + NodeType::ClbitOut(_c) => (&[Qubit(u32::MAX)], &[Clbit(u32::MAX)]), + _ => (&[], &[]), + } + } + + fn topological_nodes(&self) -> PyResult> { + let key = |node: NodeIndex| -> Result { Ok(self.sort_key(node)) }; + let nodes = + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(_) => { + unreachable!() + } + })?; + Ok(nodes.into_iter()) + } + + pub fn topological_op_nodes(&self) -> PyResult + '_> { + Ok(self.topological_nodes()?.filter(|node: &NodeIndex| { + matches!(self.dag.node_weight(*node), Some(NodeType::Operation(_))) + })) + } + + fn topological_key_sort( + &self, + py: Python, + key: &Bound, + ) -> PyResult> { + // This path (user provided key func) is not ideal, since we no longer + // use a string key after moving to Rust, in favor of using a tuple + // of the qargs and cargs interner IDs of the node. + let key = |node: NodeIndex| -> PyResult { + let node = self.get_node(py, node)?; + key.call1((node,))?.extract() + }; + Ok( + rustworkx_core::dag_algo::lexicographical_topological_sort(&self.dag, key, false, None) + .map_err(|e| match e { + rustworkx_core::dag_algo::TopologicalSortError::CycleOrBadInitialState => { + PyValueError::new_err(format!("{}", e)) + } + rustworkx_core::dag_algo::TopologicalSortError::KeyError(ref e) => { + e.clone_ref(py) + } + })? + .into_iter(), + ) + } + + #[inline] + fn has_control_flow(&self) -> bool { + CONTROL_FLOW_OP_NAMES + .iter() + .any(|x| self.op_names.contains_key(&x.to_string())) + } + + fn is_wire_idle(&self, py: Python, wire: &Wire) -> PyResult { + let (input_node, output_node) = match wire { + Wire::Qubit(qubit) => ( + self.qubit_io_map[qubit.0 as usize][0], + self.qubit_io_map[qubit.0 as usize][1], + ), + Wire::Clbit(clbit) => ( + self.clbit_io_map[clbit.0 as usize][0], + self.clbit_io_map[clbit.0 as usize][1], + ), + Wire::Var(var) => ( + self.var_input_map.get(py, var).unwrap(), + self.var_output_map.get(py, var).unwrap(), + ), + }; + + let child = self + .dag + .neighbors_directed(input_node, Outgoing) + .next() + .ok_or_else(|| { + DAGCircuitError::new_err(format!( + "Invalid dagcircuit input node {:?} has no output", + input_node + )) + })?; + + Ok(child == output_node) + } + + fn may_have_additional_wires(&self, py: Python, instr: &PackedInstruction) -> bool { + if instr.condition().is_some() { + return true; + } + let OperationRef::Instruction(inst) = instr.op.view() else { + return false; + }; + inst.control_flow() + || inst + .instruction + .bind(py) + .is_instance(imports::STORE_OP.get_bound(py)) + .unwrap() + } + + fn additional_wires( + &self, + py: Python, + op: OperationRef, + condition: Option<&PyObject>, + ) -> PyResult<(Vec, Vec)> { + 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()? { + 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() { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + vars.push(var.unbind()); + } + } + Ok((clbits, vars)) + }; + + let mut clbits = Vec::new(); + let mut vars = Vec::new(); + if let Some(condition) = condition { + let condition = condition.bind(py); + if !condition.is_none() { + if condition.is_instance(imports::EXPR.get_bound(py)).unwrap() { + let (expr_clbits, expr_vars) = wires_from_expr(condition)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } else { + for bit in self + .control_flow_module + .condition_resources(condition)? + .clbits + .bind(py) + { + clbits.push(self.clbits.find(&bit).unwrap()); + } + } + } + } + + 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()? { + vars.push(var?.unbind()) + } + if op.is_instance(imports::SWITCH_CASE_OP.get_bound(py))? { + let target = op.getattr(intern!(py, "target"))?; + 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()? { + clbits.push(self.clbits.find(&bit?).unwrap()); + } + } else { + let (expr_clbits, expr_vars) = wires_from_expr(&target)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + } else if op.is_instance(imports::STORE_OP.get_bound(py))? { + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("lvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + let (expr_clbits, expr_vars) = wires_from_expr(&op.getattr("rvalue")?)?; + for bit in expr_clbits { + clbits.push(bit); + } + for var in expr_vars { + vars.push(var); + } + } + } + Ok((clbits, vars)) + } + + /// Add a qubit or bit to the circuit. + /// + /// Args: + /// wire: the wire to be added + /// + /// This adds a pair of in and out nodes connected by an edge. + /// + /// Raises: + /// DAGCircuitError: if trying to add duplicate wire + fn add_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let (in_node, out_node) = match wire { + Wire::Qubit(qubit) => { + if (qubit.0 as usize) >= self.qubit_io_map.len() { + let input_node = self.dag.add_node(NodeType::QubitIn(qubit)); + let output_node = self.dag.add_node(NodeType::QubitOut(qubit)); + self.qubit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("qubit wire already exists!")) + } + } + Wire::Clbit(clbit) => { + if (clbit.0 as usize) >= self.clbit_io_map.len() { + let input_node = self.dag.add_node(NodeType::ClbitIn(clbit)); + let output_node = self.dag.add_node(NodeType::ClbitOut(clbit)); + self.clbit_io_map.push([input_node, output_node]); + Ok((input_node, output_node)) + } else { + Err(DAGCircuitError::new_err("classical wire already exists!")) + } + } + Wire::Var(ref var) => { + if self.var_input_map.contains_key(py, var) + || self.var_output_map.contains_key(py, var) + { + return Err(DAGCircuitError::new_err("var wire already exists!")); + } + let in_node = self.dag.add_node(NodeType::VarIn(var.clone_ref(py))); + let out_node = self.dag.add_node(NodeType::VarOut(var.clone_ref(py))); + self.var_input_map.insert(py, var.clone_ref(py), in_node); + self.var_output_map.insert(py, var.clone_ref(py), out_node); + Ok((in_node, out_node)) + } + }?; + + self.dag.add_edge(in_node, out_node, wire); + Ok(()) + } + + /// Get the nodes on the given wire. + /// + /// Note: result is empty if the wire is not in the DAG. + pub fn nodes_on_wire(&self, py: Python, wire: &Wire, only_ops: bool) -> Vec { + let mut nodes = Vec::new(); + let mut current_node = match wire { + Wire::Qubit(qubit) => self.qubit_io_map.get(qubit.0 as usize).map(|x| x[0]), + Wire::Clbit(clbit) => self.clbit_io_map.get(clbit.0 as usize).map(|x| x[0]), + Wire::Var(var) => self.var_input_map.get(py, var), + }; + + while let Some(node) = current_node { + if only_ops { + let node_weight = self.dag.node_weight(node).unwrap(); + if let NodeType::Operation(_) = node_weight { + nodes.push(node); + } + } else { + nodes.push(node); + } + + let edges = self.dag.edges_directed(node, Outgoing); + current_node = edges.into_iter().find_map(|edge| { + if edge.weight() == wire { + Some(edge.target()) + } else { + None + } + }); + } + nodes + } + + fn remove_idle_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + let [in_node, out_node] = match wire { + Wire::Qubit(qubit) => self.qubit_io_map[qubit.0 as usize], + Wire::Clbit(clbit) => self.clbit_io_map[clbit.0 as usize], + Wire::Var(var) => [ + self.var_input_map.remove(py, &var).unwrap(), + self.var_output_map.remove(py, &var).unwrap(), + ], + }; + self.dag.remove_node(in_node); + self.dag.remove_node(out_node); + Ok(()) + } + + fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let qubit = self.qubits.add(py, bit, false)?; + self.qubit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.qubits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Qubit(qubit))?; + Ok(qubit) + } + + fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + let clbit = self.clbits.add(py, bit, false)?; + self.clbit_locations.bind(py).set_item( + bit, + Py::new( + py, + BitLocations { + index: (self.clbits.len() - 1), + registers: PyList::empty_bound(py).unbind(), + }, + )?, + )?; + self.add_wire(py, Wire::Clbit(clbit))?; + Ok(clbit) + } + + pub fn get_node(&self, py: Python, node: NodeIndex) -> PyResult> { + self.unpack_into(py, node, self.dag.node_weight(node).unwrap()) + } + + /// Remove an operation node n. + /// + /// Add edges from predecessors to successors. + pub fn remove_op_node(&mut self, index: NodeIndex) { + let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::new(); + for (source, in_weight) in self + .dag + .edges_directed(index, Incoming) + .map(|x| (x.source(), x.weight())) + { + for (target, out_weight) in self + .dag + .edges_directed(index, Outgoing) + .map(|x| (x.target(), x.weight())) + { + if in_weight == out_weight { + edge_list.push((source, target, in_weight.clone())); + } + } + } + for (source, target, weight) in edge_list { + self.dag.add_edge(source, target, weight); + } + + match self.dag.remove_node(index) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node!"), + } + } + + /// Returns an iterator of the ancestors indices of a node. + pub fn ancestors(&self, node: NodeIndex) -> impl Iterator + '_ { + core_ancestors(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of the descendants of a node as DAGOpNodes and DAGOutNodes. + pub fn descendants(&self, node: NodeIndex) -> impl Iterator + '_ { + core_descendants(&self.dag, node).filter(move |next| next != &node) + } + + /// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node + /// and [DAGNode] is its successors in BFS order. + pub fn bfs_successors( + &self, + node: NodeIndex, + ) -> impl Iterator)> + '_ { + core_bfs_successors(&self.dag, node).filter(move |(_, others)| !others.is_empty()) + } + + fn pack_into(&mut self, py: Python, b: &Bound) -> Result { + Ok(if let Ok(in_node) = b.downcast::() { + let in_node = in_node.borrow(); + let wire = in_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitIn(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitIn(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(out_node) = b.downcast::() { + let out_node = out_node.borrow(); + let wire = out_node.wire.bind(py); + if wire.is_instance(imports::QUBIT.get_bound(py))? { + NodeType::QubitOut(self.qubits.find(wire).unwrap()) + } else if wire.is_instance(imports::CLBIT.get_bound(py))? { + NodeType::ClbitOut(self.clbits.find(wire).unwrap()) + } else { + NodeType::VarIn(wire.clone().unbind()) + } + } else if let Ok(op_node) = b.downcast::() { + let op_node = op_node.borrow(); + let qubits = self.qargs_interner.insert_owned( + self.qubits + .map_bits(op_node.instruction.qubits.bind(py))? + .collect(), + ); + let clbits = self.cargs_interner.insert_owned( + self.clbits + .map_bits(op_node.instruction.clbits.bind(py))? + .collect(), + ); + let params = (!op_node.instruction.params.is_empty()) + .then(|| Box::new(op_node.instruction.params.clone())); + let inst = PackedInstruction { + op: op_node.instruction.operation.clone(), + qubits, + clbits, + params, + extra_attrs: op_node.instruction.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: op_node.instruction.py_op.clone(), + }; + NodeType::Operation(inst) + } else { + return Err(PyTypeError::new_err("Invalid type for DAGNode")); + }) + } + + fn unpack_into(&self, py: Python, id: NodeIndex, weight: &NodeType) -> PyResult> { + let dag_node = match weight { + NodeType::QubitIn(qubit) => Py::new( + py, + DAGInNode::new(py, 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)), + )? + .into_any(), + NodeType::ClbitIn(clbit) => Py::new( + py, + DAGInNode::new(py, 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)), + )? + .into_any(), + NodeType::Operation(packed) => { + let qubits = self.qargs_interner.get(packed.qubits); + let clbits = self.cargs_interner.get(packed.clbits); + Py::new( + py, + ( + 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().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) }, + ), + )? + .into_any() + } + NodeType::VarIn(var) => { + Py::new(py, DAGInNode::new(py, id, var.clone_ref(py)))?.into_any() + } + NodeType::VarOut(var) => { + Py::new(py, DAGOutNode::new(py, id, var.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, + include_directives: bool, + ) -> Box + 'a> { + let node_ops_iter = 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 + } + })) + } else { + Box::new(node_ops_iter.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 + } + })) + } + + // Filter any nodes that don't match a given predicate function + pub fn filter_op_nodes(&mut self, mut predicate: F) + 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 { + 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, py: Python) -> impl Iterator> + '_ { + let mut first_layer: Vec<_> = self.qubit_io_map.iter().map(|x| x[0]).collect(); + first_layer.extend(self.clbit_io_map.iter().map(|x| x[0])); + first_layer.extend(self.var_input_map.values(py)); + // A DAG is by definition acyclical, therefore unwrapping the layer should never fail. + layers(&self.dag, first_layer).map(|layer| match layer { + Ok(layer) => layer, + Err(_) => unreachable!("Not a DAG."), + }) + } + + /// Returns an iterator over the first layer of the `DAGCircuit``. + pub fn front_layer<'a>(&'a self, py: Python) -> Box + 'a> { + let mut graph_layers = self.multigraph_layers(py); + graph_layers.next(); + + let next_layer = graph_layers.next(); + match next_layer { + Some(layer) => Box::new(layer.into_iter().filter(|node| { + matches!(self.dag.node_weight(*node).unwrap(), NodeType::Operation(_)) + })), + None => Box::new(vec![].into_iter()), + } + } + + fn substitute_node_with_subgraph( + &mut self, + py: Python, + node: NodeIndex, + other: &DAGCircuit, + qubit_map: &HashMap, + clbit_map: &HashMap, + var_map: &Py, + ) -> PyResult> { + if self.dag.node_weight(node).is_none() { + return Err(PyIndexError::new_err(format!( + "Specified node {} is not in this graph", + node.index() + ))); + } + + // Add wire from pred to succ if no ops on mapped wire on ``other`` + for (in_dag_wire, self_wire) in qubit_map.iter() { + let [input_node, out_node] = other.qubit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Qubit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Qubit(*self_wire)); + } + } + for (in_dag_wire, self_wire) in clbit_map.iter() { + let [input_node, out_node] = other.clbit_io_map[in_dag_wire.0 as usize]; + if other.dag.find_edge(input_node, out_node).is_some() { + let pred = self + .dag + .edges_directed(node, Incoming) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + let succ = self + .dag + .edges_directed(node, Outgoing) + .find(|edge| { + if let Wire::Clbit(bit) = edge.weight() { + bit == self_wire + } else { + false + } + }) + .unwrap(); + self.dag + .add_edge(pred.source(), succ.target(), Wire::Clbit(*self_wire)); + } + } + + let bound_var_map = var_map.bind(py); + let node_filter = |node: NodeIndex| -> bool { + match other.dag[node] { + NodeType::Operation(_) => !other + .dag + .edges_directed(node, petgraph::Direction::Outgoing) + .any(|edge| match edge.weight() { + Wire::Qubit(qubit) => !qubit_map.contains_key(qubit), + Wire::Clbit(clbit) => !clbit_map.contains_key(clbit), + Wire::Var(var) => !bound_var_map.contains(var).unwrap(), + }), + _ => false, + } + }; + let reverse_qubit_map: HashMap = + 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); + for (k, v) in bound_var_map.iter() { + reverse_var_map.set_item(v, k)?; + } + // Copy nodes from other to self + let mut out_map: IndexMap = + IndexMap::with_capacity_and_hasher(other.dag.node_count(), RandomState::default()); + for old_index in other.dag.node_indices() { + if !node_filter(old_index) { + continue; + } + let mut new_node = other.dag[old_index].clone(); + if let NodeType::Operation(ref mut new_inst) = new_node { + let new_qubit_indices: Vec = other + .qargs_interner + .get(new_inst.qubits) + .iter() + .map(|old_qubit| qubit_map[old_qubit]) + .collect(); + let new_clbit_indices: Vec = other + .cargs_interner + .get(new_inst.clbits) + .iter() + .map(|old_clbit| clbit_map[old_clbit]) + .collect(); + new_inst.qubits = self.qargs_interner.insert_owned(new_qubit_indices); + new_inst.clbits = self.cargs_interner.insert_owned(new_clbit_indices); + self.increment_op(new_inst.op.name()); + } + let new_index = self.dag.add_node(new_node); + out_map.insert(old_index, new_index); + } + // If no nodes are copied bail here since there is nothing left + // to do. + if out_map.is_empty() { + match self.dag.remove_node(node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => unreachable!("Must be called with valid operation node!"), + } + // Return a new empty map to clear allocation from out_map + return Ok(IndexMap::default()); + } + // Copy edges from other to self + for edge in other.dag.edge_references().filter(|edge| { + out_map.contains_key(&edge.target()) && out_map.contains_key(&edge.source()) + }) { + self.dag.add_edge( + out_map[&edge.source()], + out_map[&edge.target()], + match edge.weight() { + Wire::Qubit(qubit) => Wire::Qubit(qubit_map[qubit]), + Wire::Clbit(clbit) => Wire::Clbit(clbit_map[clbit]), + Wire::Var(var) => Wire::Var(bound_var_map.get_item(var)?.unwrap().unbind()), + }, + ); + } + // Add edges to/from node to nodes in other + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Incoming) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (source, _target, weight) in edges { + let wire_input_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[0]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[0]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_input_map.get(py, index) + } + }; + let old_index = + wire_input_id.and_then(|x| other.dag.neighbors_directed(x, Outgoing).next()); + let target_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source, target_out, weight); + } + let edges: Vec<(NodeIndex, NodeIndex, Wire)> = self + .dag + .edges_directed(node, Outgoing) + .map(|x| (x.source(), x.target(), x.weight().clone())) + .collect(); + for (_source, target, weight) in edges { + let wire_output_id = match weight { + Wire::Qubit(qubit) => other + .qubit_io_map + .get(reverse_qubit_map[&qubit].0 as usize) + .map(|x| x[1]), + Wire::Clbit(clbit) => other + .clbit_io_map + .get(reverse_clbit_map[&clbit].0 as usize) + .map(|x| x[1]), + Wire::Var(ref var) => { + let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); + other.var_output_map.get(py, index) + } + }; + let old_index = + wire_output_id.and_then(|x| other.dag.neighbors_directed(x, Incoming).next()); + let source_out = match old_index { + Some(old_index) => match out_map.get(&old_index) { + Some(new_index) => *new_index, + None => { + // If the index isn't in the node map we've already added the edges as + // part of the idle wire handling at the top of this method so just + // move on. + continue; + } + }, + None => continue, + }; + self.dag.add_edge(source_out, target, weight); + } + // Remove node + if let NodeType::Operation(inst) = &self.dag[node] { + self.decrement_op(inst.op.name().to_string().as_str()); + } + self.dag.remove_node(node); + Ok(out_map) + } + + fn add_var(&mut self, py: Python, var: &Bound, type_: DAGVarType) -> PyResult<()> { + // The setup of the initial graph structure between an "in" and an "out" node is the same as + // the bit-related `_add_wire`, but this logically needs to do different bookkeeping around + // tracking the properties + if !var.getattr("standalone")?.extract::()? { + return Err(DAGCircuitError::new_err( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances", + )); + } + let var_name: String = var.getattr("name")?.extract::()?; + if let Some(previous) = self.vars_info.get(&var_name) { + if var.eq(previous.var.clone_ref(py))? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add var as its name shadows an existing var", + )); + } + let in_node = NodeType::VarIn(var.clone().unbind()); + let out_node = NodeType::VarOut(var.clone().unbind()); + let in_index = self.dag.add_node(in_node); + let out_index = self.dag.add_node(out_node); + self.dag + .add_edge(in_index, out_index, Wire::Var(var.clone().unbind())); + self.var_input_map + .insert(py, var.clone().unbind(), in_index); + self.var_output_map + .insert(py, var.clone().unbind(), out_index); + self.vars_by_type[type_ as usize] + .bind(py) + .add(var.clone().unbind())?; + self.vars_info.insert( + var_name, + DAGVarInfo { + var: var.clone().unbind(), + type_, + in_node: in_index, + out_node: out_index, + }, + ); + Ok(()) + } + + fn check_op_addition(&self, py: Python, inst: &PackedInstruction) -> PyResult<()> { + if let Some(condition) = inst.condition() { + self._check_condition(py, inst.op.name(), condition.bind(py))?; + } + + for b in self.qargs_interner.get(inst.qubits) { + if self.qubit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "qubit {} not found in output map", + self.qubits.get(*b).unwrap() + ))); + } + } + + for b in self.cargs_interner.get(inst.clbits) { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(*b).unwrap() + ))); + } + } + + if self.may_have_additional_wires(py, inst) { + let (clbits, vars) = self.additional_wires(py, inst.op.view(), inst.condition())?; + for b in clbits { + if !self.clbit_io_map.len() - 1 < b.0 as usize { + return Err(DAGCircuitError::new_err(format!( + "clbit {} not found in output map", + self.clbits.get(b).unwrap() + ))); + } + } + for v in vars { + if !self.var_output_map.contains_key(py, &v) { + return Err(DAGCircuitError::new_err(format!( + "var {} not found in output map", + v + ))); + } + } + } + Ok(()) + } + + /// Alternative constructor, builds a DAGCircuit with a fixed capacity. + /// + /// # Arguments: + /// - `py`: Python GIL token + /// - `num_qubits`: Number of qubits in the circuit + /// - `num_clbits`: Number of classical bits in the circuit. + /// - `num_vars`: (Optional) number of variables in the circuit. + /// - `num_ops`: (Optional) number of operations in the circuit. + /// - `num_edges`: (Optional) If known, number of edges in the circuit. + pub fn with_capacity( + py: Python, + num_qubits: usize, + num_clbits: usize, + num_vars: Option, + num_ops: Option, + num_edges: Option, + ) -> PyResult { + let num_ops: usize = num_ops.unwrap_or_default(); + let num_vars = num_vars.unwrap_or_default(); + let num_edges = num_edges.unwrap_or( + num_qubits + // 1 edge between the input node and the output node or 1st op node. + num_clbits + // 1 edge between the input node and the output node or 1st op node. + num_vars + // 1 edge between the input node and the output node or 1st op node. + num_ops, // In Average there will be 3 edges (2 qubits and 1 clbit, or 3 qubits) per op_node. + ); + + let num_nodes = num_qubits * 2 + // One input + One output node per qubit + num_clbits * 2 + // One input + One output node per clbit + num_vars * 2 + // One input + output node per variable + num_ops; + + Ok(Self { + name: None, + metadata: Some(PyDict::new_bound(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(), + qargs_interner: Interner::with_capacity(num_qubits), + cargs_interner: Interner::with_capacity(num_clbits), + qubits: BitData::with_capacity(py, "qubits".to_string(), num_qubits), + clbits: BitData::with_capacity(py, "clbits".to_string(), num_clbits), + 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_io_map: Vec::with_capacity(num_qubits), + clbit_io_map: Vec::with_capacity(num_clbits), + var_input_map: _VarIndexMap::new(py), + var_output_map: _VarIndexMap::new(py), + op_names: IndexMap::default(), + 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(), + ], + }) + } + + /// Get qargs from an intern index + pub fn get_qargs(&self, index: Interned<[Qubit]>) -> &[Qubit] { + self.qargs_interner.get(index) + } + + /// Get cargs from an intern index + pub fn get_cargs(&self, index: Interned<[Clbit]>) -> &[Clbit] { + self.cargs_interner.get(index) + } + + /// Insert a new 1q standard gate on incoming qubit + pub fn insert_1q_on_incoming_qubit( + &mut self, + new_gate: (StandardGate, &[f64]), + old_index: NodeIndex, + ) { + self.increment_op(new_gate.0.name()); + let old_node = &self.dag[old_index]; + let inst = if let NodeType::Operation(old_node) = old_node { + PackedInstruction { + op: new_gate.0.into(), + qubits: old_node.qubits, + clbits: old_node.clbits, + params: (!new_gate.1.is_empty()) + .then(|| Box::new(new_gate.1.iter().map(|x| Param::Float(*x)).collect())), + extra_attrs: ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + } + } else { + panic!("This method only works if provided index is an op node"); + }; + let new_index = self.dag.add_node(NodeType::Operation(inst)); + let (parent_index, edge_index, weight) = self + .dag + .edges_directed(old_index, Incoming) + .map(|edge| (edge.source(), edge.id(), edge.weight().clone())) + .next() + .unwrap(); + self.dag.add_edge(parent_index, new_index, weight.clone()); + self.dag.add_edge(new_index, old_index, weight); + self.dag.remove_edge(edge_index); + } + + /// Remove a sequence of 1 qubit nodes from the dag + /// This must only be called if all the nodes operate + /// on a single qubit with no other wires in or out of any nodes + pub fn remove_1q_sequence(&mut self, sequence: &[NodeIndex]) { + let (parent_index, weight) = self + .dag + .edges_directed(*sequence.first().unwrap(), Incoming) + .map(|edge| (edge.source(), edge.weight().clone())) + .next() + .unwrap(); + let child_index = self + .dag + .edges_directed(*sequence.last().unwrap(), Outgoing) + .map(|edge| edge.target()) + .next() + .unwrap(); + self.dag.add_edge(parent_index, child_index, weight); + for node in sequence { + match self.dag.remove_node(*node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node!"), + } + } + } + + /// Replace a node with individual operations from a provided callback + /// function on each qubit of that node. + #[allow(unused_variables)] + pub fn replace_node_with_1q_ops( + &mut self, + py: Python, // Unused if cache_pygates isn't enabled + node: NodeIndex, + insert: F, + ) -> PyResult<()> + where + F: Fn(&Wire) -> PyResult, + { + let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::with_capacity(2); + for (source, in_weight) in self + .dag + .edges_directed(node, Incoming) + .map(|x| (x.source(), x.weight())) + { + for (target, out_weight) in self + .dag + .edges_directed(node, Outgoing) + .map(|x| (x.target(), x.weight())) + { + if in_weight == out_weight { + edge_list.push((source, target, in_weight.clone())); + } + } + } + for (source, target, weight) in edge_list { + let new_op = insert(&weight)?; + self.increment_op(new_op.operation.name()); + let qubits = if let Wire::Qubit(qubit) = weight { + vec![qubit] + } else { + panic!("This method only works if the gate being replaced has no classical incident wires") + }; + #[cfg(feature = "cache_pygates")] + let py_op = match new_op.operation.view() { + OperationRef::Standard(_) => OnceCell::new(), + OperationRef::Gate(gate) => OnceCell::from(gate.gate.clone_ref(py)), + OperationRef::Instruction(instruction) => { + OnceCell::from(instruction.instruction.clone_ref(py)) + } + OperationRef::Operation(op) => OnceCell::from(op.operation.clone_ref(py)), + }; + let inst = PackedInstruction { + op: new_op.operation, + qubits: self.qargs_interner.insert_owned(qubits), + clbits: self.cargs_interner.get_default(), + params: (!new_op.params.is_empty()).then(|| Box::new(new_op.params)), + extra_attrs: new_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op, + }; + let new_index = self.dag.add_node(NodeType::Operation(inst)); + self.dag.add_edge(source, new_index, weight.clone()); + self.dag.add_edge(new_index, target, weight); + } + + match self.dag.remove_node(node) { + Some(NodeType::Operation(packed)) => { + let op_name = packed.op.name(); + self.decrement_op(op_name); + } + _ => panic!("Must be called with valid operation node"), + } + Ok(()) + } + + pub fn add_global_phase(&mut self, py: Python, value: &Param) -> PyResult<()> { + match value { + Param::Obj(_) => { + return Err(PyTypeError::new_err( + "Invalid parameter type, only float and parameter expression are supported", + )) + } + _ => self.set_global_phase(add_global_phase(py, &self.global_phase, value)?)?, + } + Ok(()) + } + + pub fn calibrations_empty(&self) -> bool { + self.calibrations.is_empty() + } + + pub fn has_calibration_for_index(&self, py: Python, node_index: NodeIndex) -> PyResult { + let node = &self.dag[node_index]; + if let NodeType::Operation(instruction) = node { + if !self.calibrations.contains_key(instruction.op.name()) { + return Ok(false); + } + let params = match &instruction.params { + Some(params) => { + let mut out_params = Vec::new(); + for p in params.iter() { + if let Param::ParameterExpression(exp) = p { + let exp = exp.bind(py); + if !exp.getattr(intern!(py, "parameters"))?.is_truthy()? { + let as_py_float = exp.call_method0(intern!(py, "__float__"))?; + out_params.push(as_py_float.unbind()); + continue; + } + } + out_params.push(p.to_object(py)); + } + PyTuple::new_bound(py, out_params) + } + None => PyTuple::empty_bound(py), + }; + let qargs = self.qargs_interner.get(instruction.qubits); + let qubits = PyTuple::new_bound(py, qargs.iter().map(|x| x.0)); + self.calibrations[instruction.op.name()] + .bind(py) + .contains((qubits, params).to_object(py)) + } else { + Err(DAGCircuitError::new_err("Specified node is not an op node")) + } + } + + /// Return the op name counts in the circuit + /// + /// Args: + /// py: The python token necessary for control flow recursion + /// recurse: Whether to recurse into control flow ops or not + pub fn count_ops( + &self, + py: Python, + recurse: bool, + ) -> PyResult> { + if !recurse || !self.has_control_flow() { + Ok(self.op_names.clone()) + } else { + fn inner( + py: Python, + dag: &DAGCircuit, + counts: &mut IndexMap, + ) -> PyResult<()> { + for (key, value) in dag.op_names.iter() { + counts + .entry(key.clone()) + .and_modify(|count| *count += value) + .or_insert(*value); + } + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node in dag.dag.node_weights() { + let NodeType::Operation(node) = node else { + continue; + }; + if !node.op.control_flow() { + continue; + } + let OperationRef::Instruction(inst) = node.op.view() else { + panic!("control flow op must be an instruction") + }; + let blocks = inst.instruction.bind(py).getattr("blocks")?; + for block in blocks.iter()? { + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; + inner(py, inner_dag, counts)?; + } + } + Ok(()) + } + let mut counts = + IndexMap::with_capacity_and_hasher(self.op_names.len(), RandomState::default()); + inner(py, self, &mut counts)?; + Ok(counts) + } + } + + /// Get an immutable reference to the op counts for this DAGCircuit + /// + /// This differs from count_ops() in that it doesn't handle control flow recursion at all + /// and it returns a reference instead of an owned copy. If you don't need to work with + /// control flow or ownership of the counts this is a more efficient alternative to + /// `DAGCircuit::count_ops(py, false)` + pub fn get_op_counts(&self) -> &IndexMap { + &self.op_names + } + + /// Extends the DAG with valid instances of [PackedInstruction] + pub fn extend(&mut self, py: Python, iter: I) -> PyResult> + where + I: IntoIterator, + { + // Create HashSets to keep track of each bit/var's last node + let mut qubit_last_nodes: HashMap = HashMap::default(); + 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); + + // Consume into iterator to obtain size hint + let iter = iter.into_iter(); + // Store new nodes to return + let mut new_nodes = Vec::with_capacity(iter.size_hint().1.unwrap_or_default()); + for instr in iter { + let op_name = instr.op.name(); + let (all_cbits, vars): (Vec, Option>) = { + if self.may_have_additional_wires(py, &instr) { + let mut clbits: HashSet = + HashSet::from_iter(self.cargs_interner.get(instr.clbits).iter().copied()); + let (additional_clbits, additional_vars) = + self.additional_wires(py, instr.op.view(), instr.condition())?; + for clbit in additional_clbits { + clbits.insert(clbit); + } + (clbits.into_iter().collect(), Some(additional_vars)) + } else { + (self.cargs_interner.get(instr.clbits).to_vec(), None) + } + }; + + // Increment the operation count + self.increment_op(op_name); + + // Get the correct qubit indices + let qubits_id = instr.qubits; + + // Insert op-node to graph. + let new_node = self.dag.add_node(NodeType::Operation(instr)); + new_nodes.push(new_node); + + // Check all the qubits in this instruction. + for qubit in self.qargs_interner.get(qubits_id) { + // Retrieve each qubit's last node + let qubit_last_node = *qubit_last_nodes.entry(*qubit).or_insert_with(|| { + // If the qubit is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.qubit_io_map[qubit.0 as usize][1]; + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }); + qubit_last_nodes + .entry(*qubit) + .and_modify(|val| *val = new_node); + self.dag + .add_edge(qubit_last_node, new_node, Wire::Qubit(*qubit)); + } + + // Check all the clbits in this instruction. + for clbit in all_cbits { + let clbit_last_node = *clbit_last_nodes.entry(clbit).or_insert_with(|| { + // If the qubit is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.clbit_io_map[clbit.0 as usize][1]; + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }); + clbit_last_nodes + .entry(clbit) + .and_modify(|val| *val = new_node); + self.dag + .add_edge(clbit_last_node, new_node, Wire::Clbit(clbit)); + } + + // If available, check all the vars in this instruction + for var in vars.iter().flatten() { + let var_last_node = if let Some(result) = vars_last_nodes.get_item(var)? { + let node: usize = result.extract()?; + vars_last_nodes.del_item(var)?; + NodeIndex::new(node) + } else { + // If the var is not in the last nodes collection, the edge between the output node and its predecessor. + // Then, store the predecessor's NodeIndex in the last nodes collection. + let output_node = self.var_output_map.get(py, var).unwrap(); + let (edge_id, predecessor_node) = self + .dag + .edges_directed(output_node, Incoming) + .next() + .map(|edge| (edge.id(), edge.source())) + .unwrap(); + self.dag.remove_edge(edge_id); + predecessor_node + }; + + // Because `DAGCircuit::additional_wires` can return repeated instances of vars, + // we need to make sure to skip those to avoid cycles. + vars_last_nodes.set_item(var, new_node.index())?; + if var_last_node == new_node { + continue; + } + self.dag + .add_edge(var_last_node, new_node, Wire::Var(var.clone_ref(py))); + } + } + + // Add the output_nodes back to qargs + for (qubit, node) in qubit_last_nodes { + let output_node = self.qubit_io_map[qubit.0 as usize][1]; + self.dag.add_edge(node, output_node, Wire::Qubit(qubit)); + } + + // Add the output_nodes back to cargs + for (clbit, node) in clbit_last_nodes { + let output_node = self.clbit_io_map[clbit.0 as usize][1]; + self.dag.add_edge(node, output_node, Wire::Clbit(clbit)); + } + + // Add the output_nodes back to vars + for item in vars_last_nodes.items() { + let (var, node): (PyObject, usize) = item.extract()?; + let output_node = self.var_output_map.get(py, &var).unwrap(); + self.dag + .add_edge(NodeIndex::new(node), output_node, Wire::Var(var)); + } + + Ok(new_nodes) + } + + /// Alternative constructor to build an instance of [DAGCircuit] from a `QuantumCircuit`. + pub(crate) fn from_circuit( + py: Python, + qc: QuantumCircuitData, + copy_op: bool, + qubit_order: Option>>, + clbit_order: Option>>, + ) -> PyResult { + // Extract necessary attributes + let qc_data = qc.data; + let num_qubits = qc_data.num_qubits(); + let num_clbits = qc_data.num_clbits(); + let num_ops = qc_data.__len__(); + let num_vars = qc.declared_vars.len() + qc.input_vars.len() + qc.captured_vars.len(); + + // Build DAGCircuit with capacity + let mut new_dag = DAGCircuit::with_capacity( + py, + num_qubits, + num_clbits, + Some(num_vars), + Some(num_ops), + None, + )?; + + // Assign other necessary data + new_dag.name = qc.name.map(|ob| ob.unbind()); + + // Avoid manually acquiring the GIL. + new_dag.global_phase = match qc_data.global_phase() { + Param::ParameterExpression(exp) => Param::ParameterExpression(exp.clone_ref(py)), + Param::Float(float) => Param::Float(*float), + _ => unreachable!("Incorrect parameter assigned for global phase"), + }; + + if let Some(calibrations) = qc.calibrations { + new_dag.calibrations = calibrations; + } + + new_dag.metadata = qc.metadata.map(|meta| meta.unbind()); + + // Add the qubits depending on order. + let qubit_map: Option> = if let Some(qubit_ordering) = qubit_order { + let mut ordered_vec = Vec::from_iter((0..num_qubits as u32).map(Qubit)); + qubit_ordering + .into_iter() + .try_for_each(|qubit| -> PyResult<()> { + if new_dag.qubits.find(&qubit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate qubits {}", + &qubit + ))); + } + let qubit_index = qc_data.qubits().find(&qubit).unwrap(); + ordered_vec[qubit_index.0 as usize] = + new_dag.add_qubit_unchecked(py, &qubit)?; + Ok(()) + })?; + Some(ordered_vec) + } else { + qc_data + .qubits() + .bits() + .iter() + .try_for_each(|qubit| -> PyResult<_> { + new_dag.add_qubit_unchecked(py, qubit.bind(py))?; + Ok(()) + })?; + None + }; + + // Add the clbits depending on order. + let clbit_map: Option> = if let Some(clbit_ordering) = clbit_order { + let mut ordered_vec = Vec::from_iter((0..num_clbits as u32).map(Clbit)); + clbit_ordering + .into_iter() + .try_for_each(|clbit| -> PyResult<()> { + if new_dag.clbits.find(&clbit).is_some() { + return Err(DAGCircuitError::new_err(format!( + "duplicate clbits {}", + &clbit + ))); + }; + let clbit_index = qc_data.clbits().find(&clbit).unwrap(); + ordered_vec[clbit_index.0 as usize] = + new_dag.add_clbit_unchecked(py, &clbit)?; + Ok(()) + })?; + Some(ordered_vec) + } else { + qc_data + .clbits() + .bits() + .iter() + .try_for_each(|clbit| -> PyResult<()> { + new_dag.add_clbit_unchecked(py, clbit.bind(py))?; + Ok(()) + })?; + None + }; + + // Add all of the new vars. + for var in &qc.declared_vars { + new_dag.add_var(py, var, DAGVarType::Declare)?; + } + + for var in &qc.input_vars { + new_dag.add_var(py, var, DAGVarType::Input)?; + } + + for var in &qc.captured_vars { + new_dag.add_var(py, var, DAGVarType::Capture)?; + } + + // Add all the registers + if let Some(qregs) = qc.qregs { + for qreg in qregs.iter() { + new_dag.add_qreg(py, &qreg)?; + } + } + + if let Some(cregs) = qc.cregs { + for creg in cregs.iter() { + new_dag.add_creg(py, &creg)?; + } + } + + // Pre-process and re-intern all indices again. + let instructions: Vec = qc_data + .iter() + .map(|instr| -> PyResult { + // Re-map the qubits + let new_qargs = if let Some(qubit_mapping) = &qubit_map { + let qargs = qc_data + .get_qargs(instr.qubits) + .iter() + .map(|bit| qubit_mapping[bit.0 as usize]) + .collect(); + new_dag.qargs_interner.insert_owned(qargs) + } else { + new_dag + .qargs_interner + .insert(qc_data.get_qargs(instr.qubits)) + }; + // Remap the clbits + let new_cargs = if let Some(clbit_mapping) = &clbit_map { + let qargs = qc_data + .get_cargs(instr.clbits) + .iter() + .map(|bit| clbit_mapping[bit.0 as usize]) + .collect(); + new_dag.cargs_interner.insert_owned(qargs) + } else { + new_dag + .cargs_interner + .insert(qc_data.get_cargs(instr.clbits)) + }; + // Copy the operations + + Ok(PackedInstruction { + op: if copy_op { + instr.op.py_deepcopy(py, None)? + } else { + instr.op.clone() + }, + qubits: new_qargs, + clbits: new_cargs, + params: instr.params.clone(), + extra_attrs: instr.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + }) + }) + .collect::>>()?; + + // Finally add all the instructions back + new_dag.extend(py, instructions)?; + + Ok(new_dag) + } + + /// Builds a [DAGCircuit] based on an instance of [CircuitData]. + pub fn from_circuit_data( + py: Python, + circuit_data: CircuitData, + copy_op: bool, + ) -> PyResult { + let circ = QuantumCircuitData { + data: circuit_data, + name: None, + calibrations: None, + metadata: None, + qregs: None, + cregs: None, + input_vars: Vec::new(), + captured_vars: Vec::new(), + declared_vars: Vec::new(), + }; + Self::from_circuit(py, circ, copy_op, None, None) + } +} + +/// Add to global phase. Global phase can only be Float or ParameterExpression so this +/// does not handle the full possibility of parameter values. +fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { + Ok(match [phase, other] { + [Param::Float(a), Param::Float(b)] => Param::Float(a + b), + [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( + b.clone_ref(py) + .call_method1(py, intern!(py, "__radd__"), (*a,))?, + ), + [Param::ParameterExpression(a), Param::Float(b)] => Param::ParameterExpression( + a.clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (*b,))?, + ), + [Param::ParameterExpression(a), Param::ParameterExpression(b)] => { + Param::ParameterExpression(a.clone_ref(py).call_method1( + py, + intern!(py, "__add__"), + (b,), + )?) + } + _ => panic!("Invalid global phase"), + }) +} + +type SortKeyType<'a> = (&'a [Qubit], &'a [Clbit]); diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 9af82b74fa5a..4d82cf31e813 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -12,47 +12,89 @@ #[cfg(feature = "cache_pygates")] use std::cell::OnceCell; +use std::hash::Hasher; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::QUANTUM_CIRCUIT; -use crate::operations::Operation; +use crate::operations::{Operation, Param}; +use crate::TupleLikeArg; -use numpy::IntoPyArray; +use ahash::AHasher; +use approx::relative_eq; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use numpy::IntoPyArray; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; +use pyo3::types::{PyString, PyTuple}; use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] #[derive(Clone, Debug)] pub struct DAGNode { - #[pyo3(get, set)] - pub _node_id: isize, + pub node: Option, +} + +impl DAGNode { + #[inline] + pub fn py_nid(&self) -> isize { + self.node + .map(|node| node.index().try_into().unwrap()) + .unwrap_or(-1) + } } #[pymethods] impl DAGNode { #[new] #[pyo3(signature=(nid=-1))] - fn new(nid: isize) -> Self { - DAGNode { _node_id: nid } + fn py_new(nid: isize) -> PyResult { + Ok(DAGNode { + node: match nid { + -1 => None, + nid => { + let index: usize = match nid.try_into() { + Ok(index) => index, + Err(_) => { + return Err(PyValueError::new_err( + "Invalid node index, must be -1 or a non-negative integer", + )) + } + }; + Some(NodeIndex::new(index)) + } + }, + }) } - fn __getstate__(&self) -> isize { - self._node_id + #[getter(_node_id)] + fn get_py_node_id(&self) -> isize { + self.py_nid() } - fn __setstate__(&mut self, nid: isize) { - self._node_id = nid; + #[setter(_node_id)] + fn set_py_node_id(&mut self, nid: isize) { + self.node = match nid { + -1 => None, + nid => Some(NodeIndex::new(nid.try_into().unwrap())), + } + } + + fn __getstate__(&self) -> Option { + self.node.map(|node| node.index()) + } + + fn __setstate__(&mut self, index: Option) { + self.node = index.map(NodeIndex::new); } fn __lt__(&self, other: &DAGNode) -> bool { - self._node_id < other._node_id + self.py_nid() < other.py_nid() } fn __gt__(&self, other: &DAGNode) -> bool { - self._node_id > other._node_id + self.py_nid() > other.py_nid() } fn __str__(_self: &Bound) -> String { @@ -60,7 +102,7 @@ impl DAGNode { } fn __hash__(&self, py: Python) -> PyResult { - self._node_id.into_py(py).bind(py).hash() + self.py_nid().into_py(py).bind(py).hash() } } @@ -76,96 +118,124 @@ pub struct DAGOpNode { impl DAGOpNode { #[new] #[pyo3(signature = (op, qargs=None, cargs=None, *, dag=None))] - fn new( + pub fn py_new( py: Python, - op: &Bound, - qargs: Option<&Bound>, - cargs: Option<&Bound>, - dag: Option<&Bound>, - ) -> PyResult<(Self, DAGNode)> { - let qargs = - qargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - let cargs = - cargs.map_or_else(|| Ok(PyTuple::empty_bound(py)), PySequenceMethods::to_tuple)?; - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() + op: Bound, + qargs: Option, + cargs: Option, + #[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 instruction = CircuitInstruction { + operation: py_op.operation, + qubits: qargs.unbind(), + clbits: cargs.unbind(), + params: py_op.params, + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }; + + Py::new( + py, + ( + DAGOpNode { + instruction, + sort_key, + }, + DAGNode { node: None }, + ), + ) + } + + fn __hash__(slf: PyRef<'_, Self>) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write(slf.instruction.operation.name().as_bytes()); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + // This check is more restrictive by design as it's intended to replace + // object identitity for set/dict membership and not be a semantic equivalence + // check. We have an implementation of that as part of `DAGCircuit.__eq__` and + // this method is specifically to ensure nodes are the same. This means things + // like parameter equality are stricter to reject things like + // Param::Float(0.1) == Param::ParameterExpression(0.1) (if the expression was + // a python parameter equivalent to a bound value). + let Ok(other) = other.downcast::() else { + return Ok(false); + }; + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + + if super_.py_nid() != other_super.py_nid() { + return Ok(false); + } + if !slf + .instruction + .operation + .py_eq(py, &borrowed_other.instruction.operation)? + { + return Ok(false); + } + let params_eq = if slf.instruction.operation.try_standard_gate().is_some() { + let mut params_eq = true; + for (a, b) in slf + .instruction + .params + .iter() + .zip(borrowed_other.instruction.params.iter()) + { + let res = match [a, b] { + [Param::Float(float_a), Param::Float(float_b)] => { + relative_eq!(float_a, float_b, max_relative = 1e-10) + } + [Param::ParameterExpression(param_a), Param::ParameterExpression(param_b)] => { + param_a.bind(py).eq(param_b)? } + [Param::Obj(param_a), Param::Obj(param_b)] => param_a.bind(py).eq(param_b)?, + _ => false, + }; + if !res { + params_eq = false; + break; } } - None => qargs.str()?.into_any(), + params_eq + } else { + // We've already evaluated the parameters are equal here via the Python space equality + // check so if we're not comparing standard gates and we've reached this point we know + // the parameters are already equal. + true }; - Ok(( - DAGOpNode { - instruction: CircuitInstruction::py_new( - op, - Some(qargs.into_any()), - Some(cargs.into_any()), - )?, - sort_key: sort_key.unbind(), - }, - DAGNode { _node_id: -1 }, - )) + + Ok(params_eq + && slf + .instruction + .qubits + .bind(py) + .eq(borrowed_other.instruction.qubits.clone_ref(py))? + && slf + .instruction + .clbits + .bind(py) + .eq(borrowed_other.instruction.clbits.clone_ref(py))?) } - #[pyo3(signature = (instruction, /, *, dag=None, deepcopy=false))] + #[pyo3(signature = (instruction, /, *, deepcopy=false))] #[staticmethod] fn from_instruction( py: Python, mut instruction: CircuitInstruction, - dag: Option<&Bound>, deepcopy: bool, ) -> PyResult { - let qargs = instruction.qubits.bind(py); - let cargs = instruction.clbits.bind(py); - - let sort_key = match dag { - Some(dag) => { - let cache = dag - .getattr(intern!(py, "_key_cache"))? - .downcast_into_exact::()?; - let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); - match cache.get_item(&cache_key)? { - Some(key) => key, - None => { - let indices: PyResult> = qargs - .iter() - .chain(cargs.iter()) - .map(|bit| { - dag.call_method1(intern!(py, "find_bit"), (bit,))? - .getattr(intern!(py, "index")) - }) - .collect(); - let index_strs: Vec<_> = - indices?.into_iter().map(|i| format!("{:04}", i)).collect(); - let key = PyString::new_bound(py, index_strs.join(",").as_str()); - cache.set_item(&cache_key, &key)?; - key.into_any() - } - } - } - None => qargs.str()?.into_any(), - }; + let sort_key = instruction.qubits.bind(py).str().unwrap().into(); if deepcopy { instruction.operation = instruction.operation.py_deepcopy(py, None)?; #[cfg(feature = "cache_pygates")] @@ -173,17 +243,16 @@ impl DAGOpNode { instruction.py_op = OnceCell::new(); } } - let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); + let base = PyClassInitializer::from(DAGNode { node: None }); let sub = base.add_subclass(DAGOpNode { instruction, - sort_key: sort_key.unbind(), + sort_key, }); Ok(Py::new(py, sub)?.to_object(py)) } - fn __reduce__(slf: PyRef) -> PyResult { - let py = slf.py(); - let state = (slf.as_ref()._node_id, &slf.sort_key); + 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::(), ( @@ -197,8 +266,8 @@ impl DAGOpNode { } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } @@ -247,16 +316,16 @@ impl DAGOpNode { #[getter] fn num_qubits(&self) -> u32 { - self.instruction.op().num_qubits() + self.instruction.operation.num_qubits() } #[getter] fn num_clbits(&self) -> u32 { - self.instruction.op().num_clbits() + self.instruction.operation.num_clbits() } #[getter] - fn get_qargs(&self, py: Python) -> Py { + pub fn get_qargs(&self, py: Python) -> Py { self.instruction.qubits.clone_ref(py) } @@ -266,7 +335,7 @@ impl DAGOpNode { } #[getter] - fn get_cargs(&self, py: Python) -> Py { + pub fn get_cargs(&self, py: Python) -> Py { self.instruction.clbits.clone_ref(py) } @@ -278,7 +347,7 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> Py { - self.instruction.op().name().into_py(py) + self.instruction.operation.name().into_py(py) } #[getter] @@ -293,40 +362,34 @@ impl DAGOpNode { #[getter] fn matrix(&self, py: Python) -> Option { - let matrix = self.instruction.op().matrix(&self.instruction.params); + let matrix = self.instruction.operation.matrix(&self.instruction.params); matrix.map(|mat| mat.into_pyarray_bound(py).into()) } #[getter] fn label(&self) -> Option<&str> { - self.instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.label.as_deref()) + self.instruction.extra_attrs.label() } #[getter] fn condition(&self, py: Python) -> Option { self.instruction .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + .condition() + .map(|x| x.clone_ref(py)) } #[getter] fn duration(&self, py: Python) -> Option { self.instruction .extra_attrs - .as_ref() - .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + .duration() + .map(|x| x.clone_ref(py)) } #[getter] fn unit(&self) -> Option<&str> { - self.instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.unit.as_deref()) + self.instruction.extra_attrs.unit() } /// Is the :class:`.Operation` contained in this node a Qiskit standard gate? @@ -357,36 +420,13 @@ impl DAGOpNode { #[setter] fn set_label(&mut self, val: Option) { - match self.instruction.extra_attrs.as_mut() { - Some(attrs) => attrs.label = val, - None => { - if val.is_some() { - self.instruction.extra_attrs = Some(Box::new( - crate::circuit_instruction::ExtraInstructionAttributes { - label: val, - duration: None, - unit: None, - condition: None, - }, - )) - } - } - }; - if let Some(attrs) = &self.instruction.extra_attrs { - if attrs.label.is_none() - && attrs.duration.is_none() - && attrs.unit.is_none() - && attrs.condition.is_none() - { - self.instruction.extra_attrs = None; - } - } + self.instruction.extra_attrs.set_label(val); } #[getter] fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { self.instruction - .op() + .operation .definition(&self.instruction.params) .map(|data| { QUANTUM_CIRCUIT @@ -420,36 +460,69 @@ impl DAGOpNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGInNode { #[pyo3(get)] - wire: PyObject, + 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) }, + ) + } +} + #[pymethods] impl DAGInNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGInNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } + /// Returns a representation of the DAGInNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGInNode(wire={})", self.wire.bind(py).repr()?)) @@ -460,38 +533,71 @@ impl DAGInNode { #[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] pub struct DAGOutNode { #[pyo3(get)] - wire: PyObject, + 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) }, + ) + } +} + #[pymethods] impl DAGOutNode { #[new] - fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + fn py_new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { Ok(( DAGOutNode { wire, - sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + sort_key: intern!(py, "[]").clone().into(), }, - DAGNode { _node_id: -1 }, + DAGNode { node: None }, )) } fn __reduce__(slf: PyRef, py: Python) -> PyObject { - let state = (slf.as_ref()._node_id, &slf.sort_key); + let state = (slf.as_ref().node.map(|node| node.index()), &slf.sort_key); (py.get_type_bound::(), (&slf.wire,), state).into_py(py) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { - let (nid, sort_key): (isize, PyObject) = state.extract()?; - slf.as_mut()._node_id = nid; + let (index, sort_key): (Option, PyObject) = state.extract()?; + slf.as_mut().node = index.map(NodeIndex::new); slf.sort_key = sort_key; Ok(()) } + fn __hash__(slf: PyRef<'_, Self>, py: Python) -> PyResult { + let super_ = slf.as_ref(); + let mut hasher = AHasher::default(); + hasher.write_isize(super_.py_nid()); + hasher.write_isize(slf.wire.bind(py).hash()?); + Ok(hasher.finish()) + } + /// Returns a representation of the DAGOutNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!("DAGOutNode(wire={})", self.wire.bind(py).repr()?)) } + + fn __eq__(slf: PyRef, py: Python, other: &Bound) -> PyResult { + match other.downcast::() { + Ok(other) => { + let borrowed_other = other.borrow(); + let other_super = borrowed_other.as_ref(); + let super_ = slf.as_ref(); + Ok(super_.py_nid() == other_super.py_nid() + && slf.wire.bind(py).eq(borrowed_other.wire.clone_ref(py))?) + } + Err(_) => Ok(false), + } + } } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs new file mode 100644 index 000000000000..8558535788a0 --- /dev/null +++ b/crates/circuit/src/dot_utils.rs @@ -0,0 +1,109 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// 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. + +// This module is forked from rustworkx at: +// https://github.com/Qiskit/rustworkx/blob/c4256daf96fc3c08c392450ed33bc0987cdb15ff/src/dot_utils.rs +// and has been modified to generate a dot file from a Rust DAGCircuit instead +// of a rustworkx PyGraph object + +use std::collections::BTreeMap; +use std::io::prelude::*; + +use crate::dag_circuit::{DAGCircuit, Wire}; +use pyo3::prelude::*; +use rustworkx_core::petgraph::visit::{ + EdgeRef, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef, +}; + +static TYPE: [&str; 2] = ["graph", "digraph"]; +static EDGE: [&str; 2] = ["--", "->"]; + +pub fn build_dot( + py: Python, + dag: &DAGCircuit, + file: &mut T, + graph_attrs: Option>, + node_attrs: Option, + edge_attrs: Option, +) -> PyResult<()> +where + T: Write, +{ + let graph = dag.dag(); + writeln!(file, "{} {{", TYPE[graph.is_directed() as usize])?; + if let Some(graph_attr_map) = graph_attrs { + for (key, value) in graph_attr_map.iter() { + writeln!(file, "{}={} ;", key, value)?; + } + } + + for node in graph.node_references() { + let node_weight = dag.get_node(py, node.id())?; + writeln!( + file, + "{} {};", + graph.to_index(node.id()), + attr_map_to_string(py, node_attrs.as_ref(), node_weight)? + )?; + } + for edge in graph.edge_references() { + let edge_weight = match edge.weight() { + Wire::Qubit(qubit) => dag.qubits().get(*qubit).unwrap(), + Wire::Clbit(clbit) => dag.clbits().get(*clbit).unwrap(), + Wire::Var(var) => var, + }; + writeln!( + file, + "{} {} {} {};", + graph.to_index(edge.source()), + EDGE[graph.is_directed() as usize], + graph.to_index(edge.target()), + attr_map_to_string(py, edge_attrs.as_ref(), edge_weight)? + )?; + } + writeln!(file, "}}")?; + Ok(()) +} + +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>, + weight: T, +) -> PyResult { + if attrs.is_none() { + return Ok("".to_string()); + } + let attr_callable = |node: T| -> PyResult> { + let res = attrs.unwrap().call1(py, (node.to_object(py),))?; + res.extract(py) + }; + + let attrs = attr_callable(weight)?; + if attrs.is_empty() { + return Ok("".to_string()); + } + let attr_string = attrs + .iter() + .map(|(key, value)| { + if ATTRS_TO_ESCAPE.contains(&key.as_str()) { + format!("{}=\"{}\"", key, value) + } else { + format!("{}={}", key, value) + } + }) + .collect::>() + .join(", "); + Ok(format!("[{}]", attr_string)) +} diff --git a/crates/circuit/src/error.rs b/crates/circuit/src/error.rs new file mode 100644 index 000000000000..b28e6b8f513b --- /dev/null +++ b/crates/circuit/src/error.rs @@ -0,0 +1,16 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::import_exception; + +import_exception!(qiskit.dagcircuit.exceptions, DAGCircuitError); +import_exception!(qiskit.dagcircuit.exceptions, DAGDependencyError); diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index a38783ef7e7f..6b04b8512fb0 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -387,12 +387,12 @@ pub fn cu_gate(theta: f64, phi: f64, lam: f64, gamma: f64) -> GateArray2Q { C_ZERO, c64(0., gamma).exp() * cos_theta, C_ZERO, - c64(0., gamma + phi).exp() * (-1.) * sin_theta, + c64(0., gamma + lam).exp() * (-1.) * sin_theta, ], [C_ZERO, C_ZERO, C_ONE, C_ZERO], [ C_ZERO, - c64(0., gamma + lam).exp() * sin_theta, + c64(0., gamma + phi).exp() * sin_theta, C_ZERO, c64(0., gamma + phi + lam).exp() * cos_theta, ], diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index d9d439bb4745..5c77f8ef7d17 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -57,6 +57,7 @@ impl ImportOnceCell { } pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static BUILTIN_SET: ImportOnceCell = ImportOnceCell::new("builtins", "set"); pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); pub static INSTRUCTION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); @@ -65,6 +66,10 @@ pub static CONTROL_FLOW_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp"); pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static QUANTUM_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumregister", "QuantumRegister"); +pub static CLASSICAL_REGISTER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister"); pub static PARAMETER_EXPRESSION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); pub static QUANTUM_CIRCUIT: ImportOnceCell = @@ -73,6 +78,17 @@ pub static SINGLETON_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static VARIABLE_MAPPER: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit._classical_resource_map", "VariableMapper"); +pub static IF_ELSE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "IfElseOp"); +pub static FOR_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ForLoopOp"); +pub static SWITCH_CASE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "SwitchCaseOp"); +pub static WHILE_LOOP_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "WhileLoopOp"); +pub static STORE_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Store"); +pub static EXPR: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classical.expr", "Expr"); +pub static ITER_VARS: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.classical.expr", "iter_vars"); +pub static DAG_NODE: ImportOnceCell = ImportOnceCell::new("qiskit.dagcircuit", "DAGNode"); pub static CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "ControlledGate"); pub static ANNOTATED_OPERATION: ImportOnceCell = @@ -80,7 +96,24 @@ pub static ANNOTATED_OPERATION: ImportOnceCell = pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static QI_OPERATOR: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Operator"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); +pub static CIRCUIT_TO_DAG: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "circuit_to_dag"); +pub static DAG_TO_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.converters", "dag_to_circuit"); +pub static LEGACY_CONDITION_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_legacy_condition_eq"); +pub static CONDITION_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_condition_op_eq"); +pub static SWITCH_CASE_OP_CHECK: ImportOnceCell = + ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_switch_case_eq"); +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 UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( + "qiskit.circuit.library.generalized_gates.unitary", + "UnitaryGate", +); /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index f22bb80ae052..a72efb037afb 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -10,124 +10,208 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use std::borrow::Borrow; +use std::fmt; use std::hash::Hash; -use std::sync::Arc; - -use hashbrown::HashMap; -use pyo3::exceptions::PyRuntimeError; -use pyo3::prelude::*; - -#[derive(Clone, Copy, Debug)] -pub struct Index(u32); - -pub enum InternerKey { - Index(Index), - Value(T), +use std::marker::PhantomData; + +use indexmap::IndexSet; + +/// A key to retrieve a value (by reference) from an interner of the same type. This is narrower +/// than a true reference, at the cost that it is explicitly not lifetime bound to the interner it +/// came from; it is up to the user to ensure that they never attempt to query an interner with a +/// key from a different interner. +#[derive(Debug, Eq, PartialEq)] +pub struct Interned { + index: u32, + // Storing the type of the interned value adds a small amount more type safety to the interner + // keys when there's several interners in play close to each other. We use `*const T` because + // the `Interned value` is like a non-lifetime-bound reference to data stored in the interner; + // `Interned` doesn't own the data (which would be implied by `T`), and it's not using the + // static lifetime system (which would be implied by `&'_ T`, and require us to propagate the + // lifetime bound). + _type: PhantomData<*const T>, } - -impl From for InternerKey { - fn from(value: Index) -> Self { - InternerKey::Index(value) +// The `PhantomData` marker prevents various useful things from being derived (for `Clone` and +// `Copy` it's an awkward effect of the derivation system), so we have manual implementations. +impl Clone for Interned { + fn clone(&self) -> Self { + *self } } - -pub struct InternerValue<'a, T> { - pub index: Index, - pub value: &'a T, -} - -impl IntoPy for Index { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) +impl Copy for Interned {} +unsafe impl Send for Interned {} +unsafe impl Sync for Interned {} + +/// An append-only data structure for interning generic Rust types. +/// +/// The interner can lookup keys using a reference type, and will create the corresponding owned +/// allocation on demand, if a matching entry is not already stored. It returns manual keys into +/// itself (the `Interned` type), rather than raw references; the `Interned` type is narrower than a +/// true reference. +/// +/// This is only implemented for owned types that implement `Default`, so that the convenience +/// method `Interner::get_default` can work reliably and correctly; the "default" index needs to be +/// guaranteed to be reserved and present for safety. +/// +/// # Examples +/// +/// ```rust +/// let mut interner = Interner::<[usize]>::new(); +/// +/// // These are of type `Interned<[usize]>`. +/// let default_empty = interner.get_default(); +/// let empty = interner.insert(&[]); +/// let other_empty = interner.insert(&[]); +/// let key = interner.insert(&[0, 1, 2, 3, 4]); +/// +/// assert_eq!(empty, other_empty); +/// assert_eq!(empty, default_empty); +/// assert_ne!(empty, key); +/// +/// assert_eq!(interner.get(empty), &[]); +/// assert_eq!(interner.get(key), &[0, 1, 2, 3, 4]); +/// ``` +#[derive(Default)] +pub struct Interner(IndexSet<::Owned, ::ahash::RandomState>); + +// `Clone` and `Debug` can't use the derivation mechanism because the values that are actually +// stored are of type `::Owned`, which the derive system doesn't reason about. +impl Clone for Interner +where + T: ?Sized + ToOwned, + ::Owned: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) } } - -pub struct CacheFullError; - -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") +impl fmt::Debug for Interner +where + T: ?Sized + ToOwned, + ::Owned: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_tuple("Interner").field(&self.0).finish() } } -/// An append-only data structure for interning generic -/// Rust types. -#[derive(Clone, Debug)] -pub struct IndexedInterner { - entries: Vec>, - index_lookup: HashMap, Index>, -} - -pub trait Interner { - type Key; - type Output; - - /// Takes ownership of the provided key and returns the interned - /// type. - fn intern(self, value: Self::Key) -> Self::Output; -} - -impl<'a, T> Interner for &'a IndexedInterner { - type Key = Index; - type Output = InternerValue<'a, T>; +impl Interner +where + T: ?Sized + ToOwned, + ::Owned: Hash + Eq + Default, +{ + /// Construct a new interner. The stored type must have a default value, in order for + /// `Interner::get_default` to reliably work correctly without a hash lookup (though ideally + /// we'd just use specialisation to do that). + pub fn new() -> Self { + Self::with_capacity(1) + } - fn intern(self, index: Index) -> Self::Output { - let value = self.entries.get(index.0 as usize).unwrap(); - InternerValue { - index, - value: value.as_ref(), + /// Retrieve the key corresponding to the default store, without any hash or equality lookup. + /// For example, if the interned type is `[Clbit]`, the default key corresponds to the empty + /// slice `&[]`. This is a common operation with the cargs interner, for things like pushing + /// gates. + /// + /// In an ideal world, we wouldn't have the `Default` trait bound on `new`, but would use + /// specialisation to insert the default key only if the stored value implemented `Default` + /// (we'd still trait-bound this method). + #[inline(always)] + pub fn get_default(&self) -> Interned { + Interned { + index: 0, + _type: PhantomData, } } + + /// Create an interner with enough space to hold `capacity` entries. + /// + /// Note that the default item of the interner is always allocated and given a key immediately, + /// which will use one slot of the capacity. + pub fn with_capacity(capacity: usize) -> Self { + let mut set = IndexSet::with_capacity_and_hasher(capacity, ::ahash::RandomState::new()); + set.insert(Default::default()); + Self(set) + } } -impl<'a, T> Interner for &'a mut IndexedInterner +impl Interner where - T: Eq + Hash, + T: ?Sized + ToOwned, + ::Owned: Hash + Eq, { - type Key = InternerKey; - type Output = Result, CacheFullError>; + /// Retrieve a reference to the stored value for this key. + pub fn get(&self, index: Interned) -> &T { + self.0 + .get_index(index.index as usize) + .expect( + "the caller is responsible for only using interner keys from the correct interner", + ) + .borrow() + } - fn intern(self, key: Self::Key) -> Self::Output { - match key { - InternerKey::Index(index) => { - let value = self.entries.get(index.0 as usize).unwrap(); - Ok(InternerValue { - index, - value: value.as_ref(), - }) - } - InternerKey::Value(value) => { - if let Some(index) = self.index_lookup.get(&value).copied() { - Ok(InternerValue { - index, - value: self.entries.get(index.0 as usize).unwrap(), - }) - } else { - let args = Arc::new(value); - let index: Index = - Index(self.entries.len().try_into().map_err(|_| CacheFullError)?); - self.entries.push(args.clone()); - Ok(InternerValue { - index, - value: self.index_lookup.insert_unique_unchecked(args, index).0, - }) - } - } + /// Internal worker function that inserts an owned value assuming that the value didn't + /// previously exist in the map. + fn insert_new(&mut self, value: ::Owned) -> u32 { + let index = self.0.len(); + if index == u32::MAX as usize { + panic!("interner is out of space"); } + let _inserted = self.0.insert(value); + debug_assert!(_inserted); + index as u32 } -} -impl IndexedInterner { - pub fn new() -> Self { - IndexedInterner { - entries: Vec::new(), - index_lookup: HashMap::new(), + /// Get an interner key corresponding to the given referenced type. If not already stored, this + /// function will allocate a new owned value to use as the storage. + /// + /// If you already have an owned value, use `insert_owned`, but in general this function will be + /// more efficient *unless* you already had the value for other reasons. + pub fn insert(&mut self, value: &T) -> Interned + where + T: Hash + Eq, + { + let index = match self.0.get_index_of(value) { + Some(index) => index as u32, + None => self.insert_new(value.to_owned()), + }; + Interned { + index, + _type: PhantomData, + } + } + + /// Get an interner key corresponding to the given owned type. If not already stored, the value + /// will be used as the key, otherwise it will be dropped. + /// + /// If you don't already have the owned value, use `insert`; this will only allocate if the + /// lookup fails. + pub fn insert_owned(&mut self, value: ::Owned) -> Interned { + let index = match self.0.get_index_of(&value) { + Some(index) => index as u32, + None => self.insert_new(value), + }; + Interned { + index, + _type: PhantomData, } } } -impl Default for IndexedInterner { - fn default() -> Self { - Self::new() +#[cfg(test)] +mod test { + use super::*; + + #[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()), &[]); + assert_eq!(interner.insert_owned(Vec::new()), interner.get_default()); + assert_eq!(interner.insert(&[]), interner.get_default()); + + let capacity = Interner::::with_capacity(4); + assert_eq!(capacity.get_default(), capacity.get_default()); + assert_eq!(capacity.get(capacity.get_default()), ""); } } diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 739bf998a611..dcff558ade64 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,25 +13,50 @@ pub mod bit_data; pub mod circuit_data; pub mod circuit_instruction; +pub mod converters; +pub mod dag_circuit; pub mod dag_node; +mod dot_utils; +mod error; pub mod gate_matrix; pub mod imports; +mod interner; pub mod operations; pub mod packed_instruction; pub mod parameter_table; pub mod slice; pub mod util; -mod interner; +mod rustworkx_core_vnext; use pyo3::prelude::*; +use pyo3::types::{PySequence, PyTuple}; pub type BitType = u32; -#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq, FromPyObject)] pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct Clbit(pub BitType); +pub struct TupleLikeArg<'py> { + value: Bound<'py, PyTuple>, +} + +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( + ob.py(), + ob.iter()? + .map(|o| Ok(o?.unbind())) + .collect::>>()?, + ), + }; + Ok(TupleLikeArg { value }) + } +} + impl From for Qubit { fn from(value: BitType) -> Self { Qubit(value) @@ -58,11 +83,12 @@ impl From for BitType { pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 7134662e1ac6..ad76e9d44008 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use approx::relative_eq; use std::f64::consts::PI; use crate::circuit_data::CircuitData; @@ -35,6 +36,29 @@ pub enum Param { Obj(PyObject), } +impl Param { + pub fn eq(&self, py: Python, other: &Param) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(a == b), + [Self::Float(a), Self::ParameterExpression(b)] => b.bind(py).eq(a), + [Self::ParameterExpression(a), Self::Float(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(_), Self::Float(_)] => Ok(false), + [Self::Float(_), Self::Obj(_)] => Ok(false), + [Self::Obj(a), Self::ParameterExpression(b)] => a.bind(py).eq(b), + [Self::Obj(a), Self::Obj(b)] => a.bind(py).eq(b), + [Self::ParameterExpression(a), Self::Obj(b)] => a.bind(py).eq(b), + } + } + + pub fn is_close(&self, py: Python, other: &Param, max_relative: f64) -> PyResult { + match [self, other] { + [Self::Float(a), Self::Float(b)] => Ok(relative_eq!(a, b, max_relative = max_relative)), + _ => self.eq(py, other), + } + } +} + impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { Ok(if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? { @@ -101,6 +125,24 @@ impl Param { Param::Obj(ob.clone().unbind()) }) } + + /// Clones the [Param] object safely by reference count or copying. + pub fn clone_ref(&self, py: Python) -> Self { + match self { + Param::ParameterExpression(exp) => Param::ParameterExpression(exp.clone_ref(py)), + Param::Float(float) => Param::Float(*float), + Param::Obj(obj) => Param::Obj(obj.clone_ref(py)), + } + } +} + +// This impl allows for shared usage between [Param] and &[Param]. +// Such blanked impl doesn't exist inherently due to Rust's type system limitations. +// See https://doc.rust-lang.org/std/convert/trait.AsRef.html#reflexivity for more information. +impl AsRef for Param { + fn as_ref(&self) -> &Param { + self + } } /// Struct to provide iteration over Python-space `Parameter` instances within a `Param`. @@ -131,6 +173,7 @@ pub trait Operation { /// `PackedInstruction::op`, and in turn is a view object onto a `PackedOperation`. /// /// This is the main way that we interact immutably with general circuit operations from Rust space. +#[derive(Debug)] pub enum OperationRef<'a> { Standard(StandardGate), Gate(&'a PyGate), @@ -382,22 +425,28 @@ impl StandardGate { &self, py: Python, params: Option<&[Param]>, - extra_attrs: Option<&ExtraInstructionAttributes>, + extra_attrs: &ExtraInstructionAttributes, ) -> 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), }; - if let Some(extra) = extra_attrs { + let (label, unit, duration, condition) = ( + extra_attrs.label(), + extra_attrs.unit(), + extra_attrs.duration(), + extra_attrs.condition(), + ); + if label.is_some() || unit.is_some() || duration.is_some() || condition.is_some() { let kwargs = [ - ("label", extra.label.to_object(py)), - ("unit", extra.unit.to_object(py)), - ("duration", extra.duration.to_object(py)), + ("label", label.to_object(py)), + ("unit", extra_attrs.py_unit(py).into_any()), + ("duration", duration.to_object(py)), ] .into_py_dict_bound(py); let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; - if let Some(ref condition) = extra.condition { + if let Some(condition) = condition { out = out.call_method0(py, "to_mutable")?; out.setattr(py, "condition", condition)?; } @@ -2008,7 +2057,8 @@ fn clone_param(param: &Param, py: Python) -> Param { } } -fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { +/// Multiply a ``Param`` with a float. +pub fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { match param { Param::Float(theta) => Param::Float(theta * mult), Param::ParameterExpression(theta) => Param::ParameterExpression( @@ -2021,7 +2071,24 @@ fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { } } -fn add_param(param: &Param, summand: f64, py: Python) -> Param { +/// Multiply two ``Param``s. +pub fn multiply_params(param1: Param, param2: Param, py: Python) -> Param { + match (¶m1, ¶m2) { + (Param::Float(theta), Param::Float(lambda)) => Param::Float(theta * lambda), + (param, Param::Float(theta)) => multiply_param(param, *theta, py), + (Param::Float(theta), param) => multiply_param(param, *theta, py), + (Param::ParameterExpression(p1), Param::ParameterExpression(p2)) => { + Param::ParameterExpression( + p1.clone_ref(py) + .call_method1(py, intern!(py, "__rmul__"), (p2,)) + .expect("Parameter expression multiplication failed"), + ) + } + _ => unreachable!("Unsupported multiplication."), + } +} + +pub fn add_param(param: &Param, summand: f64, py: Python) -> Param { match param { Param::Float(theta) => Param::Float(*theta + summand), Param::ParameterExpression(theta) => Param::ParameterExpression( @@ -2182,10 +2249,7 @@ impl Operation for PyGate { fn standard_gate(&self) -> Option { Python::with_gil(|py| -> Option { match self.gate.getattr(py, intern!(py, "_standard_gate")) { - Ok(stdgate) => match stdgate.extract(py) { - Ok(out_gate) => out_gate, - Err(_) => None, - }, + Ok(stdgate) => stdgate.extract(py).unwrap_or_default(), Err(_) => None, } }) diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 9c4f19aa1ba3..82c678031a15 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -16,13 +16,20 @@ use std::ptr::NonNull; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyType}; +use ndarray::Array2; +use num_complex::Complex64; use smallvec::SmallVec; +use crate::circuit_data::CircuitData; use crate::circuit_instruction::ExtraInstructionAttributes; -use crate::imports::DEEPCOPY; -use crate::operations::{OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate}; +use crate::imports::{get_std_gate_class, DEEPCOPY}; +use crate::interner::Interned; +use crate::operations::{ + Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, +}; +use crate::{Clbit, Qubit}; /// The logical discriminant of `PackedOperation`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -331,6 +338,78 @@ impl PackedOperation { .into()), } } + + /// Whether the Python class that we would use to represent the inner `Operation` object in + /// Python space would be an instance of the given Python type. This does not construct the + /// Python-space `Operator` instance if it can be avoided (i.e. for standard gates). + pub fn py_op_is_instance(&self, py_type: &Bound) -> PyResult { + let py = py_type.py(); + let py_op = match self.view() { + OperationRef::Standard(standard) => { + return get_std_gate_class(py, standard)? + .bind(py) + .downcast::()? + .is_subclass(py_type) + } + OperationRef::Gate(gate) => gate.gate.bind(py), + OperationRef::Instruction(instruction) => instruction.instruction.bind(py), + OperationRef::Operation(operation) => operation.operation.bind(py), + }; + py_op.is_instance(py_type) + } +} + +impl Operation for PackedOperation { + fn name(&self) -> &str { + let view = self.view(); + let name = match view { + OperationRef::Standard(ref standard) => standard.name(), + OperationRef::Gate(gate) => gate.name(), + OperationRef::Instruction(instruction) => instruction.name(), + OperationRef::Operation(operation) => operation.name(), + }; + // SAFETY: all of the inner parts of the view are owned by `self`, so it's valid for us to + // forcibly reborrowing up to our own lifetime. We avoid using `` + // just to avoid a further _potential_ unsafeness, were its implementation to start doing + // something weird with the lifetimes. `str::from_utf8_unchecked` and + // `slice::from_raw_parts` are both trivially safe because they're being called on immediate + // values from a validated `str`. + unsafe { + ::std::str::from_utf8_unchecked(::std::slice::from_raw_parts(name.as_ptr(), name.len())) + } + } + #[inline] + fn num_qubits(&self) -> u32 { + self.view().num_qubits() + } + #[inline] + fn num_clbits(&self) -> u32 { + self.view().num_clbits() + } + #[inline] + fn num_params(&self) -> u32 { + self.view().num_params() + } + #[inline] + fn control_flow(&self) -> bool { + self.view().control_flow() + } + #[inline] + fn matrix(&self, params: &[Param]) -> Option> { + self.view().matrix(params) + } + #[inline] + fn definition(&self, params: &[Param]) -> Option { + self.view().definition(params) + } + #[inline] + fn standard_gate(&self) -> Option { + self.view().standard_gate() + } + #[inline] + fn directive(&self) -> bool { + self.view().directive() + } } impl From for PackedOperation { @@ -414,11 +493,11 @@ impl Drop for PackedOperation { pub struct PackedInstruction { pub op: PackedOperation, /// The index under which the interner has stored `qubits`. - pub qubits: crate::interner::Index, + pub qubits: Interned<[Qubit]>, /// The index under which the interner has stored `clbits`. - pub clbits: crate::interner::Index, + pub clbits: Interned<[Clbit]>, pub params: Option>>, - pub extra_attrs: Option>, + pub extra_attrs: ExtraInstructionAttributes, #[cfg(feature = "cache_pygates")] /// This is hidden in a `OnceCell` because it's just an on-demand cache; we don't create this @@ -435,15 +514,6 @@ pub struct PackedInstruction { } impl PackedInstruction { - /// Immutably view the contained operation. - /// - /// If you only care whether the contained operation is a `StandardGate` or not, you can use - /// `PackedInstruction::standard_gate`, which is a bit cheaper than this function. - #[inline] - pub fn op(&self) -> OperationRef { - self.op.view() - } - /// Access the standard gate in this `PackedInstruction`, if it is one. If the instruction /// refers to a Python-space object, `None` is returned. #[inline] @@ -469,6 +539,23 @@ impl PackedInstruction { .unwrap_or(&mut []) } + /// Does this instruction contain any compile-time symbolic `ParameterExpression`s? + pub fn is_parameterized(&self) -> bool { + self.params_view() + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } + + #[inline] + pub fn condition(&self) -> Option<&Py> { + self.extra_attrs.condition() + } + + #[inline] + pub fn label(&self) -> Option<&str> { + self.extra_attrs.label() + } + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this /// instruction. This may construct the reference if the `PackedInstruction` is a standard /// gate with no already stored operation. @@ -482,7 +569,7 @@ impl PackedInstruction { OperationRef::Standard(standard) => standard.create_py_op( py, self.params.as_deref().map(SmallVec::as_slice), - self.extra_attrs.as_deref(), + &self.extra_attrs, ), OperationRef::Gate(gate) => Ok(gate.gate.clone_ref(py)), OperationRef::Instruction(instruction) => Ok(instruction.instruction.clone_ref(py)), @@ -510,4 +597,30 @@ impl PackedInstruction { } Ok(out) } + + /// Check equality of the operation, including Python-space checks, if appropriate. + pub fn py_op_eq(&self, py: Python, other: &Self) -> PyResult { + match (self.op.view(), other.op.view()) { + (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::Gate(left), OperationRef::Gate(right)) => { + left.gate.bind(py).eq(&right.gate) + } + (OperationRef::Instruction(left), OperationRef::Instruction(right)) => { + left.instruction.bind(py).eq(&right.instruction) + } + (OperationRef::Operation(left), OperationRef::Operation(right)) => { + left.operation.bind(py).eq(&right.operation) + } + // Handle the case we end up with a pygate for a standard gate + // this typically only happens if it's a ControlledGate in python + // and we have mutable state set. + (OperationRef::Standard(_left), OperationRef::Gate(right)) => { + self.unpack_py_op(py)?.bind(py).eq(&right.gate) + } + (OperationRef::Gate(left), OperationRef::Standard(_right)) => { + other.unpack_py_op(py)?.bind(py).eq(&left.gate) + } + _ => Ok(false), + } + } } diff --git a/crates/circuit/src/rustworkx_core_vnext.rs b/crates/circuit/src/rustworkx_core_vnext.rs new file mode 100644 index 000000000000..e69ee88a93e7 --- /dev/null +++ b/crates/circuit/src/rustworkx_core_vnext.rs @@ -0,0 +1,1417 @@ +// 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. + +// TODO: delete once we move to a version of Rustworkx which includes +// this implementation as part of rustworkx-core. +// PR: https://github.com/Qiskit/rustworkx/pull/1235 +pub mod isomorphism { + pub mod vf2 { + #![allow(clippy::too_many_arguments)] + // This module was originally forked from petgraph's isomorphism module @ v0.5.0 + // to handle PyDiGraph inputs instead of petgraph's generic Graph. However it has + // since diverged significantly from the original petgraph implementation. + + use std::cmp::{Ordering, Reverse}; + use std::convert::Infallible; + use std::error::Error; + use std::fmt::{Debug, Display, Formatter}; + use std::iter::Iterator; + use std::marker; + use std::ops::Deref; + + use hashbrown::HashMap; + use rustworkx_core::dictmap::*; + + use rustworkx_core::petgraph::data::{Build, Create, DataMap}; + use rustworkx_core::petgraph::stable_graph::NodeIndex; + use rustworkx_core::petgraph::visit::{ + Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, + IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, + NodeCount, NodeIndexable, + }; + use rustworkx_core::petgraph::{Directed, Incoming, Outgoing}; + + use rayon::slice::ParallelSliceMut; + + /// Returns `true` if we can map every element of `xs` to a unique + /// element of `ys` while using `matcher` func to compare two elements. + fn is_subset( + xs: &[T1], + ys: &[T2], + matcher: &mut F, + ) -> Result + where + F: FnMut(T1, T2) -> Result, + { + let mut valid = vec![true; ys.len()]; + for &a in xs { + let mut found = false; + for (&b, free) in ys.iter().zip(valid.iter_mut()) { + if *free && matcher(a, b)? { + found = true; + *free = false; + break; + } + } + + if !found { + return Ok(false); + } + } + + Ok(true) + } + + #[inline] + fn sorted(x: &mut (N, N)) { + let (a, b) = x; + if b < a { + std::mem::swap(a, b) + } + } + + /// Returns the adjacency matrix of a graph as a dictionary + /// with `(i, j)` entry equal to number of edges from node `i` to node `j`. + fn adjacency_matrix(graph: G) -> HashMap<(NodeIndex, NodeIndex), usize> + where + G: GraphProp + GraphBase + EdgeCount + IntoEdgeReferences, + { + let mut matrix = HashMap::with_capacity(graph.edge_count()); + for edge in graph.edge_references() { + let mut item = (edge.source(), edge.target()); + if !graph.is_directed() { + sorted(&mut item); + } + let entry = matrix.entry(item).or_insert(0); + *entry += 1; + } + matrix + } + + /// Returns the number of edges from node `a` to node `b`. + fn edge_multiplicity( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + ) -> usize + where + G: GraphProp + GraphBase, + { + let mut item = (a, b); + if !graph.is_directed() { + sorted(&mut item); + } + *matrix.get(&item).unwrap_or(&0) + } + + /// Nodes `a`, `b` are adjacent if the number of edges + /// from node `a` to node `b` is greater than `val`. + fn is_adjacent( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + val: usize, + ) -> bool + where + G: GraphProp + GraphBase, + { + edge_multiplicity(graph, matrix, a, b) >= val + } + + trait NodeSorter + where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph: GraphBase + + Create + + Data; + + fn sort(&self, _: G) -> Vec; + + fn reorder(&self, graph: G) -> (Self::OutputGraph, HashMap) { + let order = self.sort(graph); + + let mut new_graph = + Self::OutputGraph::with_capacity(graph.node_count(), graph.edge_count()); + let mut id_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for node_index in order { + let node_data = graph.node_weight(node_index).unwrap(); + let new_index = new_graph.add_node(node_data.clone()); + id_map.insert(node_index, new_index); + } + for edge in graph.edge_references() { + let edge_w = edge.weight(); + let p_index = id_map[&edge.source()]; + let c_index = id_map[&edge.target()]; + new_graph.add_edge(p_index, c_index, edge_w.clone()); + } + ( + new_graph, + id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), + ) + } + } + + /// Sort nodes based on node ids. + struct DefaultIdSorter {} + + impl DefaultIdSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for DefaultIdSorter + where + G: Deref + + GraphBase + + DataMap + + NodeCount + + EdgeCount + + IntoEdgeReferences + + IntoNodeIdentifiers, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + graph.node_identifiers().collect() + } + } + + /// Sort nodes based on VF2++ heuristic. + struct Vf2ppSorter {} + + impl Vf2ppSorter { + pub fn new() -> Self { + Self {} + } + } + + impl NodeSorter for Vf2ppSorter + where + G: Deref + + GraphProp + + GraphBase + + DataMap + + NodeCount + + NodeIndexable + + EdgeCount + + IntoNodeIdentifiers + + IntoEdgesDirected, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + let n = graph.node_bound(); + + let dout: Vec = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Outgoing) + .count() + }) + .collect(); + + let mut din: Vec = vec![0; n]; + if graph.is_directed() { + din = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Incoming) + .count() + }) + .collect(); + } + + let mut conn_in: Vec = vec![0; n]; + let mut conn_out: Vec = vec![0; n]; + + let mut order: Vec = Vec::with_capacity(n); + + // Process BFS level + let mut process = |mut vd: Vec| -> Vec { + // repeatedly bring largest element in front. + for i in 0..vd.len() { + let (index, &item) = vd[i..] + .iter() + .enumerate() + .max_by_key(|&(_, &node)| { + ( + conn_in[node], + dout[node], + conn_out[node], + din[node], + Reverse(node), + ) + }) + .unwrap(); + + vd.swap(i, i + index); + order.push(NodeIndex::new(item)); + + for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) { + conn_in[graph.to_index(neigh)] += 1; + } + + if graph.is_directed() { + for neigh in graph.neighbors_directed(graph.from_index(item), Incoming) + { + conn_out[graph.to_index(neigh)] += 1; + } + } + } + vd + }; + + let mut seen: Vec = vec![false; n]; + + // Create BFS Tree from root and process each level. + let mut bfs_tree = |root: usize| { + if seen[root] { + return; + } + + let mut next_level: Vec = Vec::new(); + + seen[root] = true; + next_level.push(root); + while !next_level.is_empty() { + let this_level = next_level; + let this_level = process(this_level); + + next_level = Vec::new(); + for bfs_node in this_level { + for neighbor in + graph.neighbors_directed(graph.from_index(bfs_node), Outgoing) + { + let neigh = graph.to_index(neighbor); + if !seen[neigh] { + seen[neigh] = true; + next_level.push(neigh); + } + } + } + } + }; + + let mut sorted_nodes: Vec = + graph.node_identifiers().map(|node| node.index()).collect(); + sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node], Reverse(node))); + sorted_nodes.reverse(); + + for node in sorted_nodes { + bfs_tree(node); + } + + order + } + } + + #[derive(Debug)] + pub struct Vf2State { + pub graph: G, + /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, + /// NodeIndex::end() for no mapping. + mapping: Vec, + /// out[i] is non-zero if i is in either M_0(s) or Tout_0(s) + /// These are all the next vertices that are not mapped yet, but + /// have an outgoing edge from the mapping. + out: Vec, + /// ins[i] is non-zero if i is in either M_0(s) or Tin_0(s) + /// These are all the incoming vertices, those not mapped yet, but + /// have an edge from them into the mapping. + /// Unused if graph is undirected -- it's identical with out in that case. + ins: Vec, + out_size: usize, + ins_size: usize, + adjacency_matrix: HashMap<(NodeIndex, NodeIndex), usize>, + generation: usize, + _etype: marker::PhantomData, + } + + impl Vf2State + where + G: GraphBase + GraphProp + NodeCount + EdgeCount, + for<'a> &'a G: GraphBase + + GraphProp + + NodeCount + + EdgeCount + + IntoEdgesDirected, + { + pub fn new(graph: G) -> Self { + let c0 = graph.node_count(); + let is_directed = graph.is_directed(); + let adjacency_matrix = adjacency_matrix(&graph); + Vf2State { + graph, + mapping: vec![NodeIndex::end(); c0], + out: vec![0; c0], + ins: vec![0; c0 * (is_directed as usize)], + out_size: 0, + ins_size: 0, + adjacency_matrix, + generation: 0, + _etype: marker::PhantomData, + } + } + + /// Return **true** if we have a complete mapping + pub fn is_complete(&self) -> bool { + self.generation == self.mapping.len() + } + + /// Add mapping **from** <-> **to** to the state. + pub fn push_mapping(&mut self, from: NodeIndex, to: NodeIndex) { + self.generation += 1; + let s = self.generation; + self.mapping[from.index()] = to; + // update T0 & T1 ins/outs + // T0out: Node in G0 not in M0 but successor of a node in M0. + // st.out[0]: Node either in M0 or successor of M0 + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == 0 { + self.out[ix.index()] = s; + self.out_size += 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == 0 { + self.ins[ix.index()] = s; + self.ins_size += 1; + } + } + } + } + + /// Restore the state to before the last added mapping + pub fn pop_mapping(&mut self, from: NodeIndex) { + let s = self.generation; + self.generation -= 1; + + // undo (n, m) mapping + self.mapping[from.index()] = NodeIndex::end(); + + // unmark in ins and outs + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == s { + self.out[ix.index()] = 0; + self.out_size -= 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == s { + self.ins[ix.index()] = 0; + self.ins_size -= 1; + } + } + } + } + + /// Find the next (least) node in the Tout set. + pub fn next_out_index(&self, from_index: usize) -> Option { + self.out[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the Tin set. + pub fn next_in_index(&self, from_index: usize) -> Option { + self.ins[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the N - M set. + pub fn next_rest_index(&self, from_index: usize) -> Option { + self.mapping[from_index..] + .iter() + .enumerate() + .find(|&(_, elt)| *elt == NodeIndex::end()) + .map(|(index, _)| index) + } + } + + #[derive(Debug)] + pub enum IsIsomorphicError { + NodeMatcherErr(NME), + EdgeMatcherErr(EME), + } + + impl Display for IsIsomorphicError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + IsIsomorphicError::NodeMatcherErr(e) => { + write!(f, "Node match callback failed with: {}", e) + } + IsIsomorphicError::EdgeMatcherErr(e) => { + write!(f, "Edge match callback failed with: {}", e) + } + } + } + } + + impl Error for IsIsomorphicError {} + + pub struct NoSemanticMatch; + + pub trait NodeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result; + } + + impl NodeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result { + Ok(true) + } + } + + impl NodeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + n0: G0::NodeId, + n1: G1::NodeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.node_weight(n0), g1.node_weight(n1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + pub trait EdgeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result; + } + + impl EdgeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _e0: G0::EdgeId, + _e1: G1::EdgeId, + ) -> Result { + Ok(true) + } + } + + impl EdgeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, + { + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.edge_weight(e0), g1.edge_weight(e1)) { + self(x, y) + } else { + Ok(false) + } + } + } + + /// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic. + /// + /// Using the VF2 algorithm, examining both syntactic and semantic + /// graph isomorphism (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub fn is_isomorphic( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Result> + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) + || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) + { + return Ok(false); + } + + let mut vf2 = Vf2Algorithm::new( + g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + ); + + match vf2.next() { + Some(Ok(_)) => Ok(true), + Some(Err(e)) => Err(e), + None => Ok(false), + } + } + + #[derive(Copy, Clone, PartialEq, Debug)] + enum OpenList { + Out, + In, + Other, + } + + #[derive(Clone, PartialEq, Debug)] + enum Frame { + Outer, + Inner { nodes: [N; 2], open_list: OpenList }, + Unwind { nodes: [N; 2], open_list: OpenList }, + } + + /// An iterator which uses the VF2(++) algorithm to produce isomorphic matches + /// between two graphs, examining both syntactic and semantic graph isomorphism + /// (graph structure and matching node and edge weights). + /// + /// The graphs should not be multigraphs. + pub struct Vf2Algorithm + where + G0: GraphBase + Data, + G1: GraphBase + Data, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub st: (Vf2State, Vf2State), + pub node_match: NM, + pub edge_match: EM, + ordering: Ordering, + induced: bool, + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, + call_limit: Option, + _counter: usize, + } + + impl Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + pub fn new( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Self { + let (g0, node_map_g0) = if id_order { + DefaultIdSorter::new().reorder(g0) + } else { + Vf2ppSorter::new().reorder(g0) + }; + + let (g1, node_map_g1) = if id_order { + DefaultIdSorter::new().reorder(g1) + } else { + Vf2ppSorter::new().reorder(g1) + }; + + let st = (Vf2State::new(g0), Vf2State::new(g1)); + Vf2Algorithm { + st, + node_match, + edge_match, + ordering, + induced, + node_map_g0, + node_map_g1, + stack: vec![Frame::Outer], + call_limit, + _counter: 0, + } + } + + fn mapping(&self) -> DictMap { + let mut mapping: DictMap = DictMap::new(); + self.st + .1 + .mapping + .iter() + .enumerate() + .for_each(|(index, val)| { + mapping.insert(self.node_map_g0[&val.index()], self.node_map_g1[&index]); + }); + + mapping + } + + fn next_candidate( + st: &mut (Vf2State, Vf2State), + ) -> Option<(NodeIndex, NodeIndex, OpenList)> { + // Try the out list + let mut to_index = st.1.next_out_index(0); + let mut from_index = None; + let mut open_list = OpenList::Out; + + if to_index.is_some() { + from_index = st.0.next_out_index(0); + open_list = OpenList::Out; + } + // Try the in list + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_in_index(0); + + if to_index.is_some() { + from_index = st.0.next_in_index(0); + open_list = OpenList::In; + } + } + // Try the other list -- disconnected graph + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_rest_index(0); + if to_index.is_some() { + from_index = st.0.next_rest_index(0); + open_list = OpenList::Other; + } + } + match (from_index, to_index) { + (Some(n), Some(m)) => Some((NodeIndex::new(n), NodeIndex::new(m), open_list)), + // No more candidates + _ => None, + } + } + + fn next_from_ix( + st: &mut (Vf2State, Vf2State), + nx: NodeIndex, + open_list: OpenList, + ) -> Option { + // Find the next node index to try on the `from` side of the mapping + let start = nx.index() + 1; + let cand0 = match open_list { + OpenList::Out => st.0.next_out_index(start), + OpenList::In => st.0.next_in_index(start), + OpenList::Other => st.0.next_rest_index(start), + } + .map(|c| c + start); // compensate for start offset. + match cand0 { + None => None, // no more candidates + Some(ix) => { + debug_assert!(ix >= start); + Some(NodeIndex::new(ix)) + } + } + } + + fn pop_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Restore state. + st.0.pop_mapping(nodes[0]); + st.1.pop_mapping(nodes[1]); + } + + fn push_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Add mapping nx <-> mx to the state + st.0.push_mapping(nodes[0], nodes[1]); + st.1.push_mapping(nodes[1], nodes[0]); + } + + fn is_feasible( + st: &mut (Vf2State, Vf2State), + nodes: [NodeIndex; 2], + node_match: &mut NM, + edge_match: &mut EM, + ordering: Ordering, + induced: bool, + ) -> Result> { + // Check syntactic feasibility of mapping by ensuring adjacencies + // of nx map to adjacencies of mx. + // + // nx == map to => mx + // + // R_succ + // + // Check that every neighbor of nx is mapped to a neighbor of mx, + // then check the reverse, from mx to nx. Check that they have the same + // count of edges. + // + // Note: We want to check the lookahead measures here if we can, + // R_out: Equal for G0, G1: Card(Succ(G, n) ^ Tout); for both Succ and Pred + // R_in: Same with Tin + // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, + // Ñ is G0 - M - Tin - Tout + let end = NodeIndex::end(); + let mut succ_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + succ_count[0] += 1; + if !induced { + continue; + } + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.0.graph, &st.0.adjacency_matrix, nodes[0], n_neigh); + + let has_edge = + is_adjacent(&st.1.graph, &st.1.adjacency_matrix, nodes[1], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors(nodes[1]) { + succ_count[1] += 1; + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + continue; + } + let val = + edge_multiplicity(&st.1.graph, &st.1.adjacency_matrix, nodes[1], n_neigh); + + let has_edge = + is_adjacent(&st.0.graph, &st.0.adjacency_matrix, nodes[0], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { + return Ok(false); + } + // R_pred + if st.0.graph.is_directed() { + let mut pred_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + pred_count[0] += 1; + if !induced { + continue; + } + // the self loop case is handled in outgoing + let m_neigh = st.0.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.0.graph, + &st.0.adjacency_matrix, + n_neigh, + nodes[0], + ); + + let has_edge = is_adjacent( + &st.1.graph, + &st.1.adjacency_matrix, + m_neigh, + nodes[1], + val, + ); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + pred_count[1] += 1; + // the self loop case is handled in outgoing + let m_neigh = st.1.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity( + &st.1.graph, + &st.1.adjacency_matrix, + n_neigh, + nodes[1], + ); + + let has_edge = is_adjacent( + &st.0.graph, + &st.0.adjacency_matrix, + m_neigh, + nodes[0], + val, + ); + if !has_edge { + return Ok(false); + } + } + if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + macro_rules! field { + ($x:ident, 0) => { + $x.0 + }; + ($x:ident, 1) => { + $x.1 + }; + ($x:ident, 1 - 0) => { + $x.1 + }; + ($x:ident, 1 - 1) => { + $x.0 + }; + } + macro_rules! rule { + ($arr:ident, $j:tt, $dir:expr) => {{ + let mut count = 0; + for n_neigh in field!(st, $j).graph.neighbors_directed(nodes[$j], $dir) { + let index = n_neigh.index(); + if field!(st, $j).$arr[index] > 0 && st.$j.mapping[index] == end { + count += 1; + } + } + count + }}; + } + // R_out + if rule!(out, 0, Outgoing) + .cmp(&rule!(out, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + if st.0.graph.is_directed() + && rule!(out, 0, Incoming) + .cmp(&rule!(out, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + // R_in + if st.0.graph.is_directed() { + if rule!(ins, 0, Outgoing) + .cmp(&rule!(ins, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + + if rule!(ins, 0, Incoming) + .cmp(&rule!(ins, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + } + // R_new + if induced { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && (st.0.ins.is_empty() || st.0.ins[index] == 0) { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors(nodes[1]) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && (st.1.ins.is_empty() || st.1.ins[index] == 0) { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + if st.0.graph.is_directed() { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && st.0.ins[index] == 0 { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && st.1.ins[index] == 0 { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + } + // semantic feasibility: compare associated data for nodes + if node_match.enabled() + && !node_match + .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) + .map_err(IsIsomorphicError::NodeMatcherErr)? + { + return Ok(false); + } + // semantic feasibility: compare associated data for edges + if edge_match.enabled() { + let mut matcher = + |g0_edge: (NodeIndex, G0::EdgeId), + g1_edge: (NodeIndex, G1::EdgeId)| + -> Result> { + let (nx, e0) = g0_edge; + let (mx, e1) = g1_edge; + if nx == mx + && edge_match + .eq(&st.0.graph, &st.1.graph, e0, e1) + .map_err(IsIsomorphicError::EdgeMatcherErr)? + { + return Ok(true); + } + Ok(false) + }; + + // Used to reverse the order of edge args to the matcher + // when checking G1 subset of G0. + #[inline] + fn reverse_args(mut f: F) -> impl FnMut(T2, T1) -> R + where + F: FnMut(T1, T2) -> R, + { + move |y, x| f(x, y) + } + + // outgoing edges + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + } + } + Ok(true) + } + } + + impl Iterator for Vf2Algorithm + where + G0: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, + { + type Item = Result, IsIsomorphicError>; + + /// Return Some(mapping) if isomorphism is decided, else None. + fn next(&mut self) -> Option { + if (self + .st + .0 + .graph + .node_count() + .cmp(&self.st.1.graph.node_count()) + .then(self.ordering) + != self.ordering) + || (self + .st + .0 + .graph + .edge_count() + .cmp(&self.st.1.graph.edge_count()) + .then(self.ordering) + != self.ordering) + { + return None; + } + + // A "depth first" search of a valid mapping from graph 1 to graph 2 + + // F(s, n, m) -- evaluate state s and add mapping n <-> m + + // Find least T1out node (in st.out[1] but not in M[1]) + while let Some(frame) = self.stack.pop() { + match frame { + Frame::Unwind { + nodes, + open_list: ol, + } => { + Vf2Algorithm::::pop_state(&mut self.st, nodes); + + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return Some(Ok(self.mapping())); + } + continue; + } + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Inner { + nodes, + open_list: ol, + } => { + let feasible = match Vf2Algorithm::::is_feasible( + &mut self.st, + nodes, + &mut self.node_match, + &mut self.edge_match, + self.ordering, + self.induced, + ) { + Ok(f) => f, + Err(e) => { + return Some(Err(e)); + } + }; + + if feasible { + Vf2Algorithm::::push_state(&mut self.st, nodes); + // Check cardinalities of Tin, Tout sets + if self + .st + .0 + .out_size + .cmp(&self.st.1.out_size) + .then(self.ordering) + == self.ordering + && self + .st + .0 + .ins_size + .cmp(&self.st.1.ins_size) + .then(self.ordering) + == self.ordering + { + self._counter += 1; + if let Some(limit) = self.call_limit { + if self._counter > limit { + return None; + } + } + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + + self.stack.push(f0); + self.stack.push(Frame::Outer); + continue; + } + Vf2Algorithm::::pop_state(&mut self.st, nodes); + } + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + } + } + None + } + } + } +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 04b0c0609347..6033c7c47e49 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -13,11 +13,17 @@ use pyo3::prelude::*; use qiskit_accelerate::{ - convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, - error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, + check_map::check_map_mod, circuit_library::circuit_library, + commutation_analysis::commutation_analysis, commutation_cancellation::commutation_cancellation, + commutation_checker::commutation_checker, convert_2q_block_matrix::convert_2q_block_matrix, + dense_layout::dense_layout, error_map::error_map, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, filter_op_nodes::filter_op_nodes_mod, + gate_direction::gate_direction, inverse_cancellation::inverse_cancellation_mod, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, - sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, + pauli_exp_val::pauli_expval, + remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, results::results, + sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, + split_2q_unitaries::split_2q_unitaries_mod, star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, vf2_layout::vf2_layout, @@ -37,21 +43,32 @@ where #[pymodule] fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, qiskit_circuit::circuit, "circuit")?; + add_submodule(m, qiskit_circuit::converters::converters, "converters")?; add_submodule(m, qiskit_qasm2::qasm2, "qasm2")?; add_submodule(m, qiskit_qasm3::qasm3, "qasm3")?; + add_submodule(m, circuit_library, "circuit_library")?; + add_submodule(m, check_map_mod, "check_map")?; add_submodule(m, convert_2q_block_matrix, "convert_2q_block_matrix")?; add_submodule(m, dense_layout, "dense_layout")?; add_submodule(m, error_map, "error_map")?; add_submodule(m, euler_one_qubit_decomposer, "euler_one_qubit_decomposer")?; + add_submodule(m, inverse_cancellation_mod, "inverse_cancellation")?; + add_submodule(m, filter_op_nodes_mod, "filter_op_nodes")?; add_submodule(m, isometry, "isometry")?; add_submodule(m, nlayout, "nlayout")?; add_submodule(m, optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, pauli_expval, "pauli_expval")?; add_submodule(m, synthesis, "synthesis")?; + add_submodule( + m, + remove_diagonal_gates_before_measure, + "remove_diagonal_gates_before_measure", + )?; add_submodule(m, results, "results")?; add_submodule(m, sabre, "sabre")?; add_submodule(m, sampled_exp_val, "sampled_exp_val")?; add_submodule(m, sparse_pauli_op, "sparse_pauli_op")?; + add_submodule(m, split_2q_unitaries_mod, "split_2q_unitaries")?; add_submodule(m, star_prerouting, "star_prerouting")?; add_submodule(m, stochastic_swap, "stochastic_swap")?; add_submodule(m, target, "target")?; @@ -59,5 +76,9 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, uc_gate, "uc_gate")?; add_submodule(m, utils, "utils")?; add_submodule(m, vf2_layout, "vf2_layout")?; + add_submodule(m, gate_direction, "gate_direction")?; + add_submodule(m, commutation_checker, "commutation_checker")?; + add_submodule(m, commutation_analysis, "commutation_analysis")?; + add_submodule(m, commutation_cancellation, "commutation_cancellation")?; Ok(()) } diff --git a/docs/conf.py b/docs/conf.py index 1ebd77749b41..a84a1ff691bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,23 +114,6 @@ autosummary_generate = True autosummary_generate_overwrite = False -# The pulse library contains some names that differ only in capitalization, during the changeover -# surrounding SymbolPulse. Since these resolve to autosummary filenames that also differ only in -# capitalization, this causes problems when the documentation is built on an OS/filesystem that is -# enforcing case-insensitive semantics. This setting defines some custom names to prevent the clash -# from happening. -autosummary_filename_map = { - "qiskit.pulse.library.Constant": "qiskit.pulse.library.Constant_class.rst", - "qiskit.pulse.library.Sawtooth": "qiskit.pulse.library.Sawtooth_class.rst", - "qiskit.pulse.library.Triangle": "qiskit.pulse.library.Triangle_class.rst", - "qiskit.pulse.library.Cos": "qiskit.pulse.library.Cos_class.rst", - "qiskit.pulse.library.Sin": "qiskit.pulse.library.Sin_class.rst", - "qiskit.pulse.library.Gaussian": "qiskit.pulse.library.Gaussian_class.rst", - "qiskit.pulse.library.Drag": "qiskit.pulse.library.Drag_class.rst", - "qiskit.pulse.library.Square": "qiskit.pulse.library.Square_fun.rst", - "qiskit.pulse.library.Sech": "qiskit.pulse.library.Sech_fun.rst", -} - # We only use Google-style docstrings, and allowing Napoleon to parse Numpy-style docstrings both # slows down the build (a little) and can sometimes result in _regular_ section headings in # module-level documentation being converted into surprising things. diff --git a/pyproject.toml b/pyproject.toml index b1f7b039e407..689bf473e2f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "qiskit" description = "An open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives." -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "Apache 2.0"} authors = [ { name = "Qiskit Development Team", email = "qiskit@us.ibm.com" }, @@ -27,7 +27,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -75,23 +74,29 @@ aqc = "qiskit.transpiler.passes.synthesis.aqc_plugin:AQCSynthesisPlugin" sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis" [project.entry-points."qiskit.synthesis"] -"clifford.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisClifford" -"clifford.ag" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:AGSynthesisClifford" -"clifford.bm" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BMSynthesisClifford" -"clifford.greedy" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:GreedySynthesisClifford" -"clifford.layers" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerSynthesisClifford" -"clifford.lnn" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerLnnSynthesisClifford" -"linear_function.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisLinearFunction" -"linear_function.kms" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisLinearFunction" -"linear_function.pmh" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:PMHSynthesisLinearFunction" -"permutation.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation" -"permutation.kms" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisPermutation" -"permutation.basic" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation" -"permutation.acg" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:ACGSynthesisPermutation" -"qft.full" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisFull" -"qft.line" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisLine" -"qft.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisFull" -"permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:TokenSwapperSynthesisPermutation" +"clifford.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:DefaultSynthesisClifford" +"clifford.ag" = "qiskit.transpiler.passes.synthesis.hls_plugins:AGSynthesisClifford" +"clifford.bm" = "qiskit.transpiler.passes.synthesis.hls_plugins:BMSynthesisClifford" +"clifford.greedy" = "qiskit.transpiler.passes.synthesis.hls_plugins:GreedySynthesisClifford" +"clifford.layers" = "qiskit.transpiler.passes.synthesis.hls_plugins:LayerSynthesisClifford" +"clifford.lnn" = "qiskit.transpiler.passes.synthesis.hls_plugins:LayerLnnSynthesisClifford" +"linear_function.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:DefaultSynthesisLinearFunction" +"linear_function.kms" = "qiskit.transpiler.passes.synthesis.hls_plugins:KMSSynthesisLinearFunction" +"linear_function.pmh" = "qiskit.transpiler.passes.synthesis.hls_plugins:PMHSynthesisLinearFunction" +"mcx.n_dirty_i15" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesisNDirtyI15" +"mcx.n_clean_m15" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesisNCleanM15" +"mcx.1_clean_b95" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesis1CleanB95" +"mcx.gray_code" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesisGrayCode" +"mcx.noaux_v24" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesisNoAuxV24" +"mcx.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:MCXSynthesisDefault" +"permutation.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:BasicSynthesisPermutation" +"permutation.kms" = "qiskit.transpiler.passes.synthesis.hls_plugins:KMSSynthesisPermutation" +"permutation.basic" = "qiskit.transpiler.passes.synthesis.hls_plugins:BasicSynthesisPermutation" +"permutation.acg" = "qiskit.transpiler.passes.synthesis.hls_plugins:ACGSynthesisPermutation" +"qft.full" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisFull" +"qft.line" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisLine" +"qft.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisFull" +"permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.hls_plugins:TokenSwapperSynthesisPermutation" [project.entry-points."qiskit.transpiler.init"] default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" @@ -134,12 +139,12 @@ include = ["qiskit", "qiskit.*"] [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py39', 'py310', 'py311'] [tool.cibuildwheel] manylinux-x86_64-image = "manylinux2014" manylinux-i686-image = "manylinux2014" -skip = "pp* cp36-* cp37-* *musllinux* *win32 *i686 cp38-macosx_arm64" +skip = "pp* cp36-* cp37-* cp38-* *musllinux* *win32 *i686 cp38-macosx_arm64" test-skip = "*win32 *linux_i686" test-command = "python {project}/examples/python/stochastic_swap.py" # We need to use pre-built versions of Numpy and Scipy in the tests; they have a @@ -191,7 +196,7 @@ extension-pkg-allow-list = [ "tweedledum", ] load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] -py-version = "3.8" # update it when bumping minimum supported python version +py-version = "3.9" # update it when bumping minimum supported python version [tool.pylint.basic] good-names = ["a", "b", "i", "j", "k", "d", "n", "m", "ex", "v", "w", "x", "y", "z", "Run", "_", "logger", "q", "c", "r", "qr", "cr", "qc", "nd", "pi", "op", "b", "ar", "br", "p", "cp", "ax", "dt", "__unittest", "iSwapGate", "mu"] diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 6091bfa90346..25137d7a5918 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -43,14 +43,6 @@ " Qiskit unfortunately cannot enforce this requirement during environment resolution." " See https://qisk.it/packaging-1-0 for more detail." ) -if sys.version_info < (3, 9): - warnings.warn( - "Using Qiskit with Python 3.8 is deprecated as of the 1.1.0 release. " - "Support for running Qiskit with Python 3.8 will be removed in the " - "1.3.0 release, which coincides with when Python 3.8 goes end of life.", - DeprecationWarning, - ) - from . import _accelerate import qiskit._numpy_compat @@ -60,6 +52,8 @@ # We manually define them on import so people can directly import qiskit._accelerate.* submodules # and not have to rely on attribute access. No action needed for top-level extension packages. sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit +sys.modules["qiskit._accelerate.circuit_library"] = _accelerate.circuit_library +sys.modules["qiskit._accelerate.converters"] = _accelerate.converters sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map @@ -73,6 +67,9 @@ sys.modules["qiskit._accelerate.pauli_expval"] = _accelerate.pauli_expval sys.modules["qiskit._accelerate.qasm2"] = _accelerate.qasm2 sys.modules["qiskit._accelerate.qasm3"] = _accelerate.qasm3 +sys.modules["qiskit._accelerate.remove_diagonal_gates_before_measure"] = ( + _accelerate.remove_diagonal_gates_before_measure +) sys.modules["qiskit._accelerate.results"] = _accelerate.results sys.modules["qiskit._accelerate.sabre"] = _accelerate.sabre sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val @@ -85,6 +82,15 @@ sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear sys.modules["qiskit._accelerate.synthesis.clifford"] = _accelerate.synthesis.clifford +sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker +sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis +sys.modules["qiskit._accelerate.commutation_cancellation"] = _accelerate.commutation_cancellation +sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.split_2q_unitaries"] = _accelerate.split_2q_unitaries +sys.modules["qiskit._accelerate.gate_direction"] = _accelerate.gate_direction +sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation +sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map +sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 65a88519a0de..ebaef39c2a43 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -579,6 +579,16 @@ :class:`Qubit` nor :class:`Clbit` operands, but has an explicit :attr:`~Store.lvalue` and :attr:`~Store.rvalue`. +For example, to determine the parity of a bitstring ``cr`` and store it in another register ``creg``, +the :class:`Store` instruction can be used in the following way:: + + parity = expr.lift(cr[0]) + for i in range(1,n): + parity = expr.bit_xor(cr[i], parity) + qc.store(creg[0], parity) + + + .. autoclass:: Store :show-inheritance: :members: @@ -676,6 +686,13 @@ ControlFlowOp +For convenience, there is a :class:`frozenset` instance containing the :attr:`.Instruction.name` +attributes of each of the control-flow operations. + +.. data:: CONTROL_FLOW_OP_NAMES + + Set of the instruction names of Qiskit's known control-flow operations. + These control-flow operations (:class:`IfElseOp`, :class:`WhileLoopOp`, :class:`SwitchCaseOp` and :class:`ForLoopOp`) all have specific state that defines the branching conditions and strategies, but contain all the different subcircuit blocks that might be entered in @@ -1247,6 +1264,7 @@ def __array__(self, dtype=None, copy=None): CASE_DEFAULT, BreakLoopOp, ContinueLoopOp, + CONTROL_FLOW_OP_NAMES, ) from .annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index e070fe9b9b6f..2d474c7bac4e 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -12,47 +12,10 @@ """Code from commutative_analysis pass that checks commutation relations between DAG nodes.""" -from functools import lru_cache from typing import List, Union, Set, Optional -import numpy as np -from qiskit import QiskitError -from qiskit.circuit import Qubit from qiskit.circuit.operation import Operation -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.quantum_info.operators import Operator - -_skipped_op_names = {"measure", "reset", "delay", "initialize"} -_no_cache_op_names = {"annotated"} - -_supported_ops = { - "h", - "x", - "y", - "z", - "sx", - "sxdg", - "t", - "tdg", - "s", - "sdg", - "cx", - "cy", - "cz", - "swap", - "iswap", - "ecr", - "ccx", - "cswap", -} - - -@lru_cache(maxsize=None) -def _identity_op(num_qubits): - """Cached identity matrix""" - return Operator( - np.eye(2**num_qubits), input_dims=(2,) * num_qubits, output_dims=(2,) * num_qubits - ) +from qiskit._accelerate.commutation_checker import CommutationChecker as RustChecker class CommutationChecker: @@ -70,20 +33,7 @@ def __init__( *, gates: Optional[Set[str]] = None, ): - super().__init__() - if standard_gate_commutations is None: - self._standard_commutations = {} - else: - self._standard_commutations = standard_gate_commutations - self._cache_max_entries = cache_max_entries - - # self._cached_commutation has the same structure as standard_gate_commutations, i.e. a - # dict[pair of gate names][relative placement][tuple of gate parameters] := True/False - self._cached_commutations = {} - self._current_cache_entries = 0 - self._cache_miss = 0 - self._cache_hit = 0 - self._gate_names = gates + self.cc = RustChecker(standard_gate_commutations, cache_max_entries, gates) def commute_nodes( self, @@ -92,15 +42,7 @@ def commute_nodes( max_num_qubits: int = 3, ) -> bool: """Checks if two DAGOpNodes commute.""" - qargs1 = op1.qargs - cargs1 = op2.cargs - if not op1.is_standard_gate(): - op1 = op1.op - qargs2 = op2.qargs - cargs2 = op2.cargs - if not op2.is_standard_gate(): - op2 = op2.op - return self.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) + return self.cc.commute_nodes(op1, op2, max_num_qubits) def commute( self, @@ -131,71 +73,15 @@ def commute( Returns: bool: whether two operations commute. """ - # Skip gates that are not specified. - if self._gate_names is not None: - if op1.name not in self._gate_names or op2.name not in self._gate_names: - return False - - structural_commutation = _commutation_precheck( - op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits - ) - - if structural_commutation is not None: - return structural_commutation - - first_op_tuple, second_op_tuple = _order_operations( - op1, qargs1, cargs1, op2, qargs2, cargs2 - ) - first_op, first_qargs, _ = first_op_tuple - second_op, second_qargs, _ = second_op_tuple - - skip_cache = first_op.name in _no_cache_op_names or second_op.name in _no_cache_op_names - - if skip_cache: - return _commute_matmul(first_op, first_qargs, second_op, second_qargs) - - commutation_lookup = self.check_commutation_entries( - first_op, first_qargs, second_op, second_qargs - ) - - if commutation_lookup is not None: - return commutation_lookup - - # Compute commutation via matrix multiplication - is_commuting = _commute_matmul(first_op, first_qargs, second_op, second_qargs) - - # Store result in this session's commutation_library - # TODO implement LRU cache or similar - # Rebuild cache if current cache exceeded max size - if self._current_cache_entries >= self._cache_max_entries: - self.clear_cached_commutations() - - first_params = getattr(first_op, "params", []) - second_params = getattr(second_op, "params", []) - if len(first_params) > 0 or len(second_params) > 0: - self._cached_commutations.setdefault((first_op.name, second_op.name), {}).setdefault( - _get_relative_placement(first_qargs, second_qargs), {} - )[ - (_hashable_parameters(first_params), _hashable_parameters(second_params)) - ] = is_commuting - else: - self._cached_commutations.setdefault((first_op.name, second_op.name), {})[ - _get_relative_placement(first_qargs, second_qargs) - ] = is_commuting - self._current_cache_entries += 1 - - return is_commuting + return self.cc.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) def num_cached_entries(self): """Returns number of cached entries""" - return self._current_cache_entries + return self.cc.num_cached_entries() def clear_cached_commutations(self): """Clears the dictionary holding cached commutations""" - self._current_cache_entries = 0 - self._cache_miss = 0 - self._cache_hit = 0 - self._cached_commutations = {} + self.cc.clear_cached_commutations() def check_commutation_entries( self, @@ -215,272 +101,6 @@ def check_commutation_entries( Return: bool: True if the gates commute and false if it is not the case. """ - - # We don't precompute commutations for parameterized gates, yet - commutation = _query_commutation( - first_op, - first_qargs, - second_op, - second_qargs, - self._standard_commutations, - ) - - if commutation is not None: - return commutation - - commutation = _query_commutation( - first_op, - first_qargs, - second_op, - second_qargs, - self._cached_commutations, - ) - if commutation is None: - self._cache_miss += 1 - else: - self._cache_hit += 1 - return commutation - - -def _hashable_parameters(params): - """Convert the parameters of a gate into a hashable format for lookup in a dictionary. - - This aims to be fast in common cases, and is not intended to work outside of the lifetime of a - single commutation pass; it does not handle mutable state correctly if the state is actually - changed.""" - try: - hash(params) - return params - except TypeError: - pass - if isinstance(params, (list, tuple)): - return tuple(_hashable_parameters(x) for x in params) - if isinstance(params, np.ndarray): - # Using the bytes of the matrix as key is runtime efficient but requires more space: 128 bits - # times the number of parameters instead of a single 64 bit id. However, by using the bytes as - # an id, we can reuse the cached commutations between different passes. - return (np.ndarray, params.tobytes()) - # Catch anything else with a slow conversion. - return ("fallback", str(params)) - - -def is_commutation_supported(op, qargs, max_num_qubits): - """ - Filter operations whose commutation is not supported due to bugs in transpiler passes invoking - commutation analysis. - Args: - op (Operation): operation to be checked for commutation relation. - qargs (list[Qubit]): qubits the operation acts on. - max_num_qubits (int): The maximum number of qubits to check commutativity for. - - Return: - True if determining the commutation of op is currently supported - """ - # If the number of qubits is beyond what we check, stop here and do not even check in the - # pre-defined supported operations - if len(qargs) > max_num_qubits: - return False - - # Check if the operation is pre-approved, otherwise go through the checks - if op.name in _supported_ops: - return True - - # Commutation of ControlFlow gates also not supported yet. This may be pending a control flow graph. - if op.name in CONTROL_FLOW_OP_NAMES: - return False - - if getattr(op, "_directive", False) or op.name in _skipped_op_names: - return False - - if getattr(op, "is_parameterized", False) and op.is_parameterized(): - return False - - return True - - -def _commutation_precheck( - op1: Operation, - qargs1: List, - cargs1: List, - op2: Operation, - qargs2: List, - cargs2: List, - max_num_qubits, -): - # Bug in CommutativeCancellation, e.g. see gh-8553 - if getattr(op1, "condition", False) or getattr(op2, "condition", False): - return False - - if set(qargs1).isdisjoint(qargs2) and set(cargs1).isdisjoint(cargs2): - return True - - if not is_commutation_supported(op1, qargs1, max_num_qubits) or not is_commutation_supported( - op2, qargs2, max_num_qubits - ): - return False - - return None - - -def _get_relative_placement(first_qargs: List[Qubit], second_qargs: List[Qubit]) -> tuple: - """Determines the relative qubit placement of two gates. Note: this is NOT symmetric. - - Args: - first_qargs (DAGOpNode): first gate - second_qargs (DAGOpNode): second gate - - Return: - A tuple that describes the relative qubit placement: E.g. - _get_relative_placement(CX(0, 1), CX(1, 2)) would return (None, 0) as there is no overlap on - the first qubit of the first gate but there is an overlap on the second qubit of the first gate, - i.e. qubit 0 of the second gate. Likewise, - _get_relative_placement(CX(1, 2), CX(0, 1)) would return (1, None) - """ - qubits_g2 = {q_g1: i_g1 for i_g1, q_g1 in enumerate(second_qargs)} - return tuple(qubits_g2.get(q_g0, None) for q_g0 in first_qargs) - - -@lru_cache(maxsize=10**3) -def _persistent_id(op_name: str) -> int: - """Returns an integer id of a string that is persistent over different python executions (note that - hash() can not be used, i.e. its value can change over two python executions) - Args: - op_name (str): The string whose integer id should be determined. - Return: - The integer id of the input string. - """ - return int.from_bytes(bytes(op_name, encoding="utf-8"), byteorder="big", signed=True) - - -def _order_operations( - op1: Operation, qargs1: List, cargs1: List, op2: Operation, qargs2: List, cargs2: List -): - """Orders two operations in a canonical way that is persistent over - @different python versions and executions - Args: - op1: first operation. - qargs1: first operation's qubits. - cargs1: first operation's clbits. - op2: second operation. - qargs2: second operation's qubits. - cargs2: second operation's clbits. - Return: - The input operations in a persistent, canonical order. - """ - op1_tuple = (op1, qargs1, cargs1) - op2_tuple = (op2, qargs2, cargs2) - least_qubits_op, most_qubits_op = ( - (op1_tuple, op2_tuple) if op1.num_qubits < op2.num_qubits else (op2_tuple, op1_tuple) - ) - # prefer operation with the least number of qubits as first key as this results in shorter keys - if op1.num_qubits != op2.num_qubits: - return least_qubits_op, most_qubits_op - else: - return ( - (op1_tuple, op2_tuple) - if _persistent_id(op1.name) < _persistent_id(op2.name) - else (op2_tuple, op1_tuple) - ) - - -def _query_commutation( - first_op: Operation, - first_qargs: List, - second_op: Operation, - second_qargs: List, - _commutation_lib: dict, -) -> Union[bool, None]: - """Queries and returns the commutation of a pair of operations from a provided commutation library - Args: - first_op: first operation. - first_qargs: first operation's qubits. - first_cargs: first operation's clbits. - second_op: second operation. - second_qargs: second operation's qubits. - second_cargs: second operation's clbits. - _commutation_lib (dict): dictionary of commutation relations - Return: - True if first_op and second_op commute, False if they do not commute and - None if the commutation is not in the library - """ - - commutation = _commutation_lib.get((first_op.name, second_op.name), None) - - # Return here if the commutation is constant over all relative placements of the operations - if commutation is None or isinstance(commutation, bool): - return commutation - - # If we arrive here, there is an entry in the commutation library but it depends on the - # placement of the operations and also possibly on operation parameters - if isinstance(commutation, dict): - commutation_after_placement = commutation.get( - _get_relative_placement(first_qargs, second_qargs), None - ) - # if we have another dict in commutation_after_placement, commutation depends on params - if isinstance(commutation_after_placement, dict): - # Param commutation entry exists and must be a dict - first_params = getattr(first_op, "params", []) - second_params = getattr(second_op, "params", []) - return commutation_after_placement.get( - ( - _hashable_parameters(first_params), - _hashable_parameters(second_params), - ), - None, - ) - else: - # queried commutation is True, False or None - return commutation_after_placement - else: - raise ValueError("Expected commutation to be None, bool or a dict") - - -def _commute_matmul( - first_ops: Operation, first_qargs: List, second_op: Operation, second_qargs: List -): - qarg = {q: i for i, q in enumerate(first_qargs)} - num_qubits = len(qarg) - for q in second_qargs: - if q not in qarg: - qarg[q] = num_qubits - num_qubits += 1 - - first_qarg = tuple(qarg[q] for q in first_qargs) - second_qarg = tuple(qarg[q] for q in second_qargs) - - from qiskit.dagcircuit.dagnode import DAGOpNode - - # If we have a DAGOpNode here we've received a StandardGate definition from - # rust and we can manually pull the matrix to use for the Operators - if isinstance(first_ops, DAGOpNode): - first_ops = first_ops.matrix - if isinstance(second_op, DAGOpNode): - second_op = second_op.matrix - - # try to generate an Operator out of op, if this succeeds we can determine commutativity, otherwise - # return false - try: - operator_1 = Operator( - first_ops, input_dims=(2,) * len(first_qarg), output_dims=(2,) * len(first_qarg) - ) - operator_2 = Operator( - second_op, input_dims=(2,) * len(second_qarg), output_dims=(2,) * len(second_qarg) + return self.cc.library.check_commutation_entries( + first_op, first_qargs, second_op, second_qargs ) - except QiskitError: - return False - - if first_qarg == second_qarg: - # Use full composition if possible to get the fastest matmul paths. - op12 = operator_1.compose(operator_2) - op21 = operator_2.compose(operator_1) - else: - # Expand operator_1 to be large enough to contain operator_2 as well; this relies on qargs1 - # being the lowest possible indices so the identity can be tensored before it. - extra_qarg2 = num_qubits - len(first_qarg) - if extra_qarg2: - id_op = _identity_op(extra_qarg2) - operator_1 = id_op.tensor(operator_1) - op12 = operator_1.compose(operator_2, qargs=second_qarg, front=False) - op21 = operator_1.compose(operator_2, qargs=second_qarg, front=True) - ret = op12 == op21 - return ret diff --git a/qiskit/circuit/controlflow/__init__.py b/qiskit/circuit/controlflow/__init__.py index 12831ccaaaed..2679b45d7782 100644 --- a/qiskit/circuit/controlflow/__init__.py +++ b/qiskit/circuit/controlflow/__init__.py @@ -25,3 +25,4 @@ CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case")) +"""Set of the instruction names of Qiskit's known control-flow operations.""" diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 37fd19e2022a..2f1c09668252 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -97,7 +97,10 @@ def __pow__(self, exponent: float) -> "Gate": return self.power(exponent) def _return_repeat(self, exponent: float) -> "Gate": - return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=self.params) + gate = Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) + gate.validate_parameter = self.validate_parameter + gate.params = self.params + return gate def control( self, diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 39266cf4ca17..9dd1fdebf08a 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -555,6 +555,9 @@ QAOAAnsatz, ) from .data_preparation import ( + z_feature_map, + zz_feature_map, + pauli_feature_map, PauliFeatureMap, ZFeatureMap, ZZFeatureMap, diff --git a/qiskit/circuit/library/basis_change/qft.py b/qiskit/circuit/library/basis_change/qft.py index 2ec6dd69cb79..15668ec51e11 100644 --- a/qiskit/circuit/library/basis_change/qft.py +++ b/qiskit/circuit/library/basis_change/qft.py @@ -315,8 +315,10 @@ def __init__( """ super().__init__(name="qft", num_qubits=num_qubits, params=[]) - def __array__(self, dtype=complex): + def __array__(self, dtype=complex, copy=None): """Return a numpy array for the QFTGate.""" + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") n = self.num_qubits nums = np.arange(2**n) outer = np.outer(nums, nums) diff --git a/qiskit/circuit/library/data_preparation/__init__.py b/qiskit/circuit/library/data_preparation/__init__.py index 192308a3a7f5..475046d78338 100644 --- a/qiskit/circuit/library/data_preparation/__init__.py +++ b/qiskit/circuit/library/data_preparation/__init__.py @@ -38,15 +38,18 @@ """ -from .pauli_feature_map import PauliFeatureMap -from .z_feature_map import ZFeatureMap -from .zz_feature_map import ZZFeatureMap +from .pauli_feature_map import PauliFeatureMap, pauli_feature_map, z_feature_map, zz_feature_map +from ._z_feature_map import ZFeatureMap +from ._zz_feature_map import ZZFeatureMap from .state_preparation import StatePreparation, UniformSuperpositionGate from .initializer import Initialize __all__ = [ + "pauli_feature_map", "PauliFeatureMap", + "z_feature_map", "ZFeatureMap", + "zz_feature_map", "ZZFeatureMap", "StatePreparation", "UniformSuperpositionGate", diff --git a/qiskit/circuit/library/data_preparation/z_feature_map.py b/qiskit/circuit/library/data_preparation/_z_feature_map.py similarity index 52% rename from qiskit/circuit/library/data_preparation/z_feature_map.py rename to qiskit/circuit/library/data_preparation/_z_feature_map.py index 902de1a6a5ac..451776067114 100644 --- a/qiskit/circuit/library/data_preparation/z_feature_map.py +++ b/qiskit/circuit/library/data_preparation/_z_feature_map.py @@ -14,6 +14,7 @@ from typing import Callable, Optional import numpy as np +from qiskit.utils.deprecation import deprecate_func from .pauli_feature_map import PauliFeatureMap @@ -25,13 +26,13 @@ class ZFeatureMap(PauliFeatureMap): .. parsed-literal:: - ┌───┐┌──────────────┐┌───┐┌──────────────┐ - ┤ H ├┤ U1(2.0*x[0]) ├┤ H ├┤ U1(2.0*x[0]) ├ - ├───┤├──────────────┤├───┤├──────────────┤ - ┤ H ├┤ U1(2.0*x[1]) ├┤ H ├┤ U1(2.0*x[1]) ├ - ├───┤├──────────────┤├───┤├──────────────┤ - ┤ H ├┤ U1(2.0*x[2]) ├┤ H ├┤ U1(2.0*x[2]) ├ - └───┘└──────────────┘└───┘└──────────────┘ + ┌───┐┌─────────────┐┌───┐┌─────────────┐ + ┤ H ├┤ P(2.0*x[0]) ├┤ H ├┤ P(2.0*x[0]) ├ + ├───┤├─────────────┤├───┤├─────────────┤ + ┤ H ├┤ U(2.0*x[1]) ├┤ H ├┤ P(2.0*x[1]) ├ + ├───┤├─────────────┤├───┤├─────────────┤ + ┤ H ├┤ P(2.0*x[2]) ├┤ H ├┤ P(2.0*x[2]) ├ + └───┘└─────────────┘└───┘└─────────────┘ This is a sub-class of :class:`~qiskit.circuit.library.PauliFeatureMap` where the Pauli strings are fixed as `['Z']`. As a result the first order expansion will be a circuit without @@ -40,38 +41,48 @@ class ZFeatureMap(PauliFeatureMap): Examples: >>> prep = ZFeatureMap(3, reps=3, insert_barriers=True) - >>> print(prep) - ┌───┐ ░ ┌──────────────┐ ░ ┌───┐ ░ ┌──────────────┐ ░ ┌───┐ ░ ┌──────────────┐ - q_0: ┤ H ├─░─┤ U1(2.0*x[0]) ├─░─┤ H ├─░─┤ U1(2.0*x[0]) ├─░─┤ H ├─░─┤ U1(2.0*x[0]) ├ - ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ - q_1: ┤ H ├─░─┤ U1(2.0*x[1]) ├─░─┤ H ├─░─┤ U1(2.0*x[1]) ├─░─┤ H ├─░─┤ U1(2.0*x[1]) ├ - ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ - q_2: ┤ H ├─░─┤ U1(2.0*x[2]) ├─░─┤ H ├─░─┤ U1(2.0*x[2]) ├─░─┤ H ├─░─┤ U1(2.0*x[2]) ├ - └───┘ ░ └──────────────┘ ░ └───┘ ░ └──────────────┘ ░ └───┘ ░ └──────────────┘ + >>> print(prep.decompose()) + ┌───┐ ░ ┌─────────────┐ ░ ┌───┐ ░ ┌─────────────┐ ░ ┌───┐ ░ ┌─────────────┐ + q_0: ┤ H ├─░─┤ P(2.0*x[0]) ├─░─┤ H ├─░─┤ P(2.0*x[0]) ├─░─┤ H ├─░─┤ P(2.0*x[0]) ├ + ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ + q_1: ┤ H ├─░─┤ P(2.0*x[1]) ├─░─┤ H ├─░─┤ P(2.0*x[1]) ├─░─┤ H ├─░─┤ P(2.0*x[1]) ├ + ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ + q_2: ┤ H ├─░─┤ P(2.0*x[2]) ├─░─┤ H ├─░─┤ P(2.0*x[2]) ├─░─┤ H ├─░─┤ P(2.0*x[2]) ├ + └───┘ ░ └─────────────┘ ░ └───┘ ░ └─────────────┘ ░ └───┘ ░ └─────────────┘ >>> data_map = lambda x: x[0]*x[0] + 1 # note: input is an array >>> prep = ZFeatureMap(3, reps=1, data_map_func=data_map) - >>> print(prep) - ┌───┐┌───────────────────────┐ - q_0: ┤ H ├┤ U1(2.0*x[0]**2 + 2.0) ├ - ├───┤├───────────────────────┤ - q_1: ┤ H ├┤ U1(2.0*x[1]**2 + 2.0) ├ - ├───┤├───────────────────────┤ - q_2: ┤ H ├┤ U1(2.0*x[2]**2 + 2.0) ├ - └───┘└───────────────────────┘ - - >>> classifier = ZFeatureMap(3, reps=1) + RY(3, reps=1) - >>> print(classifier) - ┌───┐┌──────────────┐┌──────────┐ ┌──────────┐ - q_0: ┤ H ├┤ U1(2.0*x[0]) ├┤ RY(θ[0]) ├─■──■─┤ RY(θ[3]) ├──────────── - ├───┤├──────────────┤├──────────┤ │ │ └──────────┘┌──────────┐ - q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ RY(θ[1]) ├─■──┼──────■──────┤ RY(θ[4]) ├ - ├───┤├──────────────┤├──────────┤ │ │ ├──────────┤ - q_2: ┤ H ├┤ U1(2.0*x[2]) ├┤ RY(θ[2]) ├────■──────■──────┤ RY(θ[5]) ├ - └───┘└──────────────┘└──────────┘ └──────────┘ + >>> print(prep.decompose()) + ┌───┐┌──────────────────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]**2 + 2.0) ├ + ├───┤├──────────────────────┤ + q_1: ┤ H ├┤ P(2.0*x[1]**2 + 2.0) ├ + ├───┤├──────────────────────┤ + q_2: ┤ H ├┤ P(2.0*x[2]**2 + 2.0) ├ + └───┘└──────────────────────┘ + + >>> from qiskit.circuit.library import TwoLocal + >>> ry = TwoLocal(3, "ry", "cz", reps=1) + >>> classifier = ZFeatureMap(3, reps=1) + ry + >>> print(classifier.decompose()) + ┌───┐┌─────────────┐┌──────────┐ ┌──────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├┤ RY(θ[0]) ├─■──■─┤ RY(θ[3]) ├──────────── + ├───┤├─────────────┤├──────────┤ │ │ └──────────┘┌──────────┐ + q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ RY(θ[1]) ├─■──┼──────■──────┤ RY(θ[4]) ├ + ├───┤├─────────────┤├──────────┤ │ │ ├──────────┤ + q_2: ┤ H ├┤ P(2.0*x[2]) ├┤ RY(θ[2]) ├────■──────■──────┤ RY(θ[5]) ├ + └───┘└─────────────┘└──────────┘ └──────────┘ """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use the z_feature_map function as a replacement. Note that this will no longer " + "return a BlueprintCircuit, but just a plain QuantumCircuit." + ), + pending=True, + ) def __init__( self, feature_dimension: int, diff --git a/qiskit/circuit/library/data_preparation/zz_feature_map.py b/qiskit/circuit/library/data_preparation/_zz_feature_map.py similarity index 66% rename from qiskit/circuit/library/data_preparation/zz_feature_map.py rename to qiskit/circuit/library/data_preparation/_zz_feature_map.py index 5500bb6691be..2a170513dfdb 100644 --- a/qiskit/circuit/library/data_preparation/zz_feature_map.py +++ b/qiskit/circuit/library/data_preparation/_zz_feature_map.py @@ -12,8 +12,9 @@ """Second-order Pauli-Z expansion circuit.""" -from typing import Callable, List, Union, Optional +from typing import Callable, List, Union, Optional, Dict, Tuple import numpy as np +from qiskit.utils.deprecation import deprecate_func from .pauli_feature_map import PauliFeatureMap @@ -24,13 +25,13 @@ class ZZFeatureMap(PauliFeatureMap): .. parsed-literal:: - ┌───┐┌─────────────────┐ - ┤ H ├┤ U1(2.0*φ(x[0])) ├──■────────────────────────────■──────────────────────────────────── - ├───┤├─────────────────┤┌─┴─┐┌──────────────────────┐┌─┴─┐ - ┤ H ├┤ U1(2.0*φ(x[1])) ├┤ X ├┤ U1(2.0*φ(x[0],x[1])) ├┤ X ├──■────────────────────────────■── - ├───┤├─────────────────┤└───┘└──────────────────────┘└───┘┌─┴─┐┌──────────────────────┐┌─┴─┐ - ┤ H ├┤ U1(2.0*φ(x[2])) ├──────────────────────────────────┤ X ├┤ U1(2.0*φ(x[1],x[2])) ├┤ X ├ - └───┘└─────────────────┘ └───┘└──────────────────────┘└───┘ + ┌───┐┌────────────────┐ + ┤ H ├┤ P(2.0*φ(x[0])) ├──■───────────────────────────■─────────────────────────────────── + ├───┤├────────────────┤┌─┴─┐┌─────────────────────┐┌─┴─┐ + ┤ H ├┤ P(2.0*φ(x[1])) ├┤ X ├┤ P(2.0*φ(x[0],x[1])) ├┤ X ├──■───────────────────────────■── + ├───┤├────────────────┤└───┘└─────────────────────┘└───┘┌─┴─┐┌─────────────────────┐┌─┴─┐ + ┤ H ├┤ P(2.0*φ(x[2])) ├─────────────────────────────────┤ X ├┤ P(2.0*φ(x[1],x[2])) ├┤ X ├ + └───┘└────────────────┘ └───┘└─────────────────────┘└───┘ where :math:`\varphi` is a classical non-linear function, which defaults to :math:`\varphi(x) = x` if and :math:`\varphi(x,y) = (\pi - x)(\pi - y)`. @@ -39,12 +40,12 @@ class ZZFeatureMap(PauliFeatureMap): >>> from qiskit.circuit.library import ZZFeatureMap >>> prep = ZZFeatureMap(2, reps=1) - >>> print(prep) - ┌───┐┌──────────────┐ - q_0: ┤ H ├┤ U1(2.0*x[0]) ├──■───────────────────────────────────────■── - ├───┤├──────────────┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐ - q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ - └───┘└──────────────┘└───┘└─────────────────────────────────┘└───┘ + >>> print(prep.decompose()) + ┌───┐┌─────────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├──■──────────────────────────────────────■── + ├───┤├─────────────┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐ + 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 = ZZFeatureMap(3) + EfficientSU2(3) @@ -71,11 +72,23 @@ class ZZFeatureMap(PauliFeatureMap): OrderedDict([('ZZFeatureMap', 1), ('EfficientSU2', 1)]) """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use the z_feature_map function as a replacement. Note that this will no longer " + "return a BlueprintCircuit, but just a plain QuantumCircuit." + ), + pending=True, + ) def __init__( self, feature_dimension: int, reps: int = 2, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + entanglement: Union[ + str, + Dict[int, List[Tuple[int]]], + Callable[[int], Union[str, Dict[int, List[Tuple[int]]]]], + ] = "full", data_map_func: Optional[Callable[[np.ndarray], float]] = None, parameter_prefix: str = "x", insert_barriers: bool = False, @@ -87,7 +100,7 @@ def __init__( feature_dimension: Number of features. reps: The number of repeated circuits, has a min. value of 1. entanglement: Specifies the entanglement structure. Refer to - :class:`~qiskit.circuit.library.NLocal` for detail. + :class:`~qiskit.circuit.library.PauliFeatureMap` for detail. data_map_func: A mapping function for data x. parameter_prefix: The prefix used if default parameters are generated. insert_barriers: If True, barriers are inserted in between the evolution instructions diff --git a/qiskit/circuit/library/data_preparation/initializer.py b/qiskit/circuit/library/data_preparation/initializer.py index 0e38f067403c..984c7c0d310a 100644 --- a/qiskit/circuit/library/data_preparation/initializer.py +++ b/qiskit/circuit/library/data_preparation/initializer.py @@ -21,6 +21,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.instruction import Instruction +from qiskit.circuit.library.generalized_gates import Isometry from .state_preparation import StatePreparation if typing.TYPE_CHECKING: @@ -86,9 +87,11 @@ def gates_to_uncompute(self) -> QuantumCircuit: """Call to create a circuit with gates that take the desired vector to zero. Returns: - Circuit to take ``self.params`` vector to :math:`|{00\\ldots0}\\rangle` + QuantumCircuit: circuit to take ``self.params`` vector to :math:`|{00\\ldots0}\\rangle` """ - return self._stateprep._gates_to_uncompute() + + isom = Isometry(self.params, 0, 0) + return isom._gates_to_uncompute() @property def params(self): diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index 03bbc031ec63..a40deb6ea18b 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -12,17 +12,315 @@ """The Pauli expansion circuit module.""" -from typing import Optional, Callable, List, Union +from __future__ import annotations + +from collections.abc import Sequence, Mapping +from typing import Optional, Callable, List, Union, Dict, Tuple from functools import reduce import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.circuit import Parameter, ParameterVector +from qiskit.circuit import Parameter, ParameterVector, ParameterExpression from qiskit.circuit.library.standard_gates import HGate +from qiskit.utils.deprecation import deprecate_func +from qiskit._accelerate.circuit_library import pauli_feature_map as _fast_map from ..n_local.n_local import NLocal +def _normalize_entanglement( + entanglement: str | Mapping[int, Sequence[Sequence[int]]] +) -> str | dict[int, list[tuple[int]]]: + if isinstance(entanglement, str): + return entanglement + + return { + num_paulis: [tuple(connections) for connections in ent] + for num_paulis, ent in entanglement.items() + } + + +def pauli_feature_map( + feature_dimension: int, + reps: int = 2, + entanglement: ( + str + | Mapping[int, Sequence[Sequence[int]]] + | Callable[[int], str | Mapping[int, Sequence[Sequence[int]]]] + ) = "full", + alpha: float = 2.0, + paulis: list[str] | None = None, + data_map_func: Callable[[Parameter], ParameterExpression] | None = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "PauliFeatureMap", +) -> QuantumCircuit: + r"""The Pauli expansion circuit. + + The Pauli expansion circuit is a data encoding circuit that transforms input data + :math:`\vec{x} \in \mathbb{R}^n`, where :math:`n` is the ``feature_dimension``, as + + .. math:: + + U_{\Phi(\vec{x})}=\exp\left(i\sum_{S \in \mathcal{I}} + \phi_S(\vec{x})\prod_{i\in S} P_i\right). + + Here, :math:`S` is a set of qubit indices that describes the connections in the feature map, + :math:`\mathcal{I}` is a set containing all these index sets, and + :math:`P_i \in \{I, X, Y, Z\}`. Per default the data-mapping + :math:`\phi_S` is + + .. math:: + + \phi_S(\vec{x}) = \begin{cases} + x_i \text{ if } S = \{i\} \\ + \prod_{j \in S} (\pi - x_j) \text{ if } |S| > 1 + \end{cases}. + + The possible connections can be set using the ``entanglement`` and ``paulis`` arguments. + For example, for single-qubit :math:`Z` rotations and two-qubit :math:`YY` interactions + between all qubit pairs, we can set:: + + + circuit = pauli_feature_map(..., paulis=["Z", "YY"], entanglement="full") + + which will produce blocks of the form + + .. parsed-literal:: + + ┌───┐┌─────────────┐┌──────────┐ ┌───────────┐ + ┤ H ├┤ P(2.0*x[0]) ├┤ RX(pi/2) ├──■──────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤├─────────────┤├──────────┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐├───────────┤ + ┤ H ├┤ P(2.0*x[1]) ├┤ RX(pi/2) ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ RX(-pi/2) ├ + └───┘└─────────────┘└──────────┘└───┘└────────────────────────────────┘└───┘└───────────┘ + + The circuit contains ``reps`` repetitions of this transformation. + + Please refer to :func:`.z_feature_map` for the case of single-qubit Pauli-:math:`Z` rotations + and to :func:`.zz_feature_map` for the single- and two-qubit Pauli-:math:`Z` rotations. + + Examples: + + >>> prep = pauli_feature_map(2, reps=1, paulis=["ZZ"]) + >>> print(prep) + ┌───┐ + q_0: ┤ H ├──■──────────────────────────────────────■── + ├───┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐ + q_1: ┤ H ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ + └───┘└───┘└────────────────────────────────┘└───┘ + + >>> prep = pauli_feature_map(2, reps=1, paulis=["Z", "XX"]) + >>> print(prep) + ┌───┐┌─────────────┐┌───┐ ┌───┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├┤ H ├──■──────────────────────────────────────■──┤ H ├ + ├───┤├─────────────┤├───┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐├───┤ + q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ H ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ H ├ + └───┘└─────────────┘└───┘└───┘└────────────────────────────────┘└───┘└───┘ + + >>> prep = pauli_feature_map(2, reps=1, paulis=["ZY"]) + >>> print(prep) + ┌───┐┌──────────┐ ┌───────────┐ + q_0: ┤ H ├┤ RX(pi/2) ├──■──────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤└──────────┘┌─┴─┐┌────────────────────────────────┐┌─┴─┐└───────────┘ + q_1: ┤ H ├────────────┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├───────────── + └───┘ └───┘└────────────────────────────────┘└───┘ + + >>> from qiskit.circuit.library import EfficientSU2 + >>> prep = pauli_feature_map(3, reps=3, paulis=["Z", "YY", "ZXZ"]) + >>> wavefunction = EfficientSU2(3) + >>> classifier = prep.compose(wavefunction) + >>> classifier.num_parameters + 27 + >>> classifier.count_ops() + OrderedDict([('cx', 39), ('rx', 36), ('u1', 21), ('h', 15), ('ry', 12), ('rz', 12)]) + + References: + + [1] Havlicek et al. Supervised learning with quantum enhanced feature spaces, + `Nature 567, 209-212 (2019) `__. + """ + # create parameter vector used in the Pauli feature map + parameters = ParameterVector(parameter_prefix, feature_dimension) + + # the Rust implementation expects the entanglement to be a str or list[tuple[int]] (or the + # callable to return these types), therefore we normalize the entanglement here + if callable(entanglement): + normalized = lambda offset: _normalize_entanglement(entanglement(offset)) + else: + normalized = _normalize_entanglement(entanglement) + + # construct from Rust + circuit = QuantumCircuit._from_circuit_data( + _fast_map( + feature_dimension, + paulis=paulis, + entanglement=normalized, + reps=reps, + parameters=parameters, + data_map_func=data_map_func, + alpha=alpha, + insert_barriers=insert_barriers, + ) + ) + circuit.name = name + + return circuit + + +def z_feature_map( + feature_dimension: int, + reps: int = 2, + entanglement: ( + str | Sequence[Sequence[int]] | Callable[[int], str | Sequence[Sequence[int]]] + ) = "full", + alpha: float = 2.0, + data_map_func: Callable[[Parameter], ParameterExpression] | None = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "ZFeatureMap", +) -> QuantumCircuit: + """The first order Pauli Z-evolution circuit. + + On 3 qubits and with 2 repetitions the circuit is represented by: + + .. parsed-literal:: + + ┌───┐┌─────────────┐┌───┐┌─────────────┐ + ┤ H ├┤ P(2.0*x[0]) ├┤ H ├┤ P(2.0*x[0]) ├ + ├───┤├─────────────┤├───┤├─────────────┤ + ┤ H ├┤ U(2.0*x[1]) ├┤ H ├┤ P(2.0*x[1]) ├ + ├───┤├─────────────┤├───┤├─────────────┤ + ┤ H ├┤ P(2.0*x[2]) ├┤ H ├┤ P(2.0*x[2]) ├ + └───┘└─────────────┘└───┘└─────────────┘ + + This is a sub-class of :class:`~qiskit.circuit.library.PauliFeatureMap` where the Pauli + strings are fixed as `['Z']`. As a result the first order expansion will be a circuit without + entangling gates. + + Examples: + + >>> prep = z_feature_map(3, reps=3, insert_barriers=True) + >>> print(prep) + ┌───┐ ░ ┌─────────────┐ ░ ┌───┐ ░ ┌─────────────┐ ░ ┌───┐ ░ ┌─────────────┐ + q_0: ┤ H ├─░─┤ P(2.0*x[0]) ├─░─┤ H ├─░─┤ P(2.0*x[0]) ├─░─┤ H ├─░─┤ P(2.0*x[0]) ├ + ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ + q_1: ┤ H ├─░─┤ P(2.0*x[1]) ├─░─┤ H ├─░─┤ P(2.0*x[1]) ├─░─┤ H ├─░─┤ P(2.0*x[1]) ├ + ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ ░ ├───┤ ░ ├─────────────┤ + q_2: ┤ H ├─░─┤ P(2.0*x[2]) ├─░─┤ H ├─░─┤ P(2.0*x[2]) ├─░─┤ H ├─░─┤ P(2.0*x[2]) ├ + └───┘ ░ └─────────────┘ ░ └───┘ ░ └─────────────┘ ░ └───┘ ░ └─────────────┘ + + >>> data_map = lambda x: x[0]*x[0] + 1 # note: input is an array + >>> prep = z_feature_map(3, reps=1, data_map_func=data_map) + >>> print(prep) + ┌───┐┌──────────────────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]**2 + 2.0) ├ + ├───┤├──────────────────────┤ + q_1: ┤ H ├┤ P(2.0*x[1]**2 + 2.0) ├ + ├───┤├──────────────────────┤ + q_2: ┤ H ├┤ P(2.0*x[2]**2 + 2.0) ├ + └───┘└──────────────────────┘ + + >>> from qiskit.circuit.library import TwoLocal + >>> ry = TwoLocal(3, "ry", "cz", reps=1).decompose() + >>> classifier = z_feature_map(3, reps=1) + ry + >>> print(classifier) + ┌───┐┌─────────────┐┌──────────┐ ┌──────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├┤ RY(θ[0]) ├─■──■─┤ RY(θ[3]) ├──────────── + ├───┤├─────────────┤├──────────┤ │ │ └──────────┘┌──────────┐ + q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ RY(θ[1]) ├─■──┼──────■──────┤ RY(θ[4]) ├ + ├───┤├─────────────┤├──────────┤ │ │ ├──────────┤ + q_2: ┤ H ├┤ P(2.0*x[2]) ├┤ RY(θ[2]) ├────■──────■──────┤ RY(θ[5]) ├ + └───┘└─────────────┘└──────────┘ └──────────┘ + + """ + return pauli_feature_map( + feature_dimension=feature_dimension, + reps=reps, + entanglement=entanglement, + alpha=alpha, + paulis=["z"], + data_map_func=data_map_func, + parameter_prefix=parameter_prefix, + insert_barriers=insert_barriers, + name=name, + ) + + +def zz_feature_map( + feature_dimension: int, + reps: int = 2, + entanglement: ( + str | Sequence[Sequence[int]] | Callable[[int], str | Sequence[Sequence[int]]] + ) = "full", + alpha: float = 2.0, + data_map_func: Callable[[Parameter], ParameterExpression] | None = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "ZZFeatureMap", +) -> QuantumCircuit: + r"""Second-order Pauli-Z evolution circuit. + + For 3 qubits and 1 repetition and linear entanglement the circuit is represented by: + + .. parsed-literal:: + + ┌───┐┌────────────────┐ + ┤ H ├┤ P(2.0*φ(x[0])) ├──■───────────────────────────■─────────────────────────────────── + ├───┤├────────────────┤┌─┴─┐┌─────────────────────┐┌─┴─┐ + ┤ H ├┤ P(2.0*φ(x[1])) ├┤ X ├┤ P(2.0*φ(x[0],x[1])) ├┤ X ├──■───────────────────────────■── + ├───┤├────────────────┤└───┘└─────────────────────┘└───┘┌─┴─┐┌─────────────────────┐┌─┴─┐ + ┤ H ├┤ P(2.0*φ(x[2])) ├─────────────────────────────────┤ X ├┤ P(2.0*φ(x[1],x[2])) ├┤ X ├ + └───┘└────────────────┘ └───┘└─────────────────────┘└───┘ + + where :math:`\varphi` is a classical non-linear function, which defaults to :math:`\varphi(x) = x` + if and :math:`\varphi(x,y) = (\pi - x)(\pi - y)`. + + Examples: + + >>> from qiskit.circuit.library import ZZFeatureMap + >>> prep = zz_feature_map(2, reps=1) + >>> print(prep) + ┌───┐┌─────────────┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├──■──────────────────────────────────────■── + ├───┤├─────────────┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐ + 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) + >>> classifier.num_parameters + 15 + >>> classifier.parameters # 'x' for the data preparation, 'θ' for the SU2 parameters + ParameterView([ + ParameterVectorElement(x[0]), ParameterVectorElement(x[1]), + ParameterVectorElement(x[2]), ParameterVectorElement(θ[0]), + ParameterVectorElement(θ[1]), ParameterVectorElement(θ[2]), + ParameterVectorElement(θ[3]), ParameterVectorElement(θ[4]), + ParameterVectorElement(θ[5]), ParameterVectorElement(θ[6]), + ParameterVectorElement(θ[7]), ParameterVectorElement(θ[8]), + ParameterVectorElement(θ[9]), ParameterVectorElement(θ[10]), + ParameterVectorElement(θ[11]), ParameterVectorElement(θ[12]), + ParameterVectorElement(θ[13]), ParameterVectorElement(θ[14]), + ParameterVectorElement(θ[15]), ParameterVectorElement(θ[16]), + ParameterVectorElement(θ[17]), ParameterVectorElement(θ[18]), + ParameterVectorElement(θ[19]), ParameterVectorElement(θ[20]), + ParameterVectorElement(θ[21]), ParameterVectorElement(θ[22]), + ParameterVectorElement(θ[23]) + ]) + """ + return pauli_feature_map( + feature_dimension=feature_dimension, + reps=reps, + entanglement=entanglement, + alpha=alpha, + paulis=["z", "zz"], + data_map_func=data_map_func, + parameter_prefix=parameter_prefix, + insert_barriers=insert_barriers, + name=name, + ) + + class PauliFeatureMap(NLocal): r"""The Pauli Expansion circuit. @@ -57,11 +355,11 @@ class PauliFeatureMap(NLocal): .. parsed-literal:: - ┌───┐┌──────────────┐┌──────────┐ ┌───────────┐ - ┤ H ├┤ U1(2.0*x[0]) ├┤ RX(pi/2) ├──■───────────────────────────────────────■──┤ RX(-pi/2) ├ - ├───┤├──────────────┤├──────────┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐├───────────┤ - ┤ H ├┤ U1(2.0*x[1]) ├┤ RX(pi/2) ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ RX(-pi/2) ├ - └───┘└──────────────┘└──────────┘└───┘└─────────────────────────────────┘└───┘└───────────┘ + ┌───┐┌─────────────┐┌──────────┐ ┌───────────┐ + ┤ H ├┤ P(2.0*x[0]) ├┤ RX(pi/2) ├──■──────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤├─────────────┤├──────────┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐├───────────┤ + ┤ H ├┤ P(2.0*x[1]) ├┤ RX(pi/2) ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ RX(-pi/2) ├ + └───┘└─────────────┘└──────────┘└───┘└────────────────────────────────┘└───┘└───────────┘ The circuit contains ``reps`` repetitions of this transformation. @@ -71,28 +369,28 @@ class PauliFeatureMap(NLocal): Examples: >>> prep = PauliFeatureMap(2, reps=1, paulis=['ZZ']) - >>> print(prep) + >>> print(prep.decompose()) ┌───┐ - q_0: ┤ H ├──■───────────────────────────────────────■── - ├───┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐ - q_1: ┤ H ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ - └───┘└───┘└─────────────────────────────────┘└───┘ + q_0: ┤ H ├──■──────────────────────────────────────■── + ├───┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐ + q_1: ┤ H ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ + └───┘└───┘└────────────────────────────────┘└───┘ >>> prep = PauliFeatureMap(2, reps=1, paulis=['Z', 'XX']) - >>> print(prep) - ┌───┐┌──────────────┐┌───┐ ┌───┐ - q_0: ┤ H ├┤ U1(2.0*x[0]) ├┤ H ├──■───────────────────────────────────────■──┤ H ├ - ├───┤├──────────────┤├───┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐├───┤ - q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ H ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ H ├ - └───┘└──────────────┘└───┘└───┘└─────────────────────────────────┘└───┘└───┘ + >>> print(prep.decompose()) + ┌───┐┌─────────────┐┌───┐ ┌───┐ + q_0: ┤ H ├┤ P(2.0*x[0]) ├┤ H ├──■──────────────────────────────────────■──┤ H ├ + ├───┤├─────────────┤├───┤┌─┴─┐┌────────────────────────────────┐┌─┴─┐├───┤ + q_1: ┤ H ├┤ P(2.0*x[1]) ├┤ H ├┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ H ├ + └───┘└─────────────┘└───┘└───┘└────────────────────────────────┘└───┘└───┘ >>> prep = PauliFeatureMap(2, reps=1, paulis=['ZY']) - >>> print(prep) - ┌───┐┌──────────┐ ┌───────────┐ - q_0: ┤ H ├┤ RX(pi/2) ├──■───────────────────────────────────────■──┤ RX(-pi/2) ├ - ├───┤└──────────┘┌─┴─┐┌─────────────────────────────────┐┌─┴─┐└───────────┘ - q_1: ┤ H ├────────────┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├───────────── - └───┘ └───┘└─────────────────────────────────┘└───┘ + >>> print(prep.decompose()) + ┌───┐┌──────────┐ ┌───────────┐ + q_0: ┤ H ├┤ RX(pi/2) ├──■──────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤└──────────┘┌─┴─┐┌────────────────────────────────┐┌─┴─┐└───────────┘ + q_1: ┤ H ├────────────┤ X ├┤ P(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├───────────── + └───┘ └───┘└────────────────────────────────┘└───┘ >>> from qiskit.circuit.library import EfficientSU2 >>> prep = PauliFeatureMap(3, reps=3, paulis=['Z', 'YY', 'ZXZ']) @@ -105,18 +403,27 @@ class PauliFeatureMap(NLocal): References: - - [1] Havlicek et al. Supervised learning with quantum enhanced feature spaces, `Nature 567, 209-212 (2019) `__. - """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use the pauli_feature_map function as a replacement. Note that this will no longer " + "return a BlueprintCircuit, but just a plain QuantumCircuit." + ), + pending=True, + ) def __init__( self, feature_dimension: Optional[int] = None, reps: int = 2, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + entanglement: Union[ + str, + Dict[int, List[Tuple[int]]], + Callable[[int], Union[str, Dict[int, List[Tuple[int]]]]], + ] = "full", alpha: float = 2.0, paulis: Optional[List[str]] = None, data_map_func: Optional[Callable[[np.ndarray], float]] = None, @@ -129,8 +436,13 @@ def __init__( Args: feature_dimension: Number of qubits in the circuit. reps: The number of repeated circuits. - entanglement: Specifies the entanglement structure. Refer to - :class:`~qiskit.circuit.library.NLocal` for detail. + entanglement: Specifies the entanglement structure. Can be a string (``'full'``, + ``'linear'``, ``'reverse_linear'``, ``'circular'`` or ``'sca'``) or can be a + dictionary where the keys represent the number of qubits and the values are list + of integer-pairs specifying the indices of qubits that are entangled with one + another, for example: ``{1: [(0,), (2,)], 2: [(0,1), (2,0)]}`` or can be a + ``Callable[[int], Union[str | Dict[...]]]`` to return an entanglement specific for + a repetition alpha: The Pauli rotation factor, multiplicative to the pauli rotations paulis: A list of strings for to-be-used paulis. If None are provided, ``['Z', 'ZZ']`` will be used. @@ -153,6 +465,7 @@ def __init__( name=name, ) + self._prefix = parameter_prefix self._data_map_func = data_map_func or self_product self._paulis = paulis or ["Z", "ZZ"] self._alpha = alpha @@ -281,6 +594,45 @@ def cx_chain(circuit, inverse=False): basis_change(evo, inverse=True) return evo + def get_entangler_map( + self, rep_num: int, block_num: int, num_block_qubits: int + ) -> Sequence[Sequence[int]]: + + # if entanglement is a Callable[[int], Union[str | Dict[...]]] + if callable(self._entanglement): + entanglement = self._entanglement(rep_num) + else: + entanglement = self._entanglement + + # entanglement is Dict[int, List[List[int]]] + if isinstance(entanglement, dict): + if all( + isinstance(e2, (int, np.int32, np.int64)) + for key in entanglement.keys() + for en in entanglement[key] + for e2 in en + ): + for qb, ent in entanglement.items(): + for en in ent: + if len(en) != qb: + raise ValueError( + f"For num_qubits = {qb}, entanglement must be a " + f"tuple of length {qb}. You specified {en}." + ) + + # Check if the entanglement is specified for all the pauli blocks being used + for pauli in self.paulis: + if len(pauli) not in entanglement.keys(): + raise ValueError(f"No entanglement specified for {pauli} pauli.") + + return entanglement[num_block_qubits] + + else: + # if the entanglement is not Dict[int, List[List[int]]] or + # Dict[int, List[Tuple[int]]] then we fall back on the original + # `get_entangler_map()` method from NLocal + return super().get_entangler_map(rep_num, block_num, num_block_qubits) + def self_product(x: np.ndarray) -> float: """ diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 26c37334cfe4..52006a8bb378 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -173,8 +173,8 @@ def _define_synthesis_isom(self): q = QuantumRegister(self.num_qubits, "q") initialize_circuit = QuantumCircuit(q, name="init_def") - isom = Isometry(self._params_arg, 0, 0) - initialize_circuit.append(isom, q[:]) + isom = Isometry(self.params, 0, 0) + initialize_circuit.compose(isom.definition, copy=False, inplace=True) # invert the circuit to create the desired vector from zero (assuming # the qubits are in the zero state) diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 2a750195dab3..f948a458ad7b 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -31,9 +31,11 @@ ) from qiskit.exceptions import QiskitError from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping +from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map from ..blueprintcircuit import BlueprintCircuit + if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -1037,51 +1039,11 @@ def get_entangler_map( Raises: ValueError: If the entanglement mode ist not supported. """ - n, m = num_circuit_qubits, num_block_qubits - if m > n: - raise ValueError( - "The number of block qubits must be smaller or equal to the number of " - "qubits in the circuit." - ) - - if entanglement == "pairwise" and num_block_qubits > 2: - raise ValueError("Pairwise entanglement is not defined for blocks with more than 2 qubits.") - - if entanglement == "full": - return list(itertools.combinations(list(range(n)), m)) - elif entanglement == "reverse_linear": - # reverse linear connectivity. In the case of m=2 and the entanglement_block='cx' - # then it's equivalent to 'full' entanglement - reverse = [tuple(range(n - i - m, n - i)) for i in range(n - m + 1)] - return reverse - elif entanglement in ["linear", "circular", "sca", "pairwise"]: - linear = [tuple(range(i, i + m)) for i in range(n - m + 1)] - # if the number of block qubits is 1, we don't have to add the 'circular' part - if entanglement == "linear" or m == 1: - return linear - - if entanglement == "pairwise": - return linear[::2] + linear[1::2] - - # circular equals linear plus top-bottom entanglement (if there's space for it) - if n > m: - circular = [tuple(range(n - m + 1, n)) + (0,)] + linear - else: - circular = linear - if entanglement == "circular": - return circular - - # sca is circular plus shift and reverse - shifted = circular[-offset:] + circular[:-offset] - if offset % 2 == 1: # if odd, reverse the qubit indices - sca = [ind[::-1] for ind in shifted] - else: - sca = shifted - - return sca - - else: - raise ValueError(f"Unsupported entanglement type: {entanglement}") + try: + return fast_entangler_map(num_circuit_qubits, num_block_qubits, entanglement, offset) + except Exception as exc: + # need this as Rust is now raising a QiskitError, where this function was raising ValueError + raise ValueError("Something went wrong in Rust space, here's the error:") from exc _StdlibGateResult = collections.namedtuple("_StdlibGateResult", ("gate", "num_params")) diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index a4741386343f..19a2bf770da2 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -54,6 +54,13 @@ def get_standard_gate_name_mapping(): from qiskit.circuit.delay import Delay from qiskit.circuit.reset import Reset + lambda_ = Parameter("λ") + theta = Parameter("ϴ") + phi = Parameter("φ") + gamma = Parameter("γ") + beta = Parameter("β") + time = Parameter("t") + # Standard gates library mapping, multicontrolled gates not included since they're # variable width gates = [ @@ -61,38 +68,37 @@ def get_standard_gate_name_mapping(): SXGate(), XGate(), CXGate(), - RZGate(Parameter("λ")), - RGate(Parameter("ϴ"), Parameter("φ")), - Reset(), + RZGate(lambda_), + RGate(theta, phi), C3SXGate(), CCXGate(), DCXGate(), CHGate(), - CPhaseGate(Parameter("ϴ")), - CRXGate(Parameter("ϴ")), - CRYGate(Parameter("ϴ")), - CRZGate(Parameter("ϴ")), + CPhaseGate(theta), + CRXGate(theta), + CRYGate(theta), + CRZGate(theta), CSwapGate(), CSXGate(), - CUGate(Parameter("ϴ"), Parameter("φ"), Parameter("λ"), Parameter("γ")), - CU1Gate(Parameter("λ")), - CU3Gate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")), + CUGate(theta, phi, lambda_, gamma), + CU1Gate(lambda_), + CU3Gate(theta, phi, lambda_), CYGate(), CZGate(), CCZGate(), - GlobalPhaseGate(Parameter("ϴ")), + GlobalPhaseGate(theta), HGate(), - PhaseGate(Parameter("ϴ")), + PhaseGate(theta), RCCXGate(), RC3XGate(), - RXGate(Parameter("ϴ")), - RXXGate(Parameter("ϴ")), - RYGate(Parameter("ϴ")), - RYYGate(Parameter("ϴ")), - RZZGate(Parameter("ϴ")), - RZXGate(Parameter("ϴ")), - XXMinusYYGate(Parameter("ϴ"), Parameter("β")), - XXPlusYYGate(Parameter("ϴ"), Parameter("β")), + RXGate(theta), + RXXGate(theta), + RYGate(theta), + RYYGate(theta), + RZZGate(theta), + RZXGate(theta), + XXMinusYYGate(theta, beta), + XXPlusYYGate(theta, beta), ECRGate(), SGate(), SdgGate(), @@ -103,13 +109,14 @@ def get_standard_gate_name_mapping(): SXdgGate(), TGate(), TdgGate(), - UGate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")), - U1Gate(Parameter("λ")), - U2Gate(Parameter("φ"), Parameter("λ")), - U3Gate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")), + UGate(theta, phi, lambda_), + U1Gate(lambda_), + U2Gate(phi, lambda_), + U3Gate(theta, phi, lambda_), YGate(), ZGate(), - Delay(Parameter("t")), + Delay(time), + Reset(), Measure(), ] name_mapping = {gate.name: gate for gate in gates} diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index c4619ca27858..93e772ff842d 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -189,6 +189,21 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): cphase_to_cu1.append(CU1Gate(theta), [0, 1]) _sel.add_equivalence(CPhaseGate(theta), cphase_to_cu1) +# CPhaseGate +# +# global phase: ϴ/4 +# ┌─────────┐ +# q_0: ─■──── q_0: ─■─────────┤ Rz(ϴ/2) ├ +# │P(ϴ) ≡ │ZZ(-ϴ/2) ├─────────┤ +# q_1: ─■──── q_1: ─■─────────┤ Rz(ϴ/2) ├ +# └─────────┘ +theta = Parameter("theta") +cphase_to_rzz = QuantumCircuit(2, global_phase=theta / 4) +cphase_to_rzz.rzz(-theta / 2, 0, 1) +cphase_to_rzz.rz(theta / 2, 0) +cphase_to_rzz.rz(theta / 2, 1) +_sel.add_equivalence(CPhaseGate(theta), cphase_to_rzz) + # RGate # # ┌────────┐ ┌───────────────────────┐ @@ -394,6 +409,19 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): def_rzx.append(inst, qargs, cargs) _sel.add_equivalence(RZXGate(theta), def_rzx) +# RZXGate to RZZGate +# ┌─────────┐ +# q_0: ┤0 ├ q_0: ──────■─────────── +# │ Rzx(ϴ) │ ≡ ┌───┐ │ZZ(ϴ) ┌───┐ +# q_1: ┤1 ├ q_1: ┤ H ├─■──────┤ H ├ +# └─────────┘ └───┘ └───┘ +theta = Parameter("theta") +rzx_to_rzz = QuantumCircuit(2) +rzx_to_rzz.h(1) +rzx_to_rzz.rzz(theta, 0, 1) +rzx_to_rzz.h(1) +_sel.add_equivalence(RZXGate(theta), rzx_to_rzz) + # RYGate # @@ -654,6 +682,21 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): rzz_to_rzx.h(1) _sel.add_equivalence(RZZGate(theta), rzz_to_rzx) +# RZZ to CPhase +# +# global phase: ϴ/2 +# ┌───────┐ +# q_0: ─■───── q_0: ─■────────┤ Rz(ϴ) ├ +# │ZZ(ϴ) ≡ │P(-2*ϴ) ├───────┤ +# q_1: ─■───── q_1: ─■────────┤ Rz(ϴ) ├ +# └───────┘ +theta = Parameter("theta") +rzz_to_cphase = QuantumCircuit(2, global_phase=theta / 2) +rzz_to_cphase.cp(-theta * 2, 0, 1) +rzz_to_cphase.rz(theta, 0) +rzz_to_cphase.rz(theta, 1) +_sel.add_equivalence(RZZGate(theta), rzz_to_cphase) + # RZZ to RYY q = QuantumRegister(2, "q") theta = Parameter("theta") diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 3ee5551b599d..f24c76c65d91 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -20,6 +20,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array from qiskit._accelerate.circuit import StandardGate +from qiskit.utils.deprecation import deprecate_func _X_ARRAY = [[0, 1], [1, 0]] _SX_ARRAY = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]] @@ -1168,6 +1169,19 @@ def inverse(self, annotated: bool = False): return MCXGate(num_ctrl_qubits=self.num_ctrl_qubits, ctrl_state=self.ctrl_state) @staticmethod + @deprecate_func( + additional_msg=( + "For an MCXGate it is no longer possible to know the number of ancilla qubits " + "that would be eventually used by the transpiler when the gate is created. " + "Instead, it is recommended to use MCXGate and let HighLevelSynthesis choose " + "the best synthesis method depending on the number of ancilla qubits available. " + "However, if a specific synthesis method using a specific number of ancilla " + "qubits is require, one can create a custom gate by calling the corresponding " + "synthesis function directly." + ), + since="1.3", + pending=True, + ) def get_num_ancilla_qubits(num_ctrl_qubits: int, mode: str = "noancilla") -> int: """Get the number of required ancilla qubits without instantiating the class. @@ -1185,23 +1199,10 @@ def get_num_ancilla_qubits(num_ctrl_qubits: int, mode: str = "noancilla") -> int def _define(self): """This definition is based on MCPhaseGate implementation.""" # pylint: disable=cyclic-import - from qiskit.circuit.quantumcircuit import QuantumCircuit + from qiskit.synthesis.multi_controlled import synth_mcx_noaux_v24 - q = QuantumRegister(self.num_qubits, name="q") - qc = QuantumCircuit(q) - if self.num_qubits == 4: - qc._append(C3XGate(), q[:], []) - self.definition = qc - elif self.num_qubits == 5: - qc._append(C4XGate(), q[:], []) - self.definition = qc - else: - q_controls = list(range(self.num_ctrl_qubits)) - q_target = self.num_ctrl_qubits - qc.h(q_target) - qc.mcp(numpy.pi, q_controls, q_target) - qc.h(q_target) - self.definition = qc + qc = synth_mcx_noaux_v24(self.num_ctrl_qubits) + self.definition = qc @property def num_ancilla_qubits(self): @@ -1280,6 +1281,17 @@ def __new__( return gate return super().__new__(cls) + @deprecate_func( + additional_msg=( + "It is recommended to use MCXGate and let HighLevelSynthesis choose " + "the best synthesis method depending on the number of ancilla qubits available. " + "If this specific synthesis method is required, one can specify it using the " + "high-level-synthesis plugin `gray_code` for MCX gates, or, alternatively, " + "one can use synth_mcx_gray_code to construct the gate directly." + ), + since="1.3", + pending=True, + ) def __init__( self, num_ctrl_qubits: int, @@ -1305,15 +1317,9 @@ def inverse(self, annotated: bool = False): def _define(self): """Define the MCX gate using the Gray code.""" # pylint: disable=cyclic-import - from qiskit.circuit.quantumcircuit import QuantumCircuit - from .u1 import MCU1Gate - from .h import HGate + from qiskit.synthesis.multi_controlled import synth_mcx_gray_code - q = QuantumRegister(self.num_qubits, name="q") - qc = QuantumCircuit(q, name=self.name) - qc._append(HGate(), [q[-1]], []) - qc._append(MCU1Gate(numpy.pi, num_ctrl_qubits=self.num_ctrl_qubits), q[:], []) - qc._append(HGate(), [q[-1]], []) + qc = synth_mcx_gray_code(self.num_ctrl_qubits) self.definition = qc @@ -1330,6 +1336,17 @@ class MCXRecursive(MCXGate): 2. Iten et al., 2015. https://arxiv.org/abs/1501.06911 """ + @deprecate_func( + additional_msg=( + "It is recommended to use MCXGate and let HighLevelSynthesis choose " + "the best synthesis method depending on the number of ancilla qubits available. " + "If this specific synthesis method is required, one can specify it using the " + "high-level-synthesis plugin '1_clean_b95' for MCX gates, or, alternatively, " + "one can use synth_mcx_1_clean to construct the gate directly." + ), + since="1.3", + pending=True, + ) def __init__( self, num_ctrl_qubits: int, @@ -1409,6 +1426,18 @@ def __new__( unit=unit, ) + @deprecate_func( + additional_msg=( + "It is recommended to use MCXGate and let HighLevelSynthesis choose " + "the best synthesis method depending on the number of ancilla qubits available. " + "If this specific synthesis method is required, one can specify it using the " + "high-level-synthesis plugins `n_clean_m15` (using clean ancillas) or " + "`n_dirty_i15` (using dirty ancillas) for MCX gates. Alternatively, one can " + "use synth_mcx_n_dirty_i15 and synth_mcx_n_clean_m15 to construct the gate directly." + ), + since="1.3", + pending=True, + ) def __init__( self, num_ctrl_qubits: int, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 9413141fb757..780efba5faf1 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1152,17 +1152,41 @@ def __init__( """The unit that :attr:`duration` is specified in.""" self.metadata = {} if metadata is None else metadata """Arbitrary user-defined metadata for the circuit. - + Qiskit will not examine the content of this mapping, but it will pass it through the transpiler and reattach it to the output, so you can track your own metadata.""" @classmethod - def _from_circuit_data(cls, data: CircuitData) -> typing.Self: + def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: """A private constructor from rust space circuit data.""" out = QuantumCircuit() + + if data.num_qubits > 0: + if add_regs: + qr = QuantumRegister(name="q", bits=data.qubits) + out.qregs = [qr] + out._qubit_indices = { + bit: BitLocations(index, [(qr, index)]) for index, bit in enumerate(data.qubits) + } + else: + out._qubit_indices = { + bit: BitLocations(index, []) for index, bit in enumerate(data.qubits) + } + + if data.num_clbits > 0: + if add_regs: + cr = ClassicalRegister(name="c", bits=data.clbits) + out.cregs = [cr] + out._clbit_indices = { + bit: BitLocations(index, [(cr, index)]) for index, bit in enumerate(data.clbits) + } + else: + out._clbit_indices = { + bit: BitLocations(index, []) for index, bit in enumerate(data.clbits) + } + out._data = data - out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} - out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} + return out @staticmethod @@ -2084,7 +2108,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu Remember that in the little-endian convention the leftmost operation will be at the bottom of the circuit. See also - `the docs `__ + `the docs `__ for more information. .. parsed-literal:: @@ -3013,16 +3037,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: self._ancillas.append(bit) if isinstance(register, QuantumRegister): - self.qregs.append(register) - - for idx, bit in enumerate(register): - if bit in self._qubit_indices: - self._qubit_indices[bit].registers.append((register, idx)) - else: - self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations( - self._data.num_qubits - 1, [(register, idx)] - ) + self._add_qreg(register) elif isinstance(register, ClassicalRegister): self.cregs.append(register) @@ -3041,6 +3056,16 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: raise CircuitError("expected a register") + def _add_qreg(self, qreg: QuantumRegister) -> None: + self.qregs.append(qreg) + + for idx, bit in enumerate(qreg): + if bit in self._qubit_indices: + self._qubit_indices[bit].registers.append((qreg, idx)) + else: + self._data.add_qubit(bit) + self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [(qreg, idx)]) + def add_bits(self, bits: Iterable[Bit]) -> None: """Add Bits to the circuit.""" duplicate_bits = { @@ -3212,10 +3237,17 @@ def decompose( from qiskit.converters.dag_to_circuit import dag_to_circuit dag = circuit_to_dag(self, copy_operations=True) - dag = HighLevelSynthesis().run(dag) + + if gates_to_decompose is None: + # We should not rewrite the circuit using HLS when we have gates_to_decompose, + # or else HLS will rewrite all objects with available plugins (e.g., Cliffords, + # PermutationGates, and now also MCXGates) + dag = HighLevelSynthesis().run(dag) + pass_ = Decompose(gates_to_decompose) for _ in range(reps): dag = pass_.run(dag) + # do not copy operations, this is done in the conversion with circuit_to_dag return dag_to_circuit(dag, copy_operations=False) @@ -3509,10 +3541,8 @@ def count_ops(self) -> "OrderedDict[Instruction, int]": Returns: OrderedDict: a breakdown of how many operations of each kind, sorted by amount. """ - count_ops: dict[Instruction, int] = {} - for instruction in self._data: - count_ops[instruction.operation.name] = count_ops.get(instruction.operation.name, 0) + 1 - return OrderedDict(sorted(count_ops.items(), key=lambda kv: kv[1], reverse=True)) + ops_dict = self._data.count_ops() + return OrderedDict(ops_dict) def num_nonlocal_gates(self) -> int: """Return number of non-local gates (i.e. involving 2+ qubits). diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 929089c4ac41..5e2dcf8a1560 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -23,7 +23,7 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend from qiskit.providers.backend_compat import BackendV2Converter -from qiskit.providers.models import BackendProperties +from qiskit.providers.models.backendproperties import BackendProperties from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 10a48df99778..5be9a721bafa 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -12,7 +12,8 @@ """Helper function for converting a circuit to a dag""" -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.circuit.library.blueprintcircuit import BlueprintCircuit +from qiskit._accelerate.converters import circuit_to_dag as core_circuit_to_dag def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): @@ -55,46 +56,22 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord circ.rz(0.5, q[1]).c_if(c, 2) dag = circuit_to_dag(circ) """ - dagcircuit = DAGCircuit() - dagcircuit.name = circuit.name - dagcircuit.global_phase = circuit.global_phase - dagcircuit.calibrations = circuit.calibrations - dagcircuit.metadata = circuit.metadata - - if qubit_order is None: - qubits = circuit.qubits - elif len(qubit_order) != circuit.num_qubits or set(qubit_order) != set(circuit.qubits): + # If we have an instance of BluePrintCircuit, make sure it is built by calling ._build() + if isinstance(circuit, BlueprintCircuit): + if not circuit._is_built: + circuit._build() + + if qubit_order is not None and ( + len(qubit_order) != circuit.num_qubits or set(qubit_order) != set(circuit.qubits) + ): raise ValueError("'qubit_order' does not contain exactly the same qubits as the circuit") - else: - qubits = qubit_order - if clbit_order is None: - clbits = circuit.clbits - elif len(clbit_order) != circuit.num_clbits or set(clbit_order) != set(circuit.clbits): + if clbit_order is not None and ( + len(clbit_order) != circuit.num_clbits or set(clbit_order) != set(circuit.clbits) + ): raise ValueError("'clbit_order' does not contain exactly the same clbits as the circuit") - else: - clbits = clbit_order - - dagcircuit.add_qubits(qubits) - dagcircuit.add_clbits(clbits) - - for var in circuit.iter_input_vars(): - dagcircuit.add_input_var(var) - for var in circuit.iter_captured_vars(): - dagcircuit.add_captured_var(var) - for var in circuit.iter_declared_vars(): - dagcircuit.add_declared_var(var) - - for register in circuit.qregs: - dagcircuit.add_qreg(register) - - for register in circuit.cregs: - dagcircuit.add_creg(register) - for instruction in circuit.data: - dagcircuit._apply_op_node_back( - DAGOpNode.from_instruction(instruction, dag=dagcircuit, deepcopy=copy_operations) - ) + dagcircuit = core_circuit_to_dag(circuit, copy_operations, qubit_order, clbit_order) dagcircuit.duration = circuit.duration dagcircuit.unit = circuit.unit diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 47adee456380..84ea6ef0fd24 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -13,6 +13,7 @@ """Helper function for converting a dag to a circuit.""" from qiskit.circuit import QuantumCircuit +from qiskit._accelerate.converters import dag_to_circuit as dag_to_circuit_rs def dag_to_circuit(dag, copy_operations=True): @@ -54,6 +55,8 @@ def dag_to_circuit(dag, copy_operations=True): """ name = dag.name or None + + circuit_data = dag_to_circuit_rs(dag, copy_operations) circuit = QuantumCircuit( dag.qubits, dag.clbits, @@ -69,8 +72,7 @@ def dag_to_circuit(dag, copy_operations=True): circuit.metadata = dag.metadata circuit.calibrations = dag.calibrations - for node in dag.topological_op_nodes(): - circuit._append(node._to_circuit_instruction(deepcopy=copy_operations)) + circuit._data = circuit_data circuit.duration = dag.duration circuit.unit = dag.unit diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 53cbc6f8f7f1..8738c2676a14 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -20,2406 +20,5 @@ composed, and modified. Some natural properties like depth can be computed directly from the graph. """ -from __future__ import annotations -import copy -import enum -import itertools -import math -from collections import OrderedDict, defaultdict, deque, namedtuple -from collections.abc import Callable, Sequence, Generator, Iterable -from typing import Any, Literal - -import numpy as np -import rustworkx as rx - -from qiskit.circuit import ( - ControlFlowOp, - ForLoopOp, - IfElseOp, - WhileLoopOp, - SwitchCaseOp, - _classical_resource_map, - Operation, - Store, -) -from qiskit.circuit.classical import expr -from qiskit.circuit.controlflow import condition_resources, node_resources, CONTROL_FLOW_OP_NAMES -from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.gate import Gate -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit.exceptions import DAGCircuitError -from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode -from qiskit.circuit.bit import Bit -from qiskit.pulse import Schedule -from qiskit._accelerate.euler_one_qubit_decomposer import collect_1q_runs_filter -from qiskit._accelerate.convert_2q_block_matrix import collect_2q_blocks_filter - -BitLocations = namedtuple("BitLocations", ("index", "registers")) -# The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. -_VarsMode = Literal["alike", "captures", "drop"] - - -class DAGCircuit: - """ - Quantum circuit as a directed acyclic graph. - - There are 3 types of nodes in the graph: inputs, outputs, and operations. - The nodes are connected by directed edges that correspond to qubits and - bits. - """ - - # pylint: disable=invalid-name - - def __init__(self): - """Create an empty circuit.""" - - # Circuit name. Generally, this corresponds to the name - # of the QuantumCircuit from which the DAG was generated. - self.name = None - - # Circuit metadata - self.metadata = {} - - # Cache of dag op node sort keys - self._key_cache = {} - - # Set of wire data in the DAG. A wire is an owned unit of data. Qubits are the primary - # wire type (and the only data that has _true_ wire properties from a read/write - # perspective), but clbits and classical `Var`s are too. Note: classical registers are - # _not_ wires because the individual bits are the more fundamental unit. We treat `Var`s - # as the entire wire (as opposed to individual bits of them) for scalability reasons; if a - # parametric program wants to parametrize over 16-bit angles, we can't scale to 1000s of - # those by tracking all 16 bits individually. - # - # Classical variables shouldn't be "wires"; it should be possible to have multiple reads - # without implying ordering. The initial addition of the classical variables uses the - # existing wire structure as an MVP; we expect to handle this better in a new version of the - # transpiler IR that also handles control flow more properly. - self._wires = set() - - # Map from wire to input nodes of the graph - self.input_map = OrderedDict() - - # Map from wire to output nodes of the graph - self.output_map = OrderedDict() - - # Directed multigraph whose nodes are inputs, outputs, or operations. - # Operation nodes have equal in- and out-degrees and carry - # additional data about the operation, including the argument order - # and parameter values. - # Input nodes have out-degree 1 and output nodes have in-degree 1. - # Edges carry wire labels and each operation has - # corresponding in- and out-edges with the same wire labels. - self._multi_graph = rx.PyDAG() - - # Map of qreg/creg name to Register object. - self.qregs = OrderedDict() - self.cregs = OrderedDict() - - # List of Qubit/Clbit wires that the DAG acts on. - self.qubits: list[Qubit] = [] - self.clbits: list[Clbit] = [] - - # Dictionary mapping of Qubit and Clbit instances to a tuple comprised of - # 0) corresponding index in dag.{qubits,clbits} and - # 1) a list of Register-int pairs for each Register containing the Bit and - # its index within that register. - self._qubit_indices: dict[Qubit, BitLocations] = {} - self._clbit_indices: dict[Clbit, BitLocations] = {} - # Tracking for the classical variables used in the circuit. This contains the information - # needed to insert new nodes. This is keyed by the name rather than the `Var` instance - # itself so we can ensure we don't allow shadowing or redefinition of names. - self._vars_info: dict[str, _DAGVarInfo] = {} - # Convenience stateful tracking for the individual types of nodes to allow things like - # comparisons between circuits to take place without needing to disambiguate the - # graph-specific usage information. - self._vars_by_type: dict[_DAGVarType, set[expr.Var]] = { - type_: set() for type_ in _DAGVarType - } - - self._global_phase: float | ParameterExpression = 0.0 - self._calibrations: dict[str, dict[tuple, Schedule]] = defaultdict(dict) - - self._op_names = {} - - self.duration = None - self.unit = "dt" - - @property - def wires(self): - """Return a list of the wires in order.""" - return ( - self.qubits - + self.clbits - + [var for vars in self._vars_by_type.values() for var in vars] - ) - - @property - def node_counter(self): - """ - Returns the number of nodes in the dag. - """ - return len(self._multi_graph) - - @property - def global_phase(self): - """Return the global phase of the circuit.""" - return self._global_phase - - @global_phase.setter - def global_phase(self, angle: float | ParameterExpression): - """Set the global phase of the circuit. - - Args: - angle (float, ParameterExpression) - """ - if isinstance(angle, ParameterExpression): - self._global_phase = angle - else: - # Set the phase to the [0, 2π) interval - angle = float(angle) - if not angle: - self._global_phase = 0 - else: - self._global_phase = angle % (2 * math.pi) - - @property - def calibrations(self) -> dict[str, dict[tuple, Schedule]]: - """Return calibration dictionary. - - The custom pulse definition of a given gate is of the form - {'gate_name': {(qubits, params): schedule}} - """ - return dict(self._calibrations) - - @calibrations.setter - def calibrations(self, calibrations: dict[str, dict[tuple, Schedule]]): - """Set the circuit calibration data from a dictionary of calibration definition. - - Args: - calibrations (dict): A dictionary of input in the format - {'gate_name': {(qubits, gate_params): schedule}} - """ - self._calibrations = defaultdict(dict, calibrations) - - def add_calibration(self, gate, qubits, schedule, params=None): - """Register a low-level, custom pulse definition for the given gate. - - Args: - gate (Union[Gate, str]): Gate information. - qubits (Union[int, Tuple[int]]): List of qubits to be measured. - schedule (Schedule): Schedule information. - params (Optional[List[Union[float, Parameter]]]): A list of parameters. - - Raises: - Exception: if the gate is of type string and params is None. - """ - - def _format(operand): - try: - # Using float/complex value as a dict key is not good idea. - # This makes the mapping quite sensitive to the rounding error. - # However, the mechanism is already tied to the execution model (i.e. pulse gate) - # and we cannot easily update this rule. - # The same logic exists in QuantumCircuit.add_calibration. - evaluated = complex(operand) - if np.isreal(evaluated): - evaluated = float(evaluated.real) - if evaluated.is_integer(): - evaluated = int(evaluated) - return evaluated - except TypeError: - # Unassigned parameter - return operand - - if isinstance(gate, Gate): - params = gate.params - gate = gate.name - if params is not None: - params = tuple(map(_format, params)) - else: - params = () - - self._calibrations[gate][(tuple(qubits), params)] = schedule - - def has_calibration_for(self, node): - """Return True if the dag has a calibration defined for the node operation. In this - case, the operation does not need to be translated to the device basis. - """ - if not self.calibrations or node.op.name not in self.calibrations: - return False - qubits = tuple(self.qubits.index(qubit) for qubit in node.qargs) - params = [] - for p in node.op.params: - if isinstance(p, ParameterExpression) and not p.parameters: - params.append(float(p)) - else: - params.append(p) - params = tuple(params) - return (qubits, params) in self.calibrations[node.op.name] - - def remove_all_ops_named(self, opname): - """Remove all operation nodes with the given name.""" - for n in self.named_nodes(opname): - self.remove_op_node(n) - - def add_qubits(self, qubits): - """Add individual qubit wires.""" - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError("not a Qubit instance.") - - duplicate_qubits = set(self.qubits).intersection(qubits) - if duplicate_qubits: - raise DAGCircuitError(f"duplicate qubits {duplicate_qubits}") - - for qubit in qubits: - self.qubits.append(qubit) - self._qubit_indices[qubit] = BitLocations(len(self.qubits) - 1, []) - self._add_wire(qubit) - - def add_clbits(self, clbits): - """Add individual clbit wires.""" - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError("not a Clbit instance.") - - duplicate_clbits = set(self.clbits).intersection(clbits) - if duplicate_clbits: - raise DAGCircuitError(f"duplicate clbits {duplicate_clbits}") - - for clbit in clbits: - self.clbits.append(clbit) - self._clbit_indices[clbit] = BitLocations(len(self.clbits) - 1, []) - self._add_wire(clbit) - - def add_qreg(self, qreg): - """Add all wires in a quantum register.""" - if not isinstance(qreg, QuantumRegister): - raise DAGCircuitError("not a QuantumRegister instance.") - if qreg.name in self.qregs: - raise DAGCircuitError(f"duplicate register {qreg.name}") - self.qregs[qreg.name] = qreg - existing_qubits = set(self.qubits) - for j in range(qreg.size): - if qreg[j] in self._qubit_indices: - self._qubit_indices[qreg[j]].registers.append((qreg, j)) - if qreg[j] not in existing_qubits: - self.qubits.append(qreg[j]) - self._qubit_indices[qreg[j]] = BitLocations( - len(self.qubits) - 1, registers=[(qreg, j)] - ) - self._add_wire(qreg[j]) - - def add_creg(self, creg): - """Add all wires in a classical register.""" - if not isinstance(creg, ClassicalRegister): - raise DAGCircuitError("not a ClassicalRegister instance.") - if creg.name in self.cregs: - raise DAGCircuitError(f"duplicate register {creg.name}") - self.cregs[creg.name] = creg - existing_clbits = set(self.clbits) - for j in range(creg.size): - if creg[j] in self._clbit_indices: - self._clbit_indices[creg[j]].registers.append((creg, j)) - if creg[j] not in existing_clbits: - self.clbits.append(creg[j]) - self._clbit_indices[creg[j]] = BitLocations( - len(self.clbits) - 1, registers=[(creg, j)] - ) - self._add_wire(creg[j]) - - def add_input_var(self, var: expr.Var): - """Add an input variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.CAPTURE]: - raise DAGCircuitError("cannot add inputs to a circuit with captures") - self._add_var(var, _DAGVarType.INPUT) - - def add_captured_var(self, var: expr.Var): - """Add a captured variable to the circuit. - - Args: - var: the variable to add.""" - if self._vars_by_type[_DAGVarType.INPUT]: - raise DAGCircuitError("cannot add captures to a circuit with inputs") - self._add_var(var, _DAGVarType.CAPTURE) - - def add_declared_var(self, var: expr.Var): - """Add a declared local variable to the circuit. - - Args: - var: the variable to add.""" - self._add_var(var, _DAGVarType.DECLARE) - - def _add_var(self, var: expr.Var, type_: _DAGVarType): - """Inner function to add any variable to the DAG. ``location`` should be a reference one of - the ``self._vars_*`` tracking dictionaries. - """ - # The setup of the initial graph structure between an "in" and an "out" node is the same as - # the bit-related `_add_wire`, but this logically needs to do different bookkeeping around - # tracking the properties. - if not var.standalone: - raise DAGCircuitError( - "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances" - ) - if (previous := self._vars_info.get(var.name, None)) is not None: - if previous.var == var: - raise DAGCircuitError(f"'{var}' is already present in the circuit") - raise DAGCircuitError( - f"cannot add '{var}' as its name shadows the existing '{previous.var}'" - ) - in_node = DAGInNode(wire=var) - out_node = DAGOutNode(wire=var) - in_node._node_id, out_node._node_id = self._multi_graph.add_nodes_from((in_node, out_node)) - self._multi_graph.add_edge(in_node._node_id, out_node._node_id, var) - self.input_map[var] = in_node - self.output_map[var] = out_node - self._vars_by_type[type_].add(var) - self._vars_info[var.name] = _DAGVarInfo(var, type_, in_node, out_node) - - def _add_wire(self, wire): - """Add a qubit or bit to the circuit. - - Args: - wire (Bit): the wire to be added - - This adds a pair of in and out nodes connected by an edge. - - Raises: - DAGCircuitError: if trying to add duplicate wire - """ - if wire not in self._wires: - self._wires.add(wire) - - inp_node = DAGInNode(wire=wire) - outp_node = DAGOutNode(wire=wire) - input_map_id, output_map_id = self._multi_graph.add_nodes_from([inp_node, outp_node]) - inp_node._node_id = input_map_id - outp_node._node_id = output_map_id - self.input_map[wire] = inp_node - self.output_map[wire] = outp_node - self._multi_graph.add_edge(inp_node._node_id, outp_node._node_id, wire) - else: - raise DAGCircuitError(f"duplicate wire {wire}") - - def find_bit(self, bit: Bit) -> BitLocations: - """ - Finds locations in the circuit, by mapping the Qubit and Clbit to positional index - BitLocations is defined as: BitLocations = namedtuple("BitLocations", ("index", "registers")) - - Args: - bit (Bit): The bit to locate. - - Returns: - namedtuple(int, List[Tuple(Register, int)]): A 2-tuple. The first element (``index``) - contains the index at which the ``Bit`` can be found (in either - :obj:`~DAGCircuit.qubits`, :obj:`~DAGCircuit.clbits`, depending on its - type). The second element (``registers``) is a list of ``(register, index)`` - pairs with an entry for each :obj:`~Register` in the circuit which contains the - :obj:`~Bit` (and the index in the :obj:`~Register` at which it can be found). - - 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. - """ - try: - if isinstance(bit, Qubit): - return self._qubit_indices[bit] - elif isinstance(bit, Clbit): - return self._clbit_indices[bit] - else: - raise DAGCircuitError(f"Could not locate bit of unknown type: {type(bit)}") - except KeyError as err: - raise DAGCircuitError( - f"Could not locate provided bit: {bit}. Has it been added to the DAGCircuit?" - ) from err - - def remove_clbits(self, *clbits): - """ - Remove classical bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - clbits (List[Clbit]): The bits to remove. - - Raises: - DAGCircuitError: a clbit is not a :obj:`.Clbit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(clbit, Clbit) for clbit in clbits): - raise DAGCircuitError( - f"clbits not of type Clbit: {[b for b in clbits if not isinstance(b, Clbit)]}" - ) - - clbits = set(clbits) - unknown_clbits = clbits.difference(self.clbits) - if unknown_clbits: - raise DAGCircuitError(f"clbits not in circuit: {unknown_clbits}") - - busy_clbits = {bit for bit in clbits if not self._is_wire_idle(bit)} - if busy_clbits: - raise DAGCircuitError(f"clbits not idle: {busy_clbits}") - - # remove any references to bits - cregs_to_remove = {creg for creg in self.cregs.values() if not clbits.isdisjoint(creg)} - self.remove_cregs(*cregs_to_remove) - - for clbit in clbits: - self._remove_idle_wire(clbit) - self.clbits.remove(clbit) - del self._clbit_indices[clbit] - - # Update the indices of remaining clbits - for i, clbit in enumerate(self.clbits): - self._clbit_indices[clbit] = self._clbit_indices[clbit]._replace(index=i) - - def remove_cregs(self, *cregs): - """ - Remove classical registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a creg is not a ClassicalRegister, or is not in - the circuit. - """ - if any(not isinstance(creg, ClassicalRegister) for creg in cregs): - raise DAGCircuitError( - "cregs not of type ClassicalRegister: " - f"{[r for r in cregs if not isinstance(r, ClassicalRegister)]}" - ) - - unknown_cregs = set(cregs).difference(self.cregs.values()) - if unknown_cregs: - raise DAGCircuitError(f"cregs not in circuit: {unknown_cregs}") - - for creg in cregs: - del self.cregs[creg.name] - for j in range(creg.size): - bit = creg[j] - bit_position = self._clbit_indices[bit] - bit_position.registers.remove((creg, j)) - - def remove_qubits(self, *qubits): - """ - Remove quantum bits from the circuit. All bits MUST be idle. - Any registers with references to at least one of the specified bits will - also be removed. - - Args: - qubits (List[~qiskit.circuit.Qubit]): The bits to remove. - - Raises: - DAGCircuitError: a qubit is not a :obj:`~.circuit.Qubit`, is not in the circuit, - or is not idle. - """ - if any(not isinstance(qubit, Qubit) for qubit in qubits): - raise DAGCircuitError( - f"qubits not of type Qubit: {[b for b in qubits if not isinstance(b, Qubit)]}" - ) - - qubits = set(qubits) - unknown_qubits = qubits.difference(self.qubits) - if unknown_qubits: - raise DAGCircuitError(f"qubits not in circuit: {unknown_qubits}") - - busy_qubits = {bit for bit in qubits if not self._is_wire_idle(bit)} - if busy_qubits: - raise DAGCircuitError(f"qubits not idle: {busy_qubits}") - - # remove any references to bits - qregs_to_remove = {qreg for qreg in self.qregs.values() if not qubits.isdisjoint(qreg)} - self.remove_qregs(*qregs_to_remove) - - for qubit in qubits: - self._remove_idle_wire(qubit) - self.qubits.remove(qubit) - del self._qubit_indices[qubit] - - # Update the indices of remaining qubits - for i, qubit in enumerate(self.qubits): - self._qubit_indices[qubit] = self._qubit_indices[qubit]._replace(index=i) - - def remove_qregs(self, *qregs): - """ - Remove quantum registers from the circuit, leaving underlying bits - in place. - - Raises: - DAGCircuitError: a qreg is not a QuantumRegister, or is not in - the circuit. - """ - if any(not isinstance(qreg, QuantumRegister) for qreg in qregs): - raise DAGCircuitError( - f"qregs not of type QuantumRegister: " - f"{[r for r in qregs if not isinstance(r, QuantumRegister)]}" - ) - - unknown_qregs = set(qregs).difference(self.qregs.values()) - if unknown_qregs: - raise DAGCircuitError(f"qregs not in circuit: {unknown_qregs}") - - for qreg in qregs: - del self.qregs[qreg.name] - for j in range(qreg.size): - bit = qreg[j] - bit_position = self._qubit_indices[bit] - bit_position.registers.remove((qreg, j)) - - def _is_wire_idle(self, wire): - """Check if a wire is idle. - - Args: - wire (Bit): a wire in the circuit. - - Returns: - bool: true if the wire is idle, false otherwise. - - Raises: - DAGCircuitError: the wire is not in the circuit. - """ - if wire not in self._wires: - raise DAGCircuitError(f"wire {wire} not in circuit") - - try: - child = next(self.successors(self.input_map[wire])) - except StopIteration as e: - raise DAGCircuitError( - f"Invalid dagcircuit input node {self.input_map[wire]} has no output" - ) from e - return child is self.output_map[wire] - - def _remove_idle_wire(self, wire): - """Remove an idle qubit or bit from the circuit. - - Args: - wire (Bit): the wire to be removed, which MUST be idle. - """ - inp_node = self.input_map[wire] - oup_node = self.output_map[wire] - - self._multi_graph.remove_node(inp_node._node_id) - self._multi_graph.remove_node(oup_node._node_id) - self._wires.remove(wire) - del self.input_map[wire] - del self.output_map[wire] - - def _check_condition(self, name, condition): - """Verify that the condition is valid. - - Args: - name (string): used for error reporting - condition (tuple or None): a condition tuple (ClassicalRegister, int) or (Clbit, bool) - - Raises: - DAGCircuitError: if conditioning on an invalid register - """ - if condition is None: - return - resources = condition_resources(condition) - for creg in resources.cregs: - if creg.name not in self.cregs: - raise DAGCircuitError(f"invalid creg in condition for {name}") - if not set(resources.clbits).issubset(self.clbits): - raise DAGCircuitError(f"invalid clbits in condition for {name}") - - def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var, Any]): - """Check the values of a list of wire arguments. - - For each element of args, check that amap contains it. - - Args: - args: the elements to be checked - amap: a dictionary keyed on Qubits/Clbits - - Raises: - DAGCircuitError: if a qubit is not contained in amap - """ - # Check for each wire - for wire in args: - if wire not in amap: - raise DAGCircuitError(f"wire {wire} not found in {amap}") - - def _increment_op(self, op_name): - if op_name in self._op_names: - self._op_names[op_name] += 1 - else: - self._op_names[op_name] = 1 - - def _decrement_op(self, op_name): - if self._op_names[op_name] == 1: - del self._op_names[op_name] - else: - self._op_names[op_name] -= 1 - - def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): - """Return a copy of self with the same structure but empty. - - That structure includes: - * name and other metadata - * global phase - * duration - * all the qubits and clbits, including the registers - * all the classical variables, with a mode defined by ``vars_mode``. - - Args: - vars_mode: The mode to handle realtime variables in. - - alike - The variables in the output DAG will have the same declaration semantics as - in the original circuit. For example, ``input`` variables in the source will be - ``input`` variables in the output DAG. - - captures - All variables will be converted to captured variables. This is useful when you - are building a new layer for an existing DAG that you will want to - :meth:`compose` onto the base, since :meth:`compose` can inline captures onto - the base circuit (but not other variables). - - drop - The output DAG will have no variables defined. - - Returns: - DAGCircuit: An empty copy of self. - """ - target_dag = DAGCircuit() - target_dag.name = self.name - target_dag._global_phase = self._global_phase - target_dag.duration = self.duration - target_dag.unit = self.unit - target_dag.metadata = self.metadata - target_dag._key_cache = self._key_cache - - target_dag.add_qubits(self.qubits) - target_dag.add_clbits(self.clbits) - - for qreg in self.qregs.values(): - target_dag.add_qreg(qreg) - for creg in self.cregs.values(): - target_dag.add_creg(creg) - - if vars_mode == "alike": - for var in self.iter_input_vars(): - target_dag.add_input_var(var) - for var in self.iter_captured_vars(): - target_dag.add_captured_var(var) - for var in self.iter_declared_vars(): - target_dag.add_declared_var(var) - elif vars_mode == "captures": - for var in self.iter_vars(): - target_dag.add_captured_var(var) - elif vars_mode == "drop": - pass - else: # pragma: no cover - raise ValueError(f"unknown vars_mode: '{vars_mode}'") - - return target_dag - - def _apply_op_node_back(self, node: DAGOpNode, *, check: bool = False): - additional = () - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(node.cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new in-edges from predecessors of the output nodes to the - # operation node while deleting the old in-edges of the output nodes - # and adding new edges from the operation node to each output node - self._multi_graph.insert_node_on_in_edges_multiple( - node._node_id, - [ - self.output_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def apply_operation_back( - self, - op: Operation, - qargs: Iterable[Qubit] = (), - cargs: Iterable[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the output of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if a leaf node is connected to multiple outputs - - """ - return self._apply_op_node_back( - DAGOpNode(op=op, qargs=tuple(qargs), cargs=tuple(cargs), dag=self), check=check - ) - - def apply_operation_front( - self, - op: Operation, - qargs: Sequence[Qubit] = (), - cargs: Sequence[Clbit] = (), - *, - check: bool = True, - ) -> DAGOpNode: - """Apply an operation to the input of the circuit. - - Args: - op (qiskit.circuit.Operation): the operation associated with the DAG node - qargs (tuple[~qiskit.circuit.Qubit]): qubits that op will be applied to - cargs (tuple[Clbit]): cbits that op will be applied to - check (bool): If ``True`` (default), this function will enforce that the - :class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are - :class:`~.circuit.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must* - uphold these invariants itself, but the cost of several checks will be skipped. - This is most useful when building a new DAG from a source of known-good nodes. - Returns: - DAGOpNode: the node for the op that was added to the dag - - Raises: - DAGCircuitError: if initial nodes connected to multiple out edges - """ - qargs = tuple(qargs) - cargs = tuple(cargs) - additional = () - - node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) - if _may_have_additional_wires(node): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node.op)).difference(cargs) - - if check: - self._check_condition(node.name, node.condition) - self._check_wires(node.qargs, self.output_map) - self._check_wires(node.cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node._node_id = self._multi_graph.add_node(node) - self._increment_op(node.name) - - # Add new out-edges to successors of the input nodes from the - # operation node while deleting the old out-edges of the input nodes - # and adding new edges to the operation node from each input node - self._multi_graph.insert_node_on_out_edges_multiple( - node._node_id, - [ - self.input_map[bit]._node_id - for bits in (node.qargs, node.cargs, additional) - for bit in bits - ], - ) - return node - - def compose( - self, other, qubits=None, clbits=None, front=False, inplace=True, *, inline_captures=False - ): - """Compose the ``other`` circuit onto the output of this circuit. - - A subset of input wires of ``other`` are mapped - to a subset of output wires of this circuit. - - ``other`` can be narrower or of equal width to ``self``. - - Args: - other (DAGCircuit): circuit to compose with self - qubits (list[~qiskit.circuit.Qubit|int]): qubits of self to compose onto. - clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed (not implemented yet) - inplace (bool): If True, modify the object. Otherwise return composed circuit. - inline_captures (bool): If ``True``, variables marked as "captures" in the ``other`` DAG - will inlined onto existing uses of those same variables in ``self``. If ``False``, - all variables in ``other`` are required to be distinct from ``self``, and they will - be added to ``self``. - - .. - Note: unlike `QuantumCircuit.compose`, there's no `var_remap` argument here. That's - because the `DAGCircuit` inner-block structure isn't set up well to allow the recursion, - and `DAGCircuit.compose` is generally only used to rebuild a DAG from layers within - itself than to join unrelated circuits. While there's no strong motivating use-case - (unlike the `QuantumCircuit` equivalent), it's safer and more performant to not provide - the option. - - Returns: - DAGCircuit: the composed dag (returns None if inplace==True). - - Raises: - DAGCircuitError: if ``other`` is wider or there are duplicate edge mappings. - """ - if front: - raise DAGCircuitError("Front composition not supported yet.") - - if len(other.qubits) > len(self.qubits) or len(other.clbits) > len(self.clbits): - raise DAGCircuitError( - "Trying to compose with another DAGCircuit which has more 'in' edges." - ) - - # number of qubits and clbits must match number in circuit or None - identity_qubit_map = dict(zip(other.qubits, self.qubits)) - identity_clbit_map = dict(zip(other.clbits, self.clbits)) - if qubits is None: - qubit_map = identity_qubit_map - elif len(qubits) != len(other.qubits): - raise DAGCircuitError( - "Number of items in qubits parameter does not" - " match number of qubits in the circuit." - ) - else: - qubit_map = { - other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q) - for i, q in enumerate(qubits) - } - if clbits is None: - clbit_map = identity_clbit_map - elif len(clbits) != len(other.clbits): - raise DAGCircuitError( - "Number of items in clbits parameter does not" - " match number of clbits in the circuit." - ) - else: - clbit_map = { - other.clbits[i]: (self.clbits[c] if isinstance(c, int) else c) - for i, c in enumerate(clbits) - } - edge_map = {**qubit_map, **clbit_map} or None - - # if no edge_map, try to do a 1-1 mapping in order - if edge_map is None: - edge_map = {**identity_qubit_map, **identity_clbit_map} - - # Check the edge_map for duplicate values - if len(set(edge_map.values())) != len(edge_map): - raise DAGCircuitError("duplicates in wire_map") - - # Compose - if inplace: - dag = self - else: - dag = copy.deepcopy(self) - dag.global_phase += other.global_phase - - for gate, cals in other.calibrations.items(): - dag._calibrations[gate].update(cals) - - # This is all the handling we need for realtime variables, if there's no remapping. They: - # - # * get added to the DAG and then operations involving them get appended on normally. - # * get inlined onto an existing variable, then operations get appended normally. - # * there's a clash or a failed inlining, and we just raise an error. - # - # Notably if there's no remapping, there's no need to recurse into control-flow or to do any - # Var rewriting during the Expr visits. - for var in other.iter_input_vars(): - dag.add_input_var(var) - if inline_captures: - for var in other.iter_captured_vars(): - if not dag.has_var(var): - raise DAGCircuitError( - f"Variable '{var}' to be inlined is not in the base DAG." - " If you wanted it to be automatically added, use `inline_captures=False`." - ) - else: - for var in other.iter_captured_vars(): - dag.add_captured_var(var) - for var in other.iter_declared_vars(): - dag.add_declared_var(var) - - # Ensure that the error raised here is a `DAGCircuitError` for backwards compatibility. - def _reject_new_register(reg): - raise DAGCircuitError(f"No register with '{reg.bits}' to map this expression onto.") - - variable_mapper = _classical_resource_map.VariableMapper( - dag.cregs.values(), edge_map, add_register=_reject_new_register - ) - for nd in other.topological_nodes(): - if isinstance(nd, DAGInNode): - if isinstance(nd.wire, Bit): - # if in edge_map, get new name, else use existing name - m_wire = edge_map.get(nd.wire, nd.wire) - # the mapped wire should already exist - if m_wire not in dag.output_map: - raise DAGCircuitError( - f"wire {m_wire.register.name}[{m_wire.index}] not in self" - ) - if nd.wire not in other._wires: - raise DAGCircuitError( - f"inconsistent wire type for {nd.register.name}[{nd.wire.index}] in other" - ) - # If it's a Var wire, we already checked that it exists in the destination. - elif isinstance(nd, DAGOutNode): - # ignore output nodes - pass - elif isinstance(nd, DAGOpNode): - m_qargs = [edge_map.get(x, x) for x in nd.qargs] - m_cargs = [edge_map.get(x, x) for x in nd.cargs] - inst = nd._to_circuit_instruction(deepcopy=True) - m_op = None - if inst.condition is not None: - if inst.is_control_flow(): - m_op = inst.operation - m_op.condition = variable_mapper.map_condition( - inst.condition, allow_reorder=True - ) - else: - m_op = inst.operation.c_if( - *variable_mapper.map_condition(inst.condition, allow_reorder=True) - ) - elif inst.is_control_flow() and isinstance(inst.operation, SwitchCaseOp): - m_op = inst.operation - m_op.target = variable_mapper.map_target(m_op.target) - if m_op is None: - inst = inst.replace(qubits=m_qargs, clbits=m_cargs) - else: - inst = inst.replace(operation=m_op, qubits=m_qargs, clbits=m_cargs) - dag._apply_op_node_back(DAGOpNode.from_instruction(inst), check=False) - else: - raise DAGCircuitError(f"bad node type {type(nd)}") - - if not inplace: - return dag - else: - return None - - def reverse_ops(self): - """Reverse the operations in the ``self`` circuit. - - Returns: - DAGCircuit: the reversed dag. - """ - # TODO: speed up - # pylint: disable=cyclic-import - from qiskit.converters import dag_to_circuit, circuit_to_dag - - qc = dag_to_circuit(self) - reversed_qc = qc.reverse_ops() - reversed_dag = circuit_to_dag(reversed_qc) - return reversed_dag - - def idle_wires(self, ignore=None): - """Return idle wires. - - Args: - ignore (list(str)): List of node names to ignore. Default: [] - - Yields: - Bit: Bit in idle wire. - - Raises: - DAGCircuitError: If the DAG is invalid - """ - if ignore is None: - ignore = set() - ignore_set = set(ignore) - for wire in self._wires: - if not ignore: - if self._is_wire_idle(wire): - yield wire - else: - for node in self.nodes_on_wire(wire, only_ops=True): - if node.op.name not in ignore_set: - # If we found an op node outside of ignore we can stop iterating over the wire - break - else: - yield wire - - def size(self, *, recurse: bool = False): - """Return the number of operations. If there is control flow present, this count may only - be an estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops with - known-length iterators are counted unrolled. If-else blocks sum both of the two - branches. While loops are counted as if the loop body runs once only. Defaults to - ``False`` and raises :class:`.DAGCircuitError` if any control flow is present, to - avoid silently returning a mostly meaningless number. - - Returns: - int: the circuit size - - Raises: - DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with - ``recurse=True``, or any control flow is present in a non-recursive call. - """ - length = len(self._multi_graph) - 2 * len(self._wires) - if not recurse: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Size with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - return length - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - for node in self.op_nodes(ControlFlowOp): - if isinstance(node.op, ForLoopOp): - indexset = node.op.params[0] - inner = len(indexset) * circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, WhileLoopOp): - inner = circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, (IfElseOp, SwitchCaseOp)): - inner = sum(circuit_to_dag(block).size(recurse=True) for block in node.op.blocks) - else: - raise DAGCircuitError(f"unknown control-flow type: '{node.op.name}'") - # Replace the "1" for the node itself with the actual count. - length += inner - 1 - return length - - def depth(self, *, recurse: bool = False): - """Return the circuit depth. If there is control flow present, this count may only be an - estimate, as the complete control-flow path cannot be statically known. - - Args: - recurse: if ``True``, then recurse into control-flow operations. For loops - with known-length iterators are counted as if the loop had been manually unrolled - (*i.e.* with each iteration of the loop body written out explicitly). - If-else blocks take the longer case of the two branches. While loops are counted as - if the loop body runs once only. Defaults to ``False`` and raises - :class:`.DAGCircuitError` if any control flow is present, to avoid silently - returning a nonsensical number. - - Returns: - int: the circuit depth - - Raises: - DAGCircuitError: if not a directed acyclic graph - DAGCircuitError: if unknown control flow is present in a recursive call, or any control - flow is present in a non-recursive call. - """ - if recurse: - from qiskit.converters import circuit_to_dag # pylint: disable=cyclic-import - - node_lookup = {} - for node in self.op_nodes(ControlFlowOp): - weight = len(node.op.params[0]) if isinstance(node.op, ForLoopOp) else 1 - if weight == 0: - node_lookup[node._node_id] = 0 - else: - node_lookup[node._node_id] = weight * max( - circuit_to_dag(block).depth(recurse=True) for block in node.op.blocks - ) - - def weight_fn(_source, target, _edge): - return node_lookup.get(target, 1) - - else: - if any(x in self._op_names for x in CONTROL_FLOW_OP_NAMES): - raise DAGCircuitError( - "Depth with control flow is ambiguous." - " You may use `recurse=True` to get a result," - " but see this method's documentation for the meaning of this." - ) - weight_fn = None - - try: - depth = rx.dag_longest_path_length(self._multi_graph, weight_fn) - 1 - except rx.DAGHasCycle as ex: - raise DAGCircuitError("not a DAG") from ex - return depth if depth >= 0 else 0 - - def width(self): - """Return the total number of qubits + clbits used by the circuit. - This function formerly returned the number of qubits by the calculation - return len(self._wires) - self.num_clbits() - but was changed by issue #2564 to return number of qubits + clbits - with the new function DAGCircuit.num_qubits replacing the former - semantic of DAGCircuit.width(). - """ - return len(self._wires) - - def num_qubits(self): - """Return the total number of qubits used by the circuit. - num_qubits() replaces former use of width(). - DAGCircuit.width() now returns qubits + clbits for - consistency with Circuit.width() [qiskit-terra #2564]. - """ - return len(self.qubits) - - def num_clbits(self): - """Return the total number of classical bits used by the circuit.""" - return len(self.clbits) - - def num_tensor_factors(self): - """Compute how many components the circuit can decompose into.""" - return rx.number_weakly_connected_components(self._multi_graph) - - @property - def num_vars(self): - """Total number of classical variables tracked by the circuit.""" - return len(self._vars_info) - - @property - def num_input_vars(self): - """Number of input classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.INPUT]) - - @property - def num_captured_vars(self): - """Number of captured classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.CAPTURE]) - - @property - def num_declared_vars(self): - """Number of declared local classical variables tracked by the circuit.""" - return len(self._vars_by_type[_DAGVarType.DECLARE]) - - def iter_vars(self): - """Iterable over all the classical variables tracked by the circuit.""" - return itertools.chain.from_iterable(self._vars_by_type.values()) - - def iter_input_vars(self): - """Iterable over the input classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.INPUT]) - - def iter_captured_vars(self): - """Iterable over the captured classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.CAPTURE]) - - def iter_declared_vars(self): - """Iterable over the declared local classical variables tracked by the circuit.""" - return iter(self._vars_by_type[_DAGVarType.DECLARE]) - - def has_var(self, var: str | expr.Var) -> bool: - """Is this realtime variable in the DAG? - - Args: - var: the variable or name to check. - """ - if isinstance(var, str): - return var in self._vars_info - return (info := self._vars_info.get(var.name, False)) and info.var is var - - def __eq__(self, other): - # Try to convert to float, but in case of unbound ParameterExpressions - # a TypeError will be raise, fallback to normal equality in those - # cases - try: - self_phase = float(self.global_phase) - other_phase = float(other.global_phase) - if ( - abs((self_phase - other_phase + np.pi) % (2 * np.pi) - np.pi) > 1.0e-10 - ): # TODO: atol? - return False - except TypeError: - if self.global_phase != other.global_phase: - return False - if self.calibrations != other.calibrations: - return False - - # We don't do any semantic equivalence between Var nodes, as things stand; DAGs can only be - # equal in our mind if they use the exact same UUID vars. - if self._vars_by_type != other._vars_by_type: - return False - - self_bit_indices = {bit: idx for idx, bit in enumerate(self.qubits + self.clbits)} - other_bit_indices = {bit: idx for idx, bit in enumerate(other.qubits + other.clbits)} - - self_qreg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.qregs.items() - } - self_creg_indices = { - regname: [self_bit_indices[bit] for bit in reg] for regname, reg in self.cregs.items() - } - - other_qreg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.qregs.items() - } - other_creg_indices = { - regname: [other_bit_indices[bit] for bit in reg] for regname, reg in other.cregs.items() - } - if self_qreg_indices != other_qreg_indices or self_creg_indices != other_creg_indices: - return False - - def node_eq(node_self, node_other): - return DAGNode.semantic_eq(node_self, node_other, self_bit_indices, other_bit_indices) - - return rx.is_isomorphic_node_match(self._multi_graph, other._multi_graph, node_eq) - - def topological_nodes(self, key=None): - """ - Yield nodes in topological order. - - 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. - - Returns: - generator(DAGOpNode, DAGInNode, or DAGOutNode): node in topological order - """ - - def _key(x): - return x.sort_key - - if key is None: - key = _key - - return iter(rx.lexicographical_topological_sort(self._multi_graph, key=key)) - - def topological_op_nodes(self, key: Callable | None = None) -> Generator[DAGOpNode, Any, Any]: - """ - Yield op nodes in topological order. - - Allowed to pass in specific key to break ties in top order - - 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. - - Returns: - generator(DAGOpNode): op node in topological order - """ - return (nd for nd in self.topological_nodes(key) if isinstance(nd, DAGOpNode)) - - def replace_block_with_op( - self, node_block: list[DAGOpNode], op: Operation, wire_pos_map, cycle_check=True - ): - """Replace a block of nodes with a single node. - - This is used to consolidate a block of DAGOpNodes into a single - operation. A typical example is a block of gates being consolidated - into a single ``UnitaryGate`` representing the unitary matrix of the - block. - - Args: - node_block (List[DAGNode]): A list of dag nodes that represents the - node block to be replaced - op (qiskit.circuit.Operation): The operation to replace the - block with - wire_pos_map (Dict[Bit, int]): The dictionary mapping the bits to their positions in the - output ``qargs`` or ``cargs``. This is necessary to reconstruct the arg order over - multiple gates in the combined single op node. If a :class:`.Bit` is not in the - dictionary, it will not be added to the args; this can be useful when dealing with - control-flow operations that have inherent bits in their ``condition`` or ``target`` - fields. :class:`.expr.Var` wires similarly do not need to be in this map, since - they will never be in ``qargs`` or ``cargs``. - cycle_check (bool): When set to True this method will check that - replacing the provided ``node_block`` with a single node - would introduce a cycle (which would invalidate the - ``DAGCircuit``) and will raise a ``DAGCircuitError`` if a cycle - would be introduced. This checking comes with a run time - penalty. If you can guarantee that your input ``node_block`` is - a contiguous block and won't introduce a cycle when it's - contracted to a single node, this can be set to ``False`` to - improve the runtime performance of this method. - - Raises: - DAGCircuitError: if ``cycle_check`` is set to ``True`` and replacing - the specified block introduces a cycle or if ``node_block`` is - empty. - - Returns: - DAGOpNode: The op node that replaces the block. - """ - block_qargs = set() - block_cargs = set() - block_ids = [x._node_id for x in node_block] - - # If node block is empty return early - if not node_block: - raise DAGCircuitError("Can't replace an empty node_block") - - for nd in node_block: - block_qargs |= set(nd.qargs) - block_cargs |= set(nd.cargs) - if (condition := getattr(nd, "condition", None)) is not None: - block_cargs.update(condition_resources(condition).clbits) - elif nd.name in CONTROL_FLOW_OP_NAMES and isinstance(nd.op, SwitchCaseOp): - if isinstance(nd.op.target, Clbit): - block_cargs.add(nd.op.target) - elif isinstance(nd.op.target, ClassicalRegister): - block_cargs.update(nd.op.target) - else: - block_cargs.update(node_resources(nd.op.target).clbits) - - block_qargs = [bit for bit in block_qargs if bit in wire_pos_map] - block_qargs.sort(key=wire_pos_map.get) - block_cargs = [bit for bit in block_cargs if bit in wire_pos_map] - block_cargs.sort(key=wire_pos_map.get) - new_node = DAGOpNode(op, block_qargs, block_cargs, dag=self) - - # check the op to insert matches the number of qubits we put it on - if op.num_qubits != len(block_qargs): - raise DAGCircuitError( - f"Number of qubits in the replacement operation ({op.num_qubits}) is not equal to " - f"the number of qubits in the block ({len(block_qargs)})!" - ) - - try: - new_node._node_id = self._multi_graph.contract_nodes( - block_ids, new_node, check_cycle=cycle_check - ) - except rx.DAGWouldCycle as ex: - raise DAGCircuitError( - "Replacing the specified node block would introduce a cycle" - ) from ex - - self._increment_op(op.name) - - for nd in node_block: - self._decrement_op(nd.name) - - return new_node - - def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condition=True): - """Replace one node with dag. - - Args: - node (DAGOpNode): node to substitute - input_dag (DAGCircuit): circuit that will substitute the node. - wires (list[Bit] | Dict[Bit, Bit]): gives an order for (qu)bits - in the input circuit. If a list, then the bits refer to those in the ``input_dag``, - and the order gets matched to the node wires by qargs first, then cargs, then - conditions. If a dictionary, then a mapping of bits in the ``input_dag`` to those - that the ``node`` acts on. - - Standalone :class:`~.expr.Var` nodes cannot currently be remapped as part of the - substitution; the ``input_dag`` should be defined over the correct set of variables - already. - - .. - The rule about not remapping `Var`s is to avoid performance pitfalls and reduce - complexity; the creator of the input DAG should easily be able to arrange for - the correct `Var`s to be used, and doing so avoids us needing to recurse through - control-flow operations to do deep remappings. - propagate_condition (bool): If ``True`` (default), then any ``condition`` attribute on - the operation within ``node`` is propagated to each node in the ``input_dag``. If - ``False``, then the ``input_dag`` is assumed to faithfully implement suitable - conditional logic already. This is ignored for :class:`.ControlFlowOp`\\ s (i.e. - treated as if it is ``False``); replacements of those must already fulfill the same - conditional logic or this function would be close to useless for them. - - Returns: - dict: maps node IDs from `input_dag` to their new node incarnations in `self`. - - Raises: - DAGCircuitError: if met with unexpected predecessor/successors - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError(f"expected node DAGOpNode, got {type(node)}") - - if isinstance(wires, dict): - wire_map = wires - else: - wires = input_dag.wires if wires is None else wires - node_cargs = set(node.cargs) - node_wire_order = list(node.qargs) + list(node.cargs) - # If we're not propagating it, the number of wires in the input DAG should include the - # condition as well. - if not propagate_condition and _may_have_additional_wires(node): - node_wire_order += [ - wire for wire in _additional_wires(node.op) if wire not in node_cargs - ] - if len(wires) != len(node_wire_order): - raise DAGCircuitError( - f"bit mapping invalid: expected {len(node_wire_order)}, got {len(wires)}" - ) - wire_map = dict(zip(wires, node_wire_order)) - if len(wire_map) != len(node_wire_order): - raise DAGCircuitError("bit mapping invalid: some bits have duplicate entries") - for input_dag_wire, our_wire in wire_map.items(): - if our_wire not in self.input_map: - raise DAGCircuitError(f"bit mapping invalid: {our_wire} is not in this DAG") - if isinstance(our_wire, expr.Var) or isinstance(input_dag_wire, expr.Var): - raise DAGCircuitError("`Var` nodes cannot be remapped during substitution") - # Support mapping indiscriminately between Qubit and AncillaQubit, etc. - check_type = Qubit if isinstance(our_wire, Qubit) else Clbit - if not isinstance(input_dag_wire, check_type): - raise DAGCircuitError( - f"bit mapping invalid: {input_dag_wire} and {our_wire} are different bit types" - ) - if _may_have_additional_wires(node): - node_vars = {var for var in _additional_wires(node.op) if isinstance(var, expr.Var)} - else: - node_vars = set() - dag_vars = set(input_dag.iter_vars()) - if dag_vars - node_vars: - raise DAGCircuitError( - "Cannot replace a node with a DAG with more variables." - f" Variables in node: {node_vars}." - f" Variables in DAG: {dag_vars}." - ) - for var in dag_vars: - wire_map[var] = var - - reverse_wire_map = {b: a for a, b in wire_map.items()} - # It doesn't make sense to try and propagate a condition from a control-flow op; a - # replacement for the control-flow op should implement the operation completely. - if propagate_condition and not node.is_control_flow() and node.condition is not None: - in_dag = input_dag.copy_empty_like() - # The remapping of `condition` below is still using the old code that assumes a 2-tuple. - # This is because this remapping code only makes sense in the case of non-control-flow - # operations being replaced. These can only have the 2-tuple conditions, and the - # ability to set a condition at an individual node level will be deprecated and removed - # 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. - target, value = node.condition - if isinstance(target, Clbit): - new_target = reverse_wire_map.get(target, Clbit()) - if new_target not in wire_map: - in_dag.add_clbits([new_target]) - wire_map[new_target], reverse_wire_map[target] = target, new_target - target_cargs = {new_target} - else: # ClassicalRegister - mapped_bits = [reverse_wire_map.get(bit, Clbit()) for bit in target] - for ours, theirs in zip(target, mapped_bits): - # Update to any new dummy bits we just created to the wire maps. - wire_map[theirs], reverse_wire_map[ours] = ours, theirs - new_target = ClassicalRegister(bits=mapped_bits) - in_dag.add_creg(new_target) - target_cargs = set(new_target) - new_condition = (new_target, value) - for in_node in input_dag.topological_op_nodes(): - if getattr(in_node.op, "condition", None) is not None: - raise DAGCircuitError( - "cannot propagate a condition to an element that already has one" - ) - if target_cargs.intersection(in_node.cargs): - # This is for backwards compatibility with early versions of the method, as it is - # a tested part of the API. In the newer model of a condition being an integral - # part of the operation (not a separate property to be copied over), this error - # is overzealous, because it forbids a custom instruction from implementing the - # condition within its definition rather than at the top level. - raise DAGCircuitError( - "cannot propagate a condition to an element that acts on those bits" - ) - new_op = copy.copy(in_node.op) - if new_condition: - if not isinstance(new_op, ControlFlowOp): - new_op = new_op.c_if(*new_condition) - else: - new_op.condition = new_condition - in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False) - else: - in_dag = input_dag - - if in_dag.global_phase: - self.global_phase += in_dag.global_phase - - # Add wire from pred to succ if no ops on mapped wire on ``in_dag`` - # rustworkx's substitute_node_with_subgraph lacks the DAGCircuit - # context to know what to do in this case (the method won't even see - # these nodes because they're filtered) so we manually retain the - # edges prior to calling substitute_node_with_subgraph and set the - # edge_map_fn callback kwarg to skip these edges when they're - # encountered. - for in_dag_wire, self_wire in wire_map.items(): - input_node = in_dag.input_map[in_dag_wire] - output_node = in_dag.output_map[in_dag_wire] - if in_dag._multi_graph.has_edge(input_node._node_id, output_node._node_id): - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=self_wire: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, self_wire) - for contracted_var in node_vars - dag_vars: - pred = self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - succ = self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge, wire=contracted_var: edge == wire - )[0] - self._multi_graph.add_edge(pred._node_id, succ._node_id, contracted_var) - - # Exclude any nodes from in_dag that are not a DAGOpNode or are on - # wires outside the set specified by the wires kwarg - def filter_fn(node): - if not isinstance(node, DAGOpNode): - return False - for _, _, wire in in_dag.edges(node): - if wire not in wire_map: - return False - return True - - # Map edges into and out of node to the appropriate node from in_dag - def edge_map_fn(source, _target, self_wire): - wire = reverse_wire_map[self_wire] - # successor edge - if source == node._node_id: - wire_output_id = in_dag.output_map[wire]._node_id - out_index = in_dag._multi_graph.predecessor_indices(wire_output_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - # predecessor edge - else: - wire_input_id = in_dag.input_map[wire]._node_id - out_index = in_dag._multi_graph.successor_indices(wire_input_id)[0] - # Edge directly from from input nodes to output nodes in in_dag are - # already handled prior to calling rustworkx. Don't map these edges - # in rustworkx. - if not isinstance(in_dag._multi_graph[out_index], DAGOpNode): - return None - return out_index - - # Adjust edge weights from in_dag - def edge_weight_map(wire): - return wire_map[wire] - - node_map = self._multi_graph.substitute_node_with_subgraph( - node._node_id, in_dag._multi_graph, edge_map_fn, filter_fn, edge_weight_map - ) - self._decrement_op(node.name) - - variable_mapper = _classical_resource_map.VariableMapper( - self.cregs.values(), wire_map, add_register=self.add_creg - ) - # Iterate over nodes of input_circuit and update wires in node objects migrated - # from in_dag - for old_node_index, new_node_index in node_map.items(): - # update node attributes - old_node = in_dag._multi_graph[old_node_index] - m_op = None - if not old_node.is_standard_gate() and isinstance(old_node.op, SwitchCaseOp): - m_op = SwitchCaseOp( - variable_mapper.map_target(old_node.op.target), - old_node.op.cases_specifier(), - label=old_node.op.label, - ) - elif old_node.condition is not None: - m_op = old_node.op - if old_node.is_control_flow(): - m_op.condition = variable_mapper.map_condition(m_op.condition) - else: - new_condition = variable_mapper.map_condition(m_op.condition) - if new_condition is not None: - m_op = m_op.c_if(*new_condition) - m_qargs = [wire_map[x] for x in old_node.qargs] - m_cargs = [wire_map[x] for x in old_node.cargs] - old_instruction = old_node._to_circuit_instruction() - if m_op is None: - new_instruction = old_instruction.replace(qubits=m_qargs, clbits=m_cargs) - else: - new_instruction = old_instruction.replace( - operation=m_op, qubits=m_qargs, clbits=m_cargs - ) - new_node = DAGOpNode.from_instruction(new_instruction) - new_node._node_id = new_node_index - self._multi_graph[new_node_index] = new_node - self._increment_op(new_node.name) - - return {k: self._multi_graph[v] for k, v in node_map.items()} - - def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_condition=True): - """Replace an DAGOpNode with a single operation. qargs, cargs and - conditions for the new operation will be inferred from the node to be - replaced. The new operation will be checked to match the shape of the - replaced operation. - - Args: - node (DAGOpNode): Node to be replaced - op (qiskit.circuit.Operation): The :class:`qiskit.circuit.Operation` - instance to be added to the DAG - inplace (bool): Optional, default False. If True, existing DAG node - will be modified to include op. Otherwise, a new DAG node will - be used. - propagate_condition (bool): Optional, default True. If True, a condition on the - ``node`` to be replaced will be applied to the new ``op``. This is the legacy - behavior. If either node is a control-flow operation, this will be ignored. If - the ``op`` already has a condition, :exc:`.DAGCircuitError` is raised. - - Returns: - DAGOpNode: the new node containing the added operation. - - Raises: - DAGCircuitError: If replacement operation was incompatible with - location of target node. - """ - - if not isinstance(node, DAGOpNode): - raise DAGCircuitError("Only DAGOpNodes can be replaced.") - - if node.op.num_qubits != op.num_qubits or node.op.num_clbits != op.num_clbits: - raise DAGCircuitError( - f"Cannot replace node of width ({node.op.num_qubits} qubits, " - f"{node.op.num_clbits} clbits) with " - f"operation of mismatched width ({op.num_qubits} qubits, " - f"{op.num_clbits} clbits)." - ) - - # This might include wires that are inherent to the node, like in its `condition` or - # `target` fields, so might be wider than `node.op.num_{qu,cl}bits`. - current_wires = {wire for _, _, wire in self.edges(node)} - new_wires = set(node.qargs) | set(node.cargs) | set(_additional_wires(op)) - - if propagate_condition and not ( - isinstance(node.op, ControlFlowOp) or isinstance(op, ControlFlowOp) - ): - if getattr(op, "condition", None) is not None: - raise DAGCircuitError( - "Cannot propagate a condition to an operation that already has one." - ) - if (old_condition := getattr(node.op, "condition", None)) is not None: - if not isinstance(op, Instruction): - raise DAGCircuitError("Cannot add a condition on a generic Operation.") - if not isinstance(node.op, ControlFlowOp): - op = op.c_if(*old_condition) - else: - op.condition = old_condition - new_wires.update(condition_resources(old_condition).clbits) - - if new_wires != current_wires: - # The new wires must be a non-strict subset of the current wires; if they add new wires, - # we'd not know where to cut the existing wire to insert the new dependency. - raise DAGCircuitError( - f"New operation '{op}' does not span the same wires as the old node '{node}'." - f" New wires: {new_wires}, old wires: {current_wires}." - ) - - if inplace: - if op.name != node.op.name: - self._increment_op(op.name) - self._decrement_op(node.name) - node.op = op - return node - - new_node = copy.copy(node) - new_node.op = op - self._multi_graph[node._node_id] = new_node - if op.name != node.name: - self._increment_op(op.name) - self._decrement_op(node.name) - return new_node - - def separable_circuits( - self, remove_idle_qubits: bool = False, *, vars_mode: _VarsMode = "alike" - ) -> list["DAGCircuit"]: - """Decompose the circuit into sets of qubits with no gates connecting them. - - Args: - remove_idle_qubits (bool): Flag denoting whether to remove idle qubits from - the separated circuits. If ``False``, each output circuit will contain the - same number of qubits as ``self``. - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - - Returns: - List[DAGCircuit]: The circuits resulting from separating ``self`` into sets - of disconnected qubits - - Each :class:`~.DAGCircuit` instance returned by this method will contain the same number of - clbits as ``self``. The global phase information in ``self`` will not be maintained - in the subcircuits returned by this method. - """ - connected_components = rx.weakly_connected_components(self._multi_graph) - - # Collect each disconnected subgraph - disconnected_subgraphs = [] - for components in connected_components: - disconnected_subgraphs.append(self._multi_graph.subgraph(list(components))) - - # Helper function for ensuring rustworkx nodes are returned in lexicographical, - # topological order - def _key(x): - return x.sort_key - - # Create new DAGCircuit objects from each of the rustworkx subgraph objects - decomposed_dags = [] - for subgraph in disconnected_subgraphs: - new_dag = self.copy_empty_like(vars_mode=vars_mode) - new_dag.global_phase = 0 - subgraph_is_classical = True - for node in rx.lexicographical_topological_sort(subgraph, key=_key): - if isinstance(node, DAGInNode): - if isinstance(node.wire, Qubit): - subgraph_is_classical = False - if not isinstance(node, DAGOpNode): - continue - new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - - # Ignore DAGs created for empty clbits - if not subgraph_is_classical: - decomposed_dags.append(new_dag) - - if remove_idle_qubits: - for dag in decomposed_dags: - dag.remove_qubits(*(bit for bit in dag.idle_wires() if isinstance(bit, Qubit))) - - return decomposed_dags - - def swap_nodes(self, node1, node2): - """Swap connected nodes e.g. due to commutation. - - Args: - node1 (OpNode): predecessor node - node2 (OpNode): successor node - - Raises: - DAGCircuitError: if either node is not an OpNode or nodes are not connected - """ - if not (isinstance(node1, DAGOpNode) and isinstance(node2, DAGOpNode)): - raise DAGCircuitError("nodes to swap are not both DAGOpNodes") - try: - connected_edges = self._multi_graph.get_all_edge_data(node1._node_id, node2._node_id) - except rx.NoEdgeBetweenNodes as no_common_edge: - raise DAGCircuitError("attempt to swap unconnected nodes") from no_common_edge - node1_id = node1._node_id - node2_id = node2._node_id - for edge in connected_edges[::-1]: - edge_find = lambda x, y=edge: x == y - edge_parent = self._multi_graph.find_predecessors_by_edge(node1_id, edge_find)[0] - self._multi_graph.remove_edge(edge_parent._node_id, node1_id) - self._multi_graph.add_edge(edge_parent._node_id, node2_id, edge) - edge_child = self._multi_graph.find_successors_by_edge(node2_id, edge_find)[0] - self._multi_graph.remove_edge(node1_id, node2_id) - self._multi_graph.add_edge(node2_id, node1_id, edge) - self._multi_graph.remove_edge(node2_id, edge_child._node_id) - self._multi_graph.add_edge(node1_id, edge_child._node_id, edge) - - def node(self, node_id): - """Get the node in the dag. - - Args: - node_id(int): Node identifier. - - Returns: - node: the node. - """ - return self._multi_graph[node_id] - - def nodes(self): - """Iterator for node values. - - Yield: - node: the node. - """ - yield from self._multi_graph.nodes() - - def edges(self, nodes=None): - """Iterator for edge values and source and dest node - - This works by returning the output edges from the specified nodes. If - no nodes are specified all edges from the graph are returned. - - Args: - nodes(DAGOpNode, DAGInNode, or DAGOutNode|list(DAGOpNode, DAGInNode, or DAGOutNode): - Either a list of nodes or a single input node. If none is specified, - all edges are returned from the graph. - - Yield: - edge: the edge in the same format as out_edges the tuple - (source node, destination node, edge data) - """ - if nodes is None: - nodes = self._multi_graph.nodes() - - elif isinstance(nodes, (DAGOpNode, DAGInNode, DAGOutNode)): - nodes = [nodes] - for node in nodes: - raw_nodes = self._multi_graph.out_edges(node._node_id) - for source, dest, edge in raw_nodes: - yield (self._multi_graph[source], self._multi_graph[dest], edge) - - def op_nodes(self, op=None, include_directives=True): - """Get the list of "op" nodes in the dag. - - Args: - op (Type): :class:`qiskit.circuit.Operation` subclass op nodes to - return. If None, return all op nodes. - include_directives (bool): include `barrier`, `snapshot` etc. - - Returns: - list[DAGOpNode]: the list of node ids containing the given op. - """ - nodes = [] - filter_is_nonstandard = getattr(op, "_standard_gate", None) is None - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode): - if not include_directives and node.is_directive(): - continue - if op is None or ( - # This middle catch is to avoid Python-space operation creation for most uses of - # `op`; we're usually just looking for control-flow ops, and standard gates - # aren't control-flow ops. - not (filter_is_nonstandard and node.is_standard_gate()) - and isinstance(node.op, op) - ): - nodes.append(node) - return nodes - - def gate_nodes(self): - """Get the list of gate nodes in the dag. - - Returns: - list[DAGOpNode]: the list of DAGOpNodes that represent gates. - """ - nodes = [] - for node in self.op_nodes(): - if isinstance(node.op, Gate): - nodes.append(node) - return nodes - - def named_nodes(self, *names): - """Get the set of "op" nodes with the given name.""" - named_nodes = [] - for node in self._multi_graph.nodes(): - if isinstance(node, DAGOpNode) and node.name in names: - named_nodes.append(node) - return named_nodes - - def two_qubit_ops(self): - """Get list of 2 qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) == 2: - ops.append(node) - return ops - - def multi_qubit_ops(self): - """Get list of 3+ qubit operations. Ignore directives like snapshot and barrier.""" - ops = [] - for node in self.op_nodes(include_directives=False): - if len(node.qargs) >= 3: - ops.append(node) - return ops - - def longest_path(self): - """Returns the longest path in the dag as a list of DAGOpNodes, DAGInNodes, and DAGOutNodes.""" - return [self._multi_graph[x] for x in rx.dag_longest_path(self._multi_graph)] - - def successors(self, node): - """Returns iterator of the successors of a node as DAGOpNodes and DAGOutNodes.""" - return iter(self._multi_graph.successors(node._node_id)) - - def predecessors(self, node): - """Returns iterator of the predecessors of a node as DAGOpNodes and DAGInNodes.""" - return iter(self._multi_graph.predecessors(node._node_id)) - - def op_successors(self, node): - """Returns iterator of "op" successors of a node in the dag.""" - return (succ for succ in self.successors(node) if isinstance(succ, DAGOpNode)) - - def op_predecessors(self, node): - """Returns the iterator of "op" predecessors of a node in the dag.""" - return (pred for pred in self.predecessors(node) if isinstance(pred, DAGOpNode)) - - def is_successor(self, node, node_succ): - """Checks if a second node is in the successors of node.""" - return self._multi_graph.has_edge(node._node_id, node_succ._node_id) - - def is_predecessor(self, node, node_pred): - """Checks if a second node is in the predecessors of node.""" - return self._multi_graph.has_edge(node_pred._node_id, node._node_id) - - def quantum_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a quantum edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_predecessors(self, node): - """Returns iterator of the predecessors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def ancestors(self, node): - """Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes.""" - return {self._multi_graph[x] for x in rx.ancestors(self._multi_graph, node._node_id)} - - def descendants(self, node): - """Returns set of the descendants of a node as DAGOpNodes and DAGOutNodes.""" - return {self._multi_graph[x] for x in rx.descendants(self._multi_graph, node._node_id)} - - def bfs_successors(self, node): - """ - Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node - and [DAGNode] is its successors in BFS order. - """ - return iter(rx.bfs_successors(self._multi_graph, node._node_id)) - - def quantum_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a quantum edge as Opnodes and DAGOutNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Qubit) - ) - ) - - def classical_successors(self, node): - """Returns iterator of the successors of a node that are - connected by a classical edge as DAGOpNodes and DAGInNodes.""" - return iter( - self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) - ) - ) - - def remove_op_node(self, node): - """Remove an operation node n. - - Add edges from predecessors to successors. - """ - if not isinstance(node, DAGOpNode): - raise DAGCircuitError( - f'The method remove_op_node only works on DAGOpNodes. A "{type(node)}" ' - "node type was wrongly provided." - ) - - self._multi_graph.remove_node_retain_edges_by_id(node._node_id) - self._decrement_op(node.name) - - def remove_ancestors_of(self, node): - """Remove all of the ancestor operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - # TODO: probably better to do all at once using - # multi_graph.remove_nodes_from; same for related functions ... - - for anc_node in anc: - if isinstance(anc_node, DAGOpNode): - self.remove_op_node(anc_node) - - def remove_descendants_of(self, node): - """Remove all of the descendant operation nodes of node.""" - desc = rx.descendants(self._multi_graph, node) - for desc_node in desc: - if isinstance(desc_node, DAGOpNode): - self.remove_op_node(desc_node) - - def remove_nonancestors_of(self, node): - """Remove all of the non-ancestors operation nodes of node.""" - anc = rx.ancestors(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(anc)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def remove_nondescendants_of(self, node): - """Remove all of the non-descendants operation nodes of node.""" - dec = rx.descendants(self._multi_graph, node) - comp = list(set(self._multi_graph.nodes()) - set(dec)) - for n in comp: - if isinstance(n, DAGOpNode): - self.remove_op_node(n) - - def front_layer(self): - """Return a list of op nodes in the first layer of this dag.""" - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return [] - - op_nodes = [node for node in next(graph_layers) if isinstance(node, DAGOpNode)] - - return op_nodes - - def layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit. - - A layer is a circuit whose gates act on disjoint qubits, i.e., - a layer has depth 1. The total number of layers equals the - circuit depth d. The layers are indexed from 0 to d-1 with the - earliest layer at index 0. The layers are constructed using a - greedy algorithm. Each returned layer is a dict containing - {"graph": circuit graph, "partition": list of qubit lists}. - - The returned layer contains new (but semantically equivalent) DAGOpNodes, DAGInNodes, - and DAGOutNodes. These are not the same as nodes of the original dag, but are equivalent - via DAGNode.semantic_eq(node1, node2). - - TODO: Gates that use the same cbits will end up in different - layers as this is currently implemented. This may not be - the desired behavior. - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - graph_layers = self.multigraph_layers() - try: - next(graph_layers) # Remove input nodes - except StopIteration: - return - - for graph_layer in graph_layers: - - # Get the op nodes from the layer, removing any input and output nodes. - op_nodes = [node for node in graph_layer if isinstance(node, DAGOpNode)] - - # 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 - # Drawing tools rely on _node_id to infer order of node creation - # so we need this to be preserved by layers() - op_nodes.sort(key=lambda nd: nd._node_id) - - # Stop yielding once there are no more op_nodes in a layer. - if not op_nodes: - return - - # Construct a shallow copy of self - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - for node in op_nodes: - new_layer._apply_op_node_back(node, check=False) - - # The quantum registers that have an operation in this layer. - support_list = [ - op_node.qargs for op_node in new_layer.op_nodes() if not op_node.is_directive() - ] - - yield {"graph": new_layer, "partition": support_list} - - def serial_layers(self, *, vars_mode: _VarsMode = "captures"): - """Yield a layer for all gates of this circuit. - - A serial layer is a circuit with one gate. The layers have the - same structure as in layers(). - - Args: - vars_mode: how any realtime :class:`~.expr.Var` nodes should be handled in the output - DAGs. See :meth:`copy_empty_like` for details on the modes. - """ - for next_node in self.topological_op_nodes(): - new_layer = self.copy_empty_like(vars_mode=vars_mode) - - # Save the support of the operation we add to the layer - support_list = [] - # Operation data - op = copy.copy(next_node.op) - qargs = copy.copy(next_node.qargs) - cargs = copy.copy(next_node.cargs) - - # Add node to new_layer - new_layer.apply_operation_back(op, qargs, cargs, check=False) - # Add operation to partition - if not getattr(next_node.op, "_directive", False): - support_list.append(list(qargs)) - l_dict = {"graph": new_layer, "partition": support_list} - yield l_dict - - def multigraph_layers(self): - """Yield layers of the multigraph.""" - first_layer = [x._node_id for x in self.input_map.values()] - return iter(rx.layers(self._multi_graph, first_layer)) - - def collect_runs(self, namelist): - """Return a set of non-conditional runs of "op" nodes with the given names. - - For example, "... h q[0]; cx q[0],q[1]; cx q[0],q[1]; h q[1]; .." - would produce the tuple of cx nodes as an element of the set returned - from a call to collect_runs(["cx"]). If instead the cx nodes were - "cx q[0],q[1]; cx q[1],q[0];", the method would still return the - pair in a tuple. The namelist can contain names that are not - in the circuit's basis. - - Nodes must have only one successor to continue the run. - """ - - def filter_fn(node): - return isinstance(node, DAGOpNode) and node.name in namelist and node.condition is None - - group_list = rx.collect_runs(self._multi_graph, filter_fn) - return {tuple(x) for x in group_list} - - def collect_1q_runs(self) -> list[list[DAGOpNode]]: - """Return a set of non-conditional runs of 1q "op" nodes.""" - return rx.collect_runs(self._multi_graph, collect_1q_runs_filter) - - def collect_2q_runs(self): - """Return a set of non-conditional runs of 2q "op" nodes.""" - - def color_fn(edge): - if isinstance(edge, Qubit): - return self.find_bit(edge).index - else: - return None - - return rx.collect_bicolor_runs(self._multi_graph, collect_2q_blocks_filter, color_fn) - - def nodes_on_wire(self, wire, only_ops=False): - """ - Iterator for nodes that affect a given wire. - - Args: - wire (Bit): the wire to be looked at. - only_ops (bool): True if only the ops nodes are wanted; - otherwise, all nodes are returned. - Yield: - Iterator: the successive nodes on the given wire - - Raises: - DAGCircuitError: if the given wire doesn't exist in the DAG - """ - current_node = self.input_map.get(wire, None) - - if not current_node: - raise DAGCircuitError(f"The given wire {str(wire)} is not present in the circuit") - - more_nodes = True - while more_nodes: - more_nodes = False - # allow user to just get ops on the wire - not the input/output nodes - if isinstance(current_node, DAGOpNode) or not only_ops: - yield current_node - - try: - current_node = self._multi_graph.find_adjacent_node_by_edge( - current_node._node_id, lambda x: wire == x - ) - more_nodes = True - except rx.NoSuitableNeighbors: - pass - - def count_ops(self, *, recurse: bool = True): - """Count the occurrences of operation names. - - Args: - recurse: if ``True`` (default), then recurse into control-flow operations. In all - cases, this counts only the number of times the operation appears in any possible - block; both branches of if-elses are counted, and for- and while-loop blocks are - only counted once. - - Returns: - Mapping[str, int]: a mapping of operation names to the number of times it appears. - """ - if not recurse or not CONTROL_FLOW_OP_NAMES.intersection(self._op_names): - return self._op_names.copy() - - # pylint: disable=cyclic-import - from qiskit.converters import circuit_to_dag - - def inner(dag, counts): - for name, count in dag._op_names.items(): - counts[name] += count - for node in dag.op_nodes(ControlFlowOp): - for block in node.op.blocks: - counts = inner(circuit_to_dag(block), counts) - return counts - - return dict(inner(self, defaultdict(int))) - - def count_ops_longest_path(self): - """Count the occurrences of operation names on the longest path. - - Returns a dictionary of counts keyed on the operation name. - """ - op_dict = {} - path = self.longest_path() - path = path[1:-1] # remove qubits at beginning and end of path - for node in path: - name = node.op.name - if name not in op_dict: - op_dict[name] = 1 - else: - op_dict[name] += 1 - return op_dict - - def quantum_causal_cone(self, qubit): - """ - Returns causal cone of a qubit. - - A qubit's causal cone is the set of qubits that can influence the output of that - qubit through interactions, whether through multi-qubit gates or operations. Knowing - the causal cone of a qubit can be useful when debugging faulty circuits, as it can - help identify which wire(s) may be causing the problem. - - This method does not consider any classical data dependency in the ``DAGCircuit``, - classical bit wires are ignored for the purposes of building the causal cone. - - Args: - qubit (~qiskit.circuit.Qubit): The output qubit for which we want to find the causal cone. - - Returns: - Set[~qiskit.circuit.Qubit]: The set of qubits whose interactions affect ``qubit``. - """ - # Retrieve the output node from the qubit - output_node = self.output_map.get(qubit, None) - if not output_node: - raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - - qubits_in_cone = {qubit} - queue = deque(self.quantum_predecessors(output_node)) - - # The processed_non_directive_nodes stores the set of processed non-directive nodes. - # This is an optimization to avoid considering the same non-directive node multiple - # times when reached from different paths. - # The directive nodes (such as barriers or measures) are trickier since when processing - # them we only add their predecessors that intersect qubits_in_cone. Hence, directive - # nodes have to be considered multiple times. - processed_non_directive_nodes = set() - - while queue: - node_to_check = queue.popleft() - - if isinstance(node_to_check, DAGOpNode): - # If the operation is not a directive (in particular not a barrier nor a measure), - # we do not do anything if it was already processed. Otherwise, we add its qubits - # to qubits_in_cone, and append its predecessors to queue. - if not getattr(node_to_check.op, "_directive"): - if node_to_check in processed_non_directive_nodes: - continue - qubits_in_cone = qubits_in_cone.union(set(node_to_check.qargs)) - processed_non_directive_nodes.add(node_to_check) - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode): - queue.append(pred) - else: - # Directives (such as barriers and measures) may be defined over all the qubits, - # yet not all of these qubits should be considered in the causal cone. So we - # only add those predecessors that have qubits in common with qubits_in_cone. - for pred in self.quantum_predecessors(node_to_check): - if isinstance(pred, DAGOpNode) and not qubits_in_cone.isdisjoint( - set(pred.qargs) - ): - queue.append(pred) - - return qubits_in_cone - - def properties(self): - """Return a dictionary of circuit properties.""" - summary = { - "size": self.size(), - "depth": self.depth(), - "width": self.width(), - "qubits": self.num_qubits(), - "bits": self.num_clbits(), - "factors": self.num_tensor_factors(), - "operations": self.count_ops(), - } - return summary - - def draw(self, scale=0.7, filename=None, style="color"): - """ - Draws the dag circuit. - - This function needs `Graphviz `_ to be - installed. Graphviz is not a python package and can't be pip installed - (the ``graphviz`` package on PyPI is a Python interface library for - Graphviz and does not actually install Graphviz). You can refer to - `the Graphviz documentation `__ on - how to install it. - - Args: - scale (float): scaling factor - filename (str): file path to save image to (format inferred from name) - style (str): - 'plain': B&W graph; - 'color' (default): color input/output/op nodes - - Returns: - Ipython.display.Image: if in Jupyter notebook and not saving to file, - otherwise None. - """ - from qiskit.visualization.dag_visualization import dag_drawer - - return dag_drawer(dag=self, scale=scale, filename=filename, style=style) - - -class _DAGVarType(enum.Enum): - INPUT = enum.auto() - CAPTURE = enum.auto() - DECLARE = enum.auto() - - -class _DAGVarInfo: - __slots__ = ("var", "type", "in_node", "out_node") - - def __init__(self, var: expr.Var, type_: _DAGVarType, in_node: DAGInNode, out_node: DAGOutNode): - self.var = var - self.type = type_ - self.in_node = in_node - self.out_node = out_node - - -def _may_have_additional_wires(node) -> bool: - """Return whether a given :class:`.DAGOpNode` may contain references to additional wires - locations within its :class:`.Operation`. If this is ``True``, it doesn't necessarily mean - that the operation _will_ access memory inherently, but a ``False`` return guarantees that it - won't. - - The memory might be classical bits or classical variables, such as a control-flow operation or a - store. - - Args: - operation (qiskit.dagcircuit.DAGOpNode): the operation to check. - """ - # This is separate to `_additional_wires` because most of the time there won't be any extra - # wires beyond the explicit `qargs` and `cargs` so we want a fast path to be able to skip - # creating and testing a generator for emptiness. - # - # If updating this, you most likely also need to update `_additional_wires`. - return node.condition is not None or ( - not node.is_standard_gate() and isinstance(node.op, (ControlFlowOp, Store)) - ) - - -def _additional_wires(operation) -> Iterable[Clbit | expr.Var]: - """Return an iterable over the additional tracked memory usage in this operation. These - additional wires include (for example, non-exhaustive) bits referred to by a ``condition`` or - the classical variables involved in control-flow operations. - - Args: - operation: the :class:`~.circuit.Operation` instance for a node. - - Returns: - Iterable: the additional wires inherent to this operation. - """ - # If updating this, you likely need to update `_may_have_additional_wires` too. - if (condition := getattr(operation, "condition", None)) is not None: - if isinstance(condition, expr.Expr): - yield from _wires_from_expr(condition) - else: - yield from condition_resources(condition).clbits - if isinstance(operation, ControlFlowOp): - yield from operation.iter_captured_vars() - if isinstance(operation, SwitchCaseOp): - target = operation.target - if isinstance(target, Clbit): - yield target - elif isinstance(target, ClassicalRegister): - yield from target - else: - yield from _wires_from_expr(target) - elif isinstance(operation, Store): - yield from _wires_from_expr(operation.lvalue) - yield from _wires_from_expr(operation.rvalue) - - -def _wires_from_expr(node: expr.Expr) -> Iterable[Clbit | expr.Var]: - for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): - yield var.var - elif isinstance(var.var, ClassicalRegister): - yield from var.var - else: - yield var +from qiskit._accelerate.circuit import DAGCircuit # pylint: disable=unused-import diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 9f35f6eda898..60e2a7465707 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -21,7 +21,6 @@ from qiskit.circuit import ( Clbit, ClassicalRegister, - ControlFlowOp, IfElseOp, WhileLoopOp, SwitchCaseOp, @@ -175,67 +174,3 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): } _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) - - -# Note: called from dag_node.rs. -def _semantic_eq(node1, node2, bit_indices1, bit_indices2): - """ - Check if DAG nodes are considered equivalent, e.g., as a node_match for - :func:`rustworkx.is_isomorphic_node_match`. - - Args: - node1 (DAGOpNode, DAGInNode, DAGOutNode): A node to compare. - node2 (DAGOpNode, DAGInNode, DAGOutNode): The other node to compare. - bit_indices1 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node1 - bit_indices2 (dict): Dictionary mapping Bit instances to their index - within the circuit containing node2 - - Return: - Bool: If node1 == node2 - """ - if not isinstance(node1, DAGOpNode) or not isinstance(node1, DAGOpNode): - return type(node1) is type(node2) and bit_indices1.get(node1.wire) == bit_indices2.get( - node2.wire - ) - if isinstance(node1.op, ControlFlowOp) and isinstance(node2.op, ControlFlowOp): - # While control-flow operations aren't represented natively in the DAG, we have to do - # some unpleasant dispatching and very manual handling. Once they have more first-class - # support we'll still be dispatching, but it'll look more appropriate (like the dispatch - # based on `DAGOpNode`/`DAGInNode`/`DAGOutNode` that already exists) and less like we're - # duplicating code from the `ControlFlowOp` classes. - if type(node1.op) is not type(node2.op): - return False - comparer = _SEMANTIC_EQ_CONTROL_FLOW.get(type(node1.op)) - if comparer is None: # pragma: no cover - raise RuntimeError(f"unhandled control-flow operation: {type(node1.op)}") - return comparer(node1, node2, bit_indices1, bit_indices2) - - node1_qargs = [bit_indices1[qarg] for qarg in node1.qargs] - node1_cargs = [bit_indices1[carg] for carg in node1.cargs] - - node2_qargs = [bit_indices2[qarg] for qarg in node2.qargs] - node2_cargs = [bit_indices2[carg] for carg in node2.cargs] - - # For barriers, qarg order is not significant so compare as sets - if node1.op.name == node2.op.name and node1.name in _SEMANTIC_EQ_SYMMETRIC: - node1_qargs = set(node1_qargs) - node1_cargs = set(node1_cargs) - node2_qargs = set(node2_qargs) - node2_cargs = set(node2_cargs) - - return ( - node1_qargs == node2_qargs - and node1_cargs == node2_cargs - and _legacy_condition_eq( - getattr(node1.op, "condition", None), - getattr(node2.op, "condition", None), - bit_indices1, - bit_indices2, - ) - and node1.op == node2.op - ) - - -# Bind semantic_eq from Python to Rust implementation -DAGNode.semantic_eq = staticmethod(_semantic_eq) diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index 32666c57f184..fd535d9f7278 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -43,7 +43,7 @@ from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping, GlobalPhaseGate from qiskit.providers import Provider from qiskit.providers.backend import BackendV2 -from qiskit.providers.models import BackendConfiguration +from qiskit.providers.models.backendconfiguration import BackendConfiguration from qiskit.providers.options import Options from qiskit.qobj import QasmQobj, QasmQobjConfig, QasmQobjExperiment from qiskit.result import Result diff --git a/qiskit/providers/fake_provider/fake_openpulse_2q.py b/qiskit/providers/fake_provider/fake_openpulse_2q.py index c76f4e048828..036c0bc6c1b7 100644 --- a/qiskit/providers/fake_provider/fake_openpulse_2q.py +++ b/qiskit/providers/fake_provider/fake_openpulse_2q.py @@ -16,14 +16,14 @@ import datetime import warnings -from qiskit.providers.models import ( +from qiskit.providers.models.backendconfiguration import ( GateConfig, PulseBackendConfiguration, - PulseDefaults, - Command, UchannelLO, ) + from qiskit.providers.models.backendproperties import Nduv, Gate, BackendProperties +from qiskit.providers.models.pulsedefaults import PulseDefaults, Command from qiskit.qobj import PulseQobjInstruction from .fake_backend import FakeBackend diff --git a/qiskit/providers/fake_provider/fake_openpulse_3q.py b/qiskit/providers/fake_provider/fake_openpulse_3q.py index 36b66847dad4..424cad006ed8 100644 --- a/qiskit/providers/fake_provider/fake_openpulse_3q.py +++ b/qiskit/providers/fake_provider/fake_openpulse_3q.py @@ -15,13 +15,12 @@ """ import warnings -from qiskit.providers.models import ( +from qiskit.providers.models.backendconfiguration import ( GateConfig, PulseBackendConfiguration, - PulseDefaults, - Command, UchannelLO, ) +from qiskit.providers.models.pulsedefaults import PulseDefaults, Command from qiskit.qobj import PulseQobjInstruction from .fake_backend import FakeBackend diff --git a/qiskit/providers/fake_provider/fake_pulse_backend.py b/qiskit/providers/fake_provider/fake_pulse_backend.py index fdb02f7d4350..bf772d339bc7 100644 --- a/qiskit/providers/fake_provider/fake_pulse_backend.py +++ b/qiskit/providers/fake_provider/fake_pulse_backend.py @@ -15,7 +15,8 @@ """ from qiskit.exceptions import QiskitError -from qiskit.providers.models import PulseBackendConfiguration, PulseDefaults +from qiskit.providers.models.backendconfiguration import PulseBackendConfiguration +from qiskit.providers.models.pulsedefaults import PulseDefaults from .fake_qasm_backend import FakeQasmBackend from .utils.json_decoder import decode_pulse_defaults diff --git a/qiskit/providers/fake_provider/fake_qasm_backend.py b/qiskit/providers/fake_provider/fake_qasm_backend.py index 7ad7222f7907..a570ac401601 100644 --- a/qiskit/providers/fake_provider/fake_qasm_backend.py +++ b/qiskit/providers/fake_provider/fake_qasm_backend.py @@ -19,7 +19,8 @@ import warnings from qiskit.exceptions import QiskitError -from qiskit.providers.models import BackendProperties, QasmBackendConfiguration +from qiskit.providers.models.backendproperties import BackendProperties +from qiskit.providers.models.backendconfiguration import QasmBackendConfiguration from .utils.json_decoder import ( decode_backend_configuration, diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 6374a0b0b60a..770c8762152e 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -16,6 +16,7 @@ import warnings from collections.abc import Iterable +from typing import List, Dict, Any, Union import numpy as np from qiskit import pulse @@ -35,12 +36,12 @@ from qiskit.providers import Options from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.backend import BackendV2 -from qiskit.providers.models import ( - PulseDefaults, - Command, -) -from qiskit.qobj import PulseQobjInstruction, PulseLibraryItem from qiskit.utils import optionals as _optionals +from qiskit.providers.models.pulsedefaults import Command +from qiskit.qobj.converters.pulse_instruction import QobjToInstructionConverter +from qiskit.pulse.calibration_entries import PulseQobjDef +from qiskit.providers.models.pulsedefaults import MeasurementKernel, Discriminator +from qiskit.qobj.pulse_qobj import QobjMeasurementOption # Noise default values/ranges for duration and error of supported # instructions. There are two possible formats: @@ -74,17 +75,432 @@ "frequency": (5e9, 5.5e9), } -# The number of samples determines the pulse durations of the corresponding -# instructions. This default defines pulses with durations in multiples of -# 16 dt for consistency with the pulse granularity of real IBM devices, but -# keeps the number smaller than what would be realistic for -# manageability. If needed, more realistic durations could be added in the -# future (order of 160dt for 1q gates, 1760dt for 2q gates and measure). -_PULSE_LIBRARY = [ - PulseLibraryItem(name="pulse_1", samples=np.linspace(0, 1.0, 16, dtype=np.complex128)), # 16dt - PulseLibraryItem(name="pulse_2", samples=np.linspace(0, 1.0, 32, dtype=np.complex128)), # 32dt - PulseLibraryItem(name="pulse_3", samples=np.linspace(0, 1.0, 64, dtype=np.complex128)), # 64dt -] + +class PulseDefaults: + """Internal - Description of default settings for Pulse systems. These are instructions + or settings that + may be good starting points for the Pulse user. The user may modify these defaults for custom + scheduling. + """ + + # Copy from the deprecated from qiskit.providers.models.pulsedefaults.PulseDefaults + + _data = {} + + def __init__( + self, + qubit_freq_est: List[float], + meas_freq_est: List[float], + buffer: int, + pulse_library: List[PulseLibraryItem], + cmd_def: List[Command], + meas_kernel: MeasurementKernel = None, + discriminator: Discriminator = None, + **kwargs: Dict[str, Any], + ): + """ + Validate and reformat transport layer inputs to initialize. + Args: + qubit_freq_est: Estimated qubit frequencies in GHz. + meas_freq_est: Estimated measurement cavity frequencies in GHz. + buffer: Default buffer time (in units of dt) between pulses. + pulse_library: Pulse name and sample definitions. + cmd_def: Operation name and definition in terms of Commands. + meas_kernel: The measurement kernels + discriminator: The discriminators + **kwargs: Other attributes for the super class. + """ + self._data = {} + self.buffer = buffer + self.qubit_freq_est = [freq * 1e9 for freq in qubit_freq_est] + """Qubit frequencies in Hertz.""" + self.meas_freq_est = [freq * 1e9 for freq in meas_freq_est] + """Measurement frequencies in Hertz.""" + self.pulse_library = pulse_library + self.cmd_def = cmd_def + self.instruction_schedule_map = InstructionScheduleMap() + self.converter = QobjToInstructionConverter(pulse_library) + + for inst in cmd_def: + entry = PulseQobjDef(converter=self.converter, name=inst.name) + entry.define(inst.sequence, user_provided=False) + self.instruction_schedule_map._add( + instruction_name=inst.name, + qubits=tuple(inst.qubits), + entry=entry, + ) + + if meas_kernel is not None: + self.meas_kernel = meas_kernel + if discriminator is not None: + self.discriminator = discriminator + + self._data.update(kwargs) + + def __getattr__(self, name): + try: + return self._data[name] + except KeyError as ex: + raise AttributeError(f"Attribute {name} is not defined") from ex + + def to_dict(self): + """Return a dictionary format representation of the PulseDefaults. + Returns: + dict: The dictionary form of the PulseDefaults. + """ + out_dict = { + "qubit_freq_est": self.qubit_freq_est, + "meas_freq_est": self.qubit_freq_est, + "buffer": self.buffer, + "pulse_library": [x.to_dict() for x in self.pulse_library], + "cmd_def": [x.to_dict() for x in self.cmd_def], + } + if hasattr(self, "meas_kernel"): + out_dict["meas_kernel"] = self.meas_kernel.to_dict() + if hasattr(self, "discriminator"): + out_dict["discriminator"] = self.discriminator.to_dict() + for key, value in self.__dict__.items(): + if key not in [ + "qubit_freq_est", + "meas_freq_est", + "buffer", + "pulse_library", + "cmd_def", + "meas_kernel", + "discriminator", + "converter", + "instruction_schedule_map", + ]: + out_dict[key] = value + out_dict.update(self._data) + + out_dict["qubit_freq_est"] = [freq * 1e-9 for freq in self.qubit_freq_est] + out_dict["meas_freq_est"] = [freq * 1e-9 for freq in self.meas_freq_est] + return out_dict + + @classmethod + def from_dict(cls, data): + """Create a new PulseDefaults object from a dictionary. + + Args: + data (dict): A dictionary representing the PulseDefaults + to create. It will be in the same format as output by + :meth:`to_dict`. + Returns: + PulseDefaults: The PulseDefaults from the input dictionary. + """ + schema = { + "pulse_library": PulseLibraryItem, # The class PulseLibraryItem is deprecated + "cmd_def": Command, + "meas_kernel": MeasurementKernel, + "discriminator": Discriminator, + } + + # Pulse defaults data is nested dictionary. + # To avoid deepcopy and avoid mutating the source object, create new dict here. + in_data = {} + for key, value in data.items(): + if key in schema: + with warnings.catch_warnings(): + # The class PulseLibraryItem is deprecated + warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") + if isinstance(value, list): + in_data[key] = list(map(schema[key].from_dict, value)) + else: + in_data[key] = schema[key].from_dict(value) + else: + in_data[key] = value + + return cls(**in_data) + + def __str__(self): + qubit_freqs = [freq / 1e9 for freq in self.qubit_freq_est] + meas_freqs = [freq / 1e9 for freq in self.meas_freq_est] + qfreq = f"Qubit Frequencies [GHz]\n{qubit_freqs}" + mfreq = f"Measurement Frequencies [GHz]\n{meas_freqs} " + return f"<{self.__class__.__name__}({str(self.instruction_schedule_map)}{qfreq}\n{mfreq})>" + + +def _to_complex(value: Union[List[float], complex]) -> complex: + """Convert the input value to type ``complex``. + Args: + value: Value to be converted. + Returns: + Input value in ``complex``. + Raises: + TypeError: If the input value is not in the expected format. + """ + if isinstance(value, list) and len(value) == 2: + return complex(value[0], value[1]) + elif isinstance(value, complex): + return value + + raise TypeError(f"{value} is not in a valid complex number format.") + + +class PulseLibraryItem: + """INTERNAL - An item in a pulse library.""" + + # Copy from the deprecated from qiskit.qobj.PulseLibraryItem + def __init__(self, name, samples): + """Instantiate a pulse library item. + + Args: + name (str): A name for the pulse. + samples (list[complex]): A list of complex values defining pulse + shape. + """ + self.name = name + if isinstance(samples[0], list): + self.samples = np.array([complex(sample[0], sample[1]) for sample in samples]) + else: + self.samples = samples + + def to_dict(self): + """Return a dictionary format representation of the pulse library item. + + Returns: + dict: The dictionary form of the PulseLibraryItem. + """ + return {"name": self.name, "samples": self.samples} + + @classmethod + def from_dict(cls, data): + """Create a new PulseLibraryItem object from a dictionary. + + Args: + data (dict): A dictionary for the experiment config + + Returns: + PulseLibraryItem: The object from the input dictionary. + """ + return cls(**data) + + def __repr__(self): + return f"PulseLibraryItem({self.name}, {repr(self.samples)})" + + def __str__(self): + return f"Pulse Library Item:\n\tname: {self.name}\n\tsamples: {self.samples}" + + def __eq__(self, other): + if isinstance(other, PulseLibraryItem): + if self.to_dict() == other.to_dict(): + return True + return False + + +class PulseQobjInstruction: + """Internal - A class representing a single instruction in a PulseQobj Experiment.""" + + # Copy from the deprecated from qiskit.qobj.PulseQobjInstruction + + _COMMON_ATTRS = [ + "ch", + "conditional", + "val", + "phase", + "frequency", + "duration", + "qubits", + "memory_slot", + "register_slot", + "label", + "type", + "pulse_shape", + "parameters", + ] + + def __init__( + self, + name, + t0, + ch=None, + conditional=None, + val=None, + phase=None, + duration=None, + qubits=None, + memory_slot=None, + register_slot=None, + kernels=None, + discriminators=None, + label=None, + type=None, # pylint: disable=invalid-name,redefined-builtin + pulse_shape=None, + parameters=None, + frequency=None, + ): + """Instantiate a new PulseQobjInstruction object. + + Args: + name (str): The name of the instruction + t0 (int): Pulse start time in integer **dt** units. + ch (str): The channel to apply the pulse instruction. + conditional (int): The register to use for a conditional for this + instruction + val (complex): Complex value to apply, bounded by an absolute value + of 1. + phase (float): if a ``fc`` instruction, the frame change phase in + radians. + frequency (float): if a ``sf`` instruction, the frequency in Hz. + duration (int): The duration of the pulse in **dt** units. + qubits (list): A list of ``int`` representing the qubits the + instruction operates on + memory_slot (list): If a ``measure`` instruction this is a list + of ``int`` containing the list of memory slots to store the + measurement results in (must be the same length as qubits). + If a ``bfunc`` instruction this is a single ``int`` of the + memory slot to store the boolean function result in. + register_slot (list): If a ``measure`` instruction this is a list + of ``int`` containing the list of register slots in which to + store the measurement results (must be the same length as + qubits). If a ``bfunc`` instruction this is a single ``int`` + of the register slot in which to store the result. + kernels (list): List of :class:`QobjMeasurementOption` objects + defining the measurement kernels and set of parameters if the + measurement level is 1 or 2. Only used for ``acquire`` + instructions. + discriminators (list): A list of :class:`QobjMeasurementOption` + used to set the discriminators to be used if the measurement + level is 2. Only used for ``acquire`` instructions. + label (str): Label of instruction + type (str): Type of instruction + pulse_shape (str): The shape of the parametric pulse + parameters (dict): The parameters for a parametric pulse + """ + self.name = name + self.t0 = t0 + if ch is not None: + self.ch = ch + if conditional is not None: + self.conditional = conditional + if val is not None: + self.val = val + if phase is not None: + self.phase = phase + if frequency is not None: + self.frequency = frequency + if duration is not None: + self.duration = duration + if qubits is not None: + self.qubits = qubits + if memory_slot is not None: + self.memory_slot = memory_slot + if register_slot is not None: + self.register_slot = register_slot + if kernels is not None: + self.kernels = kernels + if discriminators is not None: + self.discriminators = discriminators + if label is not None: + self.label = label + if type is not None: + self.type = type + if pulse_shape is not None: + self.pulse_shape = pulse_shape + if parameters is not None: + self.parameters = parameters + + def to_dict(self): + """Return a dictionary format representation of the Instruction. + + Returns: + dict: The dictionary form of the PulseQobjInstruction. + """ + out_dict = {"name": self.name, "t0": self.t0} + for attr in self._COMMON_ATTRS: + if hasattr(self, attr): + out_dict[attr] = getattr(self, attr) + if hasattr(self, "kernels"): + out_dict["kernels"] = [x.to_dict() for x in self.kernels] + if hasattr(self, "discriminators"): + out_dict["discriminators"] = [x.to_dict() for x in self.discriminators] + return out_dict + + def __repr__(self): + out = f'PulseQobjInstruction(name="{self.name}", t0={self.t0}' + for attr in self._COMMON_ATTRS: + attr_val = getattr(self, attr, None) + if attr_val is not None: + if isinstance(attr_val, str): + out += f', {attr}="{attr_val}"' + else: + out += f", {attr}={attr_val}" + out += ")" + return out + + def __str__(self): + out = f"Instruction: {self.name}\n" + out += f"\t\tt0: {self.t0}\n" + for attr in self._COMMON_ATTRS: + if hasattr(self, attr): + out += f"\t\t{attr}: {getattr(self, attr)}\n" + return out + + @classmethod + def from_dict(cls, data): + """Create a new PulseQobjExperimentConfig object from a dictionary. + + Args: + data (dict): A dictionary for the experiment config + + Returns: + PulseQobjInstruction: The object from the input dictionary. + """ + schema = { + "discriminators": QobjMeasurementOption, + "kernels": QobjMeasurementOption, + } + skip = ["t0", "name"] + + # Pulse instruction data is nested dictionary. + # To avoid deepcopy and avoid mutating the source object, create new dict here. + in_data = {} + for key, value in data.items(): + if key in skip: + continue + if key == "parameters": + # This is flat dictionary of parametric pulse parameters + formatted_value = value.copy() + if "amp" in formatted_value: + formatted_value["amp"] = _to_complex(formatted_value["amp"]) + in_data[key] = formatted_value + continue + if key in schema: + if isinstance(value, list): + in_data[key] = list(map(schema[key].from_dict, value)) + else: + in_data[key] = schema[key].from_dict(value) + else: + in_data[key] = value + + return cls(data["name"], data["t0"], **in_data) + + def __eq__(self, other): + if isinstance(other, PulseQobjInstruction): + if self.to_dict() == other.to_dict(): + return True + return False + + +def _pulse_library(): + # The number of samples determines the pulse durations of the corresponding + # instructions. This default defines pulses with durations in multiples of + # 16 dt for consistency with the pulse granularity of real IBM devices, but + # keeps the number smaller than what would be realistic for + # manageability. If needed, more realistic durations could be added in the + # future (order of 160dt for 1q gates, 1760dt for 2q gates and measure). + return [ + PulseLibraryItem( + name="pulse_1", samples=np.linspace(0, 1.0, 16, dtype=np.complex128) + ), # 16dt + PulseLibraryItem( + name="pulse_2", samples=np.linspace(0, 1.0, 32, dtype=np.complex128) + ), # 32dt + PulseLibraryItem( + name="pulse_3", samples=np.linspace(0, 1.0, 64, dtype=np.complex128) + ), # 64dt + ] class GenericBackendV2(BackendV2): @@ -262,7 +678,7 @@ def _get_calibration_sequence( acting on qargs. """ - pulse_library = _PULSE_LIBRARY + pulse_library = _pulse_library() # Note that the calibration pulses are different for # 1q gates vs 2q gates vs measurement instructions. if inst == "measure": @@ -352,7 +768,7 @@ def _generate_calibration_defaults(self) -> PulseDefaults: qubit_freq_est=qubit_freq_est, meas_freq_est=meas_freq_est, buffer=0, - pulse_library=_PULSE_LIBRARY, + pulse_library=_pulse_library(), cmd_def=cmd_def, ) diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index d9e63e3eb75c..d7f8307abb04 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -38,26 +38,52 @@ GateProperties Nduv """ +# pylint: disable=undefined-all-variable +__all__ = [ + "BackendConfiguration", + "PulseBackendConfiguration", + "QasmBackendConfiguration", + "UchannelLO", + "GateConfig", + "BackendProperties", + "GateProperties", + "Nduv", + "BackendStatus", + "JobStatus", + "PulseDefaults", + "Command", +] + +import importlib import warnings -from .backendconfiguration import ( - BackendConfiguration, - PulseBackendConfiguration, - QasmBackendConfiguration, - UchannelLO, - GateConfig, -) -from .backendproperties import BackendProperties, GateProperties, Nduv -from .backendstatus import BackendStatus -from .jobstatus import JobStatus -from .pulsedefaults import PulseDefaults, Command - - -warnings.warn( - "qiskit.providers.models is deprecated since Qiskit 1.2 and will be removed in Qiskit 2.0. " - "With the removal of Qobj, there is no need for these schema-conformant objects. If you still need " - "to use them, it could be because you are using a BackendV1, which is also deprecated in favor " - "of BackendV2.", - DeprecationWarning, - 2, -) + +_NAME_MAP = { + # public object name mapped to containing module + "BackendConfiguration": "qiskit.providers.models.backendconfiguration", + "PulseBackendConfiguration": "qiskit.providers.models.backendconfiguration", + "QasmBackendConfiguration": "qiskit.providers.models.backendconfiguration", + "UchannelLO": "qiskit.providers.models.backendconfiguration", + "GateConfig": "qiskit.providers.models.backendconfiguration", + "BackendProperties": "qiskit.providers.models.backendproperties", + "GateProperties": "qiskit.providers.models.backendproperties", + "Nduv": "qiskit.providers.models.backendproperties", + "BackendStatus": "qiskit.providers.models.backendstatus", + "JobStatus": "qiskit.providers.models.jobstatus", + "PulseDefaults": "qiskit.providers.models.pulsedefaults", + "Command": "qiskit.providers.models.pulsedefaults", +} + + +def __getattr__(name): + if (module_name := _NAME_MAP.get(name)) is not None: + warnings.warn( + "qiskit.providers.models is deprecated since Qiskit 1.2 and will be " + "removed in Qiskit 2.0. With the removal of Qobj, there is no need for these " + "schema-conformant objects. If you still need to use them, it could be because " + "you are using a BackendV1, which is also deprecated in favor of BackendV2.", + DeprecationWarning, + stacklevel=2, + ) + return getattr(importlib.import_module(module_name), name) + raise AttributeError(f"module 'qiskit.providers.models' has no attribute '{name}'") diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 33d428771b22..b7ba658da1be 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1773,12 +1773,13 @@ def Square( is the sign function with the convention :math:`\\text{sign}\\left(0\\right)=1`. Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the square wave. Wave range is [-`amp`,`amp`]. + duration: Pulse length in terms of the sampling period ``dt``. + amp: The magnitude of the amplitude of the square wave. Wave range is + :math:`\\left[-\\texttt{amp},\\texttt{amp}\\right]`. phase: The phase of the square wave (note that this is not equivalent to the angle of the complex amplitude). freq: The frequency of the square wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). + If not provided defaults to a single cycle (i.e :math:`\\frac{1}{\\text{duration}}`). The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). angle: The angle in radians of the complex phase factor uniformly scaling the pulse. Default value 0. diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index a40270a99b8c..6cdb0f70bba0 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -89,6 +89,35 @@ class CustomInstruction: There is a final ``builtin`` field. This is optional, and if set true will cause the instruction to be defined and available within the parsing, even if there is no definition in any included OpenQASM 2 file. + + Examples: + + Instruct the importer to use Qiskit's :class:`.ECRGate` and :class:`.RZXGate` objects to + interpret ``gate`` statements that are known to have been created from those same objects + during OpenQASM 2 export:: + + from qiskit import qasm2 + from qiskit.circuit import QuantumCircuit, library + + qc = QuantumCircuit(2) + qc.ecr(0, 1) + qc.rzx(0.3, 0, 1) + qc.rzx(0.7, 1, 0) + qc.rzx(1.5, 0, 1) + qc.ecr(1, 0) + + # This output string includes `gate ecr q0, q1 { ... }` and `gate rzx(p) q0, q1 { ... }` + # statements, since `ecr` and `rzx` are neither built-in gates nor in ``qelib1.inc``. + dumped = qasm2.dumps(qc) + + # Tell the importer how to interpret the `gate` statements, which we know are safe + # because we controlled the input OpenQASM 2 source. + custom = [ + qasm2.CustomInstruction("ecr", 0, 2, library.ECRGate), + qasm2.CustomInstruction("rzx", 1, 2, library.RZXGate), + ] + + loaded = qasm2.loads(dumped, custom_instructions=custom) """ name: str diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index a4e93f364809..42593626f2cc 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -55,6 +55,30 @@ class Operator(LinearOp): .. math:: \rho \mapsto M \rho M^\dagger. + + For example, the following operator :math:`M = X` applied to the zero state + :math:`|\psi\rangle=|0\rangle (\rho = |0\rangle\langle 0|)` changes it to the + one state :math:`|\psi\rangle=|1\rangle (\rho = |1\rangle\langle 1|)`: + + .. code-block:: python + + >>> import numpy as np + >>> from qiskit.quantum_info import Operator + >>> op = Operator(np.array([[0.0, 1.0], [1.0, 0.0]])) # Represents Pauli X operator + + >>> from qiskit.quantum_info import Statevector + >>> sv = Statevector(np.array([1.0, 0.0])) + >>> sv.evolve(op) + Statevector([0.+0.j, 1.+0.j], + dims=(2,)) + + >>> from qiskit.quantum_info import DensityMatrix + >>> dm = DensityMatrix(np.array([[1.0, 0.0], [0.0, 0.0]])) + >>> dm.evolve(op) + DensityMatrix([[0.+0.j, 0.+0.j], + [0.+0.j, 1.+0.j]], + dims=(2,)) + """ def __init__( diff --git a/qiskit/quantum_info/operators/operator_utils.py b/qiskit/quantum_info/operators/operator_utils.py new file mode 100644 index 000000000000..092252df951f --- /dev/null +++ b/qiskit/quantum_info/operators/operator_utils.py @@ -0,0 +1,76 @@ +# 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. + +""" +Additional utilities for Operators. +""" + +from __future__ import annotations + +from qiskit.quantum_info import Operator +from qiskit.quantum_info.operators.predicates import matrix_equal + + +def _equal_with_ancillas( + op1: Operator, + op2: Operator, + ancilla_qubits: list[int], + ignore_phase: bool = False, + rtol: float | None = None, + atol: float | None = None, +) -> bool: + r"""Test if two Operators are equal on the subspace where ancilla qubits + are :math:`|0\rangle`. + + Args: + op1 (Operator): an operator object. + op2 (Operator): an operator object. + ancilla_qubits (list[int]): a list of clean ancilla qubits. + ignore_phase (bool): ignore complex-phase difference between matrices. + rtol (float): relative tolerance value for comparison. + atol (float): absolute tolerance value for comparison. + + Returns: + bool: True iff operators are equal up to clean ancilla qubits. + """ + if op1.dim != op2.dim: + return False + + if atol is None: + atol = op1.atol + if rtol is None: + rtol = op1.rtol + + num_qubits = op1._op_shape._num_qargs_l + num_non_ancillas = num_qubits - len(ancilla_qubits) + + # Find a permutation that moves all ancilla qubits to the back + pattern = [] + ancillas = [] + for q in range(num_qubits): + if q not in ancilla_qubits: + pattern.append(q) + else: + ancillas.append(q) + pattern = pattern + ancillas + + # Apply this permutation to both operators + permuted1 = op1.apply_permutation(pattern) + permuted2 = op2.apply_permutation(pattern) + + # Restrict to the subspace where ancillas are 0 + restricted1 = permuted1.data[: 2**num_non_ancillas, : 2**num_qubits] + restricted2 = permuted2.data[: 2**num_non_ancillas, : 2**num_qubits] + + return matrix_equal( + restricted1, restricted2.data, ignore_phase=ignore_phase, rtol=rtol, atol=atol + ) diff --git a/qiskit/quantum_info/operators/symplectic/random.py b/qiskit/quantum_info/operators/symplectic/random.py index 06b23ca29803..848bce73aa35 100644 --- a/qiskit/quantum_info/operators/symplectic/random.py +++ b/qiskit/quantum_info/operators/symplectic/random.py @@ -14,11 +14,12 @@ """ from __future__ import annotations -import math import numpy as np from numpy.random import default_rng +from qiskit._accelerate.synthesis.clifford import random_clifford_tableau + from .clifford import Clifford from .pauli import Pauli from .pauli_list import PauliList @@ -111,155 +112,6 @@ def random_clifford(num_qubits: int, seed: int | np.random.Generator | None = No else: rng = default_rng(seed) - had, perm = _sample_qmallows(num_qubits, rng) - - gamma1 = np.diag(rng.integers(2, size=num_qubits, dtype=np.int8)) - gamma2 = np.diag(rng.integers(2, size=num_qubits, dtype=np.int8)) - delta1 = np.eye(num_qubits, dtype=np.int8) - delta2 = delta1.copy() - - _fill_tril(gamma1, rng, symmetric=True) - _fill_tril(gamma2, rng, symmetric=True) - _fill_tril(delta1, rng) - _fill_tril(delta2, rng) - - # For large num_qubits numpy.inv function called below can - # return invalid output leading to a non-symplectic Clifford - # being generated. This can be prevented by manually forcing - # block inversion of the matrix. - block_inverse_threshold = 50 - - # Compute stabilizer table - zero = np.zeros((num_qubits, num_qubits), dtype=np.int8) - prod1 = np.matmul(gamma1, delta1) % 2 - prod2 = np.matmul(gamma2, delta2) % 2 - inv1 = _inverse_tril(delta1, block_inverse_threshold).transpose() - inv2 = _inverse_tril(delta2, block_inverse_threshold).transpose() - table1 = np.block([[delta1, zero], [prod1, inv1]]) - table2 = np.block([[delta2, zero], [prod2, inv2]]) - - # Apply qubit permutation - table = table2[np.concatenate([perm, num_qubits + perm])] - - # Apply layer of Hadamards - inds = had * np.arange(1, num_qubits + 1) - inds = inds[inds > 0] - 1 - lhs_inds = np.concatenate([inds, inds + num_qubits]) - rhs_inds = np.concatenate([inds + num_qubits, inds]) - table[lhs_inds, :] = table[rhs_inds, :] - - # Apply table - tableau = np.zeros((2 * num_qubits, 2 * num_qubits + 1), dtype=bool) - tableau[:, :-1] = np.mod(np.matmul(table1, table), 2) - - # Generate random phases - tableau[:, -1] = rng.integers(2, size=2 * num_qubits) + seed = rng.integers(100000, size=1, dtype=np.uint64)[0] + tableau = random_clifford_tableau(num_qubits, seed=seed) return Clifford(tableau, validate=False) - - -def _sample_qmallows(n, rng=None): - """Sample from the quantum Mallows distribution""" - - if rng is None: - rng = np.random.default_rng() - - # Hadamard layer - had = np.zeros(n, dtype=bool) - - # Permutation layer - perm = np.zeros(n, dtype=int) - - inds = list(range(n)) - for i in range(n): - m = n - i - eps = 4 ** (-m) - r = rng.uniform(0, 1) - index = -math.ceil(math.log2(r + (1 - r) * eps)) - had[i] = index < m - if index < m: - k = index - else: - k = 2 * m - index - 1 - perm[i] = inds[k] - del inds[k] - return had, perm - - -def _fill_tril(mat, rng, symmetric=False): - """Add symmetric random ints to off diagonals""" - dim = mat.shape[0] - # Optimized for low dimensions - if dim == 1: - return - - if dim <= 4: - mat[1, 0] = rng.integers(2, dtype=np.int8) - if symmetric: - mat[0, 1] = mat[1, 0] - if dim > 2: - mat[2, 0] = rng.integers(2, dtype=np.int8) - mat[2, 1] = rng.integers(2, dtype=np.int8) - if symmetric: - mat[0, 2] = mat[2, 0] - mat[1, 2] = mat[2, 1] - if dim > 3: - mat[3, 0] = rng.integers(2, dtype=np.int8) - mat[3, 1] = rng.integers(2, dtype=np.int8) - mat[3, 2] = rng.integers(2, dtype=np.int8) - if symmetric: - mat[0, 3] = mat[3, 0] - mat[1, 3] = mat[3, 1] - mat[2, 3] = mat[3, 2] - return - - # Use numpy indices for larger dimensions - rows, cols = np.tril_indices(dim, -1) - vals = rng.integers(2, size=rows.size, dtype=np.int8) - mat[(rows, cols)] = vals - if symmetric: - mat[(cols, rows)] = vals - - -def _inverse_tril(mat, block_inverse_threshold): - """Invert a lower-triangular matrix with unit diagonal.""" - # Optimized inversion function for low dimensions - dim = mat.shape[0] - - if dim <= 2: - return mat - - if dim <= 5: - inv = mat.copy() - inv[2, 0] = mat[2, 0] ^ (mat[1, 0] & mat[2, 1]) - if dim > 3: - inv[3, 1] = mat[3, 1] ^ (mat[2, 1] & mat[3, 2]) - inv[3, 0] = mat[3, 0] ^ (mat[3, 2] & mat[2, 0]) ^ (mat[1, 0] & inv[3, 1]) - if dim > 4: - inv[4, 2] = (mat[4, 2] ^ (mat[3, 2] & mat[4, 3])) & 1 - inv[4, 1] = mat[4, 1] ^ (mat[4, 3] & mat[3, 1]) ^ (mat[2, 1] & inv[4, 2]) - inv[4, 0] = ( - mat[4, 0] - ^ (mat[1, 0] & inv[4, 1]) - ^ (mat[2, 0] & inv[4, 2]) - ^ (mat[3, 0] & mat[4, 3]) - ) - return inv % 2 - - # For higher dimensions we use Numpy's inverse function - # however this function tends to fail and result in a non-symplectic - # final matrix if n is too large. - if dim <= block_inverse_threshold: - return np.linalg.inv(mat).astype(np.int8) % 2 - - # For very large matrices we divide the matrix into 4 blocks of - # roughly equal size and use the analytic formula for the inverse - # of a block lower-triangular matrix: - # inv([[A, 0],[C, D]]) = [[inv(A), 0], [inv(D).C.inv(A), inv(D)]] - # call the inverse function recursively to compute inv(A) and invD - - dim1 = dim // 2 - mat_a = _inverse_tril(mat[0:dim1, 0:dim1], block_inverse_threshold) - mat_d = _inverse_tril(mat[dim1:dim, dim1:dim], block_inverse_threshold) - mat_c = np.matmul(np.matmul(mat_d, mat[dim1:dim, 0:dim1]), mat_a) - inv = np.block([[mat_a, np.zeros((dim1, dim - dim1), dtype=int)], [mat_c, mat_d]]) - return inv % 2 diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 10d76f8719af..4621019a9d81 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -256,6 +256,8 @@ def paulis(self, value): raise ValueError( f"incorrect number of operators: expected {len(self.paulis)}, got {len(value)}" ) + self.coeffs *= (-1j) ** value.phase + value.phase = 0 self._pauli_list = value @property diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index f1d1e3b28359..b2a92ee7caf5 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -128,6 +128,10 @@ .. autofunction:: synth_mcx_n_dirty_i15 .. autofunction:: synth_mcx_n_clean_m15 .. autofunction:: synth_mcx_1_clean_b95 +.. autofunction:: synth_mcx_noaux_v24 +.. autofunction:: synth_mcx_gray_code +.. autofunction:: synth_c3x +.. autofunction:: synth_c4x """ @@ -180,8 +184,12 @@ two_qubit_cnot_decompose, TwoQubitWeylDecomposition, ) -from .multi_controlled.mcx_with_ancillas_synth import ( +from .multi_controlled import ( synth_mcx_n_dirty_i15, synth_mcx_n_clean_m15, synth_mcx_1_clean_b95, + synth_mcx_noaux_v24, + synth_mcx_gray_code, + synth_c3x, + synth_c4x, ) diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index ac4923446bcd..5ead6945eba2 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -41,7 +41,7 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: `arXiv:2003.09412 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_bm_inner(clifford.tableau.astype(bool)) + synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True ) circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 0a679a8a7a6f..9766efe4aa8f 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -51,7 +51,7 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: `arXiv:2105.02291 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_greedy_inner(clifford.tableau.astype(bool)) + synth_clifford_greedy_inner(clifford.tableau.astype(bool)), add_regs=True ) circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py index f900ff86d17e..bb3d9c6896ff 100644 --- a/qiskit/synthesis/linear/cnot_synth.py +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -66,4 +66,4 @@ def synth_cnot_count_full_pmh( circuit_data = fast_pmh(normalized, section_size) # construct circuit from the data - return QuantumCircuit._from_circuit_data(circuit_data) + return QuantumCircuit._from_circuit_data(circuit_data, add_regs=True) diff --git a/qiskit/synthesis/linear_phase/cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cz_depth_lnn.py index 7a195f0caf96..d7dd071956e0 100644 --- a/qiskit/synthesis/linear_phase/cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cz_depth_lnn.py @@ -24,98 +24,10 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.synthesis.permutation.permutation_reverse_lnn import ( - _append_cx_stage1, - _append_cx_stage2, -) - - -def _odd_pattern1(n): - """A pattern denoted by Pj in [1] for odd number of qubits: - [n-2, n-4, n-4, ..., 3, 3, 1, 1, 0, 0, 2, 2, ..., n-3, n-3] - """ - pat = [] - pat.append(n - 2) - for i in range((n - 3) // 2): - pat.append(n - 2 * i - 4) - pat.append(n - 2 * i - 4) - for i in range((n - 1) // 2): - pat.append(2 * i) - pat.append(2 * i) - return pat - - -def _odd_pattern2(n): - """A pattern denoted by Pk in [1] for odd number of qubits: - [2, 2, 4, 4, ..., n-1, n-1, n-2, n-2, n-4, n-4, ..., 5, 5, 3, 3, 1] - """ - pat = [] - for i in range((n - 1) // 2): - pat.append(2 * i + 2) - pat.append(2 * i + 2) - for i in range((n - 3) // 2): - pat.append(n - 2 * i - 2) - pat.append(n - 2 * i - 2) - pat.append(1) - return pat - - -def _even_pattern1(n): - """A pattern denoted by Pj in [1] for even number of qubits: - [n-1, n-3, n-3, n-5, n-5, ..., 1, 1, 0, 0, 2, 2, ..., n-4, n-4, n-2] - """ - pat = [] - pat.append(n - 1) - for i in range((n - 2) // 2): - pat.append(n - 2 * i - 3) - pat.append(n - 2 * i - 3) - for i in range((n - 2) // 2): - pat.append(2 * i) - pat.append(2 * i) - pat.append(n - 2) - return pat - -def _even_pattern2(n): - """A pattern denoted by Pk in [1] for even number of qubits: - [2, 2, 4, 4, ..., n-2, n-2, n-1, n-1, ..., 3, 3, 1, 1] - """ - pat = [] - for i in range((n - 2) // 2): - pat.append(2 * (i + 1)) - pat.append(2 * (i + 1)) - for i in range(n // 2): - pat.append(n - 2 * i - 1) - pat.append(n - 2 * i - 1) - return pat - - -def _create_patterns(n): - """Creating the patterns for the phase layers.""" - if (n % 2) == 0: - pat1 = _even_pattern1(n) - pat2 = _even_pattern2(n) - else: - pat1 = _odd_pattern1(n) - pat2 = _odd_pattern2(n) - pats = {} - - layer = 0 - for i in range(n): - pats[(0, i)] = (i, i) - - if (n % 2) == 0: - ind1 = (2 * n - 4) // 2 - else: - ind1 = (2 * n - 4) // 2 - 1 - ind2 = 0 - while layer < (n // 2): - for i in range(n): - pats[(layer + 1, i)] = (pat1[ind1 + i], pat2[ind2 + i]) - layer += 1 - ind1 -= 2 - ind2 += 2 - return pats +from qiskit._accelerate.synthesis.linear_phase import ( + synth_cz_depth_line_mr as synth_cz_depth_line_mr_inner, +) def synth_cz_depth_line_mr(mat: np.ndarray) -> QuantumCircuit: @@ -139,56 +51,8 @@ def synth_cz_depth_line_mr(mat: np.ndarray) -> QuantumCircuit: *Shorter stabilizer circuits via Bruhat decomposition and quantum circuit transformations*, `arXiv:1705.09176 `_. """ - num_qubits = mat.shape[0] - pats = _create_patterns(num_qubits) - patlist = [] - # s_gates[i] = 0, 1, 2 or 3 for a gate id, sdg, z or s on qubit i respectively - s_gates = np.zeros(num_qubits) - - qc = QuantumCircuit(num_qubits) - for i in range(num_qubits): - for j in range(i + 1, num_qubits): - if mat[i][j]: # CZ(i,j) gate - s_gates[i] += 2 # qc.z[i] - s_gates[j] += 2 # qc.z[j] - patlist.append((i, j - 1)) - patlist.append((i, j)) - patlist.append((i + 1, j - 1)) - patlist.append((i + 1, j)) - - for i in range((num_qubits + 1) // 2): - for j in range(num_qubits): - if pats[(i, j)] in patlist: - patcnt = patlist.count(pats[(i, j)]) - for _ in range(patcnt): - s_gates[j] += 1 # qc.sdg[j] - # Add phase gates: s, sdg or z - for j in range(num_qubits): - if s_gates[j] % 4 == 1: - qc.sdg(j) - elif s_gates[j] % 4 == 2: - qc.z(j) - elif s_gates[j] % 4 == 3: - qc.s(j) - qc = _append_cx_stage1(qc, num_qubits) - qc = _append_cx_stage2(qc, num_qubits) - s_gates = np.zeros(num_qubits) - - if (num_qubits % 2) == 0: - i = num_qubits // 2 - for j in range(num_qubits): - if pats[(i, j)] in patlist and pats[(i, j)][0] != pats[(i, j)][1]: - patcnt = patlist.count(pats[(i, j)]) - for _ in range(patcnt): - s_gates[j] += 1 # qc.sdg[j] - # Add phase gates: s, sdg or z - for j in range(num_qubits): - if s_gates[j] % 4 == 1: - qc.sdg(j) - elif s_gates[j] % 4 == 2: - qc.z(j) - elif s_gates[j] % 4 == 3: - qc.s(j) - qc = _append_cx_stage1(qc, num_qubits) - return qc + # Call Rust implementaton + return QuantumCircuit._from_circuit_data( + synth_cz_depth_line_mr_inner(mat.astype(bool)), add_regs=True + ) diff --git a/qiskit/synthesis/multi_controlled/__init__.py b/qiskit/synthesis/multi_controlled/__init__.py index 0c04823a537a..84ec17c355e4 100644 --- a/qiskit/synthesis/multi_controlled/__init__.py +++ b/qiskit/synthesis/multi_controlled/__init__.py @@ -12,8 +12,12 @@ """Module containing multi-controlled circuits synthesis""" -from .mcx_with_ancillas_synth import ( +from .mcx_synthesis import ( synth_mcx_n_dirty_i15, synth_mcx_n_clean_m15, synth_mcx_1_clean_b95, + synth_mcx_gray_code, + synth_mcx_noaux_v24, + synth_c3x, + synth_c4x, ) diff --git a/qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py b/qiskit/synthesis/multi_controlled/mcx_synthesis.py similarity index 66% rename from qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py rename to qiskit/synthesis/multi_controlled/mcx_synthesis.py index cf2325764569..10680f0fee88 100644 --- a/qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py +++ b/qiskit/synthesis/multi_controlled/mcx_synthesis.py @@ -10,20 +10,28 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Module containing multi-controlled circuits synthesis with ancillary qubits.""" +"""Module containing multi-controlled circuits synthesis with and without ancillary qubits.""" from math import ceil +import numpy as np + from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.library.standard_gates.x import C3XGate, C4XGate +from qiskit.circuit.library.standard_gates import ( + HGate, + MCU1Gate, + CU1Gate, + RC3XGate, + C3SXGate, +) def synth_mcx_n_dirty_i15( num_ctrl_qubits: int, relative_phase: bool = False, action_only: bool = False, -): - """ +) -> QuantumCircuit: + r""" Synthesize a multi-controlled X gate with :math:`k` controls using :math:`k - 2` dirty ancillary qubits producing a circuit with :math:`2 * k - 1` qubits and at most :math:`8 * k - 6` CX gates, by Iten et. al. [1]. @@ -59,7 +67,8 @@ def synth_mcx_n_dirty_i15( qc.ccx(q_controls[0], q_controls[1], q_target) return qc elif not relative_phase and num_ctrl_qubits == 3: - qc._append(C3XGate(), [*q_controls, q_target], []) + circuit = synth_c3x() + qc.compose(circuit, [*q_controls, q_target], inplace=True, copy=False) return qc num_ancillas = num_ctrl_qubits - 2 @@ -122,8 +131,8 @@ def synth_mcx_n_dirty_i15( return qc -def synth_mcx_n_clean_m15(num_ctrl_qubits: int): - """ +def synth_mcx_n_clean_m15(num_ctrl_qubits: int) -> QuantumCircuit: + r""" Synthesize a multi-controlled X gate with :math:`k` controls using :math:`k - 2` clean ancillary qubits with producing a circuit with :math:`2 * k - 1` qubits and at most :math:`6 * k - 6` CX gates, by Maslov [1]. @@ -165,8 +174,8 @@ def synth_mcx_n_clean_m15(num_ctrl_qubits: int): return qc -def synth_mcx_1_clean_b95(num_ctrl_qubits: int): - """ +def synth_mcx_1_clean_b95(num_ctrl_qubits: int) -> QuantumCircuit: + r""" Synthesize a multi-controlled X gate with :math:`k` controls using a single clean ancillary qubit producing a circuit with :math:`k + 2` qubits and at most :math:`16 * k - 8` CX gates, by Barenco et al. [1]. @@ -183,16 +192,10 @@ def synth_mcx_1_clean_b95(num_ctrl_qubits: int): """ if num_ctrl_qubits == 3: - q = QuantumRegister(4, name="q") - qc = QuantumCircuit(q, name="mcx") - qc._append(C3XGate(), q[:], []) - return qc + return synth_c3x() elif num_ctrl_qubits == 4: - q = QuantumRegister(5, name="q") - qc = QuantumCircuit(q, name="mcx") - qc._append(C4XGate(), q[:], []) - return qc + return synth_c4x() num_qubits = num_ctrl_qubits + 2 q = QuantumRegister(num_qubits, name="q") @@ -230,3 +233,124 @@ def synth_mcx_1_clean_b95(num_ctrl_qubits: int): ) return qc + + +def synth_mcx_gray_code(num_ctrl_qubits: int) -> QuantumCircuit: + r""" + Synthesize a multi-controlled X gate with :math:`k` controls using the Gray code. + + Produces a quantum circuit with :math:`k + 1` qubits. This method + produces exponentially many CX gates and should be used only for small + values of :math:`k`. + + Args: + num_ctrl_qubits: The number of control qubits. + + Returns: + The synthesized quantum circuit. + """ + num_qubits = num_ctrl_qubits + 1 + q = QuantumRegister(num_qubits, name="q") + qc = QuantumCircuit(q, name="mcx_gray") + qc._append(HGate(), [q[-1]], []) + qc._append(MCU1Gate(np.pi, num_ctrl_qubits=num_ctrl_qubits), q[:], []) + qc._append(HGate(), [q[-1]], []) + return qc + + +def synth_mcx_noaux_v24(num_ctrl_qubits: int) -> QuantumCircuit: + r""" + Synthesize a multi-controlled X gate with :math:`k` controls based on + the implementation for MCPhaseGate. + + In turn, the MCPhase gate uses the decomposition for multi-controlled + special unitaries described in [1]. + + Produces a quantum circuit with :math:`k + 1` qubits. + The number of CX-gates is quadratic in :math:`k`. + + Args: + num_ctrl_qubits: The number of control qubits. + + Returns: + The synthesized quantum circuit. + + References: + 1. Vale et. al., *Circuit Decomposition of Multicontrolled Special Unitary + Single-Qubit Gates*, IEEE TCAD 43(3) (2024), + `arXiv:2302.06377 `_ + """ + if num_ctrl_qubits == 3: + return synth_c3x() + + if num_ctrl_qubits == 4: + return synth_c4x() + + num_qubits = num_ctrl_qubits + 1 + q = QuantumRegister(num_qubits, name="q") + qc = QuantumCircuit(q) + q_controls = list(range(num_ctrl_qubits)) + q_target = num_ctrl_qubits + qc.h(q_target) + qc.mcp(np.pi, q_controls, q_target) + qc.h(q_target) + return qc + + +def synth_c3x() -> QuantumCircuit: + """Efficient synthesis of 3-controlled X-gate.""" + + q = QuantumRegister(4, name="q") + qc = QuantumCircuit(q, name="mcx") + qc.h(3) + qc.p(np.pi / 8, [0, 1, 2, 3]) + qc.cx(0, 1) + qc.p(-np.pi / 8, 1) + qc.cx(0, 1) + qc.cx(1, 2) + qc.p(-np.pi / 8, 2) + qc.cx(0, 2) + qc.p(np.pi / 8, 2) + qc.cx(1, 2) + qc.p(-np.pi / 8, 2) + qc.cx(0, 2) + qc.cx(2, 3) + qc.p(-np.pi / 8, 3) + qc.cx(1, 3) + qc.p(np.pi / 8, 3) + qc.cx(2, 3) + qc.p(-np.pi / 8, 3) + qc.cx(0, 3) + qc.p(np.pi / 8, 3) + qc.cx(2, 3) + qc.p(-np.pi / 8, 3) + qc.cx(1, 3) + qc.p(np.pi / 8, 3) + qc.cx(2, 3) + qc.p(-np.pi / 8, 3) + qc.cx(0, 3) + qc.h(3) + return qc + + +def synth_c4x() -> QuantumCircuit: + """Efficient synthesis of 4-controlled X-gate.""" + + q = QuantumRegister(5, name="q") + qc = QuantumCircuit(q, name="mcx") + + rules = [ + (HGate(), [q[4]], []), + (CU1Gate(np.pi / 2), [q[3], q[4]], []), + (HGate(), [q[4]], []), + (RC3XGate(), [q[0], q[1], q[2], q[3]], []), + (HGate(), [q[4]], []), + (CU1Gate(-np.pi / 2), [q[3], q[4]], []), + (HGate(), [q[4]], []), + (RC3XGate().inverse(), [q[0], q[1], q[2], q[3]], []), + (C3SXGate(), [q[0], q[1], q[2], q[4]], []), + ] + for instr, qargs, cargs in rules: + qc._append(instr, qargs, cargs) + + return qc diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index f60f20f9524e..60da6ed6e82b 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -224,7 +224,8 @@ def _decompose(self, unitary, simplify=True, atol=DEFAULT_ATOL): return QuantumCircuit._from_circuit_data( euler_one_qubit_decomposer.unitary_to_circuit( unitary, [self.basis], 0, None, simplify, atol - ) + ), + add_regs=True, ) @property diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index 2fd892a0427e..7dd5ae99dc4c 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -42,7 +42,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc Returns: The synthesized quantum circuit. """ - return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern)) + return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern), add_regs=True) def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit: @@ -75,4 +75,4 @@ def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircui *Routing Permutations on Graphs Via Matchings.*, `(Full paper) `_ """ - return QuantumCircuit._from_circuit_data(_synth_permutation_acg(pattern)) + return QuantumCircuit._from_circuit_data(_synth_permutation_acg(pattern), add_regs=True) diff --git a/qiskit/synthesis/permutation/permutation_lnn.py b/qiskit/synthesis/permutation/permutation_lnn.py index d5da6929463c..5f1bfbeaa1a8 100644 --- a/qiskit/synthesis/permutation/permutation_lnn.py +++ b/qiskit/synthesis/permutation/permutation_lnn.py @@ -49,4 +49,6 @@ def synth_permutation_depth_lnn_kms(pattern: list[int] | np.ndarray[int]) -> Qua # In the permutation synthesis code below the notation is opposite: # [2, 4, 3, 0, 1] means that 0 maps to 2, 1 to 3, 2 to 3, 3 to 0, and 4 to 1. # This is why we invert the pattern. - return QuantumCircuit._from_circuit_data(_synth_permutation_depth_lnn_kms(pattern)) + return QuantumCircuit._from_circuit_data( + _synth_permutation_depth_lnn_kms(pattern), add_regs=True + ) diff --git a/qiskit/synthesis/permutation/permutation_reverse_lnn.py b/qiskit/synthesis/permutation/permutation_reverse_lnn.py index 26287a06177e..ccc820d97e62 100644 --- a/qiskit/synthesis/permutation/permutation_reverse_lnn.py +++ b/qiskit/synthesis/permutation/permutation_reverse_lnn.py @@ -14,6 +14,9 @@ """ from qiskit.circuit import QuantumCircuit +from qiskit._accelerate.synthesis.permutation import ( + synth_permutation_reverse_lnn_kms as synth_permutation_reverse_lnn_kms_inner, +) def _append_cx_stage1(qc, n): @@ -84,7 +87,7 @@ def synth_permutation_reverse_lnn_kms(num_qubits: int) -> QuantumCircuit: `arXiv:quant-ph/0701194 `_ """ - qc = QuantumCircuit(num_qubits) - _append_reverse_permutation_lnn_kms(qc, num_qubits) - - return qc + # Call Rust implementation + return QuantumCircuit._from_circuit_data( + synth_permutation_reverse_lnn_kms_inner(num_qubits), add_regs=True + ) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 86c5cba8295f..d4c7702da35a 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -24,8 +24,6 @@ arXiv:1811.12926 [quant-ph] (2018). """ from __future__ import annotations -import cmath -import math import io import base64 import warnings @@ -91,41 +89,18 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): QiskitError: if decomposition isn't possible. """ special_unitary_matrix = np.asarray(special_unitary_matrix, dtype=complex) - # extract the right component - R = special_unitary_matrix[:2, :2].copy() - detR = R[0, 0] * R[1, 1] - R[0, 1] * R[1, 0] - if abs(detR) < 0.1: - R = special_unitary_matrix[2:, :2].copy() - detR = R[0, 0] * R[1, 1] - R[0, 1] * R[1, 0] - if abs(detR) < 0.1: - raise QiskitError("decompose_two_qubit_product_gate: unable to decompose: detR < 0.1") - R /= np.sqrt(detR) - - # extract the left component - temp = np.kron(np.eye(2), R.T.conj()) - temp = special_unitary_matrix.dot(temp) - L = temp[::2, ::2] - detL = L[0, 0] * L[1, 1] - L[0, 1] * L[1, 0] - if abs(detL) < 0.9: - raise QiskitError("decompose_two_qubit_product_gate: unable to decompose: detL < 0.9") - L /= np.sqrt(detL) - phase = cmath.phase(detL) / 2 + (L, R, phase) = two_qubit_decompose.decompose_two_qubit_product_gate(special_unitary_matrix) temp = np.kron(L, R) deviation = abs(abs(temp.conj().T.dot(special_unitary_matrix).trace()) - 4) + if deviation > 1.0e-13: raise QiskitError( "decompose_two_qubit_product_gate: decomposition failed: " f"deviation too large: {deviation}" ) - return L, R, phase - - -_ipx = np.array([[0, 1j], [1j, 0]], dtype=complex) -_ipy = np.array([[0, 1], [-1, 0]], dtype=complex) -_ipz = np.array([[1j, 0], [0, -1j]], dtype=complex) -_id = np.array([[1, 0], [0, 1]], dtype=complex) + return (L, R, phase) class TwoQubitWeylDecomposition: @@ -233,13 +208,13 @@ def circuit( circuit_data = self._inner_decomposition.circuit( euler_basis=euler_basis, simplify=simplify, atol=atol ) - return QuantumCircuit._from_circuit_data(circuit_data) + return QuantumCircuit._from_circuit_data(circuit_data, add_regs=True) def actual_fidelity(self, **kwargs) -> float: """Calculates the actual fidelity of the decomposed circuit to the input unitary.""" circ = self.circuit(**kwargs) trace = np.trace(Operator(circ).data.T.conj() @ self.unitary_matrix) - return trace_to_fid(trace) + return two_qubit_decompose.trace_to_fid(trace) def __repr__(self): """Represent with enough precision to allow copy-paste debugging of all corner cases""" @@ -460,40 +435,6 @@ def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): return circ -def Ud(a, b, c): - r"""Generates the array :math:`e^{(i a XX + i b YY + i c ZZ)}`""" - return np.array( - [ - [cmath.exp(1j * c) * math.cos(a - b), 0, 0, 1j * cmath.exp(1j * c) * math.sin(a - b)], - [0, cmath.exp(-1j * c) * math.cos(a + b), 1j * cmath.exp(-1j * c) * math.sin(a + b), 0], - [0, 1j * cmath.exp(-1j * c) * math.sin(a + b), cmath.exp(-1j * c) * math.cos(a + b), 0], - [1j * cmath.exp(1j * c) * math.sin(a - b), 0, 0, cmath.exp(1j * c) * math.cos(a - b)], - ], - dtype=complex, - ) - - -def trace_to_fid(trace): - r"""Average gate fidelity is - - .. math:: - - \bar{F} = \frac{d + |\mathrm{Tr} (U_\text{target} \cdot U^{\dag})|^2}{d(d+1)} - - M. Horodecki, P. Horodecki and R. Horodecki, PRA 60, 1888 (1999)""" - return (4 + abs(trace) ** 2) / 20 - - -def rz_array(theta): - """Return numpy array for Rz(theta). - - Rz(theta) = diag(exp(-i*theta/2),exp(i*theta/2)) - """ - return np.array( - [[cmath.exp(-1j * theta / 2.0), 0], [0, cmath.exp(1j * theta / 2.0)]], dtype=complex - ) - - class TwoQubitBasisDecomposer: """A class for decomposing 2-qubit unitaries into minimal number of uses of a 2-qubit basis gate. @@ -639,7 +580,8 @@ def __call__( """ if use_dag: - from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode + from qiskit.dagcircuit.dagcircuit import DAGCircuit + from qiskit.dagcircuit.dagnode import DAGOpNode sequence = self._inner_decomposer( np.asarray(unitary, dtype=complex), @@ -659,7 +601,7 @@ def __call__( op = CircuitInstruction.from_standard( gate, qubits=tuple(q[x] for x in qubits), params=params ) - node = DAGOpNode.from_instruction(op, dag=dag) + node = DAGOpNode.from_instruction(op) dag._apply_op_node_back(node) return dag else: @@ -671,7 +613,7 @@ def __call__( approximate, _num_basis_uses=_num_basis_uses, ) - return QuantumCircuit._from_circuit_data(circ_data) + return QuantumCircuit._from_circuit_data(circ_data, add_regs=True) else: sequence = self._inner_decomposer( np.asarray(unitary, dtype=complex), diff --git a/qiskit/transpiler/instruction_durations.py b/qiskit/transpiler/instruction_durations.py index 85d89bb16a18..56f8b5587c0c 100644 --- a/qiskit/transpiler/instruction_durations.py +++ b/qiskit/transpiler/instruction_durations.py @@ -18,6 +18,7 @@ from qiskit.circuit import Barrier, Delay, Instruction, ParameterExpression from qiskit.circuit.duration import duration_in_dt from qiskit.providers import Backend +from qiskit.providers.backend import BackendV2 from qiskit.transpiler.exceptions import TranspilerError from qiskit.utils.units import apply_prefix @@ -75,6 +76,9 @@ def from_backend(cls, backend: Backend): TranspilerError: If dt and dtm is different in the backend. """ # All durations in seconds in gate_length + if isinstance(backend, BackendV2): + return backend.target.durations() + instruction_durations = [] backend_properties = backend.properties() if hasattr(backend_properties, "_gates"): diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 1feabeaef048..ca4e3545a98b 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -71,6 +71,7 @@ Collect1qRuns Collect2qBlocks CollectMultiQBlocks + CollectAndCollapse CollectLinearFunctions CollectCliffords ConsolidateBlocks @@ -238,6 +239,7 @@ from .optimization import TemplateOptimization from .optimization import InverseCancellation from .optimization import EchoRZXWeylDecomposition +from .optimization import CollectAndCollapse from .optimization import CollectLinearFunctions from .optimization import CollectCliffords from .optimization import ResetAfterMeasureSimplification diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a5a3936b1d7e..5737385af15b 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -313,10 +313,7 @@ def _replace_node(self, dag, node, instr_map): if node.params: parameter_map = dict(zip(target_params, node.params)) for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - dag=target_dag, - ) + new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs ) @@ -366,7 +363,6 @@ def _replace_node(self, dag, node, instr_map): for inner_node in target_dag.topological_op_nodes(): new_node = DAGOpNode.from_instruction( inner_node._to_circuit_instruction(), - dag=target_dag, ) new_node.qargs = tuple( node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 73e1d4ac5484..0c6d780f052a 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -58,7 +58,9 @@ def run(self, dag): continue if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if self.target is not None: diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 99bf95147ae2..51e116033bb6 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -66,7 +66,9 @@ def run(self, dag): for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) continue if getattr(node.op, "_directive", False): diff --git a/qiskit/transpiler/passes/calibration/rzx_templates.py b/qiskit/transpiler/passes/calibration/rzx_templates.py index 406e5e75de04..10f4d19ebd9e 100644 --- a/qiskit/transpiler/passes/calibration/rzx_templates.py +++ b/qiskit/transpiler/passes/calibration/rzx_templates.py @@ -20,17 +20,6 @@ from qiskit.circuit.library.templates import rzx -class RZXTemplateMap(Enum): - """Mapping of instruction name to decomposition template.""" - - ZZ1 = rzx.rzx_zz1() - ZZ2 = rzx.rzx_zz2() - ZZ3 = rzx.rzx_zz3() - YZ = rzx.rzx_yz() - XZ = rzx.rzx_xz() - CY = rzx.rzx_cy() - - def rzx_templates(template_list: List[str] = None) -> Dict: """Convenience function to get the cost_dict and templates for template matching. @@ -40,6 +29,17 @@ def rzx_templates(template_list: List[str] = None) -> Dict: Returns: Decomposition templates and cost values. """ + + class RZXTemplateMap(Enum): + """Mapping of instruction name to decomposition template.""" + + ZZ1 = rzx.rzx_zz1() + ZZ2 = rzx.rzx_zz2() + ZZ3 = rzx.rzx_zz3() + YZ = rzx.rzx_yz() + XZ = rzx.rzx_xz() + CY = rzx.rzx_cy() + if template_list is None: template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 629dc32061fc..7514ac6b421f 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -118,6 +118,6 @@ def run(self, dag): } out_layout = Layout(final_layout_mapping) self.property_set["final_layout"] = out_layout - new_dag._global_phase = dag._global_phase + new_dag.global_phase = dag.global_phase return new_dag diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 4ce94ecdb62f..78af67ad9118 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -101,7 +101,10 @@ class SabreLayout(TransformationPass): **References:** - [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem + [1] Henry Zou and Matthew Treinish and Kevin Hartman and Alexander Ivrii and Jake Lishman. + "LightSABRE: A Lightweight and Enhanced SABRE Algorithm" + `arXiv:2409.08368 `__ + [2] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem for NISQ-era quantum devices." ASPLOS 2019. `arXiv:1809.02573 `_ """ @@ -310,7 +313,7 @@ def run(self, dag): mapped_dag.add_captured_var(var) for var in dag.iter_declared_vars(): mapped_dag.add_declared_var(var) - mapped_dag._global_phase = dag._global_phase + mapped_dag.global_phase = dag.global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) } diff --git a/qiskit/transpiler/passes/layout/sabre_pre_layout.py b/qiskit/transpiler/passes/layout/sabre_pre_layout.py index 4a7ea35e3f2a..1fd4f60e254f 100644 --- a/qiskit/transpiler/passes/layout/sabre_pre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_pre_layout.py @@ -31,6 +31,11 @@ class SabrePreLayout(AnalysisPass): ``sabre_starting_layouts`` (``list[Layout]``) An optional list of :class:`~.Layout` objects to use for additional Sabre layout trials. + **References:** + + [1] Henry Zou and Matthew Treinish and Kevin Hartman and Alexander Ivrii and Jake Lishman. + "LightSABRE: A Lightweight and Enhanced SABRE Algorithm" + `arXiv:2409.08368 `__ """ def __init__( diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index a9796850a689..8e2883b27781 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -39,3 +39,4 @@ from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated from .split_2q_unitaries import Split2QUnitaries +from .collect_and_collapse import CollectAndCollapse diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 12ed7145eec7..d801e4775937 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -12,11 +12,9 @@ """Analysis pass to find commutation relations between DAG nodes.""" -from collections import defaultdict - from qiskit.circuit.commutation_library import SessionCommutationChecker as scc -from qiskit.dagcircuit import DAGOpNode from qiskit.transpiler.basepasses import AnalysisPass +from qiskit._accelerate.commutation_analysis import analyze_commutations class CommutationAnalysis(AnalysisPass): @@ -33,6 +31,7 @@ def __init__(self, *, _commutation_checker=None): # do not care about commutations of all gates, but just a subset if _commutation_checker is None: _commutation_checker = scc + self.comm_checker = _commutation_checker def run(self, dag): @@ -42,49 +41,4 @@ def run(self, dag): into the ``property_set``. """ # Initiate the commutation set - self.property_set["commutation_set"] = defaultdict(list) - - # Build a dictionary to keep track of the gates on each qubit - # The key with format (wire) will store the lists of commutation sets - # The key with format (node, wire) will store the index of the commutation set - # on the specified wire, thus, for example: - # self.property_set['commutation_set'][wire][(node, wire)] will give the - # commutation set that contains node. - - for wire in dag.qubits: - self.property_set["commutation_set"][wire] = [] - - # Add edges to the dictionary for each qubit - for node in dag.topological_op_nodes(): - for _, _, edge_wire in dag.edges(node): - self.property_set["commutation_set"][(node, edge_wire)] = -1 - - # Construct the commutation set - for wire in dag.qubits: - - for current_gate in dag.nodes_on_wire(wire): - - current_comm_set = self.property_set["commutation_set"][wire] - if not current_comm_set: - current_comm_set.append([current_gate]) - - if current_gate not in current_comm_set[-1]: - does_commute = True - - # Check if the current gate commutes with all the gates in the current block - for prev_gate in current_comm_set[-1]: - does_commute = ( - isinstance(current_gate, DAGOpNode) - and isinstance(prev_gate, DAGOpNode) - and self.comm_checker.commute_nodes(current_gate, prev_gate) - ) - if not does_commute: - break - - if does_commute: - current_comm_set[-1].append(current_gate) - else: - current_comm_set.append([current_gate]) - - temp_len = len(current_comm_set) - self.property_set["commutation_set"][(current_gate, wire)] = temp_len - 1 + self.property_set["commutation_set"] = analyze_commutations(dag, self.comm_checker.cc) diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index adfc4d73a221..130ff0609354 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -11,23 +11,16 @@ # that they have been altered from the originals. """Cancel the redundant (self-adjoint) gates through commutation relations.""" - -from collections import defaultdict -import numpy as np - -from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.passes.optimization.commutation_analysis import CommutationAnalysis -from qiskit.dagcircuit import DAGCircuit, DAGInNode, DAGOutNode -from qiskit.circuit.commutation_library import CommutationChecker, StandardGateCommutations +from qiskit.circuit.commutation_library import StandardGateCommutations + from qiskit.circuit.library.standard_gates.u1 import U1Gate -from qiskit.circuit.library.standard_gates.rx import RXGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.library.standard_gates.rz import RZGate -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit._accelerate import commutation_cancellation +from qiskit._accelerate.commutation_checker import CommutationChecker +from qiskit.transpiler.passes.utils.control_flow import trivial_recurse _CUTOFF_PRECISION = 1e-5 @@ -59,6 +52,7 @@ def __init__(self, basis_gates=None, target=None): self.basis = set(basis_gates) else: self.basis = set() + self.target = target if target is not None: self.basis = set(target.operation_names) @@ -70,12 +64,11 @@ def __init__(self, basis_gates=None, target=None): # build a commutation checker restricted to the gates we cancel -- the others we # do not have to investigate, which allows to save time - commutation_checker = CommutationChecker( + self._commutation_checker = CommutationChecker( StandardGateCommutations, gates=self._gates | self._z_rotations | self._x_rotations ) - self.requires.append(CommutationAnalysis(_commutation_checker=commutation_checker)) - + @trivial_recurse def run(self, dag): """Run the CommutativeCancellation pass on `dag`. @@ -85,139 +78,5 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - var_z_gate = None - z_var_gates = [gate for gate in dag.count_ops().keys() if gate in self._var_z_map] - if z_var_gates: - # prioritize z gates in circuit - var_z_gate = self._var_z_map[next(iter(z_var_gates))] - else: - z_var_gates = [gate for gate in self.basis if gate in self._var_z_map] - if z_var_gates: - var_z_gate = self._var_z_map[next(iter(z_var_gates))] - - # Gate sets to be cancelled - cancellation_sets = defaultdict(lambda: []) - - # Traverse each qubit to generate the cancel dictionaries - # Cancel dictionaries: - # - For 1-qubit gates the key is (gate_type, qubit_id, commutation_set_id), - # the value is the list of gates that share the same gate type, qubit, commutation set. - # - For 2qbit gates the key: (gate_type, first_qbit, sec_qbit, first commutation_set_id, - # sec_commutation_set_id), the value is the list gates that share the same gate type, - # qubits and commutation sets. - for wire in dag.qubits: - wire_commutation_set = self.property_set["commutation_set"][wire] - - for com_set_idx, com_set in enumerate(wire_commutation_set): - if isinstance(com_set[0], (DAGInNode, DAGOutNode)): - continue - for node in com_set: - num_qargs = len(node.qargs) - if any(isinstance(p, ParameterExpression) for p in node.params): - continue # no support for cancellation of parameterized gates - if num_qargs == 1 and node.name in self._gates: - cancellation_sets[(node.name, wire, com_set_idx)].append(node) - if num_qargs == 1 and node.name in self._z_rotations: - cancellation_sets[("z_rotation", wire, com_set_idx)].append(node) - if num_qargs == 1 and node.name in ["rx", "x"]: - cancellation_sets[("x_rotation", wire, com_set_idx)].append(node) - # Don't deal with Y rotation, because Y rotation doesn't commute with CNOT, so - # it should be dealt with by optimized1qgate pass - elif num_qargs == 2 and node.qargs[0] == wire: - second_qarg = node.qargs[1] - q2_key = ( - node.name, - wire, - second_qarg, - com_set_idx, - self.property_set["commutation_set"][(node, second_qarg)], - ) - cancellation_sets[q2_key].append(node) - - for cancel_set_key in cancellation_sets: - if cancel_set_key[0] == "z_rotation" and var_z_gate is None: - continue - set_len = len(cancellation_sets[cancel_set_key]) - if set_len > 1 and cancel_set_key[0] in self._gates: - gates_to_cancel = cancellation_sets[cancel_set_key] - for c_node in gates_to_cancel[: (set_len // 2) * 2]: - dag.remove_op_node(c_node) - - elif set_len > 1 and cancel_set_key[0] in ["z_rotation", "x_rotation"]: - run = cancellation_sets[cancel_set_key] - run_qarg = run[0].qargs[0] - total_angle = 0.0 # lambda - total_phase = 0.0 - for current_node in run: - if ( - current_node.condition is not None - or len(current_node.qargs) != 1 - or current_node.qargs[0] != run_qarg - ): - raise RuntimeError("internal error") - - if current_node.name in ["p", "u1", "rz", "rx"]: - current_angle = float(current_node.params[0]) - elif current_node.name in ["z", "x"]: - current_angle = np.pi - elif current_node.name == "t": - current_angle = np.pi / 4 - elif current_node.name == "s": - current_angle = np.pi / 2 - else: - raise RuntimeError( - f"Angle for operation {current_node.name } is not defined" - ) - - # Compose gates - total_angle = current_angle + total_angle - if current_node.definition: - total_phase += current_node.definition.global_phase - - # Replace the data of the first node in the run - if cancel_set_key[0] == "z_rotation": - new_op = var_z_gate(total_angle) - elif cancel_set_key[0] == "x_rotation": - new_op = RXGate(total_angle) - else: - raise RuntimeError("impossible case") - - new_op_phase = 0 - if np.mod(total_angle, (2 * np.pi)) > _CUTOFF_PRECISION: - new_qarg = QuantumRegister(1, "q") - new_dag = DAGCircuit() - new_dag.add_qreg(new_qarg) - new_dag.apply_operation_back(new_op, [new_qarg[0]]) - dag.substitute_node_with_dag(run[0], new_dag) - if new_op.definition: - new_op_phase = new_op.definition.global_phase - - dag.global_phase = total_phase - new_op_phase - - # Delete the other nodes in the run - for current_node in run[1:]: - dag.remove_op_node(current_node) - - if np.mod(total_angle, (2 * np.pi)) < _CUTOFF_PRECISION: - dag.remove_op_node(run[0]) - - dag = self._handle_control_flow_ops(dag) - - return dag - - def _handle_control_flow_ops(self, dag): - """ - This is similar to transpiler/passes/utils/control_flow.py except that the - commutation analysis is redone for the control flow blocks. - """ - - pass_manager = PassManager([CommutationAnalysis(), self]) - for node in dag.op_nodes(): - if node.name not in CONTROL_FLOW_OP_NAMES: - continue - mapped_blocks = [] - for block in node.op.blocks: - new_circ = pass_manager.run(block) - mapped_blocks.append(new_circ) - node.op = node.op.replace_blocks(mapped_blocks) + commutation_cancellation.cancel_commutations(dag, self._commutation_checker, self.basis) return dag diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 5f9744fc860a..97324e2376cd 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -12,6 +12,7 @@ """Cancel pairs of inverse gates exploiting commutation relations.""" from qiskit.circuit.commutation_library import SessionCommutationChecker as scc + from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.quantum_info import Operator from qiskit.quantum_info.operators.predicates import matrix_equal @@ -34,6 +35,7 @@ def __init__(self, matrix_based: bool = False, max_qubits: int = 4): """ self._matrix_based = matrix_based self._max_qubits = max_qubits + self.comm_checker = scc super().__init__() def _skip_node(self, node): @@ -92,7 +94,6 @@ def run(self, dag: DAGCircuit): removed = [False for _ in range(circ_size)] - cc = scc phase_update = 0 for idx1 in range(0, circ_size): @@ -118,13 +119,9 @@ def run(self, dag: DAGCircuit): matched_idx2 = idx2 break - if not cc.commute( - topo_sorted_nodes[idx1].op, - topo_sorted_nodes[idx1].qargs, - topo_sorted_nodes[idx1].cargs, - topo_sorted_nodes[idx2].op, - topo_sorted_nodes[idx2].qargs, - topo_sorted_nodes[idx2].cargs, + if not self.comm_checker.commute_nodes( + topo_sorted_nodes[idx1], + topo_sorted_nodes[idx2], max_num_qubits=self._max_qubits, ): break diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 12f7285af9dc..49f227e8a746 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -202,7 +202,11 @@ def _handle_control_flow_ops(self, dag): for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks) + dag.substitute_node( + node, + node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), + propagate_condition=False, + ) return dag def _check_not_in_basis(self, dag, gate_name, qargs): diff --git a/qiskit/transpiler/passes/optimization/hoare_opt.py b/qiskit/transpiler/passes/optimization/hoare_opt.py index a77f1985f696..e47074fc6142 100644 --- a/qiskit/transpiler/passes/optimization/hoare_opt.py +++ b/qiskit/transpiler/passes/optimization/hoare_opt.py @@ -203,20 +203,24 @@ def _traverse_dag(self, dag): """ import z3 - for node in dag.topological_op_nodes(): + # Pre-generate all DAG nodes, since we later iterate over them, while + # potentially modifying and removing some of them. + nodes = list(dag.topological_op_nodes()) + for node in nodes: gate = node.op - ctrlqb, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) + _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) ctrl_ones = z3.And(*ctrlvar) - remove_ctrl, new_dag, qb_idx = self._remove_control(gate, ctrlvar, trgtvar) + remove_ctrl, new_dag, _ = self._remove_control(gate, ctrlvar, trgtvar) if remove_ctrl: - dag.substitute_node_with_dag(node, new_dag) - gate = gate.base_gate - node.op = gate.to_mutable() - node.name = gate.name - node.qargs = tuple((ctrlqb + trgtqb)[qi] for qi in qb_idx) + # We are replacing a node by a new node over a smaller number of qubits. + # This can be done using substitute_node_with_dag, which furthermore returns + # a mapping from old node ids to new nodes. + mapped_nodes = dag.substitute_node_with_dag(node, new_dag) + node = next(iter(mapped_nodes.values())) + gate = node.op _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) ctrl_ones = z3.And(*ctrlvar) diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index f5523432c26e..40876679e8d9 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -20,6 +20,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit._accelerate.inverse_cancellation import inverse_cancellation + class InverseCancellation(TransformationPass): """Cancel specific Gates which are inverses of each other when they occur back-to- @@ -81,96 +83,11 @@ def run(self, dag: DAGCircuit): Returns: DAGCircuit: Transformed DAG. """ - if self.self_inverse_gates: - dag = self._run_on_self_inverse(dag) - if self.inverse_gate_pairs: - dag = self._run_on_inverse_pairs(dag) - return dag - - def _run_on_self_inverse(self, dag: DAGCircuit): - """ - Run self-inverse gates on `dag`. - - Args: - dag: the directed acyclic graph to run on. - self_inverse_gates: list of gates who cancel themeselves in pairs - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.self_inverse_gate_names.intersection(op_counts): - return dag - # Sets of gate runs by name, for instance: [{(H 0, H 0), (H 1, H 1)}, {(X 0, X 0}] - for gate in self.self_inverse_gates: - gate_name = gate.name - gate_count = op_counts.get(gate_name, 0) - if gate_count <= 1: - continue - gate_runs = dag.collect_runs([gate_name]) - for gate_cancel_run in gate_runs: - partitions = [] - chunk = [] - max_index = len(gate_cancel_run) - 1 - for i, cancel_gate in enumerate(gate_cancel_run): - if cancel_gate.op == gate: - chunk.append(cancel_gate) - else: - if chunk: - partitions.append(chunk) - chunk = [] - continue - if i == max_index or cancel_gate.qargs != gate_cancel_run[i + 1].qargs: - partitions.append(chunk) - chunk = [] - # Remove an even number of gates from each chunk - for chunk in partitions: - if len(chunk) % 2 == 0: - dag.remove_op_node(chunk[0]) - for node in chunk[1:]: - dag.remove_op_node(node) - return dag - - def _run_on_inverse_pairs(self, dag: DAGCircuit): - """ - Run inverse gate pairs on `dag`. - - Args: - dag: the directed acyclic graph to run on. - inverse_gate_pairs: list of gates with inverse angles that cancel each other. - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.inverse_gate_pairs_names.intersection(op_counts): - return dag - - for pair in self.inverse_gate_pairs: - gate_0_name = pair[0].name - gate_1_name = pair[1].name - if gate_0_name not in op_counts or gate_1_name not in op_counts: - continue - gate_cancel_runs = dag.collect_runs([gate_0_name, gate_1_name]) - for dag_nodes in gate_cancel_runs: - i = 0 - while i < len(dag_nodes) - 1: - if ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[0] - and dag_nodes[i + 1].op == pair[1] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - elif ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[1] - and dag_nodes[i + 1].op == pair[0] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - else: - i = i + 1 + inverse_cancellation( + dag, + self.inverse_gate_pairs, + self.self_inverse_gates, + self.inverse_gate_pairs_names, + self.self_inverse_gate_names, + ) return dag diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index e7c502c9ef9f..ac7673060746 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -35,7 +35,6 @@ from qiskit.circuit import Qubit from qiskit.circuit.quantumcircuitdata import CircuitInstruction from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.dagcircuit.dagnode import DAGOpNode logger = logging.getLogger(__name__) @@ -82,9 +81,12 @@ def __init__(self, basis=None, target=None): """ super().__init__() - self._basis_gates = basis + if basis: + self._basis_gates = set(basis) + else: + self._basis_gates = None self._target = target - self._global_decomposers = [] + self._global_decomposers = None self._local_decomposers_cache = {} if basis: @@ -209,48 +211,12 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - runs = [] - qubits = [] - bases = [] - for run in dag.collect_1q_runs(): - qubit = dag.find_bit(run[0].qargs[0]).index - runs.append(run) - qubits.append(qubit) - bases.append(self._get_decomposer(qubit)) - best_sequences = euler_one_qubit_decomposer.optimize_1q_gates_decomposition( - runs, qubits, bases, simplify=True, error_map=self.error_map + euler_one_qubit_decomposer.optimize_1q_gates_decomposition( + dag, + target=self._target, + global_decomposers=self._global_decomposers, + basis_gates=self._basis_gates, ) - for index, best_circuit_sequence in enumerate(best_sequences): - run = runs[index] - qubit = qubits[index] - if self._target is None: - basis = self._basis_gates - else: - basis = self._target.operation_names_for_qargs((qubit,)) - if best_circuit_sequence is not None: - (old_error, new_error, best_circuit_sequence) = best_circuit_sequence - if self._substitution_checks( - dag, - run, - best_circuit_sequence, - basis, - qubit, - old_error=old_error, - new_error=new_error, - ): - first_node_id = run[0]._node_id - qubit = run[0].qargs - for gate, angles in best_circuit_sequence: - op = CircuitInstruction.from_standard(gate, qubit, angles) - node = DAGOpNode.from_instruction(op, dag=dag) - node._node_id = dag._multi_graph.add_node(node) - dag._increment_op(gate.name) - dag._multi_graph.insert_node_on_in_edges(node._node_id, first_node_id) - dag.global_phase += best_circuit_sequence.global_phase - # Delete the other nodes in the run - for current_node in run: - dag.remove_op_node(current_node) - return dag def _error(self, circuit, qubit): diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index fe6fe7f49e78..0a583259c800 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -125,7 +125,9 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: # Handle control-flow for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): - node.op = control_flow.map_blocks(self.run, node.op) + dag.substitute_node( + node, control_flow.map_blocks(self.run, node.op), propagate_condition=False + ) # First, optimize every node in the DAG. dag, opt1 = self._canonicalize(dag) @@ -163,7 +165,7 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: node.op.modifiers = canonical_modifiers else: # no need for annotated operations - node.op = cur + dag.substitute_node(node, cur, propagate_condition=False) did_something = True return dag, did_something diff --git a/qiskit/transpiler/passes/optimization/remove_diagonal_gates_before_measure.py b/qiskit/transpiler/passes/optimization/remove_diagonal_gates_before_measure.py index be4c79aa47e6..3f72cb4a5cc0 100644 --- a/qiskit/transpiler/passes/optimization/remove_diagonal_gates_before_measure.py +++ b/qiskit/transpiler/passes/optimization/remove_diagonal_gates_before_measure.py @@ -12,24 +12,13 @@ """Remove diagonal gates (including diagonal 2Q gates) before a measurement.""" -from qiskit.circuit import Measure -from qiskit.circuit.library.standard_gates import ( - RZGate, - ZGate, - TGate, - SGate, - TdgGate, - SdgGate, - U1Gate, - CZGate, - CRZGate, - CU1Gate, - RZZGate, -) -from qiskit.dagcircuit import DAGOpNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow +from qiskit._accelerate.remove_diagonal_gates_before_measure import ( + remove_diagonal_gates_before_measure, +) + class RemoveDiagonalGatesBeforeMeasure(TransformationPass): """Remove diagonal gates (including diagonal 2Q gates) before a measurement. @@ -48,22 +37,5 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - diagonal_1q_gates = (RZGate, ZGate, TGate, SGate, TdgGate, SdgGate, U1Gate) - diagonal_2q_gates = (CZGate, CRZGate, CU1Gate, RZZGate) - - nodes_to_remove = set() - for measure in dag.op_nodes(Measure): - predecessor = next(dag.quantum_predecessors(measure)) - - if isinstance(predecessor, DAGOpNode) and isinstance(predecessor.op, diagonal_1q_gates): - nodes_to_remove.add(predecessor) - - if isinstance(predecessor, DAGOpNode) and isinstance(predecessor.op, diagonal_2q_gates): - successors = dag.quantum_successors(predecessor) - if all(isinstance(s, DAGOpNode) and isinstance(s.op, Measure) for s in successors): - nodes_to_remove.add(predecessor) - - for node_to_remove in nodes_to_remove: - dag.remove_op_node(node_to_remove) - + remove_diagonal_gates_before_measure(dag) return dag diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 7508c9440a6e..f6958a00a4c1 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -9,75 +9,32 @@ # 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. + """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" -from typing import Optional from qiskit.transpiler.basepasses import TransformationPass -from qiskit.circuit.quantumcircuitdata import CircuitInstruction -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.circuit.library.generalized_gates import UnitaryGate -from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit._accelerate.split_2q_unitaries import split_2q_unitaries class Split2QUnitaries(TransformationPass): - """Attempt to splits two-qubit gates in a :class:`.DAGCircuit` into two single-qubit gates + """Attempt to splits two-qubit unitaries in a :class:`.DAGCircuit` into two single-qubit gates. - This pass will analyze all the two qubit gates in the circuit and analyze the gate's unitary - matrix to determine if the gate is actually a product of 2 single qubit gates. In these - cases the 2q gate can be simplified into two single qubit gates and this pass will - perform this optimization and will replace the two qubit gate with two single qubit - :class:`.UnitaryGate`. + This pass will analyze all :class:`.UnitaryGate` instances and determine whether the + matrix is actually a product of 2 single qubit gates. In these cases the 2q gate can be + simplified into two single qubit gates and this pass will perform this optimization and will + replace the two qubit gate with two single qubit :class:`.UnitaryGate`. """ - def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): - """Split2QUnitaries initializer. - + def __init__(self, fidelity: float = 1.0 - 1e-16): + """ Args: - fidelity (float): Allowed tolerance for splitting two-qubit unitaries and gate decompositions + fidelity: Allowed tolerance for splitting two-qubit unitaries and gate decompositions. """ super().__init__() self.requested_fidelity = fidelity - def run(self, dag: DAGCircuit): + def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the Split2QUnitaries pass on `dag`.""" - for node in dag.topological_op_nodes(): - # skip operations without two-qubits and for which we can not determine a potential 1q split - if ( - len(node.cargs) > 0 - or len(node.qargs) != 2 - or node.matrix is None - or node.is_parameterized() - ): - continue - - decomp = TwoQubitWeylDecomposition(node.op, fidelity=self.requested_fidelity) - if ( - decomp._inner_decomposition.specialization - == TwoQubitWeylDecomposition._specializations.IdEquiv - ): - new_dag = DAGCircuit() - new_dag.add_qubits(node.qargs) - - ur = decomp.K1r - ur_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ur), qubits=(node.qargs[0],)), dag=new_dag - ) - - ul = decomp.K1l - ul_node = DAGOpNode.from_instruction( - CircuitInstruction(UnitaryGate(ul), qubits=(node.qargs[1],)), dag=new_dag - ) - new_dag._apply_op_node_back(ur_node) - new_dag._apply_op_node_back(ul_node) - new_dag.global_phase = decomp.global_phase - dag.substitute_node_with_dag(node, new_dag) - elif ( - decomp._inner_decomposition.specialization - == TwoQubitWeylDecomposition._specializations.SWAPEquiv - ): - # TODO maybe also look into swap-gate-like gates? Things to consider: - # * As the qubit mapping may change, we'll always need to build a new dag in this pass - # * There may not be many swap-gate-like gates in an arbitrary input circuit - # * Removing swap gates from a user-routed input circuit here is unexpected - pass + split_2q_unitaries(dag, self.requested_fidelity) return dag diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 788f0d995754..238444067168 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -41,8 +41,9 @@ class SabreSwap(TransformationPass): r"""Map input circuit onto a backend topology via insertion of SWAPs. Implementation of the SWAP-based heuristic search from the SABRE qubit - mapping paper [1] (Algorithm 1). The heuristic aims to minimize the number - of lossy SWAPs inserted and the depth of the circuit. + mapping paper [2] (Algorithm 1) with the modifications from the LightSABRE + paper [1]. The heuristic aims to minimize the number of lossy SWAPs inserted + and the depth of the circuit. This algorithm starts from an initial layout of virtual qubits onto physical qubits, and iterates over the circuit DAG until all gates are exhausted, @@ -69,7 +70,10 @@ class SabreSwap(TransformationPass): **References:** - [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem + [1] Henry Zou and Matthew Treinish and Kevin Hartman and Alexander Ivrii and Jake Lishman. + "LightSABRE: A Lightweight and Enhanced SABRE Algorithm" + `arXiv:2409.08368 `__ + [2] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem for NISQ-era quantum devices." ASPLOS 2019. `arXiv:1809.02573 `_ """ @@ -372,7 +376,7 @@ def empty_dag(block): empty.add_clbits(block.clbits) for creg in block.cregs: empty.add_creg(creg) - empty._global_phase = block.global_phase + empty.global_phase = block.global_phase return empty def apply_swaps(dest_dag, swaps, layout): @@ -388,7 +392,7 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): the virtual qubit in the root source DAG that it is bound to.""" swap_map, node_order, node_block_results = result for node_id in node_order: - node = source_dag._multi_graph[node_id] + node = source_dag.node(node_id) if node_id in swap_map: apply_swaps(dest_dag, swap_map[node_id], layout) if not node.is_control_flow(): diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 3732802b770e..efbd7e37f626 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -38,6 +38,7 @@ from qiskit._accelerate import stochastic_swap as stochastic_swap_rs from qiskit._accelerate import nlayout from qiskit.transpiler.passes.layout import disjoint_utils +from qiskit.utils import deprecate_func from .utils import get_swap_map_dag @@ -59,6 +60,12 @@ class StochasticSwap(TransformationPass): the circuit. """ + @deprecate_func( + since="1.3", + removal_timeline="in the 2.0 release", + additional_msg="The StochasticSwap transpilation pass is a suboptimal " + "routing algorithm and has been superseded by the SabreSwap pass.", + ) def __init__(self, coupling_map, trials=20, seed=None, fake_run=False, initial_layout=None): """StochasticSwap initializer. @@ -76,6 +83,7 @@ def __init__(self, coupling_map, trials=20, seed=None, fake_run=False, initial_l initial_layout (Layout): starting layout at beginning of pass. """ super().__init__() + if isinstance(coupling_map, Target): self.target = coupling_map self.coupling_map = self.target.build_coupling_map() @@ -215,7 +223,7 @@ def _layer_permutation(self, dag, layer_partition, layout, qubit_subset, couplin int_layout = nlayout.NLayout(layout_mapping, num_qubits, coupling.size()) trial_circuit = DAGCircuit() # SWAP circuit for slice of swaps in this trial - trial_circuit.add_qubits(layout.get_virtual_bits()) + trial_circuit.add_qubits(list(layout.get_virtual_bits())) edges = np.asarray(coupling.get_edges(), dtype=np.uint32).ravel() cdist = coupling._dist_matrix @@ -265,9 +273,7 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit): # Output any swaps if best_depth > 0: logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth) - dag.compose( - best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True - ) + dag.compose(best_circuit, qubits=list(best_circuit.qubits), inline_captures=True) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 0e351161f7df..2217d32f847c 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -370,7 +370,10 @@ def _pad( op = prev_node.op theta_l, phi_l, lam_l = op.params op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l) - prev_node.op = op + new_prev_node = dag.substitute_node(prev_node, op, propagate_condition=False) + start_time = self.property_set["node_start_time"].pop(prev_node) + if start_time is not None: + self.property_set["node_start_time"][new_prev_node] = start_time sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 2c7c97ec856a..d9f3d77f2915 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -72,9 +72,9 @@ def _get_node_duration( # Note that node duration is updated (but this is analysis pass) op = node.op.to_mutable() op.duration = duration - node.op = op + dag.substitute_node(node, op, propagate_condition=False) else: - duration = node.op.duration + duration = node.duration if isinstance(duration, ParameterExpression): raise TranspilerError( diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 08ac932d8aeb..f4f70210b785 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -108,7 +108,7 @@ def run(self, dag: DAGCircuit): op = node.op.to_mutable() op.duration = duration op.unit = time_unit - node.op = op + dag.substitute_node(node, op, propagate_condition=False) self.property_set["time_unit"] = time_unit return dag diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 247898182cba..31036de69645 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -10,153 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - """ - -High Level Synthesis Plugins ------------------------------ - -Clifford Synthesis -'''''''''''''''''' - -.. list-table:: Plugins for :class:`qiskit.quantum_info.Clifford` (key = ``"clifford"``) - :header-rows: 1 - - * - Plugin name - - Plugin class - - Targeted connectivity - - Description - * - ``"ag"`` - - :class:`~.AGSynthesisClifford` - - all-to-all - - greedily optimizes CX-count - * - ``"bm"`` - - :class:`~.BMSynthesisClifford` - - all-to-all - - optimal count for `n=2,3`; used in ``"default"`` for `n=2,3` - * - ``"greedy"`` - - :class:`~.GreedySynthesisClifford` - - all-to-all - - greedily optimizes CX-count; used in ``"default"`` for `n>=4` - * - ``"layers"`` - - :class:`~.LayerSynthesisClifford` - - all-to-all - - - * - ``"lnn"`` - - :class:`~.LayerLnnSynthesisClifford` - - linear - - many CX-gates but guarantees CX-depth of at most `7*n+2` - * - ``"default"`` - - :class:`~.DefaultSynthesisClifford` - - all-to-all - - usually best for optimizing CX-count (and optimal CX-count for `n=2,3`) - -.. autosummary:: - :toctree: ../stubs/ - - AGSynthesisClifford - BMSynthesisClifford - GreedySynthesisClifford - LayerSynthesisClifford - LayerLnnSynthesisClifford - DefaultSynthesisClifford - - -Linear Function Synthesis -''''''''''''''''''''''''' - -.. list-table:: Plugins for :class:`.LinearFunction` (key = ``"linear"``) - :header-rows: 1 - - * - Plugin name - - Plugin class - - Targeted connectivity - - Description - * - ``"kms"`` - - :class:`~.KMSSynthesisLinearFunction` - - linear - - many CX-gates but guarantees CX-depth of at most `5*n` - * - ``"pmh"`` - - :class:`~.PMHSynthesisLinearFunction` - - all-to-all - - greedily optimizes CX-count; used in ``"default"`` - * - ``"default"`` - - :class:`~.DefaultSynthesisLinearFunction` - - all-to-all - - best for optimizing CX-count - -.. autosummary:: - :toctree: ../stubs/ - - KMSSynthesisLinearFunction - PMHSynthesisLinearFunction - DefaultSynthesisLinearFunction - - -Permutation Synthesis -''''''''''''''''''''' - -.. list-table:: Plugins for :class:`.PermutationGate` (key = ``"permutation"``) - :header-rows: 1 - - * - Plugin name - - Plugin class - - Targeted connectivity - - Description - * - ``"basic"`` - - :class:`~.BasicSynthesisPermutation` - - all-to-all - - optimal SWAP-count; used in ``"default"`` - * - ``"acg"`` - - :class:`~.ACGSynthesisPermutation` - - all-to-all - - guarantees SWAP-depth of at most `2` - * - ``"kms"`` - - :class:`~.KMSSynthesisPermutation` - - linear - - many SWAP-gates, but guarantees SWAP-depth of at most `n` - * - ``"token_swapper"`` - - :class:`~.TokenSwapperSynthesisPermutation` - - any - - greedily optimizes SWAP-count for arbitrary connectivity - * - ``"default"`` - - :class:`~.BasicSynthesisPermutation` - - all-to-all - - best for optimizing SWAP-count - -.. autosummary:: - :toctree: ../stubs/ - - BasicSynthesisPermutation - ACGSynthesisPermutation - KMSSynthesisPermutation - TokenSwapperSynthesisPermutation - - -QFT Synthesis -''''''''''''' - -.. list-table:: Plugins for :class:`.QFTGate` (key = ``"qft"``) - :header-rows: 1 - - * - Plugin name - - Plugin class - - Targeted connectivity - * - ``"full"`` - - :class:`~.QFTSynthesisFull` - - all-to-all - * - ``"line"`` - - :class:`~.QFTSynthesisLine` - - linear - * - ``"default"`` - - :class:`~.QFTSynthesisFull` - - all-to-all - -.. autosummary:: - :toctree: ../stubs/ - - QFTSynthesisFull - QFTSynthesisLine +High-level-synthesis transpiler pass. """ from __future__ import annotations @@ -166,7 +21,6 @@ from collections.abc import Callable import numpy as np -import rustworkx as rx from qiskit.circuit.annotated_operation import Modifier from qiskit.circuit.operation import Operation @@ -175,13 +29,11 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence -from qiskit.circuit.library import LinearFunction from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -189,32 +41,8 @@ ControlModifier, PowerModifier, ) -from qiskit.circuit.library import QFTGate -from qiskit.synthesis.clifford import ( - synth_clifford_full, - synth_clifford_layers, - synth_clifford_depth_lnn, - synth_clifford_greedy, - synth_clifford_ag, - synth_clifford_bm, -) -from qiskit.synthesis.linear import ( - synth_cnot_count_full_pmh, - synth_cnot_depth_line_kms, - calc_inverse_matrix, -) -from qiskit.synthesis.linear.linear_circuits_utils import transpose_cx_circ -from qiskit.synthesis.permutation import ( - synth_permutation_basic, - synth_permutation_acg, - synth_permutation_depth_lnn_kms, -) -from qiskit.synthesis.qft import ( - synth_qft_full, - synth_qft_line, -) -from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin +from .plugin import HighLevelSynthesisPluginManager from .qubit_tracker import QubitTracker if typing.TYPE_CHECKING: @@ -452,6 +280,15 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return self._run(dag, tracker) def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: + # Check if HighLevelSynthesis can be skipped. + for node in dag.op_nodes(): + qubits = tuple(dag.find_bit(q).index for q in node.qargs) + if not self._definitely_skip_node(node, qubits, dag): + break + else: + # The for-loop terminates without reaching the break statement + return dag + # Start by analyzing the nodes in the DAG. This for-loop is a first version of a potentially # more elaborate approach to find good operation/ancilla allocations. It greedily iterates # over the nodes, checking whether we can synthesize them, while keeping track of the @@ -465,24 +302,21 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: used_qubits = None # check if synthesis for the operation can be skipped - if ( - dag.has_calibration_for(node) - or len(node.qargs) < self._min_qubits - or node.is_directive() - or self._definitely_skip_node(node, qubits) - ): + if self._definitely_skip_node(node, qubits, dag): pass # next check control flow elif node.is_control_flow(): - node.op = control_flow.map_blocks( - partial(self._run, tracker=tracker.copy()), node.op + dag.substitute_node( + node, + control_flow.map_blocks(partial(self._run, tracker=tracker.copy()), node.op), + propagate_condition=False, ) # now we are free to synthesize else: # this returns the synthesized operation and the qubits it acts on -- note that this - # may be different than the original qubits, since we may use auxiliary qubits + # may be different from the original qubits, since we may use auxiliary qubits synthesized, used_qubits = self._synthesize_operation(node.op, qubits, tracker) # if the synthesis changed the operation (i.e. it is not None), store the result @@ -505,7 +339,7 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: if len(synthesized_nodes) == 0: return dag - # Otherwise we will rebuild with the new operations. Note that we could also + # Otherwise, we will rebuild with the new operations. Note that we could also # check if no operation changed in size and substitute in-place, but rebuilding is # generally as fast or faster, unless very few operations are changed. out = dag.copy_empty_like() @@ -801,19 +635,32 @@ def _apply_annotations( return synthesized - def _definitely_skip_node(self, node: DAGOpNode, qubits: tuple[int] | None) -> bool: + def _definitely_skip_node( + self, node: DAGOpNode, qubits: tuple[int] | None, dag: DAGCircuit + ) -> bool: """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will attempt to synthesise it) without accessing its Python-space `Operation`. This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the node (which is _most_ nodes).""" + + if ( + dag.has_calibration_for(node) + or len(node.qargs) < self._min_qubits + or node.is_directive() + ): + return True + return ( # The fast path is just for Rust-space standard gates (which excludes # `AnnotatedOperation`). node.is_standard_gate() - # If it's a controlled gate, we might choose to do funny things to it. - and not node.is_controlled_gate() + # We don't have the fast-path for controlled gates over 3 or more qubits. + # However, we most probably want the fast-path for controlled 2-qubit gates + # (such as CX, CZ, CY, CH, CRX, and so on), so "_definitely_skip_node" should + # not immediately return False when encountering a controlled gate over 2 qubits. + and not (node.is_controlled_gate() and node.num_qubits >= 3) # If there are plugins to try, they need to be tried. and not self._methods_to_try(node.name) # If all the above constraints hold, and it's already supported or the basis translator @@ -838,424 +685,6 @@ def _instruction_supported(self, name: str, qubits: tuple[int] | None) -> bool: return self._target.instruction_supported(operation_name=name, qargs=qubits) -class DefaultSynthesisClifford(HighLevelSynthesisPlugin): - """The default clifford synthesis plugin. - - For N <= 3 qubits this is the optimal CX cost decomposition by Bravyi, Maslov. - For N > 3 qubits this is done using the general non-optimal greedy compilation - routine from reference by Bravyi, Hu, Maslov, Shaydulin. - - This plugin name is :``clifford.default`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - decomposition = synth_clifford_full(high_level_object) - return decomposition - - -class AGSynthesisClifford(HighLevelSynthesisPlugin): - """Clifford synthesis plugin based on the Aaronson-Gottesman method. - - This plugin name is :``clifford.ag`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - decomposition = synth_clifford_ag(high_level_object) - return decomposition - - -class BMSynthesisClifford(HighLevelSynthesisPlugin): - """Clifford synthesis plugin based on the Bravyi-Maslov method. - - The method only works on Cliffords with at most 3 qubits, for which it - constructs the optimal CX cost decomposition. - - This plugin name is :``clifford.bm`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - if high_level_object.num_qubits <= 3: - decomposition = synth_clifford_bm(high_level_object) - else: - decomposition = None - return decomposition - - -class GreedySynthesisClifford(HighLevelSynthesisPlugin): - """Clifford synthesis plugin based on the greedy synthesis - Bravyi-Hu-Maslov-Shaydulin method. - - This plugin name is :``clifford.greedy`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - decomposition = synth_clifford_greedy(high_level_object) - return decomposition - - -class LayerSynthesisClifford(HighLevelSynthesisPlugin): - """Clifford synthesis plugin based on the Bravyi-Maslov method - to synthesize Cliffords into layers. - - This plugin name is :``clifford.layers`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - decomposition = synth_clifford_layers(high_level_object) - return decomposition - - -class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin): - """Clifford synthesis plugin based on the Bravyi-Maslov method - to synthesize Cliffords into layers, with each layer synthesized - adhering to LNN connectivity. - - This plugin name is :``clifford.lnn`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Clifford.""" - decomposition = synth_clifford_depth_lnn(high_level_object) - return decomposition - - -class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin): - """The default linear function synthesis plugin. - - This plugin name is :``linear_function.default`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given LinearFunction.""" - decomposition = synth_cnot_count_full_pmh(high_level_object.linear) - return decomposition - - -class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): - """Linear function synthesis plugin based on the Kutin-Moulton-Smithline method. - - This plugin name is :``linear_function.kms`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - - The plugin supports the following plugin-specific options: - - * use_inverted: Indicates whether to run the algorithm on the inverse matrix - and to invert the synthesized circuit. - In certain cases this provides a better decomposition than the direct approach. - * use_transposed: Indicates whether to run the algorithm on the transposed matrix - and to invert the order of CX gates in the synthesized circuit. - In certain cases this provides a better decomposition than the direct approach. - - """ - - 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" - ) - - use_inverted = options.get("use_inverted", False) - use_transposed = options.get("use_transposed", False) - - mat = high_level_object.linear.astype(bool, copy=False) - - if use_transposed: - mat = np.transpose(mat) - if use_inverted: - mat = calc_inverse_matrix(mat) - - decomposition = synth_cnot_depth_line_kms(mat) - - if use_transposed: - decomposition = transpose_cx_circ(decomposition) - if use_inverted: - decomposition = decomposition.inverse() - - return decomposition - - -class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): - """Linear function synthesis plugin based on the Patel-Markov-Hayes method. - - This plugin name is :``linear_function.pmh`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - - The plugin supports the following plugin-specific options: - - * section size: The size of each section used in the Patel–Markov–Hayes algorithm [1]. - * use_inverted: Indicates whether to run the algorithm on the inverse matrix - and to invert the synthesized circuit. - In certain cases this provides a better decomposition than the direct approach. - * use_transposed: Indicates whether to run the algorithm on the transposed matrix - and to invert the order of CX gates in the synthesized circuit. - In certain cases this provides a better decomposition than the direct approach. - - References: - 1. Patel, Ketan N., Igor L. Markov, and John P. Hayes, - *Optimal synthesis of linear reversible circuits*, - Quantum Information & Computation 8.3 (2008): 282-294. - `arXiv:quant-ph/0302002 [quant-ph] `_ - """ - - 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" - ) - - section_size = options.get("section_size", 2) - use_inverted = options.get("use_inverted", False) - use_transposed = options.get("use_transposed", False) - - mat = high_level_object.linear.astype(bool, copy=False) - - if use_transposed: - mat = np.transpose(mat) - if use_inverted: - mat = calc_inverse_matrix(mat) - - decomposition = synth_cnot_count_full_pmh(mat, section_size=section_size) - - if use_transposed: - decomposition = transpose_cx_circ(decomposition) - if use_inverted: - decomposition = decomposition.inverse() - - return decomposition - - -class KMSSynthesisPermutation(HighLevelSynthesisPlugin): - """The permutation synthesis plugin based on the Kutin, Moulton, Smithline method. - - This plugin name is :``permutation.kms`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Permutation.""" - decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) - return decomposition - - -class BasicSynthesisPermutation(HighLevelSynthesisPlugin): - """The permutation synthesis plugin based on sorting. - - This plugin name is :``permutation.basic`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Permutation.""" - decomposition = synth_permutation_basic(high_level_object.pattern) - return decomposition - - -class ACGSynthesisPermutation(HighLevelSynthesisPlugin): - """The permutation synthesis plugin based on the Alon, Chung, Graham method. - - This plugin name is :``permutation.acg`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Permutation.""" - decomposition = synth_permutation_acg(high_level_object.pattern) - return decomposition - - -class QFTSynthesisFull(HighLevelSynthesisPlugin): - """Synthesis plugin for QFT gates using all-to-all connectivity. - - This plugin name is :``qft.full`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - - The plugin supports the following additional options: - - * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, - which is the default) or the "QFT-with-reversal" operation (if ``True``). - Some implementation of the ``QFTGate`` include a layer of swap gates at the end - of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` - itself is the last gate in the circuit. - * approximation_degree (int): The degree of approximation (0 for no approximation). - It is possible to implement the QFT approximately by ignoring - controlled-phase rotations with the angle beneath a threshold. This is discussed - in more detail in [1] or [2]. - * insert_barriers (bool): If True, barriers are inserted as visualization improvement. - * inverse (bool): If True, the inverse Fourier transform is constructed. - * name (str): The name of the circuit. - - References: - 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, - *Approximate Quantum Fourier Transform and Decoherence*, - Physical Review A (1996). - `arXiv:quant-ph/9601018 [quant-ph] `_ - 2. Donny Cheung, - *Improved Bounds for the Approximate QFT* (2004), - `arXiv:quant-ph/0403071 [quant-ph] `_ - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given QFTGate.""" - if not isinstance(high_level_object, QFTGate): - raise TranspilerError( - "The synthesis plugin 'qft.full` only applies to objects of type QFTGate." - ) - - reverse_qubits = options.get("reverse_qubits", False) - approximation_degree = options.get("approximation_degree", 0) - insert_barriers = options.get("insert_barriers", False) - inverse = options.get("inverse", False) - name = options.get("name", None) - - decomposition = synth_qft_full( - num_qubits=high_level_object.num_qubits, - do_swaps=not reverse_qubits, - approximation_degree=approximation_degree, - insert_barriers=insert_barriers, - inverse=inverse, - name=name, - ) - return decomposition - - -class QFTSynthesisLine(HighLevelSynthesisPlugin): - """Synthesis plugin for QFT gates using linear connectivity. - - This plugin name is :``qft.line`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - - The plugin supports the following additional options: - - * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, - which is the default) or the "QFT-with-reversal" operation (if ``True``). - Some implementation of the ``QFTGate`` include a layer of swap gates at the end - of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` - itself is the last gate in the circuit. - * approximation_degree (int): the degree of approximation (0 for no approximation). - It is possible to implement the QFT approximately by ignoring - controlled-phase rotations with the angle beneath a threshold. This is discussed - in more detail in [1] or [2]. - - References: - 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, - *Approximate Quantum Fourier Transform and Decoherence*, - Physical Review A (1996). - `arXiv:quant-ph/9601018 [quant-ph] `_ - 2. Donny Cheung, - *Improved Bounds for the Approximate QFT* (2004), - `arXiv:quant-ph/0403071 [quant-ph] `_ - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given QFTGate.""" - if not isinstance(high_level_object, QFTGate): - raise TranspilerError( - "The synthesis plugin 'qft.line` only applies to objects of type QFTGate." - ) - - reverse_qubits = options.get("reverse_qubits", False) - approximation_degree = options.get("approximation_degree", 0) - - decomposition = synth_qft_line( - num_qubits=high_level_object.num_qubits, - do_swaps=not reverse_qubits, - approximation_degree=approximation_degree, - ) - return decomposition - - -class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): - """The permutation synthesis plugin based on the token swapper algorithm. - - This plugin name is :``permutation.token_swapper`` which can be used as the key on - an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. - - In more detail, this plugin is used to synthesize objects of type `PermutationGate`. - When synthesis succeeds, the plugin outputs a quantum circuit consisting only of swap - gates. When synthesis does not succeed, the plugin outputs `None`. - - If either `coupling_map` or `qubits` is None, then the synthesized circuit - is not required to adhere to connectivity constraints, as is the case - when the synthesis is done before layout/routing. - - On the other hand, if both `coupling_map` and `qubits` are specified, the synthesized - circuit is supposed to adhere to connectivity constraints. At the moment, the - plugin only creates swap gates between qubits in `qubits`, i.e. it does not use - any other qubits in the coupling map (if such synthesis is not possible, the - plugin outputs `None`). - - The plugin supports the following plugin-specific options: - - * trials: The number of trials for the token swapper to perform the mapping. The - circuit with the smallest number of SWAPs is returned. - * seed: The argument to the token swapper specifying the seed for random trials. - * parallel_threshold: The argument to the token swapper specifying the number of nodes - in the graph beyond which the algorithm will use parallel processing. - - For more details on the token swapper algorithm, see to the paper: - `arXiv:1902.09102 `__. - """ - - def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - """Run synthesis for the given Permutation.""" - - trials = options.get("trials", 5) - seed = options.get("seed", 0) - parallel_threshold = options.get("parallel_threshold", 50) - - pattern = high_level_object.pattern - pattern_as_dict = {j: i for i, j in enumerate(pattern)} - - # When the plugin is called from the HighLevelSynthesis transpiler pass, - # the coupling map already takes target into account. - if coupling_map is None or qubits is None: - # The abstract synthesis uses a fully connected coupling map, allowing - # arbitrary connections between qubits. - used_coupling_map = CouplingMap.from_full(len(pattern)) - else: - # The concrete synthesis uses the coupling map restricted to the set of - # qubits over which the permutation gate is defined. If we allow using other - # qubits in the coupling map, replacing the node in the DAGCircuit that - # defines this PermutationGate by the DAG corresponding to the constructed - # decomposition becomes problematic. Note that we allow the reduced - # coupling map to be disconnected. - used_coupling_map = coupling_map.reduce(qubits, check_if_connected=False) - - graph = used_coupling_map.graph.to_undirected() - swapper = ApproximateTokenSwapper(graph, seed=seed) - - try: - swapper_result = swapper.map( - pattern_as_dict, trials, parallel_threshold=parallel_threshold - ) - except rx.InvalidMapping: - swapper_result = None - - if swapper_result is not None: - decomposition = QuantumCircuit(len(graph.node_indices())) - for swap in swapper_result: - decomposition.swap(*swap) - return decomposition - - return None - - def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: circuit = QuantumCircuit(inst.num_qubits, inst.num_clbits) circuit.append(inst, circuit.qubits, circuit.clbits) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py new file mode 100644 index 000000000000..3dce030c1982 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -0,0 +1,928 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 2023. +# +# 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. + + +""" + +High Level Synthesis Plugins +----------------------------- + +Clifford Synthesis +'''''''''''''''''' + +.. list-table:: Plugins for :class:`qiskit.quantum_info.Clifford` (key = ``"clifford"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Targeted connectivity + - Description + * - ``"ag"`` + - :class:`~.AGSynthesisClifford` + - all-to-all + - greedily optimizes CX-count + * - ``"bm"`` + - :class:`~.BMSynthesisClifford` + - all-to-all + - optimal count for `n=2,3`; used in ``"default"`` for `n=2,3` + * - ``"greedy"`` + - :class:`~.GreedySynthesisClifford` + - all-to-all + - greedily optimizes CX-count; used in ``"default"`` for `n>=4` + * - ``"layers"`` + - :class:`~.LayerSynthesisClifford` + - all-to-all + - + * - ``"lnn"`` + - :class:`~.LayerLnnSynthesisClifford` + - linear + - many CX-gates but guarantees CX-depth of at most `7*n+2` + * - ``"default"`` + - :class:`~.DefaultSynthesisClifford` + - all-to-all + - usually best for optimizing CX-count (and optimal CX-count for `n=2,3`) + +.. autosummary:: + :toctree: ../stubs/ + + AGSynthesisClifford + BMSynthesisClifford + GreedySynthesisClifford + LayerSynthesisClifford + LayerLnnSynthesisClifford + DefaultSynthesisClifford + + +Linear Function Synthesis +''''''''''''''''''''''''' + +.. list-table:: Plugins for :class:`.LinearFunction` (key = ``"linear"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Targeted connectivity + - Description + * - ``"kms"`` + - :class:`~.KMSSynthesisLinearFunction` + - linear + - many CX-gates but guarantees CX-depth of at most `5*n` + * - ``"pmh"`` + - :class:`~.PMHSynthesisLinearFunction` + - all-to-all + - greedily optimizes CX-count; used in ``"default"`` + * - ``"default"`` + - :class:`~.DefaultSynthesisLinearFunction` + - all-to-all + - best for optimizing CX-count + +.. autosummary:: + :toctree: ../stubs/ + + KMSSynthesisLinearFunction + PMHSynthesisLinearFunction + DefaultSynthesisLinearFunction + + +Permutation Synthesis +''''''''''''''''''''' + +.. list-table:: Plugins for :class:`.PermutationGate` (key = ``"permutation"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Targeted connectivity + - Description + * - ``"basic"`` + - :class:`~.BasicSynthesisPermutation` + - all-to-all + - optimal SWAP-count; used in ``"default"`` + * - ``"acg"`` + - :class:`~.ACGSynthesisPermutation` + - all-to-all + - guarantees SWAP-depth of at most `2` + * - ``"kms"`` + - :class:`~.KMSSynthesisPermutation` + - linear + - many SWAP-gates, but guarantees SWAP-depth of at most `n` + * - ``"token_swapper"`` + - :class:`~.TokenSwapperSynthesisPermutation` + - any + - greedily optimizes SWAP-count for arbitrary connectivity + * - ``"default"`` + - :class:`~.BasicSynthesisPermutation` + - all-to-all + - best for optimizing SWAP-count + +.. autosummary:: + :toctree: ../stubs/ + + BasicSynthesisPermutation + ACGSynthesisPermutation + KMSSynthesisPermutation + TokenSwapperSynthesisPermutation + + +QFT Synthesis +''''''''''''' + +.. list-table:: Plugins for :class:`.QFTGate` (key = ``"qft"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Targeted connectivity + * - ``"full"`` + - :class:`~.QFTSynthesisFull` + - all-to-all + * - ``"line"`` + - :class:`~.QFTSynthesisLine` + - linear + * - ``"default"`` + - :class:`~.QFTSynthesisFull` + - all-to-all + +.. autosummary:: + :toctree: ../stubs/ + + QFTSynthesisFull + QFTSynthesisLine + + +MCX Synthesis +''''''''''''' + +The following table lists synthesis plugins available for an :class:`.MCXGate` gate +with `k` control qubits. If the available number of clean/dirty auxiliary qubits is +not sufficient, the corresponding synthesis method will return `None`. + +.. list-table:: Plugins for :class:`.MCXGate` (key = ``"mcx"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Number of clean ancillas + - Number of dirty ancillas + - Description + * - ``"gray_code"`` + - :class:`~.MCXSynthesisGrayCode` + - `0` + - `0` + - exponentially many CX gates; use only for small values of `k` + * - ``"noaux_v24"`` + - :class:`~.MCXSynthesisNoAuxV24` + - `0` + - `0` + - quadratic number of CX gates; use instead of ``"gray_code"`` for large values of `k` + * - ``"n_clean_m15"`` + - :class:`~.MCXSynthesisNCleanM15` + - `k-2` + - `0` + - at most `6*k-6` CX gates + * - ``"n_dirty_i15"`` + - :class:`~.MCXSynthesisNDirtyI15` + - `0` + - `k-2` + - at most `8*k-6` CX gates + * - ``"1_clean_b95"`` + - :class:`~.MCXSynthesis1CleanB95` + - `1` + - `0` + - at most `16*k-8` CX gates + * - ``"default"`` + - :class:`~.MCXSynthesisDefault` + - any + - any + - chooses the best algorithm based on the ancillas available + +.. autosummary:: + :toctree: ../stubs/ + + MCXSynthesisGrayCode + MCXSynthesisNoAuxV24 + MCXSynthesisNCleanM15 + MCXSynthesisNDirtyI15 + MCXSynthesis1CleanB95 + MCXSynthesisDefault +""" + +import numpy as np +import rustworkx as rx + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.library import LinearFunction, QFTGate, MCXGate, C3XGate, C4XGate +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.coupling import CouplingMap + +from qiskit.synthesis.clifford import ( + synth_clifford_full, + synth_clifford_layers, + synth_clifford_depth_lnn, + synth_clifford_greedy, + synth_clifford_ag, + synth_clifford_bm, +) +from qiskit.synthesis.linear import ( + synth_cnot_count_full_pmh, + synth_cnot_depth_line_kms, + calc_inverse_matrix, +) +from qiskit.synthesis.linear.linear_circuits_utils import transpose_cx_circ +from qiskit.synthesis.permutation import ( + synth_permutation_basic, + synth_permutation_acg, + synth_permutation_depth_lnn_kms, +) +from qiskit.synthesis.qft import ( + synth_qft_full, + synth_qft_line, +) +from qiskit.synthesis.multi_controlled import ( + synth_mcx_n_dirty_i15, + synth_mcx_n_clean_m15, + synth_mcx_1_clean_b95, + synth_mcx_gray_code, + synth_mcx_noaux_v24, +) +from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper +from .plugin import HighLevelSynthesisPlugin + + +class DefaultSynthesisClifford(HighLevelSynthesisPlugin): + """The default clifford synthesis plugin. + + For N <= 3 qubits this is the optimal CX cost decomposition by Bravyi, Maslov. + For N > 3 qubits this is done using the general non-optimal greedy compilation + routine from reference by Bravyi, Hu, Maslov, Shaydulin. + + This plugin name is :``clifford.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + decomposition = synth_clifford_full(high_level_object) + return decomposition + + +class AGSynthesisClifford(HighLevelSynthesisPlugin): + """Clifford synthesis plugin based on the Aaronson-Gottesman method. + + This plugin name is :``clifford.ag`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + decomposition = synth_clifford_ag(high_level_object) + return decomposition + + +class BMSynthesisClifford(HighLevelSynthesisPlugin): + """Clifford synthesis plugin based on the Bravyi-Maslov method. + + The method only works on Cliffords with at most 3 qubits, for which it + constructs the optimal CX cost decomposition. + + This plugin name is :``clifford.bm`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + if high_level_object.num_qubits <= 3: + decomposition = synth_clifford_bm(high_level_object) + else: + decomposition = None + return decomposition + + +class GreedySynthesisClifford(HighLevelSynthesisPlugin): + """Clifford synthesis plugin based on the greedy synthesis + Bravyi-Hu-Maslov-Shaydulin method. + + This plugin name is :``clifford.greedy`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + decomposition = synth_clifford_greedy(high_level_object) + return decomposition + + +class LayerSynthesisClifford(HighLevelSynthesisPlugin): + """Clifford synthesis plugin based on the Bravyi-Maslov method + to synthesize Cliffords into layers. + + This plugin name is :``clifford.layers`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + decomposition = synth_clifford_layers(high_level_object) + return decomposition + + +class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin): + """Clifford synthesis plugin based on the Bravyi-Maslov method + to synthesize Cliffords into layers, with each layer synthesized + adhering to LNN connectivity. + + This plugin name is :``clifford.lnn`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Clifford.""" + decomposition = synth_clifford_depth_lnn(high_level_object) + return decomposition + + +class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin): + """The default linear function synthesis plugin. + + This plugin name is :``linear_function.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given LinearFunction.""" + decomposition = synth_cnot_count_full_pmh(high_level_object.linear) + return decomposition + + +class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): + """Linear function synthesis plugin based on the Kutin-Moulton-Smithline method. + + This plugin name is :``linear_function.kms`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following plugin-specific options: + + * use_inverted: Indicates whether to run the algorithm on the inverse matrix + and to invert the synthesized circuit. + In certain cases this provides a better decomposition than the direct approach. + * use_transposed: Indicates whether to run the algorithm on the transposed matrix + and to invert the order of CX gates in the synthesized circuit. + In certain cases this provides a better decomposition than the direct approach. + + """ + + 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" + ) + + use_inverted = options.get("use_inverted", False) + use_transposed = options.get("use_transposed", False) + + mat = high_level_object.linear.astype(bool, copy=False) + + if use_transposed: + mat = np.transpose(mat) + if use_inverted: + mat = calc_inverse_matrix(mat) + + decomposition = synth_cnot_depth_line_kms(mat) + + if use_transposed: + decomposition = transpose_cx_circ(decomposition) + if use_inverted: + decomposition = decomposition.inverse() + + return decomposition + + +class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): + """Linear function synthesis plugin based on the Patel-Markov-Hayes method. + + This plugin name is :``linear_function.pmh`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following plugin-specific options: + + * section size: The size of each section used in the Patel–Markov–Hayes algorithm [1]. + * use_inverted: Indicates whether to run the algorithm on the inverse matrix + and to invert the synthesized circuit. + In certain cases this provides a better decomposition than the direct approach. + * use_transposed: Indicates whether to run the algorithm on the transposed matrix + and to invert the order of CX gates in the synthesized circuit. + In certain cases this provides a better decomposition than the direct approach. + + References: + 1. Patel, Ketan N., Igor L. Markov, and John P. Hayes, + *Optimal synthesis of linear reversible circuits*, + Quantum Information & Computation 8.3 (2008): 282-294. + `arXiv:quant-ph/0302002 [quant-ph] `_ + """ + + 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" + ) + + section_size = options.get("section_size", 2) + use_inverted = options.get("use_inverted", False) + use_transposed = options.get("use_transposed", False) + + mat = high_level_object.linear.astype(bool, copy=False) + + if use_transposed: + mat = np.transpose(mat) + if use_inverted: + mat = calc_inverse_matrix(mat) + + decomposition = synth_cnot_count_full_pmh(mat, section_size=section_size) + + if use_transposed: + decomposition = transpose_cx_circ(decomposition) + if use_inverted: + decomposition = decomposition.inverse() + + return decomposition + + +class KMSSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the Kutin, Moulton, Smithline method. + + This plugin name is :``permutation.kms`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) + return decomposition + + +class BasicSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on sorting. + + This plugin name is :``permutation.basic`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_basic(high_level_object.pattern) + return decomposition + + +class ACGSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the Alon, Chung, Graham method. + + This plugin name is :``permutation.acg`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_acg(high_level_object.pattern) + return decomposition + + +class QFTSynthesisFull(HighLevelSynthesisPlugin): + """Synthesis plugin for QFT gates using all-to-all connectivity. + + This plugin name is :``qft.full`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following additional options: + + * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, + which is the default) or the "QFT-with-reversal" operation (if ``True``). + Some implementation of the ``QFTGate`` include a layer of swap gates at the end + of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` + itself is the last gate in the circuit. + * approximation_degree (int): The degree of approximation (0 for no approximation). + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in [1] or [2]. + * insert_barriers (bool): If True, barriers are inserted as visualization improvement. + * inverse (bool): If True, the inverse Fourier transform is constructed. + * name (str): The name of the circuit. + + References: + 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, + *Approximate Quantum Fourier Transform and Decoherence*, + Physical Review A (1996). + `arXiv:quant-ph/9601018 [quant-ph] `_ + 2. Donny Cheung, + *Improved Bounds for the Approximate QFT* (2004), + `arXiv:quant-ph/0403071 [quant-ph] `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given QFTGate.""" + if not isinstance(high_level_object, QFTGate): + raise TranspilerError( + "The synthesis plugin 'qft.full` only applies to objects of type QFTGate." + ) + + reverse_qubits = options.get("reverse_qubits", False) + approximation_degree = options.get("approximation_degree", 0) + insert_barriers = options.get("insert_barriers", False) + inverse = options.get("inverse", False) + name = options.get("name", None) + + decomposition = synth_qft_full( + num_qubits=high_level_object.num_qubits, + do_swaps=not reverse_qubits, + approximation_degree=approximation_degree, + insert_barriers=insert_barriers, + inverse=inverse, + name=name, + ) + return decomposition + + +class QFTSynthesisLine(HighLevelSynthesisPlugin): + """Synthesis plugin for QFT gates using linear connectivity. + + This plugin name is :``qft.line`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following additional options: + + * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, + which is the default) or the "QFT-with-reversal" operation (if ``True``). + Some implementation of the ``QFTGate`` include a layer of swap gates at the end + of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` + itself is the last gate in the circuit. + * approximation_degree (int): the degree of approximation (0 for no approximation). + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in [1] or [2]. + + References: + 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, + *Approximate Quantum Fourier Transform and Decoherence*, + Physical Review A (1996). + `arXiv:quant-ph/9601018 [quant-ph] `_ + 2. Donny Cheung, + *Improved Bounds for the Approximate QFT* (2004), + `arXiv:quant-ph/0403071 [quant-ph] `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given QFTGate.""" + if not isinstance(high_level_object, QFTGate): + raise TranspilerError( + "The synthesis plugin 'qft.line` only applies to objects of type QFTGate." + ) + + reverse_qubits = options.get("reverse_qubits", False) + approximation_degree = options.get("approximation_degree", 0) + + decomposition = synth_qft_line( + num_qubits=high_level_object.num_qubits, + do_swaps=not reverse_qubits, + approximation_degree=approximation_degree, + ) + return decomposition + + +class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the token swapper algorithm. + + This plugin name is :``permutation.token_swapper`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + In more detail, this plugin is used to synthesize objects of type `PermutationGate`. + When synthesis succeeds, the plugin outputs a quantum circuit consisting only of swap + gates. When synthesis does not succeed, the plugin outputs `None`. + + If either `coupling_map` or `qubits` is None, then the synthesized circuit + is not required to adhere to connectivity constraints, as is the case + when the synthesis is done before layout/routing. + + On the other hand, if both `coupling_map` and `qubits` are specified, the synthesized + circuit is supposed to adhere to connectivity constraints. At the moment, the + plugin only creates swap gates between qubits in `qubits`, i.e. it does not use + any other qubits in the coupling map (if such synthesis is not possible, the + plugin outputs `None`). + + The plugin supports the following plugin-specific options: + + * trials: The number of trials for the token swapper to perform the mapping. The + circuit with the smallest number of SWAPs is returned. + * seed: The argument to the token swapper specifying the seed for random trials. + * parallel_threshold: The argument to the token swapper specifying the number of nodes + in the graph beyond which the algorithm will use parallel processing. + + For more details on the token swapper algorithm, see to the paper: + `arXiv:1902.09102 `__. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Permutation.""" + + trials = options.get("trials", 5) + seed = options.get("seed", 0) + parallel_threshold = options.get("parallel_threshold", 50) + + pattern = high_level_object.pattern + pattern_as_dict = {j: i for i, j in enumerate(pattern)} + + # When the plugin is called from the HighLevelSynthesis transpiler pass, + # the coupling map already takes target into account. + if coupling_map is None or qubits is None: + # The abstract synthesis uses a fully connected coupling map, allowing + # arbitrary connections between qubits. + used_coupling_map = CouplingMap.from_full(len(pattern)) + else: + # The concrete synthesis uses the coupling map restricted to the set of + # qubits over which the permutation gate is defined. If we allow using other + # qubits in the coupling map, replacing the node in the DAGCircuit that + # defines this PermutationGate by the DAG corresponding to the constructed + # decomposition becomes problematic. Note that we allow the reduced + # coupling map to be disconnected. + used_coupling_map = coupling_map.reduce(qubits, check_if_connected=False) + + graph = used_coupling_map.graph.to_undirected() + swapper = ApproximateTokenSwapper(graph, seed=seed) + + try: + swapper_result = swapper.map( + pattern_as_dict, trials, parallel_threshold=parallel_threshold + ) + except rx.InvalidMapping: + swapper_result = None + + if swapper_result is not None: + decomposition = QuantumCircuit(len(graph.node_indices())) + for swap in swapper_result: + decomposition.swap(*swap) + return decomposition + + return None + + +class MCXSynthesisNDirtyI15(HighLevelSynthesisPlugin): + r"""Synthesis plugin for a multi-controlled X gate based on the paper + by Iten et al. (2016). + + See [1] for details. + + This plugin name is :``mcx.n_dirty_i15`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For a multi-controlled X gate with :math:`k\ge 3` control qubits this synthesis + method requires :math:`k - 2` additional dirty auxiliary qubits. The synthesized + circuit consists of :math:`2 * k - 1` qubits and at most :math:`8 * k - 6` CX gates. + + The plugin supports the following plugin-specific options: + + * num_clean_ancillas: The number of clean auxiliary qubits available. + * num_dirty_ancillas: The number of dirty auxiliary qubits available. + * relative_phase: When set to ``True``, the method applies the optimized multi-controlled + X gate up to a relative phase, in a way that, by lemma 8 of [1], the relative + phases of the ``action part`` cancel out with the phases of the ``reset part``. + * action_only: when set to ``True``, the method applies only the ``action part`` + of lemma 8 of [1]. + + References: + 1. Iten et. al., *Quantum Circuits for Isometries*, Phys. Rev. A 93, 032318 (2016), + `arXiv:1501.06911 `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + num_ctrl_qubits = high_level_object.num_ctrl_qubits + num_clean_ancillas = options.get("num_clean_ancillas", 0) + num_dirty_ancillas = options.get("num_dirty_ancillas", 0) + relative_phase = options.get("relative_phase", False) + action_only = options.get("actions_only", False) + + if num_ctrl_qubits >= 3 and num_dirty_ancillas + num_clean_ancillas < num_ctrl_qubits - 2: + # This synthesis method is not applicable as there are not enough ancilla qubits + return None + + decomposition = synth_mcx_n_dirty_i15(num_ctrl_qubits, relative_phase, action_only) + return decomposition + + +class MCXSynthesisNCleanM15(HighLevelSynthesisPlugin): + r"""Synthesis plugin for a multi-controlled X gate based on the paper by + Maslov (2016). + + See [1] for details. + + This plugin name is :``mcx.n_clean_m15`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For a multi-controlled X gate with :math:`k\ge 3` control qubits this synthesis + method requires :math:`k - 2` additional clean auxiliary qubits. The synthesized + circuit consists of :math:`2 * k - 1` qubits and at most :math:`6 * k - 6` CX gates. + + The plugin supports the following plugin-specific options: + + * num_clean_ancillas: The number of clean auxiliary qubits available. + + References: + 1. Maslov., Phys. Rev. A 93, 022311 (2016), + `arXiv:1508.03273 `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + num_ctrl_qubits = high_level_object.num_ctrl_qubits + num_clean_ancillas = options.get("num_clean_ancillas", 0) + + if num_ctrl_qubits >= 3 and num_clean_ancillas < num_ctrl_qubits - 2: + # This synthesis method is not applicable as there are not enough ancilla qubits + return None + + decomposition = synth_mcx_n_clean_m15(num_ctrl_qubits) + return decomposition + + +class MCXSynthesis1CleanB95(HighLevelSynthesisPlugin): + r"""Synthesis plugin for a multi-controlled X gate based on the paper by + Barenco et al. (1995). + + See [1] for details. + + This plugin name is :``mcx.1_clean_b95`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For a multi-controlled X gate with :math:`k\ge 5` control qubits this synthesis + method requires a single additional clean auxiliary qubit. The synthesized + circuit consists of :math:`k + 2` qubits and at most :math:`16 * k - 8` CX gates. + + The plugin supports the following plugin-specific options: + + * num_clean_ancillas: The number of clean auxiliary qubits available. + + References: + 1. Barenco et. al., Phys.Rev. A52 3457 (1995), + `arXiv:quant-ph/9503016 `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + num_ctrl_qubits = high_level_object.num_ctrl_qubits + + if num_ctrl_qubits <= 2: + # The method requires at least 3 control qubits + return None + + num_clean_ancillas = options.get("num_clean_ancillas", 0) + + if num_ctrl_qubits >= 5 and num_clean_ancillas == 0: + # This synthesis method is not applicable as there are not enough ancilla qubits + return None + + decomposition = synth_mcx_1_clean_b95(num_ctrl_qubits) + return decomposition + + +class MCXSynthesisGrayCode(HighLevelSynthesisPlugin): + r"""Synthesis plugin for a multi-controlled X gate based on the Gray code. + + This plugin name is :``mcx.gray_code`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For a multi-controlled X gate with :math:`k` control qubits this synthesis + method requires no additional clean auxiliary qubits. The synthesized + circuit consists of :math:`k + 1` qubits. + + It is not recommended to use this method for large values of :math:`k + 1` + as it produces exponentially many gates. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + num_ctrl_qubits = high_level_object.num_ctrl_qubits + decomposition = synth_mcx_gray_code(num_ctrl_qubits) + return decomposition + + +class MCXSynthesisNoAuxV24(HighLevelSynthesisPlugin): + r"""Synthesis plugin for a multi-controlled X gate based on the + implementation for MCPhaseGate, which is in turn based on the + paper by Vale et al. (2024). + + See [1] for details. + + This plugin name is :``mcx.noaux_v24`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For a multi-controlled X gate with :math:`k` control qubits this synthesis + method requires no additional clean auxiliary qubits. The synthesized + circuit consists of :math:`k + 1` qubits. + + References: + 1. Vale et. al., *Circuit Decomposition of Multicontrolled Special Unitary + Single-Qubit Gates*, IEEE TCAD 43(3) (2024), + `arXiv:2302.06377 `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + num_ctrl_qubits = high_level_object.num_ctrl_qubits + decomposition = synth_mcx_noaux_v24(num_ctrl_qubits) + return decomposition + + +class MCXSynthesisDefault(HighLevelSynthesisPlugin): + r"""The default synthesis plugin for a multi-controlled X gate. + + This plugin name is :``mcx.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given MCX gate.""" + + if not isinstance(high_level_object, (MCXGate, C3XGate, C4XGate)): + # Unfortunately we occasionally have custom instructions called "mcx" + # which get wrongly caught by the plugin interface. A simple solution is + # to return None in this case, since HLS would proceed to examine + # their definition as it should. + return None + + # Iteratively run other synthesis methods available + + if ( + decomposition := MCXSynthesisNCleanM15().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + if ( + decomposition := MCXSynthesisNDirtyI15().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + if ( + decomposition := MCXSynthesis1CleanB95().run( + high_level_object, coupling_map, target, qubits, **options + ) + ) is not None: + return decomposition + + return MCXSynthesisNoAuxV24().run( + high_level_object, coupling_map, target, qubits, **options + ) diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index c57c6d76f9fb..a56c48e9cf96 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -378,7 +378,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** is :class:`~.KMSSynthesisPermutation`. This particular synthesis algorithm created a circuit adhering to the linear nearest-neighbor connectivity. -.. automodule:: qiskit.transpiler.passes.synthesis.high_level_synthesis +.. automodule:: qiskit.transpiler.passes.synthesis.hls_plugins :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 1ce2f2800c09..e31f6918f452 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -52,9 +52,10 @@ RGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError -from qiskit.providers.models import BackendProperties +from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments @@ -511,7 +512,7 @@ def _run_main_loop( for node in dag.op_nodes(): if node.name not in CONTROL_FLOW_OP_NAMES: continue - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( [ dag_to_circuit( self._run_main_loop( @@ -530,6 +531,7 @@ def _run_main_loop( for block in node.op.blocks ] ) + dag.substitute_node(node, new_op, propagate_condition=False) out_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): @@ -572,15 +574,13 @@ def _run_main_loop( user_gate_node._to_circuit_instruction().replace( params=user_gate_node.params, qubits=tuple(qubits[x] for x in qargs), - ), - dag=out_dag, + ) ) else: node = DAGOpNode.from_instruction( CircuitInstruction.from_standard( gate, tuple(qubits[x] for x in qargs), params - ), - dag=out_dag, + ) ) out_dag._apply_op_node_back(node) out_dag.global_phase += global_phase diff --git a/qiskit/transpiler/passes/utils/check_gate_direction.py b/qiskit/transpiler/passes/utils/check_gate_direction.py index e797be95c4a1..5251bfc8de96 100644 --- a/qiskit/transpiler/passes/utils/check_gate_direction.py +++ b/qiskit/transpiler/passes/utils/check_gate_direction.py @@ -12,9 +12,11 @@ """Check if the gates follow the right direction with respect to the coupling map.""" -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import AnalysisPass +from qiskit._accelerate.gate_direction import ( + check_gate_direction_coupling, + check_gate_direction_target, +) class CheckGateDirection(AnalysisPass): @@ -34,42 +36,6 @@ def __init__(self, coupling_map, target=None): self.coupling_map = coupling_map self.target = target - def _coupling_map_visit(self, dag, wire_map, edges=None): - if edges is None: - edges = self.coupling_map.get_edges() - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - - if not self._coupling_map_visit(circuit_to_dag(block), inner_wire_map, edges): - return False - elif ( - len(node.qargs) == 2 - and (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) not in edges - ): - return False - return True - - def _target_visit(self, dag, wire_map): - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) - } - if not self._target_visit(circuit_to_dag(block), inner_wire_map): - return False - elif len(node.qargs) == 2 and not self.target.instruction_supported( - node.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) - ): - return False - return True - def run(self, dag): """Run the CheckGateDirection pass on `dag`. @@ -79,9 +45,8 @@ def run(self, dag): Args: dag (DAGCircuit): DAG to check. """ - wire_map = {bit: i for i, bit in enumerate(dag.qubits)} self.property_set["is_direction_mapped"] = ( - self._coupling_map_visit(dag, wire_map) - if self.target is None - else self._target_visit(dag, wire_map) + check_gate_direction_target(dag, self.target) + if self.target + else check_gate_direction_coupling(dag, set(self.coupling_map.get_edges())) ) diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index bd78c65de5f4..4048d93df22c 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -14,7 +14,8 @@ from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.target import Target -from qiskit.converters import circuit_to_dag + +from qiskit._accelerate import check_map class CheckMap(AnalysisPass): @@ -67,25 +68,11 @@ def run(self, dag): if not self.qargs: self.property_set[self.property_set_field] = True return - wire_map = {bit: index for index, bit in enumerate(dag.qubits)} - self.property_set[self.property_set_field] = self._recurse(dag, wire_map) - - def _recurse(self, dag, wire_map) -> bool: - for node in dag.op_nodes(include_directives=False): - if node.is_control_flow(): - for block in node.op.blocks: - inner_wire_map = { - inner: wire_map[outer] for inner, outer in zip(block.qubits, node.qargs) - } - if not self._recurse(circuit_to_dag(block), inner_wire_map): - return False - elif ( - len(node.qargs) == 2 - and not dag.has_calibration_for(node) - and (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) not in self.qargs - ): - self.property_set["check_map_msg"] = ( - f"{node.name}({wire_map[node.qargs[0]]}, {wire_map[node.qargs[1]]}) failed" - ) - return False - return True + res = check_map.check_map(dag, self.qargs) + if res is None: + self.property_set[self.property_set_field] = True + return + self.property_set[self.property_set_field] = False + self.property_set["check_map_msg"] = ( + f"{res[0]}({dag.qubits[res[1][0]]}, {dag.qubits[res[1][1]]}) failed" + ) diff --git a/qiskit/transpiler/passes/utils/control_flow.py b/qiskit/transpiler/passes/utils/control_flow.py index 3739852b4c7e..4fc95587e782 100644 --- a/qiskit/transpiler/passes/utils/control_flow.py +++ b/qiskit/transpiler/passes/utils/control_flow.py @@ -45,7 +45,7 @@ def trivial_recurse(method): use :func:`map_blocks` as:: if isinstance(node.op, ControlFlowOp): - node.op = map_blocks(self.run, node.op) + dag.substitute_node(node, map_blocks(self.run, node.op)) from with :meth:`.BasePass.run`.""" @@ -54,8 +54,12 @@ def out(self, dag): def bound_wrapped_method(dag): return out(self, dag) - for node in dag.op_nodes(ControlFlowOp): - node.op = map_blocks(bound_wrapped_method, node.op) + control_flow_nodes = dag.control_flow_op_nodes() + if control_flow_nodes is not None: + for node in control_flow_nodes: + dag.substitute_node( + node, map_blocks(bound_wrapped_method, node.op), propagate_condition=False + ) return method(self, dag) return out diff --git a/qiskit/transpiler/passes/utils/filter_op_nodes.py b/qiskit/transpiler/passes/utils/filter_op_nodes.py index 344d2280e3f4..75b824332aee 100644 --- a/qiskit/transpiler/passes/utils/filter_op_nodes.py +++ b/qiskit/transpiler/passes/utils/filter_op_nodes.py @@ -18,6 +18,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow +from qiskit._accelerate.filter_op_nodes import filter_op_nodes + class FilterOpNodes(TransformationPass): """Remove all operations that match a filter function @@ -59,7 +61,5 @@ def __init__(self, predicate: Callable[[DAGOpNode], bool]): @control_flow.trivial_recurse def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the RemoveBarriers pass on `dag`.""" - for node in dag.op_nodes(): - if not self.predicate(node): - dag.remove_op_node(node) + filter_op_nodes(dag, self.predicate) return dag diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 79493ae8ad25..8ea77f7ccd46 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -175,7 +175,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): if isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_coupling_map( circuit_to_dag(block), @@ -188,6 +188,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue @@ -222,7 +223,7 @@ def _run_target(self, dag, wire_map): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): if isinstance(node.op, ControlFlowOp): - node.op = node.op.replace_blocks( + new_op = node.op.replace_blocks( dag_to_circuit( self._run_target( circuit_to_dag(block), @@ -234,6 +235,7 @@ def _run_target(self, dag, wire_map): ) for block in node.op.blocks ) + dag.substitute_node(node, new_op, propagate_condition=False) continue if len(node.qargs) != 2: continue diff --git a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py index 40390a02cade..acb748bb6f95 100644 --- a/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py +++ b/qiskit/transpiler/passes/utils/merge_adjacent_barriers.py @@ -108,7 +108,7 @@ def _collect_potential_merges(dag, barriers): for next_barrier in barriers[1:]: # Ensure barriers are adjacent before checking if they are mergeable. - if dag._multi_graph.has_edge(end_of_barrier._node_id, next_barrier._node_id): + if dag._has_edge(end_of_barrier._node_id, next_barrier._node_id): # Remove all barriers that have already been included in this new barrier from the # set of ancestors/descendants as they will be removed from the new DAG when it is diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 4c53bc47b31c..0ebbff430301 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -13,6 +13,7 @@ """Pass Manager Configuration class.""" import pprint +import warnings from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.instruction_durations import InstructionDurations @@ -113,11 +114,17 @@ def __init__( def from_backend(cls, backend, _skip_target=False, **pass_manager_options): """Construct a configuration based on a backend and user input. - This method automatically gererates a PassManagerConfig object based on the backend's + This method automatically generates a PassManagerConfig object based on the backend's features. User options can be used to overwrite the configuration. + .. deprecated:: 1.3 + The method ``PassManagerConfig.from_backend`` will stop supporting inputs of type + :class:`.BackendV1` in the `backend` parameter in a future release no + earlier than 2.0. :class:`.BackendV1` is deprecated and implementations should move + to :class:`.BackendV2`. + Args: - backend (BackendV1): The backend that provides the configuration. + backend (BackendV1 or BackendV2): The backend that provides the configuration. pass_manager_options: User-defined option-value pairs. Returns: @@ -126,12 +133,21 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): Raises: AttributeError: If the backend does not support a `configuration()` method. """ - res = cls(**pass_manager_options) backend_version = getattr(backend, "version", 0) + if backend_version == 1: + warnings.warn( + "The method PassManagerConfig.from_backend will stop supporting inputs of " + f"type `BackendV1` ( {backend} ) in the `backend` parameter in a future " + "release no earlier than 2.0. `BackendV1` is deprecated and implementations " + "should move to `BackendV2`.", + category=DeprecationWarning, + stacklevel=2, + ) if not isinstance(backend_version, int): backend_version = 0 if backend_version < 2: config = backend.configuration() + res = cls(**pass_manager_options) if res.basis_gates is None: if backend_version < 2: res.basis_gates = getattr(config, "basis_gates", None) diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 353ad8c50b15..830618845352 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -358,6 +358,7 @@ def generate_preset_pass_manager( # Parse non-target dependent pm options initial_layout = _parse_initial_layout(initial_layout) approximation_degree = _parse_approximation_degree(approximation_degree) + seed_transpiler = _parse_seed_transpiler(seed_transpiler) pm_options = { "target": target, @@ -532,3 +533,11 @@ def _parse_approximation_degree(approximation_degree): if approximation_degree < 0.0 or approximation_degree > 1.0: raise TranspilerError("Approximation degree must be in [0.0, 1.0]") return approximation_degree + + +def _parse_seed_transpiler(seed_transpiler): + if seed_transpiler is None: + return None + if not isinstance(seed_transpiler, int) or seed_transpiler < 0: + raise ValueError("Expected non-negative integer as seed for transpiler.") + return seed_transpiler diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index f2e6c860faaa..c5d0d69d11a1 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -25,172 +25,195 @@ Qiskit Components ----------------- -.. list-table:: - :widths: 25 75 +.. py:data:: HAS_AER - * - .. py:data:: HAS_AER - - `Qiskit Aer ` provides high-performance simulators for - the quantum circuits constructed within Qiskit. + `Qiskit Aer ` provides high-performance simulators for + the quantum circuits constructed within Qiskit. - * - .. py:data:: HAS_IBMQ - - The :mod:`Qiskit IBMQ Provider ` is used for accessing IBM Quantum - hardware in the IBM cloud. +.. py:data:: HAS_IBMQ - * - .. py:data:: HAS_IGNIS - - :mod:`Qiskit Ignis ` provides tools for quantum hardware verification, noise - characterization, and error correction. + The :mod:`Qiskit IBMQ Provider ` is used for accessing IBM Quantum + hardware in the IBM cloud. - * - .. py:data:: HAS_TOQM - - `Qiskit TOQM `__ provides transpiler passes - for the `Time-optimal Qubit mapping algorithm `__. +.. py:data:: HAS_IGNIS + + :mod:`Qiskit Ignis ` provides tools for quantum hardware verification, noise + characterization, and error correction. + +.. py:data:: HAS_TOQM + + `Qiskit TOQM `__ provides transpiler passes + for the `Time-optimal Qubit mapping algorithm `__. External Python Libraries ------------------------- -.. list-table:: - :widths: 25 75 - - * - .. py:data:: HAS_CONSTRAINT - - `python-constraint `__ is a - constraint satisfaction problem solver, used in the :class:`~.CSPLayout` transpiler pass. - - * - .. py:data:: HAS_CPLEX - - The `IBM CPLEX Optimizer `__ is a - high-performance mathematical programming solver for linear, mixed-integer and quadratic - programming. This is no longer by Qiskit, but it weas historically and the optional - remains for backwards compatibility. - - * - .. py:data:: HAS_CVXPY - - `CVXPY `__ is a Python package for solving convex optimization - problems. It is required for calculating diamond norms with - :func:`.quantum_info.diamond_norm`. - - * - .. py:data:: HAS_DOCPLEX - - `IBM Decision Optimization CPLEX Modelling - `__ is a library for prescriptive - analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the - optional remains for backwards compatibility. - - * - .. py:data:: HAS_FIXTURES - - The test suite has additional features that are available if the optional `fixtures - `__ module is installed. This generally also needs - :data:`HAS_TESTTOOLS` as well. This is generally only needed for Qiskit developers. - - * - .. py:data:: HAS_IPYTHON - - If `the IPython kernel `__ is available, certain additional - visualizations and line magics are made available. - - * - .. py:data:: HAS_IPYWIDGETS - - Monitoring widgets for jobs running on external backends can be provided if `ipywidgets - `__ is available. - - * - .. py:data:: HAS_JAX - - Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX - `__ for autodifferentiation. - - * - .. py:data:: HAS_JUPYTER - - Some of the tests require a complete `Jupyter `__ installation to test - interactivity features. - - * - .. py:data:: HAS_MATPLOTLIB - - Qiskit provides several visualization tools in the :mod:`.visualization` module. - Almost all of these are built using `Matplotlib `__, which must - be installed in order to use them. - - * - .. py:data:: HAS_NETWORKX - - No longer used by Qiskit. Internally, Qiskit now uses the high-performance `rustworkx - `__ library as a core dependency, and during the - change-over period, it was sometimes convenient to convert things into the Python-only - `NetworkX `__ format. Some tests of application modules, such as - `Qiskit Nature `__ still use NetworkX. - - * - .. py:data:: HAS_NLOPT - - `NLopt `__ is a nonlinear optimization library, - used by the global optimizers in the :mod:`.algorithms.optimizers` module. - - * - .. py:data:: HAS_PIL - - PIL is a Python image-manipulation library. Qiskit actually uses the `pillow - `__ fork of PIL if it is available when generating - certain visualizations, for example of both :class:`.QuantumCircuit` and - :class:`.DAGCircuit` in certain modes. - - * - .. py:data:: HAS_PYDOT - - For some graph visualizations, Qiskit uses `pydot `__ as an - interface to GraphViz (see :data:`HAS_GRAPHVIZ`). - - * - .. py:data:: HAS_PYGMENTS - - Pygments is a code highlighter and formatter used by many environments that involve rich - display of code blocks, including Sphinx and Jupyter. Qiskit uses this when producing rich - output for these environments. - - * - .. py:data:: HAS_PYLATEX - - Various LaTeX-based visualizations, especially the circuit drawers, need access to the - `pylatexenc `__ project to work correctly. - - * - .. py:data:: HAS_QASM3_IMPORT - - The functions :func:`.qasm3.load` and :func:`.qasm3.loads` for importing OpenQASM 3 programs - into :class:`.QuantumCircuit` instances use `an external importer package - `__. - - * - .. py:data:: HAS_SEABORN - - Qiskit provides several visualization tools in the :mod:`.visualization` module. Some - of these are built using `Seaborn `__, which must be installed - in order to use them. - - * - .. py:data:: HAS_SKLEARN - - Some of the gradient functions in :mod:`.opflow.gradients` use regularisation methods from - `Scikit Learn `__. - - * - .. py:data:: HAS_SKQUANT - - Some of the optimisers in :mod:`.algorithms.optimizers` are based on those found in `Scikit - Quant `__, which must be installed to use - them. - - * - .. py:data:: HAS_SQSNOBFIT - - `SQSnobFit `__ is a library for the "stable noisy - optimization by branch and fit" algorithm. It is used by the :class:`.SNOBFIT` optimizer. - - * - .. py:data:: HAS_SYMENGINE - - `Symengine `__ is a fast C++ backend for the - symbolic-manipulation library `Sympy `__. Qiskit uses - special methods from Symengine to accelerate its handling of - :class:`~.circuit.Parameter`\\ s if available. - - * - .. py:data:: HAS_TESTTOOLS - - Qiskit's test suite has more advanced functionality available if the optional - `testtools `__ library is installed. This is generally - only needed for Qiskit developers. - - * - .. py:data:: HAS_TWEEDLEDUM - - `Tweedledum `__ is an extension library for - synthesis and optimization of circuits that may involve classical oracles. Qiskit's - :class:`.PhaseOracle` uses this, which is used in turn by amplification algorithms via - the :class:`.AmplificationProblem`. - - * - .. py:data:: HAS_Z3 - - `Z3 `__ is a theorem prover, used in the - :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. +.. py:data:: HAS_CONSTRAINT + + `python-constraint `__ is a + constraint satisfaction problem solver, used in the :class:`~.CSPLayout` transpiler pass. + +.. py:data:: HAS_CPLEX + + The `IBM CPLEX Optimizer `__ is a + high-performance mathematical programming solver for linear, mixed-integer and quadratic + programming. This is no longer by Qiskit, but it weas historically and the optional + remains for backwards compatibility. + +.. py:data:: HAS_CVXPY + + `CVXPY `__ is a Python package for solving convex optimization + problems. It is required for calculating diamond norms with + :func:`.quantum_info.diamond_norm`. + +.. py:data:: HAS_DOCPLEX + + `IBM Decision Optimization CPLEX Modelling + `__ is a library for prescriptive + analysis. Like CPLEX, this is no longer by Qiskit, but it weas historically and the + optional remains for backwards compatibility. + +.. py:data:: HAS_FIXTURES + + The test suite has additional features that are available if the optional `fixtures + `__ module is installed. This generally also needs + :data:`HAS_TESTTOOLS` as well. This is generally only needed for Qiskit developers. + +.. py:data:: HAS_IPYTHON + + If `the IPython kernel `__ is available, certain additional + visualizations and line magics are made available. + +.. py:data:: HAS_IPYWIDGETS + + Monitoring widgets for jobs running on external backends can be provided if `ipywidgets + `__ is available. + +.. py:data:: HAS_JAX + + Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX + `__ for autodifferentiation. + +.. py:data:: HAS_JUPYTER + + Some of the tests require a complete `Jupyter `__ installation to test + interactivity features. + +.. py:data:: HAS_MATPLOTLIB + + Qiskit provides several visualization tools in the :mod:`.visualization` module. + Almost all of these are built using `Matplotlib `__, which must + be installed in order to use them. + +.. py:data:: HAS_NETWORKX + + No longer used by Qiskit. Internally, Qiskit now uses the high-performance `rustworkx + `__ library as a core dependency, and during the + change-over period, it was sometimes convenient to convert things into the Python-only + `NetworkX `__ format. Some tests of application modules, such as + `Qiskit Nature `__ still use NetworkX. + +.. py:data:: HAS_NLOPT + + `NLopt `__ is a nonlinear optimization library, + used by the global optimizers in the :mod:`.algorithms.optimizers` module. + +.. py:data:: HAS_PIL + + PIL is a Python image-manipulation library. Qiskit actually uses the `pillow + `__ fork of PIL if it is available when generating + certain visualizations, for example of both :class:`.QuantumCircuit` and + :class:`.DAGCircuit` in certain modes. + +.. py:data:: HAS_PYDOT + + For some graph visualizations, Qiskit uses `pydot `__ as an + interface to GraphViz (see :data:`HAS_GRAPHVIZ`). + +.. py:data:: HAS_PYGMENTS + + Pygments is a code highlighter and formatter used by many environments that involve rich + display of code blocks, including Sphinx and Jupyter. Qiskit uses this when producing rich + output for these environments. + +.. py:data:: HAS_PYLATEX + + Various LaTeX-based visualizations, especially the circuit drawers, need access to the + `pylatexenc `__ project to work correctly. + +.. py:data:: HAS_QASM3_IMPORT + + The functions :func:`.qasm3.load` and :func:`.qasm3.loads` for importing OpenQASM 3 programs + into :class:`.QuantumCircuit` instances use `an external importer package + `__. + +.. py:data:: HAS_SEABORN + + Qiskit provides several visualization tools in the :mod:`.visualization` module. Some + of these are built using `Seaborn `__, which must be installed + in order to use them. + +.. py:data:: HAS_SKLEARN + + Some of the gradient functions in :mod:`.opflow.gradients` use regularisation methods from + `Scikit Learn `__. + +.. py:data:: HAS_SKQUANT + + Some of the optimisers in :mod:`.algorithms.optimizers` are based on those found in `Scikit + Quant `__, which must be installed to use + them. + +.. py:data:: HAS_SQSNOBFIT + + `SQSnobFit `__ is a library for the "stable noisy + optimization by branch and fit" algorithm. It is used by the :class:`.SNOBFIT` optimizer. + +.. py:data:: HAS_SYMENGINE + + `Symengine `__ is a fast C++ backend for the + symbolic-manipulation library `Sympy `__. Qiskit uses + special methods from Symengine to accelerate its handling of + :class:`~.circuit.Parameter`\\ s if available. + +.. py:data:: HAS_TESTTOOLS + + Qiskit's test suite has more advanced functionality available if the optional + `testtools `__ library is installed. This is generally + only needed for Qiskit developers. + +.. py:data:: HAS_TWEEDLEDUM + + `Tweedledum `__ is an extension library for + synthesis and optimization of circuits that may involve classical oracles. Qiskit's + :class:`.PhaseOracle` uses this, which is used in turn by amplification algorithms via + the :class:`.AmplificationProblem`. + +.. py:data:: HAS_Z3 + + `Z3 `__ is a theorem prover, used in the + :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. External Command-Line Tools --------------------------- -.. list-table:: - :widths: 25 75 +.. py:data:: HAS_GRAPHVIZ + + For some graph visualizations, Qiskit uses the `GraphViz `__ + visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + +.. py:data:: HAS_PDFLATEX - * - .. py:data:: HAS_GRAPHVIZ - - For some graph visualizations, Qiskit uses the `GraphViz `__ - visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + Visualization tools that use LaTeX in their output, such as the circuit drawers, require + ``pdflatex`` to be available. You will generally need to ensure that you have a working + LaTeX installation available, and the ``qcircuit.tex`` package. - * - .. py:data:: HAS_PDFLATEX - - Visualization tools that use LaTeX in their output, such as the circuit drawers, require - ``pdflatex`` to be available. You will generally need to ensure that you have a working - LaTeX installation available, and the ``qcircuit.tex`` package. +.. py:data:: HAS_PDFTOCAIRO - * - .. py:data:: HAS_PDFTOCAIRO - - Visualization tools that convert LaTeX-generated files into rasterized images use the - ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools - `__. + Visualization tools that convert LaTeX-generated files into rasterized images use the + ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools + `__. Lazy Checker Classes diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index e6ee03905d27..d933d38b5c4a 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -454,14 +454,12 @@ def _get_layered_instructions( clbits = new_clbits dag = circuit_to_dag(circuit) - dag.qubits = qubits - dag.clbits = clbits if justify == "none": for node in dag.topological_op_nodes(): nodes.append([node]) else: - nodes = _LayerSpooler(dag, justify, measure_map) + nodes = _LayerSpooler(dag, qubits, clbits, justify, measure_map) # Optionally remove all idle wires and instructions that are on them and # on them only. @@ -515,23 +513,25 @@ def _get_gate_span(qubits, node): def _any_crossover(qubits, node, nodes): """Return True .IFF. 'node' crosses over any 'nodes'.""" - gate_span = _get_gate_span(qubits, node) - all_indices = [] - for check_node in nodes: - if check_node != node: - all_indices += _get_gate_span(qubits, check_node) - return any(i in gate_span for i in all_indices) + return bool( + set(_get_gate_span(qubits, node)).intersection( + bit for check_node in nodes for bit in _get_gate_span(qubits, check_node) + ) + ) + + +_GLOBAL_NID = 0 class _LayerSpooler(list): """Manipulate list of layer dicts for _get_layered_instructions.""" - def __init__(self, dag, justification, measure_map): + def __init__(self, dag, qubits, clbits, justification, measure_map): """Create spool""" super().__init__() self.dag = dag - self.qubits = dag.qubits - self.clbits = dag.clbits + self.qubits = qubits + self.clbits = clbits self.justification = justification self.measure_map = measure_map self.cregs = [self.dag.cregs[reg] for reg in self.dag.cregs] @@ -660,6 +660,15 @@ def slide_from_right(self, node, index): def add(self, node, index): """Add 'node' where it belongs, starting the try at 'index'.""" + # Before we add the node, we set its node ID to be globally unique + # within this spooler. This is necessary because nodes may span + # layers (which are separate DAGs), and thus can falsely compare + # as equal if their contents and node IDs happen to be the same. + # This is particularly important for the matplotlib drawer, which + # keys several of its internal data structures with these nodes. + global _GLOBAL_NID # pylint: disable=global-statement + node._node_id = _GLOBAL_NID + _GLOBAL_NID += 1 if self.justification == "left": self.slide_from_left(node, index) else: diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index dae97b51b0a4..a6a111fe75e9 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -15,9 +15,14 @@ """ Visualization function for DAG circuit representation. """ + +import io +import subprocess + from rustworkx.visualization import graphviz_draw from qiskit.dagcircuit.dagnode import DAGOpNode, DAGInNode, DAGOutNode +from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.circuit import Qubit, Clbit, ClassicalRegister from qiskit.circuit.classical import expr from qiskit.converters import dagdependency_to_circuit @@ -26,7 +31,48 @@ from .exceptions import VisualizationError +IMAGE_TYPES = { + "canon", + "cmap", + "cmapx", + "cmapx_np", + "dia", + "dot", + "fig", + "gd", + "gd2", + "gif", + "hpgl", + "imap", + "imap_np", + "ismap", + "jpe", + "jpeg", + "jpg", + "mif", + "mp", + "pcl", + "pdf", + "pic", + "plain", + "plain-ext", + "png", + "ps", + "ps2", + "svg", + "svgz", + "vml", + "vmlz", + "vrml", + "vtx", + "wbmp", + "xdor", + "xlib", +} + + @_optionals.HAS_GRAPHVIZ.require_in_call +@_optionals.HAS_PIL.require_in_call def dag_drawer(dag, scale=0.7, filename=None, style="color"): """Plot the directed acyclic graph (dag) to represent operation dependencies in a quantum circuit. @@ -48,6 +94,8 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): Raises: VisualizationError: when style is not recognized. InvalidFileError: when filename provided is not valid + ValueError: If the file extension for ``filename`` is not an image + type supported by Graphviz. Example: .. plot:: @@ -69,6 +117,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): dag = circuit_to_dag(circ) dag_drawer(dag) """ + from PIL import Image # NOTE: use type str checking to avoid potential cyclical import # the two tradeoffs ere that it will not handle subclasses and it is @@ -215,16 +264,53 @@ def edge_attr_func(edge): e["label"] = label return e - image_type = None + image_type = "png" if filename: if "." not in filename: raise InvalidFileError("Parameter 'filename' must be in format 'name.extension'") image_type = filename.split(".")[-1] - return graphviz_draw( - dag._multi_graph, - node_attr_func, - edge_attr_func, - graph_attrs, - filename, - image_type, - ) + if image_type not in IMAGE_TYPES: + raise ValueError( + "The specified value for the image_type argument, " + f"'{image_type}' is not a valid choice. It must be one of: " + f"{IMAGE_TYPES}" + ) + + if isinstance(dag, DAGCircuit): + dot_str = dag._to_dot( + graph_attrs, + node_attr_func, + edge_attr_func, + ) + + prog = "dot" + if not filename: + dot_result = subprocess.run( + [prog, "-T", image_type], + input=dot_str.encode("utf-8"), + capture_output=True, + encoding=None, + check=True, + text=False, + ) + dot_bytes_image = io.BytesIO(dot_result.stdout) + image = Image.open(dot_bytes_image) + return image + else: + subprocess.run( + [prog, "-T", image_type, "-o", filename], + input=dot_str, + check=True, + encoding="utf8", + text=True, + ) + return None + else: + return graphviz_draw( + dag._multi_graph, + node_attr_func, + edge_attr_func, + graph_attrs, + filename, + image_type, + ) diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index b950c84c902a..eb20623f0c8c 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -945,6 +945,7 @@ def plot_gate_map( font_color, ax, filename, + planar=rx.is_planar(coupling_map.graph.to_undirected(multigraph=False)), ) @@ -966,6 +967,8 @@ def plot_coupling_map( font_color="white", ax=None, filename=None, + *, + planar=True, ): """Plots an arbitrary coupling map of qubits (embedded in a plane). @@ -987,6 +990,7 @@ def plot_coupling_map( font_color (str): The font color for the qubit labels. ax (Axes): A Matplotlib axes instance. filename (str): file path to save image to. + planar (bool): If the coupling map is planar or not. Default: ``True`` (i.e. it is planar) Returns: Figure: A Matplotlib figure instance. @@ -1057,7 +1061,14 @@ def plot_coupling_map( if font_size is None: max_characters = max(1, max(len(str(x)) for x in qubit_labels)) - font_size = max(int(20 / max_characters), 1) + if max_characters == 1: + font_size = 20 + elif max_characters == 2: + font_size = 14 + elif max_characters == 3: + font_size = 12 + else: + font_size = 1 def color_node(node): if qubit_coordinates: @@ -1065,8 +1076,6 @@ def color_node(node): "label": str(qubit_labels[node]), "color": f'"{qubit_color[node]}"', "fillcolor": f'"{qubit_color[node]}"', - "style": "filled", - "shape": "circle", "pos": f'"{qubit_coordinates[node][0]},{qubit_coordinates[node][1]}"', "pin": "True", } @@ -1075,11 +1084,11 @@ def color_node(node): "label": str(qubit_labels[node]), "color": f'"{qubit_color[node]}"', "fillcolor": f'"{qubit_color[node]}"', - "style": "filled", - "shape": "circle", } + out_dict["style"] = "filled" + out_dict["shape"] = "circle" out_dict["fontcolor"] = f'"{font_color}"' - out_dict["fontsize"] = str(font_size) + out_dict["fontsize"] = f'"{str(font_size)}!"' out_dict["height"] = str(qubit_size * px) out_dict["fixedsize"] = "True" out_dict["fontname"] = '"DejaVu Sans"' @@ -1093,9 +1102,22 @@ def color_edge(edge): } return out_dict + graph_attributes = None + if not qubit_coordinates: + if planar: + graph_attributes = { + "overlap_scaling": "-7", + "overlap": "prism", + "model": "subset", + } + else: + graph_attributes = { + "overlap": "true", + } plot = graphviz_draw( graph, method="neato", + graph_attr=graph_attributes, node_attr_fn=color_node, edge_attr_fn=color_edge, filename=filename, diff --git a/releasenotes/notes/add-mcx-plugins-85e5b248692a36db.yaml b/releasenotes/notes/add-mcx-plugins-85e5b248692a36db.yaml new file mode 100644 index 000000000000..84ec35942d0a --- /dev/null +++ b/releasenotes/notes/add-mcx-plugins-85e5b248692a36db.yaml @@ -0,0 +1,45 @@ +--- +features_synthesis: + - | + Added synthesis functions :func:`.synth_mcx_gray_code` and :func:`.synth_mcx_noaux_v24` + that synthesize multi-controlled X gates. These functions do not require additional + ancilla qubits. + - | + Added synthesis functions :func:`.synth_c3x` and :func:`.synth_c4x` that + synthesize 3-controlled and 4-controlled X-gates respectively. +features_transpiler: + - | + Added multiple high-level-synthesis plugins for synthesizing an :class:`.MCXGate`: + + * :class:`.MCXSynthesisNCleanM15`, based on :func:`.synth_mcx_n_clean_m15`. + * :class:`.MCXSynthesisNDirtyI15`, based on :func:`.synth_mcx_n_dirty_i15`. + * :class:`.MCXSynthesis1CleanB95`, based on :func:`.synth_mcx_1_clean_b95`. + * :class:`.MCXSynthesisNoAuxV24`, based on :func:`.synth_mcx_noaux_v24`. + * :class:`.MCXSynthesisGrayCode`, based on :func:`.synth_mcx_gray_code`. + + As well: + + * :class:`.MCXSynthesisDefault`, choosing the most efficient synthesis + method based on the number of clean and dirty ancilla qubits available. + + As an example, consider how the transpilation of the following circuit:: + + from qiskit.circuit import QuantumCircuit + from qiskit.compiler import transpile + + qc = QuantumCircuit(7) + qc.x(0) + qc.mcx([0, 1, 2, 3], [4]) + qc.mcx([0, 1, 2, 3, 4], [5]) + qc.mcx([0, 1, 2, 3, 4, 5], [6]) + + transpile(qc) + + For the first MCX gate, qubits ``5`` and ``6`` can be used as clean + ancillas, and the best available synthesis method ``synth_mcx_n_clean_m15`` + will get chosen. + For the second MCX gate, qubit ``6`` can be used as a clean ancilla, + the method ``synth_mcx_n_clean_m15`` no longer applies, so the method + ``synth_mcx_1_clean_b95`` will get chosen. + For the third MCX gate, there are no ancilla qubits, and the method + ``synth_mcx_noaux_v24`` will get chosen. diff --git a/releasenotes/notes/adjust-neato-settings-3adcc0ae9e245ce9.yaml b/releasenotes/notes/adjust-neato-settings-3adcc0ae9e245ce9.yaml new file mode 100644 index 000000000000..9dec5debb2ef --- /dev/null +++ b/releasenotes/notes/adjust-neato-settings-3adcc0ae9e245ce9.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes an issue with the visualizations of some backends/coupling maps that showed as folded on their own. The + `default ``neato`` setting `_ works well in most cases. However, + ``prism`` overlap returns a more regular layout for other scenarios. diff --git a/releasenotes/notes/control-flow-op-names-c66f38f8a0e15ce7.yaml b/releasenotes/notes/control-flow-op-names-c66f38f8a0e15ce7.yaml new file mode 100644 index 000000000000..94a3b11b935b --- /dev/null +++ b/releasenotes/notes/control-flow-op-names-c66f38f8a0e15ce7.yaml @@ -0,0 +1,5 @@ +--- +features_circuits: + - | + A new data attribute, :data:`qiskit.circuit.CONTROL_FLOW_OP_NAMES`, is available to easily find + and check whether a given :class:`~.circuit.Instruction` is a control-flow operation by name. diff --git a/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml b/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml new file mode 100644 index 000000000000..9f4d9d384172 --- /dev/null +++ b/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + The standard equivalence library (:data:`.SessionEquivalenceLibrary`) now has rules that can + directly convert between Qiskit's standard-library 2q continuous Ising-type interactions (e.g. + :class:`.CPhaseGate`, :class:`.RZZGate`, :class:`.RZXGate`, and so on) using local equivalence + relations. Previously, several of these conversions would go via a 2-CX form, which resulted + in less efficient circuit generation. + + .. note:: + + In general, the :class:`.BasisTranslator` is not guaranteed to find the "best" equivalence + relation for a given :class:`.Target`, but will always find an equivalence if one exists. We + rely on more expensive resynthesis and gate-optimization passes in the transpiler to improve + the output. These passes are currently not as effective for basis sets with a continuously + parametrized two-qubit interaction as they are for discrete super-controlled two-qubit + interactions. diff --git a/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml new file mode 100644 index 000000000000..090a8226c111 --- /dev/null +++ b/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml @@ -0,0 +1,49 @@ +--- +features_transpiler: + - | + The implementation of the :class:`.DAGCircuit` has been rewritten in Rust. This rewrite of + the Python class should be fully API compatible with the previous Python implementation of + the class. While the class was previously implemented using + `rustworkx `__ and its underlying data graph structure existed + in Rust, the implementation of the class and all the data was stored in Python. This new + version of :class:`.DAGCircuit` stores a Rust native representation for all its data and + is fully implemented in Rust. This new implementation should be more efficient in memory + usage as it compresses the qubit and clbit representation for instructions at rest. + It also enables speed up for transpiler passes as they can fully manipulate a + :class:`.DAGCircuit` from Rust. +upgrade_transpiler: + - | + :class:`.DAGNode` objects (and its subclasses :class:`.DAGInNode`, :class:`.DAGOutNode`, and + :class:`.DAGOpNode`) no longer return references to the same underlying object from + :class:`.DAGCircuit` methods. This was never a guarantee before that all returned nodes would + be shared reference to the same object, but with the migration of the :class:`.DAGCircuit` to + Rust when a :class:`.DAGNode` a new :class:`.DAGNode` instance is generated on the fly when + a node is returned to Python. These objects will evaluate as equal using ``==`` or similar + checks that rely on ``__eq__`` but will no longer identify as the same object. + - | + The :class:`.DAGOpNode` instances returned from the :class:`.DAGCircuit` are no longer shared + references to the underlying data stored on the DAG. In previous release it was possible to + do something like:: + + for node in dag.op_nodes(): + node.op = new_op + + however this type of mutation was always unsound as it could break the DAG's internal caching + and cause corruption of the data structure. Instead you should use the API provided by + :class:`.DAGCircuit` for mutation such as :meth:`.DAGCircuit.substitute_node`, + :meth:`.DAGCircuit.substitute_node_with_dag`, or :meth:`.DAGCircuit.contract_node`. For example + the above code block would become:: + + for node in dag.op_nodes(): + dag.substitute_node(node, new_op) + + This is similar to an upgrade note from 1.2.0 where this was noted on for mutation of the + :attr:`.DAGOpNode.op` attribute, not the :class:`.DAGOpNode` itself. However in 1.3 this extends + to the entire object, not just it's inner ``op`` attribute. In general this type of mutation was + always unsound and not supported, but could previously have potentially worked in some cases. +fixes: + - | + Fixed an issue with :meth:`.DAGCircuit.apply_operation_back` and + :meth:`.DAGCircuit.apply_operation_front` where previously if you set a + :class:`.Clbit` object to the input for the ``qargs`` argument it would silently be accepted. + This has been fixed so the type mismatch is correctly identified and an exception is raised. diff --git a/releasenotes/notes/deprecate-StochasticSwap-451f46b273602b7b.yaml b/releasenotes/notes/deprecate-StochasticSwap-451f46b273602b7b.yaml new file mode 100644 index 000000000000..c858dd6fe6ab --- /dev/null +++ b/releasenotes/notes/deprecate-StochasticSwap-451f46b273602b7b.yaml @@ -0,0 +1,50 @@ +--- +deprecations_transpiler: + - | + Deprecated :class:`.StochasticSwap` which has been superseded by :class:`.SabreSwap`. + If the class is called from the transpile function, the change would be, for example:: + + from qiskit import transpile + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler import CouplingMap + from qiskit.providers.fake_provider import GenericBackendV2 + + + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, range(1, 4)) + qc.measure_all() + + cmap = CouplingMap.from_heavy_hex(3) + backend = GenericBackendV2(num_qubits=cmap.size(), coupling_map=cmap) + + tqc = transpile( + qc, + routing_method="stochastic", + layout_method="dense", + seed_transpiler=12342, + target=backend.target + ) + + to:: + + tqc = transpile( + qc, + routing_method="sabre", + layout_method="sabre", + seed_transpiler=12342, + target=backend.target + ) + + While for a pass manager, the change would be:: + + passmanager = PassManager(StochasticSwap(coupling, 20, 13)) + new_qc = passmanager.run(qc) + + to:: + + passmanager = PassManager(SabreSwap(backend.target, "basic")) + new_qc = passmanager.run(qc) + + + diff --git a/releasenotes/notes/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml b/releasenotes/notes/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml new file mode 100644 index 000000000000..ea4e69dd245f --- /dev/null +++ b/releasenotes/notes/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Parametric controlled standard-library gates (such as :class:`.CRXGate`) will now get correctly + extracted to a Rust-space standard gate when using :meth:`.QuantumCircuit.append` and the gate + object. Previously there was a discrepancy where using the :meth:`.QuantumCircuit.crx` method + would cause a correct extraction in Rust space, but the :meth:`~.QuantumCirucit.append` form + would not. The bug should generally not have caused any unsoundness from Python. diff --git a/releasenotes/notes/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml b/releasenotes/notes/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml new file mode 100644 index 000000000000..903b9ceac2a2 --- /dev/null +++ b/releasenotes/notes/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :class:`.TwoQubitBasisDecomposer` where the Rust-based code + would panic if the given KAK gate wasn't a Rust-space :class:`StandardGate`. \ No newline at end of file diff --git a/releasenotes/notes/fix-InstructionDurations-b47a9770b424d7a0.yaml b/releasenotes/notes/fix-InstructionDurations-b47a9770b424d7a0.yaml new file mode 100644 index 000000000000..ce8b789462de --- /dev/null +++ b/releasenotes/notes/fix-InstructionDurations-b47a9770b424d7a0.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug where :meth:`.InstructionDurations.from_backend` did not work for :class:`.BackendV2` backends. + Fixed `#12760 `. \ No newline at end of file diff --git a/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml b/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml new file mode 100644 index 000000000000..fbede3d31756 --- /dev/null +++ b/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml @@ -0,0 +1,16 @@ +fixes: + - | + Fixed a bug with the ``"circular"`` and ``"sca"`` entanglement for + :class:`.NLocal` circuits and its derivatives. For entanglement blocks + of more than 2 qubits, the circular entanglement was previously missing + some connections. For example, for 4 qubits and a block size of 3 the + code previously used:: + + [(2, 3, 0), (0, 1, 2), (1, 2, 3)] + + but now is correctly adding the ``(3, 0, 1)`` connections, that is:: + + [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)] + + As such, the ``"circular"`` and ``"sca"`` entanglements use ``num_qubits`` + entangling blocks per layer. diff --git a/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml b/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml new file mode 100644 index 000000000000..406d6052a1df --- /dev/null +++ b/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed the definition of the :class:`.CUGate` matrix in Rust-space. + While this was not noticable while handling the :class:`.CUGate` purely on + Python side, this had knock-on effects when transpiler passes were using the + Rust representation, such as could happen in :class:`.Consolidate2qBlocks`. + Fixed `#13118 `__. + diff --git a/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml b/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml new file mode 100644 index 000000000000..7548dcc99564 --- /dev/null +++ b/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :class:`.HoareOptimizer` where a controlled gate was simplified + by removing its controls but the new gate was not handled correctly. diff --git a/releasenotes/notes/fix-negative-seed-pm-2813a62a020da115.yaml b/releasenotes/notes/fix-negative-seed-pm-2813a62a020da115.yaml new file mode 100644 index 000000000000..352f068537e8 --- /dev/null +++ b/releasenotes/notes/fix-negative-seed-pm-2813a62a020da115.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixed the behavior of :meth:`.generate_preset_pass_manager` to raise a `ValueError` exception if not provided with a non-negative integer `seed_transpiler` argument. diff --git a/releasenotes/notes/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml b/releasenotes/notes/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml new file mode 100644 index 000000000000..17432259be29 --- /dev/null +++ b/releasenotes/notes/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an edge case in :class:`.SabreLayout`, where in rare cases on large + devices and challenging circuits, the routing would fail. This was due to the + release valve making more than one two-qubit gate routable, where only one was expected. + Fixed `#13081 `__. \ No newline at end of file diff --git a/releasenotes/notes/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml b/releasenotes/notes/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml new file mode 100644 index 000000000000..ff1cf96dd711 --- /dev/null +++ b/releasenotes/notes/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug when :attr:`.SparsePauliOp.paulis` is set to be a :class:`.PauliList` with nonzero + phase, where subsequent calls to several :class:`.SparsePauliOp` methods would produce + incorrect results. Now when :attr:`.SparsePauliOp.paulis` is set to a :class:`.PauliList` with + nonzero phase, the phase is absorbed into :attr:`.SparsePauliOp.coeffs`, and the phase of the + input :class:`.PauliList` is set to zero. diff --git a/releasenotes/notes/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml b/releasenotes/notes/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml new file mode 100644 index 000000000000..331156cf6012 --- /dev/null +++ b/releasenotes/notes/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug in :class:`.Split2QUnitaries` where it would fail to run on circuits + with custom gates that didn't implement :meth:`__array__`. + See `#12984 `__. \ No newline at end of file diff --git a/releasenotes/notes/fix-stateprep-normalize-a8057c339ba619bd.yaml b/releasenotes/notes/fix-stateprep-normalize-a8057c339ba619bd.yaml new file mode 100644 index 000000000000..0175f8c1c026 --- /dev/null +++ b/releasenotes/notes/fix-stateprep-normalize-a8057c339ba619bd.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug in :class:`.StatePreparation` where the ``normalize`` + argument was ignored for input arrays. + Fixed `#12984 `__. diff --git a/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml b/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml new file mode 100644 index 000000000000..d838032ffaf9 --- /dev/null +++ b/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug where various synthesis methods created circuits without quantum or + classical registers. This also affected functions that internally used the synthesis + methods, such as :meth:`.Clifford.to_circuit`. + Fixed `#13041 `__. diff --git a/releasenotes/notes/fix_11990-8551c7250207fc76.yaml b/releasenotes/notes/fix_11990-8551c7250207fc76.yaml new file mode 100644 index 000000000000..68912403cd39 --- /dev/null +++ b/releasenotes/notes/fix_11990-8551c7250207fc76.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes an error when calling the method :meth:`.UnitaryGate.repeat`. + Refer to `#11990 `_ for more + details. \ No newline at end of file diff --git a/releasenotes/notes/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml b/releasenotes/notes/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml new file mode 100644 index 000000000000..3c24569455e2 --- /dev/null +++ b/releasenotes/notes/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fix a bug that caused the method :meth:`Initialize.gates_to_uncompute()` fail. diff --git a/releasenotes/notes/followup_12629-8bfcf1a3d4e6cabf.yaml b/releasenotes/notes/followup_12629-8bfcf1a3d4e6cabf.yaml new file mode 100644 index 000000000000..227946d0dffb --- /dev/null +++ b/releasenotes/notes/followup_12629-8bfcf1a3d4e6cabf.yaml @@ -0,0 +1,6 @@ +--- +deprecations_transpiler: + - | + The method :meth:`.PassManagerConfig.from_backend` will stop supporting inputs of type + :class:`.BackendV1` in the `backend` parameter in a future release no earlier than 2.0. + :class:`.BackendV1` is deprecated and implementations should move to :class:`.BackendV2`. diff --git a/releasenotes/notes/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml b/releasenotes/notes/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml new file mode 100644 index 000000000000..dbb095827b00 --- /dev/null +++ b/releasenotes/notes/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml @@ -0,0 +1,12 @@ +--- +features_transpiler: + - | + Added a new method :meth:`.DAGCircuit.control_flow_ops` which provides a fast + path to get all the :class:`.DAGOpNode` in a :class:`.DAGCircuit` that + contain a :class:`.ControlFlowOp`. This was possible before using the + :meth:`.DAGCircuit.op_nodes` method and passing the :class:`.ControlFlowOp` class + as a filter, but this new function will perform the operation faster. + - | + Ported the entirety of the :class:`.Optimize1qGatesDecomposition` transpiler + pass to Rust. This improves the runtime performance of the pass between 5x + to 10x. diff --git a/releasenotes/notes/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml b/releasenotes/notes/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml new file mode 100644 index 000000000000..967a65bcebab --- /dev/null +++ b/releasenotes/notes/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml @@ -0,0 +1,4 @@ +--- +features_transpiler: + - | + Added a Rust implementation of :class:`.CommutationAnalysis` in :func:`.analyze_commutations`. diff --git a/releasenotes/notes/oxidize-random-clifford-934f45876c14c8a0.yaml b/releasenotes/notes/oxidize-random-clifford-934f45876c14c8a0.yaml new file mode 100644 index 000000000000..236ad441992d --- /dev/null +++ b/releasenotes/notes/oxidize-random-clifford-934f45876c14c8a0.yaml @@ -0,0 +1,5 @@ +--- +features_synthesis: + - | + The function :func:`~qiskit.quantum_info.random_clifford` was ported to Rust, + improving the runtime by a factor of 3. diff --git a/releasenotes/notes/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml b/releasenotes/notes/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml new file mode 100644 index 000000000000..8acfe5439a50 --- /dev/null +++ b/releasenotes/notes/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml @@ -0,0 +1,26 @@ +--- +fixes: + - | + Fixed that the entanglement in :class:`.PauliFeatureMap` and :class:`.ZZFeatureMap` + could be given as ``List[int]`` or ``List[List[int]]``, which was incompatible with the fact + that entanglement blocks of different sizes are used. Instead, the entanglement can be + given as dictionary with ``{block_size: entanglement}`` pairs. +features_circuits: + - | + :class:`.PauliFeatureMap` and :class:`.ZZFeatureMap` now support specifying the + entanglement as a dictionary where the keys represent the number of qubits, and + the values are lists of integer tuples that define which qubits are entangled with one another. This + allows for more flexibility in constructing feature maps tailored to specific quantum algorithms. + Example usage:: + + from qiskit.circuit.library import PauliFeatureMap + entanglement = { + 1: [(0,), (2,)], + 2: [(0, 1), (1, 2)], + 3: [(0, 1, 2)], + } + qc = PauliFeatureMap(3, reps=2, paulis=['Z', 'ZZ', 'ZZZ'], entanglement=entanglement, insert_barriers=True) + qc.decompose().draw('mpl') + + + diff --git a/releasenotes/notes/port-countops-method-3ad362c20b13182c.yaml b/releasenotes/notes/port-countops-method-3ad362c20b13182c.yaml new file mode 100644 index 000000000000..93a92fa4c316 --- /dev/null +++ b/releasenotes/notes/port-countops-method-3ad362c20b13182c.yaml @@ -0,0 +1,5 @@ +--- +features_circuits: + - | + The :meth:`~.QuantumCircuit.count_ops` method in :class:`.QuantumCircuit` + has been re-written in Rust. It now runs between 3 and 9 times faster. diff --git a/releasenotes/notes/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml b/releasenotes/notes/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml new file mode 100644 index 000000000000..ee4c6933a269 --- /dev/null +++ b/releasenotes/notes/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml @@ -0,0 +1,6 @@ +--- +features_synthesis: + - | + Port :func: `.synth_cz_depth_line_mr` to Rust. This function synthesizes a CZ circuit for linear nearest neighbor (LNN) connectivity, based on the Maslov and Roetteler method. On a 350x350 binary matrix, the Rust implementation yields a speedup of about 30 times. + - | + Port :func: `.synth_permutation_reverse_lnn_kms` to Rust, which synthesizes a reverse permutation for linear nearest-neighbor architecture using the Kutin, Moulton, Smithline method. diff --git a/releasenotes/notes/py3.9-min-now-c9781484a0eb288e.yaml b/releasenotes/notes/py3.9-min-now-c9781484a0eb288e.yaml new file mode 100644 index 000000000000..a58dc69f9e4a --- /dev/null +++ b/releasenotes/notes/py3.9-min-now-c9781484a0eb288e.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The minimum supported version of Python is now 3.9, this has been raised + from the previous minimum support version of 3.8. This change was necessary + because the upstream cPython project no longer supports Python 3.8. diff --git a/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml b/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml new file mode 100644 index 000000000000..009a97720e30 --- /dev/null +++ b/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an edge case when transpiling a circuit with ``optimization_level`` 2 or 3 with an + incomplete 1-qubit basis gate set on a circuit containing 2-qubit gates, that can be + implemented as a product of single qubit gates. This bug is resolved by restricting + :class:`.Split2QUnitaries` to consider only :class:`.UnitaryGate` objects. + Fixed `#12970 `__. diff --git a/releasenotes/notes/rust-commutation-checker-c738e67efa9d292f.yaml b/releasenotes/notes/rust-commutation-checker-c738e67efa9d292f.yaml new file mode 100644 index 000000000000..bcfcbe18caf0 --- /dev/null +++ b/releasenotes/notes/rust-commutation-checker-c738e67efa9d292f.yaml @@ -0,0 +1,6 @@ +--- +features_transpiler: + - | + The the :class:`.CommutationChecker` class has been reimplemented in + Rust. This retains the same functionality as before but is now + significantly in most cases. diff --git a/releasenotes/notes/rust-paulifm-1dc7b1c2dc756614.yaml b/releasenotes/notes/rust-paulifm-1dc7b1c2dc756614.yaml new file mode 100644 index 000000000000..103e792381ab --- /dev/null +++ b/releasenotes/notes/rust-paulifm-1dc7b1c2dc756614.yaml @@ -0,0 +1,16 @@ +--- +features_circuits: + - | + Added circuit library functions :func:`.pauli_feature_map`, :func:`.z_feature_map`, + :func:`.zz_feature_map` to construct Pauli feature map circuits. These functions + are approximately 8x faster than the current circuit library objects, + :class:`.PauliFeatureMap`, :class:`.ZFeatureMap`, and :class:`.ZZFeatureMap`, + and will replace them in the future. Note, that the new functions return a plain + :class:`.QuantumCircuit` instead of a :class:`.BlueprintCircuit`. + + The functions can be used as drop-in replacement:: + + from qiskit.circuit.library import pauli_feature_map, PauliFeatureMap + + fm = pauli_feature_map(20, paulis=["z", "xx", "yyy"]) + also_fm = PauliFeatureMap(20, paulis=["z", "xx", "yyy"]).decompose() diff --git a/releasenotes/notes/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml b/releasenotes/notes/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml new file mode 100644 index 000000000000..294b4ed5bbae --- /dev/null +++ b/releasenotes/notes/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml @@ -0,0 +1,8 @@ +--- +features_transpiler: + - | + The :class:`.RemoveDiagonalGatesBeforeMeasure` transpiler pass has been upgraded to + include more diagonal gates: :class:`.PhaseGate`, :class:`.CPhaseGate`, + :class:`.CSGate`, :class:`.CSdgGate` and :class:`.CCZGate`. + In addition, the code of the :class:`.RemoveDiagonalGatesBeforeMeasure` was ported to Rust, + and is now x20 faster for a 20 qubit circuit. diff --git a/setup.py b/setup.py index 61168050547c..ad6d1f8ead86 100644 --- a/setup.py +++ b/setup.py @@ -51,5 +51,5 @@ features=features, ) ], - options={"bdist_wheel": {"py_limited_api": "cp38"}}, + options={"bdist_wheel": {"py_limited_api": "cp39"}}, ) diff --git a/test/benchmarks/assembler.py b/test/benchmarks/assembler.py deleted file mode 100644 index 03d349088c86..000000000000 --- a/test/benchmarks/assembler.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# 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. - -# pylint: disable=no-member,invalid-name,missing-docstring,no-name-in-module -# pylint: disable=attribute-defined-outside-init,unsubscriptable-object - -from qiskit.compiler import assemble -from qiskit.assembler import disassemble - -from .utils import random_circuit - - -class AssemblerBenchmarks: - params = ([8], [4096], [1, 100]) - param_names = ["n_qubits", "depth", "number of circuits"] - timeout = 600 - version = 2 - - def setup(self, n_qubits, depth, number_of_circuits): - seed = 42 - self.circuit = random_circuit(n_qubits, depth, measure=True, conditional=True, seed=seed) - self.circuits = [self.circuit] * number_of_circuits - - def time_assemble_circuit(self, _, __, ___): - assemble(self.circuits) - - -class DisassemblerBenchmarks: - params = ([8], [4096], [1, 100]) - param_names = ["n_qubits", "depth", "number of circuits"] - timeout = 600 - - def setup(self, n_qubits, depth, number_of_circuits): - seed = 424242 - self.circuit = random_circuit(n_qubits, depth, measure=True, conditional=True, seed=seed) - self.circuits = [self.circuit] * number_of_circuits - self.qobj = assemble(self.circuits) - - def time_disassemble_circuit(self, _, __, ___): - # TODO: QObj is getting deprecated. Remove once that happens - # https://github.com/Qiskit/qiskit/pull/12649 - disassemble(self.qobj) diff --git a/test/benchmarks/pulse/__init__.py b/test/benchmarks/pulse/__init__.py deleted file mode 100644 index c7325a848299..000000000000 --- a/test/benchmarks/pulse/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# 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. diff --git a/test/benchmarks/pulse/load_pulse_defaults.py b/test/benchmarks/pulse/load_pulse_defaults.py deleted file mode 100644 index 1b7fa5cc1719..000000000000 --- a/test/benchmarks/pulse/load_pulse_defaults.py +++ /dev/null @@ -1,552 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# 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. - -# pylint: disable=missing-docstring,invalid-name,no-member -# pylint: disable=attribute-defined-outside-init - -import numpy as np - -from qiskit.providers.models import PulseDefaults -from qiskit.compiler import schedule -from qiskit.circuit import QuantumCircuit, Gate - - -def gen_source(num_random_gate): - - # minimum data to instantiate pulse defaults. - # cmd def contains cx, rz, sx, u3, measure - # and random gate (custom waveform of 100 dt + 2 fc) - - qobj_dict = { - "qubit_freq_est": [5.0, 5.1], - "meas_freq_est": [7.0, 7.0], - "buffer": 0, - "pulse_library": [], - "cmd_def": [ - { - "name": "cx", - "qubits": [0, 1], - "sequence": [ - { - "ch": "d0", - "name": "fc", - "phase": -3.141592653589793, - "t0": 0, - }, - { - "ch": "d0", - "label": "Y90p_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (0.0022743565483134 + 0.14767107967944j), - "beta": 0.5218372954777448, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - }, - { - "ch": "d0", - "label": "CR90p_d0_u1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.03583301328943 - 0.0006486874906466j), - "duration": 1104, - "sigma": 64, - "width": 848, - }, - "pulse_shape": "gaussian_square", - "t0": 96, - }, - { - "ch": "d0", - "label": "CR90m_d0_u1", - "name": "parametric_pulse", - "parameters": { - "amp": (-0.03583301328943 + 0.000648687490646j), - "duration": 1104, - "sigma": 64, - "width": 848, - }, - "pulse_shape": "gaussian_square", - "t0": 1296, - }, - { - "ch": "d0", - "name": "fc", - "phase": -1.5707963267948966, - "t0": 2400, - }, - { - "ch": "d0", - "label": "X90p_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (0.14766707017470 - 0.002521280908868j), - "beta": 0.5218372954777448, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 2400, - }, - { - "ch": "d1", - "name": "fc", - "phase": -1.5707963267948966, - "t0": 0, - }, - { - "ch": "d1", - "label": "X90p_d1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.19074973504459 + 0.004525711677119j), - "beta": -1.2815198779814807, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - }, - { - "ch": "d1", - "label": "Xp_d1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.3872223088586379 + 0j), - "beta": -1.498502772395478, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 1200, - }, - { - "ch": "d1", - "label": "Y90m_d1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.00285052543950 - 0.19078212177897j), - "beta": -1.2815198779814807, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 2400, - }, - { - "ch": "u0", - "name": "fc", - "phase": -1.5707963267948966, - "t0": 0, - }, - { - "ch": "u1", - "name": "fc", - "phase": -3.141592653589793, - "t0": 0, - }, - { - "ch": "u1", - "label": "CR90p_u1", - "name": "parametric_pulse", - "parameters": { - "amp": (-0.1629668182698 - 0.8902610676540j), - "duration": 1104, - "sigma": 64, - "width": 848, - }, - "pulse_shape": "gaussian_square", - "t0": 96, - }, - { - "ch": "u1", - "label": "CR90m_u1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.16296681826986 + 0.8902610676540j), - "duration": 1104, - "sigma": 64, - "width": 848, - }, - "pulse_shape": "gaussian_square", - "t0": 1296, - }, - { - "ch": "u1", - "name": "fc", - "phase": -1.5707963267948966, - "t0": 2400, - }, - ], - }, - { - "name": "rz", - "qubits": [0], - "sequence": [ - { - "ch": "d0", - "name": "fc", - "phase": "-(P0)", - "t0": 0, - }, - { - "ch": "u1", - "name": "fc", - "phase": "-(P0)", - "t0": 0, - }, - ], - }, - { - "name": "rz", - "qubits": [1], - "sequence": [ - { - "ch": "d1", - "name": "fc", - "phase": "-(P0)", - "t0": 0, - }, - { - "ch": "u0", - "name": "fc", - "phase": "-(P0)", - "t0": 0, - }, - ], - }, - { - "name": "sx", - "qubits": [0], - "sequence": [ - { - "ch": "d0", - "label": "X90p_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (0.14766707017470 - 0.002521280908868j), - "beta": 0.5218372954777448, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - } - ], - }, - { - "name": "sx", - "qubits": [1], - "sequence": [ - { - "ch": "d1", - "label": "X90p_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (0.19074973504459 + 0.004525711677119j), - "beta": -1.2815198779814807, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - } - ], - }, - { - "name": "u3", - "qubits": [0], - "sequence": [ - { - "ch": "d0", - "name": "fc", - "phase": "-(P2)", - "t0": 0, - }, - { - "ch": "d0", - "label": "X90p_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (0.14766707017470 - 0.002521280908868j), - "beta": 0.5218372954777448, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - }, - { - "ch": "d0", - "name": "fc", - "phase": "-(P0)", - "t0": 96, - }, - { - "ch": "d0", - "label": "X90m_d0", - "name": "parametric_pulse", - "parameters": { - "amp": (-0.14767107967944 + 0.002274356548313j), - "beta": 0.5218372954777448, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 96, - }, - { - "ch": "d0", - "name": "fc", - "phase": "-(P1)", - "t0": 192, - }, - { - "ch": "u1", - "name": "fc", - "phase": "-(P2)", - "t0": 0, - }, - { - "ch": "u1", - "name": "fc", - "phase": "-(P0)", - "t0": 96, - }, - { - "ch": "u1", - "name": "fc", - "phase": "-(P1)", - "t0": 192, - }, - ], - }, - { - "name": "u3", - "qubits": [1], - "sequence": [ - { - "ch": "d1", - "name": "fc", - "phase": "-(P2)", - "t0": 0, - }, - { - "ch": "d1", - "label": "X90p_d1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.19074973504459 + 0.004525711677119j), - "beta": -1.2815198779814807, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 0, - }, - { - "ch": "d1", - "name": "fc", - "phase": "-(P0)", - "t0": 96, - }, - { - "ch": "d1", - "label": "X90m_d1", - "name": "parametric_pulse", - "parameters": { - "amp": (-0.19078212177897 - 0.002850525439509j), - "beta": -1.2815198779814807, - "duration": 96, - "sigma": 24, - }, - "pulse_shape": "drag", - "t0": 96, - }, - { - "ch": "d1", - "name": "fc", - "phase": "-(P1)", - "t0": 192, - }, - { - "ch": "u0", - "name": "fc", - "phase": "-(P2)", - "t0": 0, - }, - { - "ch": "u0", - "name": "fc", - "phase": "-(P0)", - "t0": 96, - }, - { - "ch": "u0", - "name": "fc", - "phase": "-(P1)", - "t0": 192, - }, - ], - }, - { - "name": "measure", - "qubits": [0, 1], - "sequence": [ - { - "ch": "m0", - "label": "M_m0", - "name": "parametric_pulse", - "parameters": { - "amp": (-0.3003200790496 + 0.3069634566518j), - "duration": 1792, - "sigma": 64, - "width": 1536, - }, - "pulse_shape": "gaussian_square", - "t0": 0, - }, - { - "ch": "m1", - "label": "M_m1", - "name": "parametric_pulse", - "parameters": { - "amp": (0.26292757124962 + 0.14446138680205j), - "duration": 1792, - "sigma": 64, - "width": 1536, - }, - "pulse_shape": "gaussian_square", - "t0": 0, - }, - { - "ch": "m0", - "duration": 1504, - "name": "delay", - "t0": 1792, - }, - { - "ch": "m1", - "duration": 1504, - "name": "delay", - "t0": 1792, - }, - { - "duration": 1792, - "memory_slot": [0, 1], - "name": "acquire", - "qubits": [0, 1], - "t0": 0, - }, - ], - }, - ], - } - - # add random waveform gate entries to increase overhead - for i in range(num_random_gate): - for qind in (0, 1): - samples = np.random.random(100) - - gate_name = f"ramdom_gate_{i}" - sample_name = f"random_sample_q{qind}_{i}" - - qobj_dict["pulse_library"].append( - { - "name": sample_name, - "samples": samples, - } - ) - qobj_dict["cmd_def"].append( - { - "name": gate_name, - "qubits": [qind], - "sequence": [ - { - "ch": f"d{qind}", - "name": "fc", - "phase": "-(P0)", - "t0": 0, - }, - { - "ch": f"d{qind}", - "label": gate_name, - "name": sample_name, - "t0": 0, - }, - { - "ch": f"d{qind}", - "name": "fc", - "phase": "(P0)", - "t0": 100, - }, - ], - }, - ) - - return qobj_dict - - -class PulseDefaultsBench: - - params = ([0, 10, 100, 1000],) - param_names = [ - "number of random gates", - ] - - def setup(self, num_random_gate): - self.source = gen_source(num_random_gate) - - def time_building_defaults(self, _): - PulseDefaults.from_dict(self.source) - - -class CircuitSchedulingBench: - - params = ([1, 2, 3, 15],) - param_names = [ - "number of unit cell repetition", - ] - - def setup(self, repeat_unit_cell): - source = gen_source(1) - defaults = PulseDefaults.from_dict(source) - - self.inst_map = defaults.instruction_schedule_map - self.meas_map = [[0, 1]] - self.dt = 0.222e-9 - - rng = np.random.default_rng(123) - - qc = QuantumCircuit(2) - for _ in range(repeat_unit_cell): - randdom_gate = Gate("ramdom_gate_0", 1, list(rng.random(1))) - qc.cx(0, 1) - qc.append(randdom_gate, [0]) - qc.sx(0) - qc.rz(1.57, 0) - qc.append(randdom_gate, [1]) - qc.sx(1) - qc.rz(1.57, 1) - qc.measure_all() - self.qc = qc - - def time_scheduling_circuits(self, _): - schedule( - self.qc, - inst_map=self.inst_map, - meas_map=self.meas_map, - dt=self.dt, - ) diff --git a/test/benchmarks/pulse/schedule_construction.py b/test/benchmarks/pulse/schedule_construction.py deleted file mode 100644 index 13b889a19513..000000000000 --- a/test/benchmarks/pulse/schedule_construction.py +++ /dev/null @@ -1,174 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# 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. - -# pylint: disable=missing-docstring,invalid-name,no-member -# pylint: disable=attribute-defined-outside-init - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit, Gate -from qiskit.pulse import builder, library, channels - - -class EchoedCrossResonanceConstructionBench: - def setup(self): - - with builder.build() as x_ctrl: - builder.play( - library.Drag(160, 0.2, 40, 1.5), - channels.DriveChannel(0), - ) - self.x_ctrl = x_ctrl - - with builder.build() as cr45p: - builder.play( - library.GaussianSquare(800, 0.4, 64, 544), - channels.ControlChannel(0), - ) - builder.play( - library.GaussianSquare(800, 0.1, 64, 544), - channels.DriveChannel(1), - ) - self.cr45p = cr45p - - def time_full_scratch(self): - # Full scratch in a single builder context - with builder.build(): - with builder.align_sequential(): - with builder.align_left(): - builder.play( - library.GaussianSquare(800, 0.4, 64, 544), - channels.ControlChannel(0), - ) - builder.play( - library.GaussianSquare(800, 0.1, 64, 544), - channels.DriveChannel(1), - ) - builder.play( - library.Drag(160, 0.2, 40, 1.5), - channels.DriveChannel(0), - ) - with builder.phase_offset( - np.pi, - channels.ControlChannel(0), - channels.DriveChannel(1), - ): - with builder.align_left(): - builder.play( - library.GaussianSquare(800, 0.4, 64, 544), - channels.ControlChannel(0), - ) - builder.play( - library.GaussianSquare(800, 0.1, 64, 544), - channels.DriveChannel(1), - ) - builder.play( - library.Drag(160, 0.2, 40, 1.5), - channels.DriveChannel(0), - ) - - def time_with_call(self): - # Call subroutine, internally creates reference and assign immediately - with builder.build(): - with builder.align_sequential(): - builder.call(self.cr45p) - builder.call(self.x_ctrl) - with builder.phase_offset( - np.pi, - channels.ControlChannel(0), - channels.DriveChannel(1), - ): - builder.call(self.cr45p) - builder.call(self.x_ctrl) - - def time_assign_later(self): - # Create placeholder and assign subroutine at a later time - with builder.build() as temp_sched: - with builder.align_sequential(): - builder.reference("cr45p", "q0", "q1") - builder.reference("x", "q0") - with builder.phase_offset( - np.pi, - channels.ControlChannel(0), - channels.DriveChannel(1), - ): - builder.reference("cr45p", "q0", "q1") - builder.reference("x", "q0") - - temp_sched.assign_references( - { - ("cr45p", "q0", "q1"): self.cr45p, - ("x", "q0"): self.x_ctrl, - }, - inplace=True, - ) - - -class ParameterizedScheduleBench: - - params = [3, 11, 31, 51] - - def setup(self, nscan): - self.p0 = Parameter("P0") - self.p1 = Parameter("P1") - self.p2 = Parameter("P2") - - with builder.build() as schedule: - builder.play( - library.Constant(self.p0, self.p1), - channels.DriveChannel(self.p2), - ) - self.schedule = schedule - - with builder.build() as outer_schedule: - builder.reference("subroutine") - outer_schedule.assign_references({("subroutine",): schedule}, inplace=True) - self.outer_schedule = outer_schedule - - gate = Gate("my_gate", 1, [self.p0, self.p1, self.p2]) - qc = QuantumCircuit(1) - qc.append(gate, [0]) - qc.add_calibration(gate, (0,), schedule) - self.qc = qc - - # list of parameters - self.amps = np.linspace(-1, 1, nscan) - - def time_assign_single_schedule(self, _): - - out = [] - for amp in self.amps: - assigned = self.schedule.assign_parameters( - {self.p0: 100, self.p1: amp, self.p2: 0}, - inplace=False, - ) - out.append(assigned) - - def time_assign_parameterized_subroutine(self, _): - - out = [] - for amp in self.amps: - assigned = self.outer_schedule.assign_parameters( - {self.p0: 100, self.p1: amp, self.p2: 0}, - inplace=False, - ) - out.append(assigned) - - def time_assign_through_pulse_gate(self, _): - - out = [] - for amp in self.amps: - assigned = self.qc.assign_parameters( - {self.p0: 100, self.p1: amp, self.p2: 0}, - inplace=False, - ) - out.append(assigned) diff --git a/test/benchmarks/pulse/schedule_lowering.py b/test/benchmarks/pulse/schedule_lowering.py deleted file mode 100644 index 38b09a8411be..000000000000 --- a/test/benchmarks/pulse/schedule_lowering.py +++ /dev/null @@ -1,85 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023 -# -# 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. - -# pylint: disable=missing-docstring,invalid-name,no-member -# pylint: disable=attribute-defined-outside-init - -import numpy as np - -from qiskit.pulse import builder, library, channels -from qiskit.pulse.transforms import target_qobj_transform - - -def build_complicated_schedule(): - - with builder.build() as schedule: - with builder.align_sequential(): - with builder.align_right(): - with builder.phase_offset(np.pi, channels.ControlChannel(2)): - with builder.align_sequential(): - for _ in range(5): - builder.play( - library.GaussianSquare(640, 0.1, 64, 384), - channels.ControlChannel(2), - ) - builder.play( - library.Constant(1920, 0.1), - channels.DriveChannel(1), - ) - builder.barrier( - channels.DriveChannel(0), - channels.DriveChannel(1), - channels.DriveChannel(2), - ) - builder.delay(800, channels.DriveChannel(1)) - with builder.align_left(): - builder.play( - library.Drag(160, 0.3, 40, 1.5), - channels.DriveChannel(0), - ) - builder.play( - library.Drag(320, 0.2, 80, 1.5), - channels.DriveChannel(1), - ) - builder.play( - library.Drag(480, 0.1, 120, 1.5), - channels.DriveChannel(2), - ) - builder.reference("sub") - with builder.align_left(): - for i in range(3): - builder.play( - library.GaussianSquare(1600, 0.1, 64, 1344), - channels.MeasureChannel(i), - ) - builder.acquire( - 1600, - channels.AcquireChannel(i), - channels.MemorySlot(i), - ) - - with builder.build() as subroutine: - for i in range(3): - samples = np.random.random(160) - builder.play(samples, channels.DriveChannel(i)) - schedule.assign_references({("sub",): subroutine}, inplace=True) - - return schedule - - -class ScheduleLoweringBench: - def setup(self): - self.schedule_block = build_complicated_schedule() - - def time_lowering(self): - # Lower schedule block to generate job payload - target_qobj_transform(self.schedule_block) diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index a3df1e9664a2..b7756faa7aa7 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -502,6 +502,19 @@ def test_clifford_linear_function_equivalence(self, num_qubits): self.assertEqual(Clifford(qc_to_linear_function), qc_to_clifford) self.assertEqual(qc_to_linear_function, LinearFunction(qc_to_clifford)) + @data(2, 3) + def test_repeat_method(self, num_qubits): + """Test the repeat() method.""" + rng = np.random.default_rng(127) + for num_gates, seed in zip( + [0, 5, 5 * num_qubits], rng.integers(100000, size=10, dtype=np.uint64) + ): + # create a random linear circuit + linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=seed) + operator = Operator(linear_circuit) + linear_function = LinearFunction(linear_circuit) + self.assertTrue(Operator(linear_function.repeat(2)), operator @ operator) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 2e308af48ff4..dd39aa61ba61 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -42,6 +42,10 @@ from qiskit.circuit.random.utils import random_circuit from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.quantum_info import Operator +from qiskit.exceptions import QiskitError + +from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map + from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -339,7 +343,7 @@ def get_expected_entangler_map(rep_num, mode): (2, 3, 4), ] else: - circular = [(3, 4, 0), (0, 1, 2), (1, 2, 3), (2, 3, 4)] + circular = [(3, 4, 0), (4, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4)] if mode == "circular": return circular sca = circular[-rep_num:] + circular[:-rep_num] @@ -928,5 +932,156 @@ def test_full_vs_reverse_linear(self, num_qubits): self.assertEqual(Operator(full), Operator(reverse)) +@ddt +class TestEntanglement(QiskitTestCase): + """Test getting the entanglement structure.""" + + @data( + ("linear", [(0, 1), (1, 2), (2, 3)]), + ("reverse_linear", [(2, 3), (1, 2), (0, 1)]), + ("pairwise", [(0, 1), (2, 3), (1, 2)]), + ("circular", [(3, 0), (0, 1), (1, 2), (2, 3)]), + ("full", [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + ) + @unpack + def test_2q_str(self, strategy, expected): + """Test getting by string.""" + entanglement = fast_entangler_map( + num_qubits=4, block_size=2, entanglement=strategy, offset=0 + ) + self.assertEqual(expected, entanglement) + + @data( + ("linear", [(0, 1, 2), (1, 2, 3)]), + ("reverse_linear", [(1, 2, 3), (0, 1, 2)]), + ("circular", [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)]), + ("full", [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]), + ) + @unpack + def test_3q_str(self, strategy, expected): + """Test getting by string.""" + entanglement = fast_entangler_map( + num_qubits=4, block_size=3, entanglement=strategy, offset=0 + ) + self.assertEqual(expected, entanglement) + + def test_2q_sca(self): + """Test shift, circular, alternating on 2-qubit blocks.""" + expected = { # offset: result + 0: [(3, 0), (0, 1), (1, 2), (2, 3)], + 1: [(3, 2), (0, 3), (1, 0), (2, 1)], + 2: [(1, 2), (2, 3), (3, 0), (0, 1)], + 3: [(1, 0), (2, 1), (3, 2), (0, 3)], + } + for offset in range(8): + with self.subTest(offset=offset): + entanglement = fast_entangler_map( + num_qubits=4, block_size=2, entanglement="sca", offset=offset + ) + self.assertEqual(expected[offset % 4], entanglement) + + def test_3q_sca(self): + """Test shift, circular, alternating on 3-qubit blocks.""" + circular = [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)] + for offset in range(8): + expected = circular[-(offset % 4) :] + circular[: -(offset % 4)] + if offset % 2 == 1: + expected = [tuple(reversed(indices)) for indices in expected] + with self.subTest(offset=offset): + entanglement = fast_entangler_map( + num_qubits=4, block_size=3, entanglement="sca", offset=offset + ) + self.assertEqual(expected, entanglement) + + @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise") + def test_0q(self, entanglement): + """Test the corner case of a single qubit block.""" + entanglement = fast_entangler_map( + num_qubits=3, block_size=0, entanglement=entanglement, offset=0 + ) + expect = [] + self.assertEqual(entanglement, expect) + + @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise") + def test_1q(self, entanglement): + """Test the corner case of a single qubit block.""" + entanglement = fast_entangler_map( + num_qubits=3, block_size=1, entanglement=entanglement, offset=0 + ) + expect = [(i,) for i in range(3)] + + self.assertEqual(set(entanglement), set(expect)) # order does not matter for 1 qubit + + @data("full", "reverse_linear", "linear", "circular", "sca") + def test_full_block(self, entanglement): + """Test the corner case of the block size equal the number of qubits.""" + entanglement = fast_entangler_map( + num_qubits=5, block_size=5, entanglement=entanglement, offset=0 + ) + expect = [tuple(range(5))] + + self.assertEqual(entanglement, expect) + + def test_pairwise_limit(self): + """Test pairwise raises an error above 2 qubits.""" + _ = fast_entangler_map(num_qubits=4, block_size=1, entanglement="pairwise", offset=0) + _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="pairwise", offset=0) + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=4, block_size=3, entanglement="pairwise", offset=0) + + def test_invalid_blocksize(self): + """Test the block size being too large.""" + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=2, block_size=3, entanglement="linear", offset=0) + + def test_invalid_entanglement_str(self): + """Test invalid entanglement string.""" + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="lniaer", offset=0) + + def test_as_list(self): + """Test passing a list just returns the list.""" + expected = [(0, 1), (1, 10), (2, 10)] + out = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0) + self.assertEqual(expected, out) + + def test_invalid_list(self): + """Test passing a list that does not match the block size.""" + + # TODO this test fails, somehow the error is not propagated correctly! + expected = [(0, 1), (1, 2, 10)] + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0) + + def test_callable_list(self): + """Test using a callable.""" + + def my_entanglement(offset): + return [(0, 1)] if offset % 2 == 0 else [(1, 2)] + + for offset in range(3): + with self.subTest(offset=offset): + expect = my_entanglement(offset) + result = fast_entangler_map( + num_qubits=3, block_size=2, entanglement=my_entanglement, offset=offset + ) + self.assertEqual(expect, result) + + def test_callable_str(self): + """Test using a callable.""" + + def my_entanglement(offset): + return "linear" if offset % 2 == 0 else "pairwise" + + expected = {"linear": [(0, 1), (1, 2), (2, 3)], "pairwise": [(0, 1), (2, 3), (1, 2)]} + + for offset in range(3): + with self.subTest(offset=offset): + result = fast_entangler_map( + num_qubits=4, block_size=2, entanglement=my_entanglement, offset=offset + ) + self.assertEqual(expected["linear" if offset % 2 == 0 else "pairwise"], result) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_pauli_feature_map.py b/test/python/circuit/library/test_pauli_feature_map.py index 60b7e5c28b9c..5d61944d4032 100644 --- a/test/python/circuit/library/test_pauli_feature_map.py +++ b/test/python/circuit/library/test_pauli_feature_map.py @@ -19,7 +19,16 @@ from ddt import ddt, data, unpack from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector -from qiskit.circuit.library import PauliFeatureMap, ZFeatureMap, ZZFeatureMap, HGate +from qiskit.circuit.library import ( + PauliFeatureMap, + ZFeatureMap, + ZZFeatureMap, + HGate, + pauli_feature_map, + z_feature_map, + zz_feature_map, +) +from qiskit.exceptions import QiskitError from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -221,6 +230,363 @@ def test_parameter_prefix(self): self.assertEqual(str(encoding_z_param_y.parameters), "ParameterView([Parameter(y)])") self.assertEqual(str(encoding_zz_param_y.parameters), "ParameterView([Parameter(y)])") + def test_entanglement_as_dictionary(self): + """Test whether PauliFeatureMap accepts entanglement as a dictionary and generates + correct feature map circuit""" + n_qubits = 3 + entanglement = { + 1: [(0,), (2,)], + 2: [(0, 1), (1, 2)], + 3: [(0, 1, 2)], + } + params = [np.pi / 4, np.pi / 2, np.pi] + + def z_block(circuit, q1): + circuit.p(2 * params[q1], q1) + + def zz_block(circuit, q1, q2): + param = (np.pi - params[q1]) * (np.pi - params[q2]) + circuit.cx(q1, q2) + circuit.p(2 * param, q2) + circuit.cx(q1, q2) + + def zzz_block(circuit, q1, q2, q3): + param = (np.pi - params[q1]) * (np.pi - params[q2]) * (np.pi - params[q3]) + circuit.cx(q1, q2) + circuit.cx(q2, q3) + circuit.p(2 * param, q3) + circuit.cx(q2, q3) + circuit.cx(q1, q2) + + feat_map = PauliFeatureMap( + n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement + ).assign_parameters(params) + + qc = QuantumCircuit(n_qubits) + for _ in range(2): + qc.h([0, 1, 2]) + for e1 in entanglement[1]: + z_block(qc, *e1) + for e2 in entanglement[2]: + zz_block(qc, *e2) + for e3 in entanglement[3]: + zzz_block(qc, *e3) + + self.assertTrue(Operator(feat_map).equiv(qc)) + + def test_invalid_entanglement(self): + """Test if a ValueError is raised when an invalid entanglement is passed""" + n_qubits = 3 + entanglement = { + 1: [(0, 1), (2,)], + 2: [(0, 1), (1, 2)], + 3: [(0, 1, 2)], + } + + with self.assertRaises(ValueError): + feat_map = PauliFeatureMap( + n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement + ) + feat_map.count_ops() + + def test_entanglement_not_specified(self): + """Test if an error is raised when entanglement is not explicitly specified for + all n-qubit pauli blocks""" + n_qubits = 3 + entanglement = { + 1: [(0, 1), (2,)], + 3: [(0, 1, 2)], + } + with self.assertRaises(ValueError): + feat_map = PauliFeatureMap( + n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement + ) + feat_map.count_ops() + + +@ddt +class TestPauliFeatureMap(QiskitTestCase): + """Test the Pauli feature map.""" + + @data((2, 3, ["X", "YY"]), (5, 2, ["ZZZXZ", "XZ"])) + @unpack + def test_num_parameters(self, num_qubits, reps, pauli_strings): + """Test the number of parameters equals the number of qubits, independent of reps.""" + encoding = pauli_feature_map(num_qubits, paulis=pauli_strings, reps=reps) + self.assertEqual(encoding.num_parameters, num_qubits) + + def test_pauli_zz_with_barriers(self): + """Test the generation of Pauli blocks.""" + encoding = QuantumCircuit(3) + encoding.compose(pauli_feature_map(3, paulis=["zz"], insert_barriers=True), inplace=True) + + params = encoding.parameters + + def zz(circuit, i, j): + circuit.cx(i, j) + circuit.p(2 * (np.pi - params[i]) * (np.pi - params[j]), j) + circuit.cx(i, j) + + ref = QuantumCircuit(3) + for i in range(2): + ref.h(range(3)) + ref.barrier() + zz(ref, 0, 1) + zz(ref, 0, 2) + zz(ref, 1, 2) + if i == 0: + ref.barrier() + + self.assertEqual(ref, encoding) + + def test_pauli_xyz(self): + """Test the generation of Pauli blocks.""" + encoding = QuantumCircuit(3) + encoding.compose(pauli_feature_map(3, paulis=["xyz"], reps=1), inplace=True) + # encoding = PauliFeatureMap(3, paulis=["XYZ"], reps=1).decompose() + + params = encoding.parameters + + # q_0: ─────────────■────────────────────────■────────────── + # ┌─────────┐┌─┴─┐ ┌─┴─┐┌──────────┐ + # q_1: ┤ Rx(π/2) ├┤ X ├──■──────────────■──┤ X ├┤ Rx(-π/2) ├ + # └──┬───┬──┘└───┘┌─┴─┐┌────────┐┌─┴─┐├───┤└──────────┘ + # q_2: ───┤ H ├────────┤ X ├┤ P(2.8) ├┤ X ├┤ H ├──────────── + # └───┘ └───┘└────────┘└───┘└───┘ + # X on the most-significant, bottom qubit, Z on the top + ref = QuantumCircuit(3) + ref.h(range(3)) + ref.h(2) + ref.rx(np.pi / 2, 1) + ref.cx(0, 1) + ref.cx(1, 2) + ref.p(2 * np.prod([np.pi - p for p in params]), 2) + ref.cx(1, 2) + ref.cx(0, 1) + ref.rx(-np.pi / 2, 1) + ref.h(2) + + self.assertEqual(ref, encoding) + + def test_first_order_circuit(self): + """Test a first order expansion circuit.""" + times = [0.2, 1, np.pi, -1.2] + encoding = z_feature_map(4, reps=3).assign_parameters(times) + + # ┌───┐ ┌────────┐┌───┐ ┌────────┐┌───┐ ┌────────┐ + # q_0: ┤ H ├─┤ P(0.4) ├┤ H ├─┤ P(0.4) ├┤ H ├─┤ P(0.4) ├ + # ├───┤ └┬──────┬┘├───┤ └┬──────┬┘├───┤ └┬──────┬┘ + # q_1: ┤ H ├──┤ P(2) ├─┤ H ├──┤ P(2) ├─┤ H ├──┤ P(2) ├─ + # ├───┤ ┌┴──────┤ ├───┤ ┌┴──────┤ ├───┤ ┌┴──────┤ + # q_2: ┤ H ├─┤ P(2π) ├─┤ H ├─┤ P(2π) ├─┤ H ├─┤ P(2π) ├─ + # ├───┤┌┴───────┴┐├───┤┌┴───────┴┐├───┤┌┴───────┴┐ + # q_3: ┤ H ├┤ P(-2.4) ├┤ H ├┤ P(-2.4) ├┤ H ├┤ P(-2.4) ├ + # └───┘└─────────┘└───┘└─────────┘└───┘└─────────┘ + ref = QuantumCircuit(4) + for _ in range(3): + ref.h([0, 1, 2, 3]) + for i in range(4): + ref.p(2 * times[i], i) + + self.assertTrue(Operator(encoding).equiv(ref)) + + def test_second_order_circuit(self): + """Test a second order expansion circuit.""" + times = [0.2, 1, np.pi] + encoding = zz_feature_map(3, reps=2).assign_parameters(times) + + def zz_evolution(circuit, qubit1, qubit2): + time = (np.pi - times[qubit1]) * (np.pi - times[qubit2]) + circuit.cx(qubit1, qubit2) + circuit.p(2 * time, qubit2) + circuit.cx(qubit1, qubit2) + + # ┌───┐┌────────┐ ┌───┐┌────────┐» + # q_0: ┤ H ├┤ P(0.4) ├──■─────────────────■────■────────────■──┤ H ├┤ P(0.4) ├» + # ├───┤└┬──────┬┘┌─┴─┐┌───────────┐┌─┴─┐ │ │ └───┘└────────┘» + # q_1: ┤ H ├─┤ P(2) ├─┤ X ├┤ P(12.599) ├┤ X ├──┼────────────┼────■────────────» + # ├───┤┌┴──────┤ └───┘└───────────┘└───┘┌─┴─┐┌──────┐┌─┴─┐┌─┴─┐ ┌──────┐ » + # q_2: ┤ H ├┤ P(2π) ├────────────────────────┤ X ├┤ P(0) ├┤ X ├┤ X ├─┤ P(0) ├─» + # └───┘└───────┘ └───┘└──────┘└───┘└───┘ └──────┘ » + # « » + # «q_0: ─────────────────────■─────────────────■────■────────────■───────────────» + # « ┌───┐ ┌──────┐┌─┴─┐┌───────────┐┌─┴─┐ │ │ » + # «q_1: ──■──┤ H ├─┤ P(2) ├┤ X ├┤ P(12.599) ├┤ X ├──┼────────────┼────■──────────» + # « ┌─┴─┐├───┤┌┴──────┤└───┘└───────────┘└───┘┌─┴─┐┌──────┐┌─┴─┐┌─┴─┐┌──────┐» + # «q_2: ┤ X ├┤ H ├┤ P(2π) ├───────────────────────┤ X ├┤ P(0) ├┤ X ├┤ X ├┤ P(0) ├» + # « └───┘└───┘└───────┘ └───┘└──────┘└───┘└───┘└──────┘» + # « + # «q_0: ───── + # « + # «q_1: ──■── + # « ┌─┴─┐ + # «q_2: ┤ X ├ + # « └───┘ + ref = QuantumCircuit(3) + for _ in range(2): + ref.h([0, 1, 2]) + for i in range(3): + ref.p(2 * times[i], i) + zz_evolution(ref, 0, 1) + zz_evolution(ref, 0, 2) + zz_evolution(ref, 1, 2) + + self.assertTrue(Operator(encoding).equiv(ref)) + + @combine(entanglement=["linear", "reverse_linear", "pairwise"]) + def test_zz_entanglement(self, entanglement): + """Test the ZZ feature map works with pairwise, linear and reverse_linear entanglement.""" + num_qubits = 5 + encoding = zz_feature_map(num_qubits, entanglement=entanglement, reps=1) + ops = encoding.count_ops() + expected_ops = {"h": num_qubits, "p": 2 * num_qubits - 1, "cx": 2 * (num_qubits - 1)} + self.assertEqual(ops, expected_ops) + + def test_pauli_alpha(self): + """Test Pauli rotation factor (getter, setter).""" + alpha = 1.234 + + # this is needed as the outcoming Rust circuit has no qreg + encoding = QuantumCircuit(1) + encoding.compose(pauli_feature_map(1, alpha=alpha, paulis=["z"], reps=1), inplace=True) + + ref = QuantumCircuit(1) + ref.h(0) + ref.p(alpha * encoding.parameters[0], 0) + + self.assertEqual(ref, encoding) + + def test_zzfeaturemap_raises_if_too_small(self): + """Test the ``ZZFeatureMap`` raises an error if the number of qubits is smaller than 2.""" + with self.assertRaises(QiskitError): + _ = zz_feature_map(1) + + def test_dict_entanglement(self): + """Test passing the entanglement as dictionary.""" + entanglement = {1: [(0,), (2,)], 2: [(1, 2)], 3: [(0, 1, 2)]} + circuit = QuantumCircuit(3) + circuit.compose( + pauli_feature_map(3, reps=1, paulis=["z", "xx", "yyy"], entanglement=entanglement), + inplace=True, + ) + x = circuit.parameters + + ref = QuantumCircuit(3) + ref.h(ref.qubits) + + ref.p(2 * x[0], 0) + ref.p(2 * x[2], 2) + + ref.h([1, 2]) + ref.cx(1, 2) + ref.p(2 * np.prod([np.pi - xi for xi in [x[1], x[2]]]), 2) + ref.cx(1, 2) + ref.h([1, 2]) + + ref.rx(np.pi / 2, range(3)) + ref.cx(0, 1) + ref.cx(1, 2) + ref.p(2 * np.prod([np.pi - xi for xi in x]), 2) + ref.cx(1, 2) + ref.cx(0, 1) + ref.rx(-np.pi / 2, range(3)) + + self.assertEqual(ref, circuit) + + def test_invalid_entanglement(self): + """Test if a ValueError is raised when an invalid entanglement is passed""" + n_qubits = 3 + entanglement = { + 1: [(0, 1), (2,)], + 2: [(0, 1), (1, 2)], + 3: [(0, 1, 2)], + } + + with self.assertRaises(QiskitError): + _ = pauli_feature_map( + n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement + ) + + def test_entanglement_not_specified(self): + """Test if an error is raised when entanglement is not explicitly specified for + all n-qubit pauli blocks""" + n_qubits = 3 + entanglement = { + 1: [(0, 1), (2,)], + 3: [(0, 1, 2)], + } + with self.assertRaises(QiskitError): + _ = pauli_feature_map( + n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement + ) + + def test_parameter_prefix(self): + """Test the Parameter prefix""" + encoding_pauli = pauli_feature_map( + feature_dimension=2, reps=2, paulis=["ZY"], parameter_prefix="p" + ) + encoding_z = z_feature_map(feature_dimension=2, reps=2, parameter_prefix="q") + encoding_zz = zz_feature_map(feature_dimension=2, reps=2, parameter_prefix="r") + x = ParameterVector("x", 2) + y = Parameter("y") + + self.assertEqual( + str(encoding_pauli.parameters), + "ParameterView([ParameterVectorElement(p[0]), ParameterVectorElement(p[1])])", + ) + self.assertEqual( + str(encoding_z.parameters), + "ParameterView([ParameterVectorElement(q[0]), ParameterVectorElement(q[1])])", + ) + self.assertEqual( + str(encoding_zz.parameters), + "ParameterView([ParameterVectorElement(r[0]), ParameterVectorElement(r[1])])", + ) + + encoding_pauli_param_x = encoding_pauli.assign_parameters(x) + encoding_z_param_x = encoding_z.assign_parameters(x) + encoding_zz_param_x = encoding_zz.assign_parameters(x) + + self.assertEqual( + str(encoding_pauli_param_x.parameters), + "ParameterView([ParameterVectorElement(x[0]), ParameterVectorElement(x[1])])", + ) + self.assertEqual( + str(encoding_z_param_x.parameters), + "ParameterView([ParameterVectorElement(x[0]), ParameterVectorElement(x[1])])", + ) + self.assertEqual( + str(encoding_zz_param_x.parameters), + "ParameterView([ParameterVectorElement(x[0]), ParameterVectorElement(x[1])])", + ) + + encoding_pauli_param_y = encoding_pauli.assign_parameters([1, y]) + encoding_z_param_y = encoding_z.assign_parameters([1, y]) + encoding_zz_param_y = encoding_zz.assign_parameters([1, y]) + + self.assertEqual(str(encoding_pauli_param_y.parameters), "ParameterView([Parameter(y)])") + self.assertEqual(str(encoding_z_param_y.parameters), "ParameterView([Parameter(y)])") + self.assertEqual(str(encoding_zz_param_y.parameters), "ParameterView([Parameter(y)])") + + def test_custom_data_mapping(self): + """Test passing a custom data mapping function.""" + + def my_mapping(x): + return 42 if len(x) == 1 else np.sum(x) + + encoding = QuantumCircuit(2) + encoding.compose(zz_feature_map(2, reps=1, data_map_func=my_mapping), inplace=True) + + params = encoding.parameters + ref = QuantumCircuit(2) + ref.h(range(2)) + ref.p(2 * 42, range(2)) + ref.cx(0, 1) + ref.p(2 * (params[0] + params[1]), 1) + ref.cx(0, 1) + + self.assertEqual(ref, encoding) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py index 30d504d7dc8f..a2ec59431fe3 100644 --- a/test/python/circuit/library/test_permutation.py +++ b/test/python/circuit/library/test_permutation.py @@ -104,6 +104,12 @@ def test_inverse(self): expected_inverse_perm = PermutationGate([3, 0, 5, 1, 4, 2]) self.assertTrue(np.array_equal(inverse_perm.pattern, expected_inverse_perm.pattern)) + def test_repeat(self): + """Test the ``repeat`` method.""" + pattern = [2, 4, 1, 3, 0] + perm = PermutationGate(pattern) + self.assertTrue(np.allclose(Operator(perm.repeat(2)), Operator(perm) @ Operator(perm))) + class TestPermutationGatesOnCircuit(QiskitTestCase): """Tests for quantum circuits containing permutations.""" diff --git a/test/python/circuit/library/test_state_preparation.py b/test/python/circuit/library/test_state_preparation.py index adad848a2777..b5446ed37e9c 100644 --- a/test/python/circuit/library/test_state_preparation.py +++ b/test/python/circuit/library/test_state_preparation.py @@ -113,6 +113,16 @@ def test_repeats(self): qc.append(StatePreparation("01").repeat(2), [0, 1]) self.assertEqual(qc.decompose().count_ops()["state_preparation"], 2) + def test_normalize(self): + """Test the normalization. + + Regression test of #12984. + """ + qc = QuantumCircuit(1) + qc.compose(StatePreparation([1, 1], normalize=True), range(1), inplace=True) + + self.assertTrue(Statevector(qc).equiv(np.array([1, 1]) / np.sqrt(2))) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 04e71a0dd4d7..db8663ace558 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1948,8 +1948,11 @@ def test_pre_v12_rejects_standalone_var(self, version): """Test that dumping to older QPY versions rejects standalone vars.""" a = expr.Var.new("a", types.Bool()) qc = QuantumCircuit(inputs=[a]) - with io.BytesIO() as fptr, self.assertRaisesRegex( - UnsupportedFeatureForVersion, "version 12 is required.*realtime variables" + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 12 is required.*realtime variables" + ), ): dump(qc, fptr, version=version) @@ -1959,8 +1962,9 @@ def test_pre_v12_rejects_index(self, version): # Be sure to use a register, since standalone vars would be rejected for other reasons. qc = QuantumCircuit(ClassicalRegister(2, "cr")) qc.store(expr.index(qc.cregs[0], 0), False) - with io.BytesIO() as fptr, self.assertRaisesRegex( - UnsupportedFeatureForVersion, "version 12 is required.*Index" + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex(UnsupportedFeatureForVersion, "version 12 is required.*Index"), ): dump(qc, fptr, version=version) diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 5f01e5c647a6..984851e00388 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -394,14 +394,13 @@ def test_circuit_qasm_with_mcx_gate(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - pattern = r"""OPENQASM 2.0; + expected_qasm = """OPENQASM 2.0; include "qelib1.inc"; -gate mcx q0,q1,q2,q3 { h q3; p\(pi/8\) q0; p\(pi/8\) q1; p\(pi/8\) q2; p\(pi/8\) q3; cx q0,q1; p\(-pi/8\) q1; cx q0,q1; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; p\(pi/8\) q2; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; h q3; } -gate (?Pmcx_[0-9]*) q0,q1,q2,q3 { mcx q0,q1,q2,q3; } -qreg q\[4\]; -(?P=mcx_id) q\[0\],q\[1\],q\[2\],q\[3\];""" - expected_qasm = re.compile(pattern, re.MULTILINE) - self.assertRegex(dumps(qc), expected_qasm) +gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; } +qreg q[4]; +mcx q[0],q[1],q[2],q[3];""" + + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_mcx_gate_variants(self): """Test circuit qasm() method with MCXGrayCode, MCXRecursive, MCXVChain""" @@ -420,8 +419,7 @@ def test_circuit_qasm_with_mcx_gate_variants(self): include "qelib1.inc"; gate mcu1(param0) q0,q1,q2,q3,q4,q5 {{ cu1(pi/16) q4,q5; cx q4,q3; cu1(-pi/16) q3,q5; cx q4,q3; cu1(pi/16) q3,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; }} gate mcx_gray q0,q1,q2,q3,q4,q5 {{ h q5; mcu1(pi) q0,q1,q2,q3,q4,q5; h q5; }} -gate mcx q0,q1,q2,q3 {{ h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }} -gate mcx_vchain q0,q1,q2,q3,q4 {{ mcx q0,q1,q2,q3; }} +gate mcx_vchain q0,q1,q2,q3,q4 {{ h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }} gate mcx_recursive q0,q1,q2,q3,q4,q5,q6 {{ mcx_vchain q0,q1,q2,q6,q3; mcx_vchain q3,q4,q6,q5,q0; mcx_vchain q0,q1,q2,q6,q3; mcx_vchain q3,q4,q6,q5,q0; }} gate mcx_vchain_{mcx_vchain_id} q0,q1,q2,q3,q4,q5,q6,q7,q8 {{ rccx q0,q1,q6; rccx q2,q6,q7; rccx q3,q7,q8; ccx q4,q8,q5; rccx q3,q7,q8; rccx q2,q6,q7; rccx q0,q1,q6; }} qreg q[9]; diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 677b191cb0cb..a0aeae5ca2c3 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -24,9 +24,10 @@ AnnotatedOperation, InverseModifier, ControlModifier, + Gate, ) from qiskit.circuit.commutation_library import SessionCommutationChecker as scc - +from qiskit.dagcircuit import DAGOpNode from qiskit.circuit.library import ( ZGate, XGate, @@ -44,6 +45,16 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +class NewGateCX(Gate): + """A dummy class containing an cx gate unknown to the commutation checker's library.""" + + def __init__(self): + super().__init__("new_cx", 2, []) + + 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 TestCommutationChecker(QiskitTestCase): """Test CommutationChecker class.""" @@ -51,206 +62,105 @@ def test_simple_gates(self): """Check simple commutation relations between gates, experimenting with different orders of gates, different orders of qubits, different sets of qubits over which gates are defined, and so on.""" - # should commute - res = scc.commute(ZGate(), [0], [], CXGate(), [0, 1], []) - self.assertTrue(res) + # should commute + self.assertTrue(scc.commute(ZGate(), [0], [], CXGate(), [0, 1], [])) # should not commute - res = scc.commute(ZGate(), [1], [], CXGate(), [0, 1], []) - self.assertFalse(res) - + self.assertFalse(scc.commute(ZGate(), [1], [], CXGate(), [0, 1], [])) # should not commute - res = scc.commute(XGate(), [0], [], CXGate(), [0, 1], []) - self.assertFalse(res) - + self.assertFalse(scc.commute(XGate(), [0], [], CXGate(), [0, 1], [])) # should commute - res = scc.commute(XGate(), [1], [], CXGate(), [0, 1], []) - self.assertTrue(res) - + self.assertTrue(scc.commute(XGate(), [1], [], CXGate(), [0, 1], [])) # should not commute - res = scc.commute(XGate(), [1], [], CXGate(), [1, 0], []) - self.assertFalse(res) - + self.assertFalse(scc.commute(XGate(), [1], [], CXGate(), [1, 0], [])) # should commute - res = scc.commute(XGate(), [0], [], CXGate(), [1, 0], []) - self.assertTrue(res) - + self.assertTrue(scc.commute(XGate(), [0], [], CXGate(), [1, 0], [])) # should commute - res = scc.commute(CXGate(), [1, 0], [], XGate(), [0], []) - self.assertTrue(res) - + self.assertTrue(scc.commute(CXGate(), [1, 0], [], XGate(), [0], [])) # should not commute - res = scc.commute(CXGate(), [1, 0], [], XGate(), [1], []) - self.assertFalse(res) - + self.assertFalse(scc.commute(CXGate(), [1, 0], [], XGate(), [1], [])) # should commute - res = scc.commute( - CXGate(), - [1, 0], - [], - CXGate(), - [1, 0], - [], - ) - self.assertTrue(res) - + self.assertTrue(scc.commute(CXGate(), [1, 0], [], CXGate(), [1, 0], [])) # should not commute - res = scc.commute( - CXGate(), - [1, 0], - [], - CXGate(), - [0, 1], - [], - ) - self.assertFalse(res) - + self.assertFalse(scc.commute(CXGate(), [1, 0], [], CXGate(), [0, 1], [])) # should commute - res = scc.commute( - CXGate(), - [1, 0], - [], - CXGate(), - [1, 2], - [], - ) - self.assertTrue(res) - + self.assertTrue(scc.commute(CXGate(), [1, 0], [], CXGate(), [1, 2], [])) # should not commute - res = scc.commute( - CXGate(), - [1, 0], - [], - CXGate(), - [2, 1], - [], - ) - self.assertFalse(res) - + self.assertFalse(scc.commute(CXGate(), [1, 0], [], CXGate(), [2, 1], [])) # should commute - res = scc.commute( - CXGate(), - [1, 0], - [], - CXGate(), - [2, 3], - [], - ) - self.assertTrue(res) - - res = scc.commute(XGate(), [2], [], CCXGate(), [0, 1, 2], []) - self.assertTrue(res) - - res = scc.commute(CCXGate(), [0, 1, 2], [], CCXGate(), [0, 2, 1], []) - self.assertFalse(res) + self.assertTrue(scc.commute(CXGate(), [1, 0], [], CXGate(), [2, 3], [])) + self.assertTrue(scc.commute(XGate(), [2], [], CCXGate(), [0, 1, 2], [])) + self.assertFalse(scc.commute(CCXGate(), [0, 1, 2], [], CCXGate(), [0, 2, 1], [])) def test_passing_quantum_registers(self): """Check that passing QuantumRegisters works correctly.""" qr = QuantumRegister(4) - # should commute - res = scc.commute(CXGate(), [qr[1], qr[0]], [], CXGate(), [qr[1], qr[2]], []) - self.assertTrue(res) - + self.assertTrue(scc.commute(CXGate(), [qr[1], qr[0]], [], CXGate(), [qr[1], qr[2]], [])) # should not commute - res = scc.commute(CXGate(), [qr[0], qr[1]], [], CXGate(), [qr[1], qr[2]], []) - self.assertFalse(res) + self.assertFalse(scc.commute(CXGate(), [qr[0], qr[1]], [], CXGate(), [qr[1], qr[2]], [])) def test_standard_gates_commutations(self): """Check that commutativity checker uses standard gates commutations as expected.""" scc.clear_cached_commutations() - scc.clear_cached_commutations() - res = scc.commute(ZGate(), [0], [], CXGate(), [0, 1], []) - self.assertTrue(res) + self.assertTrue(scc.commute(ZGate(), [0], [], CXGate(), [0, 1], [])) self.assertEqual(scc.num_cached_entries(), 0) def test_caching_positive_results(self): """Check that hashing positive results in commutativity checker works as expected.""" scc.clear_cached_commutations() - NewGateCX = type("MyClass", (CXGate,), {"content": {}}) - NewGateCX.name = "cx_new" - - res = scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], []) - self.assertTrue(res) - self.assertGreater(len(scc._cached_commutations), 0) + self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertGreater(scc.num_cached_entries(), 0) def test_caching_lookup_with_non_overlapping_qubits(self): """Check that commutation lookup with non-overlapping qubits works as expected.""" scc.clear_cached_commutations() - res = scc.commute(CXGate(), [0, 2], [], CXGate(), [0, 1], []) - self.assertTrue(res) - res = scc.commute(CXGate(), [0, 1], [], CXGate(), [1, 2], []) - self.assertFalse(res) - self.assertEqual(len(scc._cached_commutations), 0) + self.assertTrue(scc.commute(CXGate(), [0, 2], [], CXGate(), [0, 1], [])) + self.assertFalse(scc.commute(CXGate(), [0, 1], [], CXGate(), [1, 2], [])) + self.assertEqual(scc.num_cached_entries(), 0) def test_caching_store_and_lookup_with_non_overlapping_qubits(self): """Check that commutations storing and lookup with non-overlapping qubits works as expected.""" - cc_lenm = scc.num_cached_entries() - NewGateCX = type("MyClass", (CXGate,), {"content": {}}) - NewGateCX.name = "cx_new" - res = scc.commute(NewGateCX(), [0, 2], [], CXGate(), [0, 1], []) - self.assertTrue(res) - res = scc.commute(NewGateCX(), [0, 1], [], CXGate(), [1, 2], []) - self.assertFalse(res) - res = scc.commute(NewGateCX(), [1, 4], [], CXGate(), [1, 6], []) - self.assertTrue(res) - res = scc.commute(NewGateCX(), [5, 3], [], CXGate(), [3, 1], []) - self.assertFalse(res) - self.assertEqual(scc.num_cached_entries(), cc_lenm + 2) + 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], [])) + 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() - NewGateCX = type("MyClass", (CXGate,), {"content": {}}) - NewGateCX.name = "cx_new" - - res = scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], []) - self.assertFalse(res) - self.assertGreater(len(scc._cached_commutations), 0) + self.assertFalse(scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertGreater(scc.num_cached_entries(), 0) def test_caching_different_qubit_sets(self): """Check that hashing same commutativity results over different qubit sets works as expected.""" scc.clear_cached_commutations() - NewGateCX = type("MyClass", (CXGate,), {"content": {}}) - NewGateCX.name = "cx_new" # 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], []) - self.assertEqual(len(scc._cached_commutations), 1) - self.assertEqual(scc._cache_miss, 1) - self.assertEqual(scc._cache_hit, 3) + self.assertEqual(scc.num_cached_entries(), 1) def test_cache_with_param_gates(self): """Check commutativity between (non-parameterized) gates with parameters.""" scc.clear_cached_commutations() - res = scc.commute(RZGate(0), [0], [], XGate(), [0], []) - self.assertTrue(res) - - res = scc.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], []) - self.assertFalse(res) - res = scc.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], []) - self.assertTrue(res) + self.assertTrue(scc.commute(RZGate(0), [0], [], XGate(), [0], [])) + self.assertFalse(scc.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], [])) + self.assertTrue(scc.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], [])) - res = scc.commute(RZGate(np.pi / 2), [1], [], XGate(), [1], []) - self.assertFalse(res) + self.assertFalse(scc.commute(RZGate(np.pi / 2), [1], [], XGate(), [1], [])) self.assertEqual(scc.num_cached_entries(), 3) - self.assertEqual(scc._cache_miss, 3) - self.assertEqual(scc._cache_hit, 1) def test_gates_with_parameters(self): """Check commutativity between (non-parameterized) gates with parameters.""" - res = scc.commute(RZGate(0), [0], [], XGate(), [0], []) - self.assertTrue(res) - - res = scc.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], []) - self.assertFalse(res) - - res = scc.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], []) - self.assertTrue(res) + self.assertTrue(scc.commute(RZGate(0), [0], [], XGate(), [0], [])) + self.assertFalse(scc.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], [])) + self.assertTrue(scc.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], [])) def test_parameterized_gates(self): """Check commutativity between parameterized gates, both with free and with @@ -272,84 +182,68 @@ def test_parameterized_gates(self): self.assertFalse(cx_gate.is_parameterized()) # We should detect that these gates commute - res = scc.commute(rz_gate, [0], [], cx_gate, [0, 1], []) - self.assertTrue(res) + self.assertTrue(scc.commute(rz_gate, [0], [], cx_gate, [0, 1], [])) # We should detect that these gates commute - res = scc.commute(rz_gate, [0], [], rz_gate, [0], []) - self.assertTrue(res) + self.assertTrue(scc.commute(rz_gate, [0], [], rz_gate, [0], [])) # We should detect that parameterized gates over disjoint qubit subsets commute - res = scc.commute(rz_gate_theta, [0], [], rz_gate_theta, [1], []) - self.assertTrue(res) + self.assertTrue(scc.commute(rz_gate_theta, [0], [], rz_gate_theta, [1], [])) # We should detect that parameterized gates over disjoint qubit subsets commute - res = scc.commute(rz_gate_theta, [0], [], rz_gate_phi, [1], []) - self.assertTrue(res) + self.assertTrue(scc.commute(rz_gate_theta, [0], [], rz_gate_phi, [1], [])) # We should detect that parameterized gates over disjoint qubit subsets commute - res = scc.commute(rz_gate_theta, [2], [], cx_gate, [1, 3], []) - self.assertTrue(res) + self.assertTrue(scc.commute(rz_gate_theta, [2], [], cx_gate, [1, 3], [])) # However, for now commutativity checker should return False when checking # commutativity between a parameterized gate and some other gate, when # the two gates are over intersecting qubit subsets. # This check should be changed if commutativity checker is extended to # handle parameterized gates better. - res = scc.commute(rz_gate_theta, [0], [], cx_gate, [0, 1], []) - self.assertFalse(res) + self.assertFalse(scc.commute(rz_gate_theta, [0], [], cx_gate, [0, 1], [])) - res = scc.commute(rz_gate_theta, [0], [], rz_gate, [0], []) - self.assertFalse(res) + self.assertFalse(scc.commute(rz_gate_theta, [0], [], rz_gate, [0], [])) def test_measure(self): """Check commutativity involving measures.""" # Measure is over qubit 0, while gate is over a disjoint subset of qubits # We should be able to swap these. - res = scc.commute(Measure(), [0], [0], CXGate(), [1, 2], []) - self.assertTrue(res) + self.assertTrue(scc.commute(Measure(), [0], [0], CXGate(), [1, 2], [])) # Measure and gate have intersecting set of qubits # We should not be able to swap these. - res = scc.commute(Measure(), [0], [0], CXGate(), [0, 2], []) - self.assertFalse(res) + self.assertFalse(scc.commute(Measure(), [0], [0], CXGate(), [0, 2], [])) # Measures over different qubits and clbits - res = scc.commute(Measure(), [0], [0], Measure(), [1], [1]) - self.assertTrue(res) + self.assertTrue(scc.commute(Measure(), [0], [0], Measure(), [1], [1])) # Measures over different qubits but same classical bit # We should not be able to swap these. - res = scc.commute(Measure(), [0], [0], Measure(), [1], [0]) - self.assertFalse(res) + self.assertFalse(scc.commute(Measure(), [0], [0], Measure(), [1], [0])) # Measures over same qubits but different classical bit # ToDo: can we swap these? # Currently checker takes the safe approach and returns False. - res = scc.commute(Measure(), [0], [0], Measure(), [0], [1]) - self.assertFalse(res) + self.assertFalse(scc.commute(Measure(), [0], [0], Measure(), [0], [1])) def test_barrier(self): """Check commutativity involving barriers.""" # A gate should not commute with a barrier # (at least if these are over intersecting qubit sets). - res = scc.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [1, 2], []) - self.assertFalse(res) + self.assertFalse(scc.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [1, 2], [])) # Does it even make sense to have a barrier over a subset of qubits? # Though in this case, it probably makes sense to say that barrier and gate can be swapped. - res = scc.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [5, 6], []) - self.assertTrue(res) + self.assertTrue(scc.commute(Barrier(4), [0, 1, 2, 3], [], CXGate(), [5, 6], [])) def test_reset(self): """Check commutativity involving resets.""" # A gate should not commute with reset when the qubits intersect. - res = scc.commute(Reset(), [0], [], CXGate(), [0, 2], []) - self.assertFalse(res) + self.assertFalse(scc.commute(Reset(), [0], [], CXGate(), [0, 2], [])) # A gate should commute with reset when the qubits are disjoint. - res = scc.commute(Reset(), [0], [], CXGate(), [1, 2], []) - self.assertTrue(res) + self.assertTrue(scc.commute(Reset(), [0], [], CXGate(), [1, 2], [])) def test_conditional_gates(self): """Check commutativity involving conditional gates.""" @@ -358,22 +252,26 @@ def test_conditional_gates(self): # Currently, in all cases commutativity checker should returns False. # This is definitely suboptimal. - res = scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], []) - self.assertFalse(res) - - res = scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], []) - self.assertFalse(res) - - res = scc.commute( - CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [] + self.assertFalse( + scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], []) ) - self.assertFalse(res) - - res = scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate().c_if(cr[0], 1), [qr[0]], []) - self.assertFalse(res) - - res = scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate(), [qr[0]], []) - self.assertFalse(res) + self.assertFalse( + scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], []) + ) + self.assertFalse( + scc.commute( + CXGate().c_if(cr[0], 0), + [qr[0], qr[1]], + [], + CXGate().c_if(cr[0], 0), + [qr[0], qr[1]], + [], + ) + ) + self.assertFalse( + scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate().c_if(cr[0], 1), [qr[0]], []) + ) + self.assertFalse(scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate(), [qr[0]], [])) def test_complex_gates(self): """Check commutativity involving more complex gates.""" @@ -382,16 +280,14 @@ def test_complex_gates(self): # lf1 is equivalent to swap(0, 1), and lf2 to swap(1, 2). # These do not commute. - res = scc.commute(lf1, [0, 1, 2], [], lf2, [0, 1, 2], []) - self.assertFalse(res) + self.assertFalse(scc.commute(lf1, [0, 1, 2], [], lf2, [0, 1, 2], [])) lf3 = LinearFunction([[0, 1, 0], [0, 0, 1], [1, 0, 0]]) lf4 = LinearFunction([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) # lf3 is permutation 1->2, 2->3, 3->1. # lf3 is the inverse permutation 1->3, 2->1, 3->2. # These commute. - res = scc.commute(lf3, [0, 1, 2], [], lf4, [0, 1, 2], []) - self.assertTrue(res) + self.assertTrue(scc.commute(lf3, [0, 1, 2], [], lf4, [0, 1, 2], [])) def test_equal_annotated_operations_commute(self): """Check commutativity involving the same annotated operation.""" @@ -435,17 +331,28 @@ def test_c7x_gate(self): def test_wide_gates_over_nondisjoint_qubits(self): """Test that checking wide gates does not lead to memory problems.""" - res = scc.commute(MCXGate(29), list(range(30)), [], XGate(), [0], []) - self.assertFalse(res) - res = scc.commute(XGate(), [0], [], MCXGate(29), list(range(30)), []) - self.assertFalse(res) + self.assertFalse(scc.commute(MCXGate(29), list(range(30)), [], XGate(), [0], [])) def test_wide_gates_over_disjoint_qubits(self): """Test that wide gates still commute when they are over disjoint sets of qubits.""" - res = scc.commute(MCXGate(29), list(range(30)), [], XGate(), [30], []) - self.assertTrue(res) - res = scc.commute(XGate(), [30], [], MCXGate(29), list(range(30)), []) - self.assertTrue(res) + self.assertTrue(scc.commute(MCXGate(29), list(range(30)), [], XGate(), [30], [])) + self.assertTrue(scc.commute(XGate(), [30], [], MCXGate(29), list(range(30)), [])) + + def test_serialization(self): + """Test that the commutation checker is correctly serialized""" + import pickle + + scc.clear_cached_commutations() + self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [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=[]) + cc2.commute_nodes(dop1, dop2) + dop1 = DAGOpNode(ZGate(), qargs=[0], cargs=[]) + dop2 = DAGOpNode(CXGate(), qargs=[0, 1], cargs=[]) + cc2.commute_nodes(dop1, dop2) + self.assertEqual(cc2.num_cached_entries(), 1) if __name__ == "__main__": diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index e41b5c1f9f31..7a3a0873ae77 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -3464,9 +3464,12 @@ def test_switch_rejects_entering_case_after_close(self): def test_switch_rejects_reentering_case(self): """It shouldn't be possible to enter a case within another case.""" circuit = QuantumCircuit(1, 1) - with circuit.switch(0) as case, case(0), self.assertRaisesRegex( - CircuitError, r"Cannot enter more than one case at once" - ), case(1): + with ( + circuit.switch(0) as case, + case(0), + self.assertRaisesRegex(CircuitError, r"Cannot enter more than one case at once"), + case(1), + ): pass @ddt.data("1", 1.0, None, (1, 2)) diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index a517d5d1e4a4..03a041355b87 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -1438,7 +1438,6 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): @data( RXGate, RYGate, - RZGate, RXXGate, RYYGate, RZXGate, diff --git a/test/python/circuit/test_diagonal_gate.py b/test/python/circuit/test_diagonal_gate.py index de8d53e12d3b..bb9259999576 100644 --- a/test/python/circuit/test_diagonal_gate.py +++ b/test/python/circuit/test_diagonal_gate.py @@ -83,6 +83,19 @@ def test_npcomplex_params_conversion(self): all(isinstance(p, complex) and not isinstance(p, np.number) for p in params) ) + def test_repeat(self): + """Test the repeat() method.""" + for phases in [ + [0, 0], + np.array([0, 0.8, 1, 0]), + (2 * np.pi * np.random.rand(2**3)).tolist(), + ]: + with self.subTest(phases=phases): + diag = [np.exp(1j * ph) for ph in phases] + gate = DiagonalGate(diag) + operator = Operator(gate) + self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator)) + def _get_diag_gate_matrix(diag): return np.diagflat(diag) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 950b0c478ed8..914ac8bab985 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -54,6 +54,8 @@ CZGate, RYYGate, PhaseGate, + PauliGate, + UCPauliRotGate, CPhaseGate, UGate, CUGate, @@ -184,6 +186,18 @@ def test_xx_minus_yy_definition(self): self.assertTrue(len(decomposed_circuit) > len(circuit)) self.assertTrue(Operator(circuit).equiv(Operator(decomposed_circuit), atol=1e-7)) + def test_pauligate_repeat(self): + """Test `repeat` method for `PauliGate`.""" + gate = PauliGate("XYZ") + operator = Operator(gate) + self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator)) + + def test_ucpaulirotgate_repeat(self): + """Test `repeat` method for `UCPauliRotGate`.""" + gate = UCPauliRotGate([0.3, 0.5], "X") + operator = Operator(gate) + self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator)) + @ddt class TestStandardGates(QiskitTestCase): diff --git a/test/python/circuit/test_hamiltonian_gate.py b/test/python/circuit/test_hamiltonian_gate.py index 60fb1e9a90a9..e65e2f07ba5e 100644 --- a/test/python/circuit/test_hamiltonian_gate.py +++ b/test/python/circuit/test_hamiltonian_gate.py @@ -62,6 +62,12 @@ def test_adjoint(self): ham.adjoint().to_matrix(), np.transpose(np.conj(ham.to_matrix())) ) + def test_repeat(self): + """test repeat operation""" + ham = HamiltonianGate(np.array([[1, 0.5 + 4j], [0.5 - 4j, -0.2]]), np.pi * 0.143) + operator = Operator(ham) + self.assertTrue(np.allclose(Operator(ham.repeat(2)), operator @ operator)) + class TestHamiltonianCircuit(QiskitTestCase): """Hamiltonian gate circuit tests.""" diff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py index a37c51f48184..990df5755d59 100644 --- a/test/python/circuit/test_initializer.py +++ b/test/python/circuit/test_initializer.py @@ -465,6 +465,24 @@ def test_mutating_params(self): self.assertEqual(decom_circ.data[2].operation.name, "state_preparation") self.assertEqual(decom_circ.data[2].operation.params, ["0", "0"]) + def test_gates_to_uncompute(self): + """Test the gates_to_uncompute() method.""" + desired_vector = [0.5, 0.5, 0.5, 0.5] + initialize = Initialize(desired_vector) + qc = initialize.gates_to_uncompute().inverse() + vec = Statevector(qc) + self.assertTrue(vec == Statevector(desired_vector)) + + def test_repeat(self): + """Test the repeat() method.""" + desired_vector = np.array([0.5, 0.5, 0.5, 0.5]) + initialize = Initialize(desired_vector) + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + qc.append(initialize.repeat(2), qr) + statevector = Statevector(qc) + self.assertTrue(np.allclose(statevector, desired_vector)) + class TestInstructionParam(QiskitTestCase): """Test conversion of numpy type parameters.""" diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index 35ff639cedd5..ff497c76f48a 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -133,6 +133,21 @@ def test_isometry_inverse(self, iso): result = Operator(qc) np.testing.assert_array_almost_equal(result.data, np.identity(result.dim[0])) + @data( + np.eye(2, 2), + random_unitary(2, seed=297102).data, + np.eye(4, 4), + random_unitary(4, seed=123642).data, + random_unitary(8, seed=568288).data, + ) + def test_isometry_repeat(self, iso): + """Tests for the repeat of isometries from n to n qubits""" + iso_gate = Isometry(iso, 0, 0) + + op = Operator(iso_gate) + op_double = Operator(iso_gate.repeat(2)) + np.testing.assert_array_almost_equal(op @ op, op_double) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index b6de438d04dd..db4f1441bea3 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -66,7 +66,7 @@ def test_definitions(self): continue with self.subTest(name=name): - params = [pi] * standard_gate._num_params() + params = [0.1 * (i + 1) for i in range(standard_gate._num_params())] py_def = gate_class.base_class(*params).definition rs_def = standard_gate._get_definition(params) if py_def is None: @@ -141,7 +141,7 @@ def test_matrix(self): continue with self.subTest(name=name): - params = [0.1] * standard_gate._num_params() + params = [0.1 * (i + 1) for i in range(standard_gate._num_params())] py_def = gate_class.base_class(*params).to_matrix() rs_def = standard_gate._to_matrix(params) np.testing.assert_allclose(rs_def, py_def) @@ -227,3 +227,20 @@ def test_non_default_controls(self): circuit.append(gate, [0, 1, 2]) self.assertIsNotNone(getattr(gate, "_standard_gate", None)) np.testing.assert_almost_equal(Operator(circuit.data[0].operation).to_matrix(), op) + + def test_extracted_as_standard_gate(self): + """Test that every gate in the standard library gets correctly extracted as a Rust-space + `StandardGate` in its default configuration when passed through `append`.""" + standards = set() + qc = QuantumCircuit(4) + for name, gate in get_standard_gate_name_mapping().items(): + if gate._standard_gate is None: + # Not a standard gate. + continue + standards.add(name) + qc.append(gate, qc.qubits[: gate.num_qubits], []) + # Sanity check: the test should have found at least one standard gate in the mapping. + self.assertNotEqual(standards, set()) + + extracted = {inst.name for inst in qc.data if inst.is_standard_gate()} + self.assertEqual(standards, extracted) diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index 87541d349c3f..0277c4afb43d 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -100,6 +100,13 @@ def test_inverse_ucg(self): self.assertTrue(np.allclose(unitary_desired, unitary)) + def test_repeat(self): + """test repeat operation""" + gates = [random_unitary(2, seed=seed).data for seed in [124435, 876345, 687462, 928365]] + + uc = UCGate(gates, up_to_diagonal=False) + self.assertTrue(np.allclose(Operator(uc.repeat(2)), Operator(uc) @ Operator(uc))) + def _get_ucg_matrix(squs): return block_diag(*squs) diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 63edbdaffbf9..6ab4a93c3f7d 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -67,6 +67,11 @@ def test_adjoint(self): uni = UnitaryGate([[0, 1j], [-1j, 0]]) self.assertTrue(numpy.array_equal(uni.adjoint().to_matrix(), uni.to_matrix())) + def test_repeat(self): + """test repeat operation""" + uni = UnitaryGate([[1, 0], [0, 1j]]) + self.assertTrue(numpy.array_equal(Operator(uni.repeat(2)), Operator(uni) @ Operator(uni))) + class TestUnitaryCircuit(QiskitTestCase): """Matrix gate circuit tests.""" diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a348ad8b749d..edb3df63e46d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -928,7 +928,10 @@ def test_move_measurements(self): circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "move_measurements.qasm")) lay = [0, 1, 15, 2, 14, 3, 13, 4, 12, 5, 11, 6] - out = transpile(circ, initial_layout=lay, coupling_map=cmap, routing_method="stochastic") + with self.assertWarns(DeprecationWarning): + out = transpile( + circ, initial_layout=lay, coupling_map=cmap, routing_method="stochastic" + ) out_dag = circuit_to_dag(out) meas_nodes = out_dag.named_nodes("measure") for meas_node in meas_nodes: @@ -3317,19 +3320,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3391,19 +3394,19 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -3474,31 +3477,31 @@ def _visit_block(circuit, qubit_mapping=None): tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] - first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node = tqc_dag._find_successors_by_edge( input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIsInstance(first_meas_node.op, Measure) self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[1]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): self.assertIn(qubit_map[op_node.qargs[0]], components[2]) - op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node = tqc_dag._find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(qubit_map[op_node.qargs[0]], components[2]) - @data("sabre", "stochastic", "basic", "lookahead") + @data("sabre", "basic", "lookahead") def test_basic_connected_circuit_dense_layout(self, routing_method): """Test basic connected circuit on disjoint backend""" qc = QuantumCircuit(5) @@ -3522,8 +3525,34 @@ def test_basic_connected_circuit_dense_layout(self, routing_method): continue self.assertIn(qubits, self.backend.target[op_name]) + @data("stochastic") + def test_basic_connected_circuit_dense_layout_stochastic(self, routing_method): + """Test basic connected circuit on disjoint backend for deprecated stochastic swap""" + # TODO: Remove when StochasticSwap is removed + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.measure_all() + with self.assertWarns(DeprecationWarning): + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + # Lookahead swap skipped for performance - @data("sabre", "stochastic", "basic") + @data("sabre", "basic") def test_triple_circuit_dense_layout(self, routing_method): """Test a split circuit with one circuit component per chip.""" qc = QuantumCircuit(30) @@ -3572,7 +3601,58 @@ def test_triple_circuit_dense_layout(self, routing_method): continue self.assertIn(qubits, self.backend.target[op_name]) - @data("sabre", "stochastic", "basic", "lookahead") + @data("stochastic") + def test_triple_circuit_dense_layout_stochastic(self, routing_method): + """Test a split circuit with one circuit component per chip for deprecated StochasticSwap.""" + # TODO: Remove when StochasticSwap is removed + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + with self.assertWarns(DeprecationWarning): + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + @data("sabre", "basic", "lookahead") def test_triple_circuit_invalid_layout(self, routing_method): """Test a split circuit with one circuit component per chip.""" qc = QuantumCircuit(30) @@ -3616,8 +3696,54 @@ def test_triple_circuit_invalid_layout(self, routing_method): seed_transpiler=42, ) - # Lookahead swap skipped for performance reasons - @data("sabre", "stochastic", "basic") + @data("stochastic") + def test_triple_circuit_invalid_layout_stochastic(self, routing_method): + """Test a split circuit with one circuit component per chip for deprecated ``StochasticSwap``""" + # TODO: Remove when StochasticSwap is removed + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + with self.assertWarns(DeprecationWarning): + with self.assertRaises(TranspilerError): + transpile( + qc, + self.backend, + layout_method="trivial", + routing_method=routing_method, + seed_transpiler=42, + ) + + # Lookahead swap skipped for performance reasons, stochastic moved to new test due to deprecation + @data("sabre", "basic") def test_six_component_circuit_dense_layout(self, routing_method): """Test input circuit with more than 1 component per backend component.""" qc = QuantumCircuit(42) @@ -3678,6 +3804,71 @@ def test_six_component_circuit_dense_layout(self, routing_method): continue self.assertIn(qubits, self.backend.target[op_name]) + # Lookahead swap skipped for performance reasons + @data("stochastic") + def test_six_component_circuit_dense_layout_stochastic(self, routing_method): + """Test input circuit with more than 1 component per backend component + for deprecated ``StochasticSwap``.""" + # TODO: Remove when StochasticSwap is removed + qc = QuantumCircuit(42) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.h(30) + qc.cx(30, 31) + qc.cx(30, 32) + qc.cx(30, 33) + qc.h(34) + qc.cx(34, 35) + qc.cx(34, 36) + qc.cx(34, 37) + qc.h(38) + qc.cx(38, 39) + qc.cx(39, 40) + qc.cx(39, 41) + qc.measure_all() + with self.assertWarns(DeprecationWarning): + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + @data(0, 1, 2, 3) def test_transpile_target_with_qubits_without_ops(self, opt_level): """Test qubits without operations aren't ever used.""" diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 26f7e4788083..ef8050961066 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -18,7 +18,6 @@ import unittest from ddt import ddt, data -import rustworkx as rx from numpy import pi from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGInNode, DAGOutNode, DAGCircuitError @@ -55,18 +54,16 @@ def raise_if_dagcircuit_invalid(dag): DAGCircuitError: if DAGCircuit._multi_graph is inconsistent. """ - multi_graph = dag._multi_graph - - if not rx.is_directed_acyclic_graph(multi_graph): + if not dag._is_dag(): raise DAGCircuitError("multi_graph is not a DAG.") # Every node should be of type in, out, or op. # All input/output nodes should be present in input_map/output_map. - for node in dag._multi_graph.nodes(): + for node in dag.nodes(): if isinstance(node, DAGInNode): - assert node is dag.input_map[node.wire] + assert node == dag.input_map[node.wire] elif isinstance(node, DAGOutNode): - assert node is dag.output_map[node.wire] + assert node == dag.output_map[node.wire] elif isinstance(node, DAGOpNode): continue else: @@ -78,9 +75,7 @@ def raise_if_dagcircuit_invalid(dag): assert len(node.cargs) == node.op.num_clbits # Every edge should be labled with a known wire. - edges_outside_wires = [ - edge_data for edge_data in dag._multi_graph.edges() if edge_data not in dag.wires - ] + edges_outside_wires = [edge_data for edge_data in dag._edges() if edge_data not in dag.wires] if edges_outside_wires: raise DAGCircuitError( f"multi_graph contains one or more edges ({edges_outside_wires}) " @@ -103,7 +98,7 @@ def raise_if_dagcircuit_invalid(dag): out_node_id = dag.output_map[wire]._node_id while cur_node_id != out_node_id: - out_edges = dag._multi_graph.out_edges(cur_node_id) + out_edges = dag._out_edges(cur_node_id) edges_to_follow = [(src, dest, data) for (src, dest, data) in out_edges if data == wire] assert len(edges_to_follow) == 1 @@ -112,18 +107,15 @@ def raise_if_dagcircuit_invalid(dag): # Wires can only terminate at input/output nodes. op_counts = Counter() for op_node in dag.op_nodes(): - assert multi_graph.in_degree(op_node._node_id) == multi_graph.out_degree(op_node._node_id) + assert sum(1 for _ in dag.predecessors(op_node)) == sum(1 for _ in dag.successors(op_node)) op_counts[op_node.name] += 1 # The _op_names attribute should match the counted op names - assert op_counts == dag._op_names + assert op_counts == dag.count_ops() # Node input/output edges should match node qarg/carg/condition. for node in dag.op_nodes(): - in_edges = dag._multi_graph.in_edges(node._node_id) - out_edges = dag._multi_graph.out_edges(node._node_id) - - in_wires = {data for src, dest, data in in_edges} - out_wires = {data for src, dest, data in out_edges} + in_wires = set(dag._in_wires(node._node_id)) + out_wires = set(dag._out_wires(node._node_id)) node_cond_bits = set( node.op.condition[0][:] if getattr(node.op, "condition", None) is not None else [] @@ -516,6 +508,38 @@ def test_remove_unknown_clbit(self): self.assert_cregs_equal(self.original_cregs) self.assert_clbits_equal(self.original_clbits) + def test_remove_clbit_with_control_flow(self): + """Test clbit removal in the middle of clbits with control flow.""" + qr = QuantumRegister(1) + cr1 = ClassicalRegister(2, "a") + cr2 = ClassicalRegister(2, "b") + clbit = Clbit() + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_creg(cr1) + dag.add_creg(cr2) + dag.add_clbits([clbit]) + + inner = QuantumCircuit(1) + inner.h(0) + inner.z(0) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + dag.apply_operation_back(op, qr, ()) + dag.remove_clbits(*cr2) + self.assertEqual(dag.clbits, list(cr1) + [clbit]) + self.assertEqual(dag.cregs, {"a": cr1}) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_creg(cr1) + expected.add_clbits([clbit]) + + op = IfElseOp(expr.logic_and(expr.equal(cr1, 3), expr.logic_not(clbit)), inner, None) + expected.apply_operation_back(op, qr, ()) + + self.assertEqual(dag, expected) + class TestDagApplyOperation(QiskitTestCase): """Test adding an op node to a dag.""" @@ -575,7 +599,7 @@ def test_apply_operation_back_conditional(self): self.assertEqual(h_node.op.condition, h_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(h_node._node_id)), + sorted(self.dag._in_edges(h_node._node_id)), sorted( [ (self.dag.input_map[self.qubit2]._node_id, h_node._node_id, self.qubit2), @@ -586,7 +610,7 @@ def test_apply_operation_back_conditional(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(h_node._node_id)), + sorted(self.dag._out_edges(h_node._node_id)), sorted( [ (h_node._node_id, self.dag.output_map[self.qubit2]._node_id, self.qubit2), @@ -596,7 +620,7 @@ def test_apply_operation_back_conditional(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure(self): """Test consistency of apply_operation_back for conditional measure.""" @@ -615,7 +639,7 @@ def test_apply_operation_back_conditional_measure(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit0]._node_id, meas_node._node_id, self.qubit0), @@ -630,7 +654,7 @@ def test_apply_operation_back_conditional_measure(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit0]._node_id, self.qubit0), @@ -644,7 +668,7 @@ def test_apply_operation_back_conditional_measure(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_back_conditional_measure_to_self(self): """Test consistency of apply_operation_back for measure onto conditioning bit.""" @@ -660,7 +684,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( - sorted(self.dag._multi_graph.in_edges(meas_node._node_id)), + sorted(self.dag._in_edges(meas_node._node_id)), sorted( [ (self.dag.input_map[self.qubit1]._node_id, meas_node._node_id, self.qubit1), @@ -671,7 +695,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ) self.assertEqual( - sorted(self.dag._multi_graph.out_edges(meas_node._node_id)), + sorted(self.dag._out_edges(meas_node._node_id)), sorted( [ (meas_node._node_id, self.dag.output_map[self.qubit1]._node_id, self.qubit1), @@ -681,7 +705,7 @@ def test_apply_operation_back_conditional_measure_to_self(self): ), ) - self.assertTrue(rx.is_directed_acyclic_graph(self.dag._multi_graph)) + self.assertTrue(self.dag._is_dag()) def test_apply_operation_front(self): """The apply_operation_front() method""" @@ -935,8 +959,8 @@ def test_classical_predecessors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) predecessor_measure = self.dag.classical_predecessors(self.dag.named_nodes("measure").pop()) @@ -948,6 +972,13 @@ def test_classical_predecessors(self): self.assertIsInstance(predecessor1, DAGInNode) self.assertIsInstance(predecessor1.wire, Clbit) + def test_apply_operation_reject_invalid_qarg_carg(self): + """Test that we can't add a carg to qargs and vice versa on apply methods""" + with self.assertRaises(KeyError): + self.dag.apply_operation_back(Measure(), [self.clbit1], [self.qubit1]) + with self.assertRaises(KeyError): + self.dag.apply_operation_front(Measure(), [self.clbit1], [self.qubit1]) + def test_classical_successors(self): """The method dag.classical_successors() returns successors connected by classical edges""" @@ -969,8 +1000,8 @@ def test_classical_successors(self): self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(HGate(), [self.qubit1], []) - self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) - self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0], [self.clbit0]) + self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) successors_measure = self.dag.classical_successors(self.dag.named_nodes("measure").pop()) @@ -1067,12 +1098,12 @@ def test_topological_nodes(self): ("cx", (self.qubit2, self.qubit1)), ("cx", (self.qubit0, self.qubit2)), ("h", (self.qubit2,)), + cr[0], + cr[1], qr[0], qr[1], qr[2], cr[0], - cr[0], - cr[1], cr[1], ] self.assertEqual( @@ -2397,7 +2428,6 @@ def test_contract_var_use_to_nothing(self): expected = DAGCircuit() expected.add_input_var(a) - self.assertEqual(src, expected) def test_raise_if_var_mismatch(self): @@ -2665,7 +2695,6 @@ def test_substituting_node_preserves_args_condition(self, inplace): self.assertEqual(replacement_node.qargs, (qr[1], qr[0])) self.assertEqual(replacement_node.cargs, ()) self.assertEqual(replacement_node.op.condition, (cr, 1)) - self.assertEqual(replacement_node is node_to_be_replaced, inplace) @data(True, False) @@ -3172,24 +3201,34 @@ def test_creg_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg, 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), - (self.dag.input_map[self.creg[1]]._node_id, gate_node._node_id, self.creg[1]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.input_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] + self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.creg[1]]._node_id, self.creg[1]), ] ), ) @@ -3204,22 +3243,31 @@ def test_clbit_conditional(self): self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) self.assertEqual(gate_node.op.condition, (self.creg[0], 1)) + + gate_node_preds = list(self.dag.predecessors(gate_node)) + gate_node_in_edges = [ + (src._node_id, wire) + for (src, tgt, wire) in self.dag.edges(gate_node_preds) + if tgt == gate_node + ] + self.assertEqual( - sorted(self.dag._multi_graph.in_edges(gate_node._node_id)), + sorted(gate_node_in_edges), sorted( [ - (self.dag.input_map[self.qreg[0]]._node_id, gate_node._node_id, self.qreg[0]), - (self.dag.input_map[self.creg[0]]._node_id, gate_node._node_id, self.creg[0]), + (self.dag.input_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.input_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) + gate_node_out_edges = [(tgt._node_id, wire) for (_, tgt, wire) in self.dag.edges(gate_node)] self.assertEqual( - sorted(self.dag._multi_graph.out_edges(gate_node._node_id)), + sorted(gate_node_out_edges), sorted( [ - (gate_node._node_id, self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), - (gate_node._node_id, self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), + (self.dag.output_map[self.qreg[0]]._node_id, self.qreg[0]), + (self.dag.output_map[self.creg[0]]._node_id, self.creg[0]), ] ), ) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 12a932b83819..dd55750c55a0 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -145,7 +145,10 @@ def test_estimator_run_v1(self, backend, abelian_grouping): ): pm = generate_preset_pass_manager(optimization_level=0, backend=backend) psi1, psi2 = pm.run([psi1, psi2]) - estimator = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + estimator = BackendEstimatorV2(backend=backend, options=self._options) estimator.options.abelian_grouping = abelian_grouping # Specify the circuit and observable by indices. # calculate [ ] @@ -235,7 +238,10 @@ def test_estimator_with_pub_v1(self, backend, abelian_grouping): bind2 = BindingsArray.coerce({tuple(psi2.parameters): theta2}) pub2 = EstimatorPub(psi2, obs2, bind2) - estimator = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + estimator = BackendEstimatorV2(backend=backend, options=self._options) estimator.options.abelian_grouping = abelian_grouping result4 = estimator.run([pub1, pub2]).result() np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) @@ -264,7 +270,10 @@ def test_estimator_run_no_params_v1(self, backend, abelian_grouping): ): pm = generate_preset_pass_manager(optimization_level=0, backend=backend) circuit = pm.run(circuit) - est = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + est = BackendEstimatorV2(backend=backend, options=self._options) est.options.abelian_grouping = abelian_grouping observable = self.observable.apply_layout(circuit.layout) result = est.run([(circuit, observable)]).result() @@ -331,7 +340,13 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) def test_run_single_circuit_observable_v1(self, backend, abelian_grouping): """Test for single circuit and single observable case.""" - est = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex=r"The method PassManagerConfig\.from_backend will stop supporting inputs of " + "type `BackendV1`", + ): + # BackendEstimatorV2 wont allow BackendV1 + est = BackendEstimatorV2(backend=backend, options=self._options) est.options.abelian_grouping = abelian_grouping with self.assertWarnsRegex( DeprecationWarning, @@ -438,7 +453,11 @@ def test_run_1qubit_v1(self, backend, abelian_grouping): op = SparsePauliOp.from_list([("I", 1)]) op2 = SparsePauliOp.from_list([("Z", 1)]) - est = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -513,7 +532,10 @@ def test_run_2qubits_v1(self, backend, abelian_grouping): op2 = SparsePauliOp.from_list([("ZI", 1)]) op3 = SparsePauliOp.from_list([("IZ", 1)]) - est = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + est = BackendEstimatorV2(backend=backend, options=self._options) est.options.abelian_grouping = abelian_grouping op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -539,7 +561,47 @@ def test_run_2qubits_v1(self, backend, abelian_grouping): result = est.run([(qc2, op_6)]).result() np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) - @combine(backend=BACKENDS, abelian_grouping=[True, False]) + @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) + def test_run_errors_v1(self, backend, abelian_grouping): + """Test for errors. + To be removed once BackendV1 is removed.""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(2) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("II", 1)]) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + with self.assertRaises(ValueError): + est.run([(qc, op2)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op, [[1e4]])]).result() + with self.assertRaises(ValueError): + est.run([(qc2, op2, [[1, 2]])]).result() + with self.assertRaises(ValueError): + est.run([(qc, [op, op2], [[1]])]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=-1).result() + with self.assertRaises(ValueError): + est.run([(qc, 1j * op)], precision=0.1).result() + # precision == 0 + with self.assertRaises(ValueError): + est.run([(qc, op, None, 0)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=0).result() + # precision < 0 + with self.assertRaises(ValueError): + est.run([(qc, op, None, -1)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=-1).result() + with self.subTest("missing []"): + with self.assertRaisesRegex(ValueError, "An invalid Estimator pub-like was given"): + _ = est.run((qc, op)).result() + + @combine(backend=BACKENDS_V2, abelian_grouping=[True, False]) def test_run_errors(self, backend, abelian_grouping): """Test for errors""" qc = QuantumCircuit(1) @@ -624,7 +686,13 @@ def test_run_numpy_params_v1(self, backend, abelian_grouping): statevector_estimator = StatevectorEstimator(seed=123) target = statevector_estimator.run([(qc, op, params_list)]).result() - backend_estimator = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex=r"The method PassManagerConfig\.from_backend will stop supporting inputs of " + "type `BackendV1`", + ): + # BackendEstimatorV2 wont allow BackendV1 + backend_estimator = BackendEstimatorV2(backend=backend, options=self._options) backend_estimator.options.abelian_grouping = abelian_grouping with self.subTest("ndarrary"): @@ -662,7 +730,10 @@ def test_precision(self, backend, abelian_grouping): @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) def test_precision_v1(self, backend, abelian_grouping): """Test for precision""" - estimator = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + estimator = BackendEstimatorV2(backend=backend, options=self._options) estimator.options.abelian_grouping = abelian_grouping with self.assertWarnsRegex( DeprecationWarning, @@ -705,7 +776,10 @@ def test_diff_precision(self, backend, abelian_grouping): @combine(backend=BACKENDS_V1, abelian_grouping=[True, False]) def test_diff_precision_v1(self, backend, abelian_grouping): """Test for running different precisions at once""" - estimator = BackendEstimatorV2(backend=backend, options=self._options) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + estimator = BackendEstimatorV2(backend=backend, options=self._options) estimator.options.abelian_grouping = abelian_grouping with self.assertWarnsRegex( DeprecationWarning, @@ -792,7 +866,10 @@ def test_job_size_limit_backend_v1(self): op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) k = 5 param_list = self._rng.random(qc.num_parameters).tolist() - estimator = BackendEstimatorV2(backend=backend) + with self.assertWarns(DeprecationWarning): + # When BackendEstimatorV2 is called with a backend V1, it raises a + # DeprecationWarning from PassManagerConfig.from_backend + estimator = BackendEstimatorV2(backend=backend) with patch.object(backend, "run") as run_mock: estimator.run([(qc, op, param_list)] * k).result() self.assertEqual(run_mock.call_count, 10) diff --git a/test/python/providers/faulty_backends.py b/test/python/providers/faulty_backends.py index f57cf1b594c3..ec8ba8fed594 100644 --- a/test/python/providers/faulty_backends.py +++ b/test/python/providers/faulty_backends.py @@ -12,7 +12,7 @@ """Faulty fake backends for testing""" -from qiskit.providers.models import BackendProperties +from qiskit.providers.models.backendproperties import BackendProperties from qiskit.providers.fake_provider import Fake7QPulseV1 diff --git a/test/python/providers/test_backendstatus.py b/test/python/providers/test_backendstatus.py index 2cfa31791de3..c0cfb7f792a3 100644 --- a/test/python/providers/test_backendstatus.py +++ b/test/python/providers/test_backendstatus.py @@ -14,7 +14,7 @@ """ from qiskit.providers.fake_provider import Fake5QV1 -from qiskit.providers.models import BackendStatus +from qiskit.providers.models.backendstatus import BackendStatus from test import QiskitTestCase # pylint: disable=wrong-import-order diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 6d8716359bcc..3df3e7d5893f 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -37,8 +37,8 @@ ) from qiskit.providers.backend_compat import BackendV2Converter, convert_to_target from qiskit.providers.models.backendproperties import BackendProperties +from qiskit.providers.models.backendconfiguration import GateConfig from qiskit.providers.backend import BackendV2 -from qiskit.providers.models import GateConfig from qiskit.utils import optionals from qiskit.circuit.library import ( SXGate, diff --git a/test/python/qasm2/test_export.py b/test/python/qasm2/test_export.py index 0bba2f5c47e0..8de4bb8eb34f 100644 --- a/test/python/qasm2/test_export.py +++ b/test/python/qasm2/test_export.py @@ -387,14 +387,12 @@ def test_mcx_gate(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - pattern = r"""OPENQASM 2.0; + expected_qasm = """OPENQASM 2.0; include "qelib1.inc"; -gate mcx q0,q1,q2,q3 { h q3; p\(pi/8\) q0; p\(pi/8\) q1; p\(pi/8\) q2; p\(pi/8\) q3; cx q0,q1; p\(-pi/8\) q1; cx q0,q1; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; p\(pi/8\) q2; cx q1,q2; p\(-pi/8\) q2; cx q0,q2; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q1,q3; p\(pi/8\) q3; cx q2,q3; p\(-pi/8\) q3; cx q0,q3; h q3; } -gate (?Pmcx_[0-9]*) q0,q1,q2,q3 { mcx q0,q1,q2,q3; } -qreg q\[4\]; -(?P=mcx_id) q\[0\],q\[1\],q\[2\],q\[3\];""" - expected_qasm = re.compile(pattern, re.MULTILINE) - self.assertRegex(qasm2.dumps(qc), expected_qasm) +gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; } +qreg q[4]; +mcx q[0],q[1],q[2],q[3];""" + self.assertEqual(qasm2.dumps(qc), expected_qasm) def test_mcx_gate_variants(self): n = 5 @@ -406,13 +404,11 @@ def test_mcx_gate_variants(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - expected_qasm = f"""\ -OPENQASM 2.0; + expected_qasm = f"""OPENQASM 2.0; include "qelib1.inc"; gate mcu1(param0) q0,q1,q2,q3,q4,q5 {{ cu1(pi/16) q4,q5; cx q4,q3; cu1(-pi/16) q3,q5; cx q4,q3; cu1(pi/16) q3,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; }} gate mcx_gray q0,q1,q2,q3,q4,q5 {{ h q5; mcu1(pi) q0,q1,q2,q3,q4,q5; h q5; }} -gate mcx q0,q1,q2,q3 {{ h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }} -gate mcx_vchain q0,q1,q2,q3,q4 {{ mcx q0,q1,q2,q3; }} +gate mcx_vchain q0,q1,q2,q3,q4 {{ h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }} gate mcx_recursive q0,q1,q2,q3,q4,q5,q6 {{ mcx_vchain q0,q1,q2,q6,q3; mcx_vchain q3,q4,q6,q5,q0; mcx_vchain q0,q1,q2,q6,q3; mcx_vchain q3,q4,q6,q5,q0; }} gate mcx_vchain_{mcx_vchain_id} q0,q1,q2,q3,q4,q5,q6,q7,q8 {{ rccx q0,q1,q6; rccx q2,q6,q7; rccx q3,q7,q8; ccx q4,q8,q5; rccx q3,q7,q8; rccx q2,q6,q7; rccx q0,q1,q6; }} qreg q[9]; diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 36d716d2d85a..e78feaf3b78f 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -492,8 +492,8 @@ def test_from_circuit_with_all_types(self): # Additionally, make sure that it produces the correct clifford. expected_clifford_dict = { - "stabilizer": ["-IZX", "+ZYZ", "+ZII"], - "destabilizer": ["+ZIZ", "+ZXZ", "-XIX"], + "stabilizer": ["-IZX", "+XXZ", "-YYZ"], + "destabilizer": ["-YYI", "-XZI", "-ZXY"], } expected_clifford = Clifford.from_dict(expected_clifford_dict) self.assertEqual(combined_clifford, expected_clifford) @@ -598,6 +598,23 @@ def test_to_circuit(self, num_qubits): # Convert back to clifford and check it is the same self.assertEqual(Clifford(decomp), target) + def test_to_circuit_manual(self): + """Test a manual comparison to a known circuit. + + This also tests whether the resulting Clifford circuit has quantum registers, thereby + regression testing #13041. + """ + # this is set to a circuit that remains the same under Clifford reconstruction + circuit = QuantumCircuit(2) + circuit.z(0) + circuit.h(0) + circuit.cx(0, 1) + + cliff = Clifford(circuit) + reconstructed = cliff.to_circuit() + + self.assertEqual(circuit, reconstructed) + @combine(num_qubits=[1, 2, 3, 4, 5]) def test_to_instruction(self, num_qubits): """Test to_instruction method""" 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 4964b0c6a609..dedd84279a8d 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 @@ -1097,6 +1097,25 @@ def test_paulis_setter_rejects_bad_inputs(self): with self.assertRaisesRegex(ValueError, "incorrect number of operators"): op.paulis = PauliList([Pauli("XY"), Pauli("ZX"), Pauli("YZ")]) + def test_paulis_setter_absorbs_phase(self): + """Test that the setter for `paulis` absorbs `paulis.phase` to `self.coeffs`.""" + coeffs_init = np.array([1, 1j]) + op = SparsePauliOp(["XY", "ZX"], coeffs=coeffs_init) + paulis_new = PauliList(["-1jXY", "1jZX"]) + op.paulis = paulis_new + # Paulis attribute should have no phase: + self.assertEqual(op.paulis, PauliList(["XY", "ZX"])) + # Coeffs attribute should now include that phase: + self.assertTrue(np.allclose(op.coeffs, coeffs_init * np.array([-1j, 1j]))) + # The phase of the input array is now zero: + self.assertTrue(np.allclose(paulis_new.phase, np.array([0, 0]))) + + def test_paulis_setter_absorbs_phase_2(self): + """Test that `paulis` setter followed by `simplify()` handle phase OK.""" + spo = SparsePauliOp(["X", "X"]) + spo.paulis = ["X", "-X"] + self.assertEqual(spo.simplify(), SparsePauliOp(["I"], coeffs=[0.0 + 0.0j])) + def test_apply_layout_with_transpile(self): """Test the apply_layout method with a transpiler layout.""" psi = EfficientSU2(4, reps=4, entanglement="circular") diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index d653d6182017..725c46576a9d 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -31,6 +31,7 @@ from qiskit.transpiler.layout import Layout, TranspileLayout from qiskit.quantum_info.operators import Operator, ScalarOp from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.quantum_info.operators.operator_utils import _equal_with_ancillas from qiskit.compiler.transpiler import transpile from qiskit.circuit import Qubit from qiskit.circuit.library import Permutation, PermutationGate @@ -1255,6 +1256,29 @@ def test_apply_permutation_dimensions(self): op2 = op.apply_permutation([2, 0, 1], front=True) self.assertEqual(op2.input_dims(), (4, 2, 3)) + def test_equality_with_ancillas(self): + """Check correctness of the equal_with_ancillas method.""" + + # The two circuits below are equal provided that qubit 1 is initially |0>. + qc1 = QuantumCircuit(4) + qc1.x(0) + qc1.x(2) + qc1.cx(1, 0) + qc1.cx(1, 2) + qc1.cx(1, 3) + op1 = Operator(qc1) + + qc2 = QuantumCircuit(4) + qc2.x(0) + qc2.x(2) + op2 = Operator(qc2) + + self.assertNotEqual(op1, op2) + self.assertFalse(_equal_with_ancillas(op1, op2, [])) + self.assertTrue(_equal_with_ancillas(op1, op2, [1])) + self.assertFalse(_equal_with_ancillas(op1, op2, [2])) + self.assertTrue(_equal_with_ancillas(op1, op2, [2, 1])) + if __name__ == "__main__": unittest.main() diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index 05739db249b7..b08240197211 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -59,11 +59,11 @@ two_qubit_cnot_decompose, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, - Ud, decompose_two_qubit_product_gate, ) from qiskit._accelerate.two_qubit_decompose import two_qubit_decompose_up_to_diagonal from qiskit._accelerate.two_qubit_decompose import Specialization +from qiskit._accelerate.two_qubit_decompose import Ud from qiskit.synthesis.unitary import qsd from test import combine # pylint: disable=wrong-import-order from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -1270,6 +1270,21 @@ def test_use_dag(self, euler_bases, kak_gates, seed): requested_basis = set(oneq_gates + [kak_gate_name]) self.assertTrue(decomposition_basis.issubset(requested_basis)) + def test_non_std_gate(self): + """Test that the TwoQubitBasisDecomposer class can be correctly instantiated with a + non-standard KAK gate. + + Reproduce from: https://github.com/Qiskit/qiskit/issues/12998 + """ + # note that `CXGate(ctrl_state=0)` is not handled as a "standard" gate. + decomposer = TwoQubitBasisDecomposer(CXGate(ctrl_state=0)) + unitary = SwapGate().to_matrix() + decomposed_unitary = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(decomposed_unitary)) + self.assertNotIn("swap", decomposed_unitary.count_ops()) + self.assertNotIn("cx", decomposed_unitary.count_ops()) + self.assertEqual(3, decomposed_unitary.count_ops()["cx_o0"]) + @ddt class TestPulseOptimalDecompose(CheckDecompositions): diff --git a/test/python/transpiler/_dummy_passes.py b/test/python/transpiler/_dummy_passes.py index 64956e541024..eaddfe6d1ddd 100644 --- a/test/python/transpiler/_dummy_passes.py +++ b/test/python/transpiler/_dummy_passes.py @@ -122,10 +122,10 @@ class PassF_reduce_dag_property(DummyTP): def run(self, dag): super().run(dag) - if not hasattr(dag, "property"): - dag.property = 8 - dag.property = round(dag.property * 0.8) - logging.getLogger(logger).info("dag property = %i", dag.property) + if dag.duration is None: + dag.duration = 8 + dag.duration = round(dag.duration * 0.8) + logging.getLogger(logger).info("dag property = %i", dag.duration) return dag @@ -138,8 +138,8 @@ class PassG_calculates_dag_property(DummyAP): def run(self, dag): super().run(dag) - if hasattr(dag, "property"): - self.property_set["property"] = dag.property + if dag.duration is not None: + self.property_set["property"] = dag.duration else: self.property_set["property"] = 8 logging.getLogger(logger).info( diff --git a/test/python/transpiler/test_check_gate_direction.py b/test/python/transpiler/test_check_gate_direction.py index 0743f4c04cfc..efd38af16634 100644 --- a/test/python/transpiler/test_check_gate_direction.py +++ b/test/python/transpiler/test_check_gate_direction.py @@ -72,13 +72,13 @@ def test_true_direction(self): def test_true_direction_in_same_layer(self): """Two CXs distance_qubits 1 to each other, in the same layer - qr0:--(+)-- + qr0:---.-- | - qr1:---.--- + qr1:--(+)--- - qr2:--(+)-- + qr2:---.--- | - qr3:---.--- + qr3:--(+)-- CouplingMap map: [0]->[1]->[2]->[3] """ @@ -96,9 +96,9 @@ def test_true_direction_in_same_layer(self): def test_wrongly_mapped(self): """Needs [0]-[1] in a [0]--[2]--[1] - qr0:--(+)-- + qr0:---.--- | - qr1:---.--- + qr1:--(+)-- CouplingMap map: [0]->[2]->[1] """ @@ -115,11 +115,11 @@ def test_wrongly_mapped(self): def test_true_direction_undirected(self): """Mapped but with wrong direction - qr0:--(+)-[H]--.-- + qr0:---.--[H]-(+)- | | - qr1:---.-------|-- + qr1:--(+)------|-- | - qr2:----------(+)- + qr2:-----------.-- CouplingMap map: [1]<-[0]->[2] """ @@ -138,13 +138,13 @@ def test_true_direction_undirected(self): def test_false_direction_in_same_layer_undirected(self): """Two CXs in the same layer, but one is wrongly directed - qr0:--(+)-- + qr0:---.--- | - qr1:---.--- + qr1:--(+)-- - qr2:---.--- + qr2:--(+)-- | - qr3:--(+)-- + qr3:---.--- CouplingMap map: [0]->[1]->[2]->[3] """ diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index 58589d838e33..e2446d03da2a 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -93,6 +93,7 @@ def test_block_interrupted_by_gate(self): # but equivalent between python 3.5 and 3.7 # there is no implied topology in a block, so this isn't an issue dag_nodes = [set(dag_nodes[:4]), set(dag_nodes[4:])] + pass_nodes = [set(bl) for bl in pass_.property_set["block_list"]] self.assertEqual(dag_nodes, pass_nodes) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 71bab61708cd..88f1d99bef31 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -13,6 +13,7 @@ """Gate cancellation pass testing""" import unittest + import numpy as np from qiskit import QuantumRegister, QuantumCircuit @@ -76,7 +77,7 @@ def test_all_gates(self): expected = QuantumCircuit(qr) expected.append(RZGate(2.0), [qr[0]]) expected.rx(1.0, qr[0]) - + expected.global_phase = 0.5 self.assertEqual(expected, new_circuit) def test_commutative_circuit1(self): @@ -433,7 +434,7 @@ def test_commutative_circuit3(self): expected.append(RZGate(np.pi * 17 / 12), [qr[2]]) expected.append(RZGate(np.pi * 2 / 3), [qr[3]]) expected.cx(qr[2], qr[1]) - + expected.global_phase = 3 * np.pi / 8 self.assertEqual( expected, new_circuit, msg=f"expected:\n{expected}\nnew_circuit:\n{new_circuit}" ) diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 7b364f3ac10f..1223b37ca3ff 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -145,7 +145,7 @@ def test_decompose_oversized_instruction(self): self.assertEqual(qc1, output) def test_decomposition_preserves_qregs_order(self): - """Test decomposing a gate preserves it's definition registers order""" + """Test decomposing a gate preserves the order of registers in its definition""" qr = QuantumRegister(2, "qr1") qc1 = QuantumCircuit(qr) qc1.cx(1, 0) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 97c0b58bb377..f3b11c98bfc7 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -44,9 +44,10 @@ CU1Gate, QFTGate, IGate, + MCXGate, ) from qiskit.circuit.library.generalized_gates import LinearFunction -from qiskit.quantum_info import Clifford +from qiskit.quantum_info import Clifford, Operator, Statevector from qiskit.synthesis.linear import random_invertible_binary_matrix from qiskit.compiler import transpile from qiskit.exceptions import QiskitError @@ -59,13 +60,20 @@ high_level_synthesis_plugin_names, ) from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig +from qiskit.transpiler.passes.synthesis.hls_plugins import ( + MCXSynthesis1CleanB95, + MCXSynthesisNCleanM15, + MCXSynthesisNDirtyI15, + MCXSynthesisGrayCode, + MCXSynthesisDefault, + MCXSynthesisNoAuxV24, +) from qiskit.circuit.annotated_operation import ( AnnotatedOperation, ControlModifier, InverseModifier, PowerModifier, ) -from qiskit.quantum_info import Operator from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, @@ -1364,8 +1372,23 @@ def test_definition_with_annotations(self): lazy_gate3 = AnnotatedOperation(custom_gate, ControlModifier(2)) circuit = QuantumCircuit(6) circuit.append(lazy_gate3, [0, 1, 2, 3, 4, 5]) - transpiled_circuit = HighLevelSynthesis(basis_gates=["cx", "u"])(circuit) - self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) + + with self.subTest(qubits_initially_zero=False): + # When transpiling without assuming that qubits are initially zero, + # we should have that the Operators before and after are equal. + transpiled_circuit = HighLevelSynthesis( + basis_gates=["cx", "u"], qubits_initially_zero=False + )(circuit) + self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) + + with self.subTest(qubits_initially_zero=True): + # When transpiling assuming that qubits are initially zero, + # we should have that the Statevectors before and after + # are equal (but not the full Operators). + transpiled_circuit = HighLevelSynthesis( + basis_gates=["cx", "u"], qubits_initially_zero=True + )(circuit) + self.assertEqual(Statevector(circuit), Statevector(transpiled_circuit)) def test_definition_with_high_level_objects(self): """Test annotated gates with definitions involving annotations and @@ -2287,5 +2310,148 @@ def test_qft_line_plugin_annotated_qft(self, qft_plugin_name): self.assertEqual(Operator(qc), Operator(qct)) +@ddt +class TestMCXSynthesisPlugins(QiskitTestCase): + """Tests related to plugins for MCXGate.""" + + def test_supported_names(self): + """Test that there is a default synthesis plugin for MCXGate.""" + supported_plugin_names = high_level_synthesis_plugin_names("mcx") + self.assertIn("default", supported_plugin_names) + + def test_mcx_plugins_applicability(self): + """Test applicability of MCX synthesis plugins for MCX gates.""" + gate = MCXGate(5) + + with self.subTest(method="n_clean_m15", num_clean_ancillas=4, num_dirty_ancillas=4): + # should have a decomposition + decomposition = MCXSynthesisNCleanM15().run( + gate, num_clean_ancillas=4, num_dirty_ancillas=4 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="n_clean_m15", num_clean_ancillas=2, num_dirty_ancillas=4): + # should not have a decomposition + decomposition = MCXSynthesisNCleanM15().run( + gate, num_clean_ancillas=2, num_dirty_ancillas=4 + ) + self.assertIsNone(decomposition) + + with self.subTest(method="n_dirty_i15", num_clean_ancillas=4, num_dirty_ancillas=4): + # should have a decomposition + decomposition = MCXSynthesisNDirtyI15().run( + gate, num_clean_ancillas=4, num_dirty_ancillas=4 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="n_dirty_i15", num_clean_ancillas=2, num_dirty_ancillas=2): + # should have a decomposition + decomposition = MCXSynthesisNDirtyI15().run( + gate, num_clean_ancillas=2, num_dirty_ancillas=2 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="n_dirty_i15", num_clean_ancillas=1, num_dirty_ancillas=1): + # should not have a decomposition + decomposition = MCXSynthesisNDirtyI15().run( + gate, num_clean_ancillas=1, num_dirty_ancillas=1 + ) + self.assertIsNone(decomposition) + + with self.subTest(method="1_clean_b95", num_clean_ancillas=1, num_dirty_ancillas=0): + # should have a decomposition + decomposition = MCXSynthesis1CleanB95().run( + gate, num_clean_ancillas=1, num_dirty_ancillas=0 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="1_clean_b95", num_clean_ancillas=0, num_dirty_ancillas=1): + # should not have a decomposition + decomposition = MCXSynthesis1CleanB95().run( + gate, num_clean_ancillas=0, num_dirty_ancillas=1 + ) + self.assertIsNone(decomposition) + + with self.subTest(method="noaux_v24", num_clean_ancillas=1, num_dirty_ancillas=1): + # should have a decomposition + decomposition = MCXSynthesisNoAuxV24().run( + gate, num_clean_ancillas=1, num_dirty_ancillas=1 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="noaux_v24", num_clean_ancillas=0, num_dirty_ancillas=0): + # should have a decomposition + decomposition = MCXSynthesisNoAuxV24().run( + gate, num_clean_ancillas=0, num_dirty_ancillas=0 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="gray_code", num_clean_ancillas=1, num_dirty_ancillas=1): + # should have a decomposition + decomposition = MCXSynthesisGrayCode().run( + gate, num_clean_ancillas=1, num_dirty_ancillas=1 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="gray_code", num_clean_ancillas=0, num_dirty_ancillas=0): + # should have a decomposition + decomposition = MCXSynthesisGrayCode().run( + gate, num_clean_ancillas=0, num_dirty_ancillas=0 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="default", num_clean_ancillas=1, num_dirty_ancillas=1): + # should have a decomposition + decomposition = MCXSynthesisDefault().run( + gate, num_clean_ancillas=1, num_dirty_ancillas=1 + ) + self.assertIsNotNone(decomposition) + + with self.subTest(method="default", num_clean_ancillas=0, num_dirty_ancillas=0): + # should have a decomposition + decomposition = MCXSynthesisDefault().run( + gate, num_clean_ancillas=0, num_dirty_ancillas=0 + ) + self.assertIsNotNone(decomposition) + + @data("n_clean_m15", "n_dirty_i15", "1_clean_b95", "noaux_v24", "gray_code", "default") + def test_mcx_plugins_correctness_from_arbitrary(self, mcx_plugin_name): + """Test that all plugins return a correct Operator when qubits are not + initially zero.""" + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.mcx(control_qubits=[0, 1, 2], target_qubit=[3]) + qc.mcx(control_qubits=[2, 3, 4, 5], target_qubit=[1]) + qc.mcx(control_qubits=[5, 4, 3, 2, 1], target_qubit=[0]) + hls_config = HLSConfig(mcx=[mcx_plugin_name]) + hls_pass = HighLevelSynthesis(hls_config=hls_config, qubits_initially_zero=False) + qct = hls_pass(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + @data("n_clean_m15", "n_dirty_i15", "1_clean_b95", "noaux_v24", "gray_code", "default") + def test_mcx_plugins_correctness_from_zero(self, mcx_plugin_name): + """Test that all plugins return a correct Statevector when qubits are + initially zero.""" + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.mcx(control_qubits=[0, 1, 2], target_qubit=[3]) + qc.mcx(control_qubits=[2, 3, 4, 5], target_qubit=[1]) + qc.mcx(control_qubits=[5, 4, 3, 2, 1], target_qubit=[0]) + hls_config = HLSConfig(mcx=[mcx_plugin_name]) + hls_pass = HighLevelSynthesis(hls_config=hls_config, qubits_initially_zero=True) + qct = hls_pass(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_annotated_mcx(self): + """Test synthesis of annotated MCX gates.""" + qc = QuantumCircuit(6) + qc.h(0) + qc.append(MCXGate(3).inverse(annotated=True).control(2, annotated=True), [0, 1, 2, 3, 4, 5]) + qct = transpile(qc, qubits_initially_zero=False) + self.assertEqual(Operator(qc), Operator(qct)) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_hoare_opt.py b/test/python/transpiler/test_hoare_opt.py index b3ae3df97678..ae2f9d9ae1e0 100644 --- a/test/python/transpiler/test_hoare_opt.py +++ b/test/python/transpiler/test_hoare_opt.py @@ -661,6 +661,41 @@ def test_multiple_pass(self): self.assertEqual(result2, circuit_to_dag(expected)) + def test_remove_control_then_identity(self): + """This first simplifies a gate by removing its control, then removes the + simplified gate by canceling it with another gate. + See: https://github.com/Qiskit/qiskit-terra/issues/13079 + """ + # ┌───┐┌───┐┌───┐ + # q_0: ┤ X ├┤ X ├┤ X ├ + # └─┬─┘└───┘└─┬─┘ + # q_1: ──■─────────┼── + # ┌───┐ │ + # q_2: ┤ X ├───────■── + # └───┘ + circuit = QuantumCircuit(3) + circuit.cx(1, 0) + circuit.x(2) + circuit.x(0) + circuit.cx(2, 0) + + simplified = HoareOptimizer()(circuit) + + # The CX(1, 0) gate is removed as the control qubit q_1 is initially 0. + # The CX(2, 0) gate is first replaced by X(0) gate as the control qubit q_2 is at 1, + # then the two X(0) gates are removed. + # + # q_0: ───── + # + # q_1: ───── + # ┌───┐ + # q_2: ┤ X ├ + # └───┘ + expected = QuantumCircuit(3) + expected.x(2) + + self.assertEqual(simplified, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_instruction_durations.py b/test/python/transpiler/test_instruction_durations.py index de68fbadf86a..c59d0ff2a6cc 100644 --- a/test/python/transpiler/test_instruction_durations.py +++ b/test/python/transpiler/test_instruction_durations.py @@ -16,6 +16,7 @@ from qiskit.circuit import Delay, Parameter from qiskit.providers.fake_provider import Fake27QPulseV1 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -92,3 +93,10 @@ def test_fail_if_get_unbounded_duration_with_unit_conversion_when_dt_is_not_prov parameterized_delay = Delay(param, "s") with self.assertRaises(TranspilerError): InstructionDurations().get(parameterized_delay, 0) + + def test_from_backend_with_backendv2(self): + """Test if `from_backend()` method allows using BackendV2""" + backend = GenericBackendV2(num_qubits=4, calibrate_instructions=True, seed=42) + inst_durations = InstructionDurations.from_backend(backend) + self.assertEqual(inst_durations, backend.target.durations()) + self.assertIsInstance(inst_durations, InstructionDurations) diff --git a/test/python/transpiler/test_mappers.py b/test/python/transpiler/test_mappers.py index dc4767b19517..c3b0644c2473 100644 --- a/test/python/transpiler/test_mappers.py +++ b/test/python/transpiler/test_mappers.py @@ -71,12 +71,13 @@ def test_a_common_test(self): import unittest import os import sys +import warnings from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit, transpile from qiskit.providers.basic_provider import BasicSimulator from qiskit.qasm2 import dump from qiskit.transpiler import PassManager -from qiskit.transpiler.passes import BasicSwap, LookaheadSwap, StochasticSwap, SabreSwap +from qiskit.transpiler.passes import BasicSwap, LookaheadSwap, SabreSwap, StochasticSwap from qiskit.transpiler.passes import SetLayout from qiskit.transpiler import CouplingMap, Layout from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -104,8 +105,15 @@ def create_passmanager(self, coupling_map, initial_layout=None): if initial_layout: passmanager.append(SetLayout(Layout(initial_layout))) - # pylint: disable=not-callable - passmanager.append(self.pass_class(CouplingMap(coupling_map), **self.additional_args)) + with warnings.catch_warnings(): + # TODO: remove this filter when StochasticSwap is removed + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*StochasticSwap.*", + ) + # pylint: disable=not-callable + passmanager.append(self.pass_class(CouplingMap(coupling_map), **self.additional_args)) return passmanager def create_backend(self): diff --git a/test/python/transpiler/test_passmanager.py b/test/python/transpiler/test_passmanager.py index ce4d28bf314e..60375a820655 100644 --- a/test/python/transpiler/test_passmanager.py +++ b/test/python/transpiler/test_passmanager.py @@ -27,7 +27,7 @@ DoWhileController, ) from qiskit.transpiler import PassManager, PropertySet, TransformationPass -from qiskit.transpiler.passes import CommutativeCancellation +from qiskit.transpiler.passes import RXCalibrationBuilder from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, @@ -97,7 +97,6 @@ def test_callback_with_pass_requires(self): expected_end = QuantumCircuit(qr) expected_end.cx(qr[0], qr[2]) - expected_end_dag = circuit_to_dag(expected_end) calls = [] @@ -107,20 +106,19 @@ def callback(**kwargs): calls.append(out_dict) passmanager = PassManager() - passmanager.append(CommutativeCancellation(basis_gates=["u1", "u2", "u3", "cx"])) + passmanager.append(RXCalibrationBuilder()) passmanager.run(circuit, callback=callback) self.assertEqual(len(calls), 2) self.assertEqual(len(calls[0]), 5) self.assertEqual(calls[0]["count"], 0) - self.assertEqual(calls[0]["pass_"].name(), "CommutationAnalysis") + self.assertEqual(calls[0]["pass_"].name(), "NormalizeRXAngle") self.assertEqual(expected_start_dag, calls[0]["dag"]) self.assertIsInstance(calls[0]["time"], float) self.assertIsInstance(calls[0]["property_set"], PropertySet) self.assertEqual("MyCircuit", calls[0]["dag"].name) self.assertEqual(len(calls[1]), 5) self.assertEqual(calls[1]["count"], 1) - self.assertEqual(calls[1]["pass_"].name(), "CommutativeCancellation") - self.assertEqual(expected_end_dag, calls[1]["dag"]) + self.assertEqual(calls[1]["pass_"].name(), "RXCalibrationBuilder") self.assertIsInstance(calls[0]["time"], float) self.assertIsInstance(calls[0]["property_set"], PropertySet) self.assertEqual("MyCircuit", calls[1]["dag"].name) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index 341da357d37a..569f67738a16 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -33,7 +33,7 @@ def test_config_from_backend(self): """ with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - config = PassManagerConfig.from_backend(backend) + config = PassManagerConfig.from_backend(backend) self.assertEqual(config.basis_gates, backend.configuration().basis_gates) self.assertEqual(config.inst_map, backend.defaults().instruction_schedule_map) self.assertEqual( @@ -66,9 +66,9 @@ def test_from_backend_and_user_v1(self): with self.assertWarns(DeprecationWarning): backend = Fake20QV1() - config = PassManagerConfig.from_backend( - backend, basis_gates=["user_gate"], initial_layout=initial_layout - ) + config = PassManagerConfig.from_backend( + backend, basis_gates=["user_gate"], initial_layout=initial_layout + ) self.assertEqual(config.basis_gates, ["user_gate"]) self.assertNotEqual(config.basis_gates, backend.configuration().basis_gates) self.assertIsNone(config.inst_map) @@ -107,7 +107,8 @@ def test_from_backendv1_inst_map_is_none(self): with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() backend.defaults = lambda: None - config = PassManagerConfig.from_backend(backend) + with self.assertWarns(DeprecationWarning): + config = PassManagerConfig.from_backend(backend) self.assertIsInstance(config, PassManagerConfig) self.assertIsNone(config.inst_map) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index aa689b4c4fee..b762d84032bb 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -497,7 +497,7 @@ def test_level1_runs_vf2post_layout_when_routing_required(self): self.assertNotIn("SabreSwap", self.passes) def test_level1_runs_vf2post_layout_when_routing_method_set_and_required(self): - """Test that if we run routing as part of sabre layout VF2PostLayout runs.""" + """Test that if we run routing as part of sabre layout then VF2PostLayout runs.""" target = GenericBackendV2(num_qubits=7, coupling_map=LAGOS_CMAP, seed=42) qc = QuantumCircuit(5) qc.h(0) @@ -507,7 +507,7 @@ def test_level1_runs_vf2post_layout_when_routing_method_set_and_required(self): qc.cy(0, 4) qc.measure_all() _ = transpile( - qc, target, optimization_level=1, routing_method="stochastic", callback=self.callback + qc, target, optimization_level=1, routing_method="sabre", callback=self.callback ) # Expected call path for layout and routing is: # 1. TrivialLayout (no perfect match) @@ -518,7 +518,6 @@ def test_level1_runs_vf2post_layout_when_routing_method_set_and_required(self): self.assertIn("VF2Layout", self.passes) self.assertIn("SabreLayout", self.passes) self.assertIn("VF2PostLayout", self.passes) - self.assertIn("StochasticSwap", self.passes) def test_level1_not_runs_vf2post_layout_when_layout_method_set(self): """Test that if we don't run VF2PostLayout with custom layout_method.""" @@ -1529,6 +1528,18 @@ def test_generate_preset_pass_manager_with_list_initial_layout(self, optimizatio self.assertIsInstance(pm_object, PassManager) self.assertEqual(tqc_list, tqc_obj) + def test_parse_seed_transpiler_raises_value_error(self): + """Test that seed for transpiler is non-negative integer.""" + with self.assertRaisesRegex( + ValueError, "Expected non-negative integer as seed for transpiler." + ): + generate_preset_pass_manager(optimization_level=1, seed_transpiler=-1) + + with self.assertRaisesRegex( + ValueError, "Expected non-negative integer as seed for transpiler." + ): + generate_preset_pass_manager(seed_transpiler=0.1) + @ddt class TestIntegrationControlFlow(QiskitTestCase): diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 07d6172264d4..1dd42d662860 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -16,7 +16,7 @@ from qiskit import pulse, circuit, transpile from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 -from qiskit.providers.models import GateConfig +from qiskit.providers.models.backendconfiguration import GateConfig from qiskit.quantum_info.random import random_unitary from test import QiskitTestCase # pylint: disable=wrong-import-order diff --git a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py index 499bc86d9cf2..474a586448b8 100644 --- a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py +++ b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py @@ -50,6 +50,29 @@ def test_optimize_1rz_1measure(self): self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1phase_1measure(self): + """Remove a single PhaseGate + qr0:--P--m-- qr0:--m- + | | + qr1:-----|-- ==> qr1:--|- + | | + cr0:-----.-- cr0:--.- + """ + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.p(0.1, qr[0]) + circuit.measure(qr[0], cr[0]) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr[0], cr[0]) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1z_1measure(self): """Remove a single ZGate qr0:--Z--m-- qr0:--m- @@ -74,7 +97,7 @@ def test_optimize_1z_1measure(self): self.assertEqual(circuit_to_dag(expected), after) def test_optimize_1t_1measure(self): - """Remove a single TGate, SGate, TdgGate, SdgGate, U1Gate + """Remove a single TGate qr0:--T--m-- qr0:--m- | | qr1:-----|-- ==> qr1:--|- @@ -298,6 +321,56 @@ def test_optimize_1cz_2measure(self): self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1cs_2measure(self): + """Remove a single CSGate + qr0:-CS--m--- qr0:--m--- + | | | + qr1:--.--|-m- ==> qr1:--|-m- + | | | | + cr0:-----.-.- cr0:--.-.- + """ + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.cs(qr[0], qr[1]) + circuit.measure(qr[0], cr[0]) + circuit.measure(qr[1], cr[0]) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr[0], cr[0]) + expected.measure(qr[1], cr[0]) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + + def test_optimize_1csdg_2measure(self): + """Remove a single CSdgGate + qr0:-CSdg--m--- qr0:--m--- + | | | + qr1:----.--|-m- ==> qr1:--|-m- + | | | | + cr0:-------.-.- cr0:--.-.- + """ + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.csdg(qr[0], qr[1]) + circuit.measure(qr[0], cr[0]) + circuit.measure(qr[1], cr[0]) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr[0], cr[0]) + expected.measure(qr[1], cr[0]) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1crz_2measure(self): """Remove a single CRZGate qr0:-RZ--m--- qr0:--m--- @@ -323,6 +396,31 @@ def test_optimize_1crz_2measure(self): self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1cp_2measure(self): + """Remove a single CPhaseGate + qr0:-CP--m--- qr0:--m--- + | | | + qr1:--.--|-m- ==> qr1:--|-m- + | | | | + cr0:-----.-.- cr0:--.-.- + """ + qr = QuantumRegister(2, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.cp(0.1, qr[0], qr[1]) + circuit.measure(qr[0], cr[0]) + circuit.measure(qr[1], cr[0]) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr[0], cr[0]) + expected.measure(qr[1], cr[0]) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1cu1_2measure(self): """Remove a single CU1Gate qr0:-CU1-m--- qr0:--m--- @@ -373,6 +471,27 @@ def test_optimize_1rzz_2measure(self): self.assertEqual(circuit_to_dag(expected), after) + def test_optimize_1ccz_3measure(self): + """Remove a single CCZGate""" + qr = QuantumRegister(3, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.ccz(qr[0], qr[1], qr[2]) + circuit.measure(qr[0], cr[0]) + circuit.measure(qr[1], cr[0]) + circuit.measure(qr[2], cr[0]) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr[0], cr[0]) + expected.measure(qr[1], cr[0]) + expected.measure(qr[2], cr[0]) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + class TestRemoveDiagonalGatesBeforeMeasureOveroptimizations(QiskitTestCase): """Test situations where remove_diagonal_gates_before_measure should not optimize""" @@ -401,6 +520,22 @@ def test_optimize_1cz_1measure(self): self.assertEqual(expected, after) + def test_optimize_1ccz_1measure(self): + """Do not remove a CCZGate because measure happens on only one of the wires""" + qr = QuantumRegister(3, "qr") + cr = ClassicalRegister(1, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.ccz(qr[0], qr[1], qr[2]) + circuit.measure(qr[1], cr[0]) + dag = circuit_to_dag(circuit) + + expected = deepcopy(dag) + + pass_ = RemoveDiagonalGatesBeforeMeasure() + after = pass_.run(dag) + + self.assertEqual(expected, after) + def test_do_not_optimize_with_conditional(self): """Diagonal gates with conditionals on a measurement target. See https://github.com/Qiskit/qiskit-terra/pull/2208#issuecomment-487238819 diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 5ab8fe5c10b2..7565ee17655f 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -18,16 +18,16 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit.classical import expr, types -from qiskit.circuit.library import EfficientSU2 +from qiskit.circuit.library import EfficientSU2, QuantumVolume from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager -from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap +from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap, Unroll3qOrMore from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.compiler.transpiler import transpile from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler.passes.layout.sabre_pre_layout import SabrePreLayout from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import QiskitTestCase, slow_test # pylint: disable=wrong-import-order from ..legacy_cmaps import ALMADEN_CMAP, MUMBAI_CMAP @@ -259,14 +259,15 @@ def test_layout_many_search_trials(self): coupling_map=MUMBAI_CMAP, seed=42, ) - res = transpile( - qc, - backend, - layout_method="sabre", - routing_method="stochastic", - seed_transpiler=12345, - optimization_level=1, - ) + with self.assertWarns(DeprecationWarning): + res = transpile( + qc, + backend, + layout_method="sabre", + routing_method="stochastic", + seed_transpiler=12345, + optimization_level=1, + ) self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( @@ -306,13 +307,33 @@ def test_support_var_with_explicit_routing_pass(self): qc.cx(4, 0) cm = CouplingMap.from_line(8) - pass_ = SabreLayout( - cm, seed=0, routing_pass=StochasticSwap(cm, trials=1, seed=0, fake_run=True) - ) - _ = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = SabreLayout( + cm, seed=0, routing_pass=StochasticSwap(cm, trials=1, seed=0, fake_run=True) + ) + _ = pass_(qc) layout = pass_.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5]) + @slow_test + def test_release_valve_routes_multiple(self): + """Test Sabre works if the release valve routes more than 1 operation. + + Regression test of #13081. + """ + qv = QuantumVolume(500, seed=42) + qv.measure_all() + qc = Unroll3qOrMore()(qv) + + cmap = CouplingMap.from_heavy_hex(21) + pm = PassManager( + [ + SabreLayout(cmap, swap_trials=20, layout_trials=20, max_iterations=4, seed=100), + ] + ) + _ = pm.run(qc) + self.assertIsNotNone(pm.property_set.get("layout")) + class DensePartialSabreTrial(AnalysisPass): """Pass to run dense layout as a sabre trial.""" diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index 616d93e5b3f8..f5727bf5313a 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -18,8 +18,8 @@ import numpy as np from qiskit import QuantumCircuit, QuantumRegister, transpile -from qiskit.circuit.library import UnitaryGate, XGate, ZGate, HGate -from qiskit.circuit import Parameter, CircuitInstruction +from qiskit.circuit.library import UnitaryGate, XGate, ZGate, HGate, CPhaseGate +from qiskit.circuit import Parameter, CircuitInstruction, Gate from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.quantum_info import Operator from qiskit.transpiler import PassManager @@ -160,15 +160,11 @@ def test_no_split(self): def test_almost_identity(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(2) - qc.cp(pi * 2 ** -(26), 0, 1) + qc.unitary(CPhaseGate(pi * 2 ** -(26)).to_matrix(), [0, 1]) pm = PassManager() - pm.append(Collect2qBlocks()) - pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries(fidelity=1.0 - 1e-9)) qc_split = pm.run(qc) pm = PassManager() - pm.append(Collect2qBlocks()) - pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries()) qc_split2 = pm.run(qc) self.assertEqual(qc_split.num_nonlocal_gates(), 0) @@ -211,15 +207,62 @@ def test_single_q_gates(self): self.assertTrue(CircuitInstruction(ZGate(), qubits=[qr[1]], clbits=[]) in qc.data) self.assertTrue(CircuitInstruction(HGate(), qubits=[qr[2]], clbits=[]) in qc.data) - def test_split_qft(self): - """Test that the pass handles QFT correctly.""" - qc = QuantumCircuit(100) - qc.h(0) - for i in range(qc.num_qubits - 2, 0, -1): - qc.cp(pi * 2 ** -(qc.num_qubits - 1 - i), qc.num_qubits - 1, i) + def test_gate_no_array(self): + """ + Test that the pass doesn't fail when the circuit contains a custom gate + with no ``__array__`` implementation. + + Reproduce from: https://github.com/Qiskit/qiskit/issues/12970 + """ + + class MyGate(Gate): + """Custom gate""" + + def __init__(self): + super().__init__("mygate", 2, []) + + def to_matrix(self): + return np.eye(4, dtype=complex) + # return np.eye(4, dtype=float) + + def mygate(self, qubit1, qubit2): + return self.append(MyGate(), [qubit1, qubit2], []) + + QuantumCircuit.mygate = mygate + + qc = QuantumCircuit(2) + qc.mygate(0, 1) + pm = PassManager() pm.append(Collect2qBlocks()) pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries()) qc_split = pm.run(qc) - self.assertEqual(26, qc_split.num_nonlocal_gates()) + + self.assertTrue(Operator(qc).equiv(qc_split)) + self.assertTrue( + matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) + ) + + def test_nosplit_custom(self): + """Test a single custom gate is not split, even if it is a product. + + That is because we cannot guarantee the split gate is valid in a given basis. + Regression test for #12970. + """ + + class MyGate(Gate): + """A custom gate that could be split.""" + + def __init__(self): + super().__init__("mygate", 2, []) + + def to_matrix(self): + return np.eye(4, dtype=complex) + + qc = QuantumCircuit(2) + qc.append(MyGate(), [0, 1]) + + no_split = Split2QUnitaries()(qc) + + self.assertDictEqual({"mygate": 1}, no_split.count_ops()) diff --git a/test/python/transpiler/test_stage_plugin.py b/test/python/transpiler/test_stage_plugin.py index 1c0adc9776a0..f278096b8419 100644 --- a/test/python/transpiler/test_stage_plugin.py +++ b/test/python/transpiler/test_stage_plugin.py @@ -102,7 +102,7 @@ class TestBuiltinPlugins(QiskitTestCase): @combine( optimization_level=list(range(4)), - routing_method=["basic", "lookahead", "sabre", "stochastic"], + routing_method=["basic", "lookahead", "sabre"], ) def test_routing_plugins(self, optimization_level, routing_method): """Test all routing plugins (excluding error).""" @@ -123,6 +123,31 @@ def test_routing_plugins(self, optimization_level, routing_method): counts = backend.run(tqc, shots=1000).result().get_counts() self.assertDictAlmostEqual(counts, {"0000": 500, "1111": 500}, delta=100) + @combine( + optimization_level=list(range(4)), + routing_method=["stochastic"], + ) + def test_routing_plugin_stochastic(self, optimization_level, routing_method): + """Test stoc routing plugins (excluding error).""" + # Note remove once StochasticSwap gets removed + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.measure_all() + with self.assertWarns(DeprecationWarning): + tqc = transpile( + qc, + basis_gates=["cx", "sx", "x", "rz"], + coupling_map=CouplingMap.from_line(4), + optimization_level=optimization_level, + routing_method=routing_method, + ) + backend = BasicSimulator() + counts = backend.run(tqc, shots=1000).result().get_counts() + self.assertDictAlmostEqual(counts, {"0000": 500, "1111": 500}, delta=100) + @combine( optimization_level=list(range(4)), ) diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 4843bac8baf5..ee5d8a1dad31 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -60,8 +60,9 @@ def test_trivial_case(self): circuit.cx(qr[0], qr[2]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) self.assertEqual(dag, after) @@ -83,8 +84,9 @@ def test_trivial_in_same_layer(self): circuit.cx(qr[0], qr[1]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) self.assertEqual(dag, after) @@ -109,8 +111,9 @@ def test_permute_wires_1(self): circuit.cx(qr[1], qr[2]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 11) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 11) + after = pass_.run(dag) expected = QuantumCircuit(qr) expected.swap(qr[0], qr[2]) @@ -140,8 +143,9 @@ def test_permute_wires_2(self): circuit.h(qr[0]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 11) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 11) + after = pass_.run(dag) expected = QuantumCircuit(qr) expected.swap(qr[1], qr[2]) @@ -176,8 +180,9 @@ def test_permute_wires_3(self): circuit.cx(qr[3], qr[0]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) expected = QuantumCircuit(qr) expected.swap(qr[0], qr[1]) @@ -215,8 +220,9 @@ def test_permute_wires_4(self): circuit.cx(qr[3], qr[0]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) expected = QuantumCircuit(qr) expected.h(qr[3]) @@ -254,8 +260,9 @@ def test_permute_wires_5(self): circuit.h(qr[3]) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) expected = QuantumCircuit(qr) expected.swap(qr[0], qr[1]) @@ -283,8 +290,9 @@ def test_all_single_qubit(self): circ.measure(qr[3], cr[3]) dag = circuit_to_dag(circ) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) self.assertEqual(dag, after) def test_overoptimization_case(self): @@ -363,8 +371,9 @@ def test_overoptimization_case(self): # qr[1]: 1, # qr[2]: 2, # qr[3]: 3} - pass_ = StochasticSwap(coupling, 20, 19) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 19) + after = pass_.run(dag) self.assertEqual(expected_dag, after) @@ -389,8 +398,9 @@ def test_already_mapped(self): dag = circuit_to_dag(circ) - pass_ = StochasticSwap(coupling, 20, 13) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 13) + after = pass_.run(dag) self.assertEqual(circuit_to_dag(circ), after) def test_congestion(self): @@ -465,8 +475,9 @@ def test_congestion(self): expected.measure(qr[1], cr[1]) expected_dag = circuit_to_dag(expected) - pass_ = StochasticSwap(coupling, 20, 999) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 999) + after = pass_.run(dag) self.assertEqual(expected_dag, after) def test_only_output_cx_and_swaps_in_coupling_map(self): @@ -483,8 +494,9 @@ def test_only_output_cx_and_swaps_in_coupling_map(self): circuit.measure(qr, cr) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling, 20, 5) - after = pass_.run(dag) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, 20, 5) + after = pass_.run(dag) valid_couplings = [{qr[a], qr[b]} for (a, b) in coupling.get_edges()] @@ -505,7 +517,8 @@ def test_len_cm_vs_dag(self): circuit.measure(qr, cr) dag = circuit_to_dag(circuit) - pass_ = StochasticSwap(coupling) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling) with self.assertRaises(TranspilerError): _ = pass_.run(dag) @@ -549,8 +562,9 @@ def test_single_gates_omitted(self): expected_dag = circuit_to_dag(expected) - stochastic = StochasticSwap(CouplingMap(coupling_map), seed=0) - after = PassManager(stochastic).run(circuit) + with self.assertWarns(DeprecationWarning): + stochastic = StochasticSwap(CouplingMap(coupling_map), seed=0) + after = PassManager(stochastic).run(circuit) after = circuit_to_dag(after) self.assertEqual(expected_dag, after) @@ -578,9 +592,10 @@ def test_pre_if_else_route(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=82).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=82).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -618,9 +633,10 @@ def test_pre_if_else_route_post_x(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=431).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=431).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -660,9 +676,10 @@ def test_post_if_else_route(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=6508).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=6508).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -700,9 +717,10 @@ def test_pre_if_else2(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=38).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=38).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -738,9 +756,10 @@ def test_intra_if_else_route(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=8).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=8).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -781,9 +800,10 @@ def test_pre_intra_if_else(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=2, trials=20).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=2, trials=20).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -829,9 +849,10 @@ def test_pre_intra_post_if_else(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=1).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=1).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -871,9 +892,10 @@ def test_if_expr(self): qc.if_test(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=58).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) def test_if_else_expr(self): @@ -892,9 +914,10 @@ def test_if_else_expr(self): qc.if_else(expr.logic_and(qc.clbits[0], qc.clbits[1]), true, false, [0, 1, 2, 3], []) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=58).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) def test_standalone_vars(self): @@ -935,8 +958,9 @@ def test_standalone_vars(self): qc.cx(3, 1) cm = CouplingMap.from_line(5) - pm = PassManager([StochasticSwap(cm, seed=0), CheckMap(cm)]) - _ = pm.run(qc) + with self.assertWarns(DeprecationWarning): + pm = PassManager([StochasticSwap(cm, seed=0), CheckMap(cm)]) + _ = pm.run(qc) self.assertTrue(pm.property_set["is_swap_mapped"]) def test_no_layout_change(self): @@ -959,9 +983,10 @@ def test_no_layout_change(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=23).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=23).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -999,9 +1024,10 @@ def test_for_loop(self, nloops): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=687).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=687).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -1033,9 +1059,10 @@ def test_while_loop(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=58).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -1065,9 +1092,10 @@ def test_while_loop_expr(self): qc.while_loop(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=58).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) def test_switch_single_case(self): @@ -1083,8 +1111,9 @@ def test_switch_single_case(self): qc.switch(creg, [(0, case0)], qreg[[0, 1, 2]], creg) coupling = CouplingMap.from_line(len(qreg)) - pass_ = StochasticSwap(coupling, seed=58) - test = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) check = CheckMap(coupling) check(test) @@ -1122,8 +1151,9 @@ def test_switch_nonexhaustive(self): qc.switch(creg, [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) coupling = CouplingMap.from_line(len(qreg)) - pass_ = StochasticSwap(coupling, seed=58) - test = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) check = CheckMap(coupling) check(test) @@ -1167,8 +1197,9 @@ def test_switch_exhaustive(self, labels): qc.switch(creg, [(labels, case0)], qreg[[0, 1, 2]], creg) coupling = CouplingMap.from_line(len(qreg)) - pass_ = StochasticSwap(coupling, seed=58) - test = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) check = CheckMap(coupling) check(test) @@ -1205,8 +1236,9 @@ def test_switch_nonexhaustive_expr(self): qc.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) coupling = CouplingMap.from_line(len(qreg)) - pass_ = StochasticSwap(coupling, seed=58) - test = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) check = CheckMap(coupling) check(test) @@ -1250,8 +1282,9 @@ def test_switch_exhaustive_expr(self, labels): qc.switch(expr.bit_or(creg, 3), [(labels, case0)], qreg[[0, 1, 2]], creg) coupling = CouplingMap.from_line(len(qreg)) - pass_ = StochasticSwap(coupling, seed=58) - test = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) check = CheckMap(coupling) check(test) @@ -1295,9 +1328,10 @@ def test_nested_inner_cnot(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=seed).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=seed).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -1350,9 +1384,10 @@ def test_nested_outer_cnot(self): qc.measure(qreg, creg) dag = circuit_to_dag(qc) - cdag = StochasticSwap(coupling, seed=seed).run(dag) - check_map_pass = CheckMap(coupling) - check_map_pass.run(cdag) + with self.assertWarns(DeprecationWarning): + cdag = StochasticSwap(coupling, seed=seed).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) expected = QuantumCircuit(qreg, creg) @@ -1386,7 +1421,8 @@ def test_disjoint_looping(self): loop_body = QuantumCircuit(2) loop_body.cx(0, 1) qc.for_loop((0,), None, loop_body, [0, 2], []) - cqc = StochasticSwap(cm, seed=0)(qc) + with self.assertWarns(DeprecationWarning): + cqc = StochasticSwap(cm, seed=0)(qc) expected = QuantumCircuit(qr) efor_body = QuantumCircuit(qr[[0, 1, 2]]) @@ -1408,7 +1444,8 @@ def test_disjoint_multiblock(self): false_body = QuantumCircuit(3, 1) false_body.cx(0, 2) qc.if_else((cr[0], 1), true_body, false_body, [0, 1, 2], [0]) - cqc = StochasticSwap(cm, seed=353)(qc) + with self.assertWarns(DeprecationWarning): + cqc = StochasticSwap(cm, seed=353)(qc) expected = QuantumCircuit(qr, cr) etrue_body = QuantumCircuit(qr[[0, 1, 2]], cr[[0]]) @@ -1431,7 +1468,8 @@ def test_multiple_ops_per_layer(self): qc.cx(0, 2) with qc.for_loop((0,)): qc.cx(3, 5) - cqc = StochasticSwap(coupling, seed=0)(qc) + with self.assertWarns(DeprecationWarning): + cqc = StochasticSwap(coupling, seed=0)(qc) check_map_pass(cqc) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) @@ -1465,8 +1503,9 @@ def test_if_no_else_restores_layout(self): qc.cx(2, 0) qc.cx(7, 6) coupling = CouplingMap.from_line(8) - pass_ = StochasticSwap(coupling, seed=2022_10_13) - transpiled = pass_(qc) + with self.assertWarns(DeprecationWarning): + pass_ = StochasticSwap(coupling, seed=2022_10_13) + transpiled = pass_(qc) # Check the pass claims to have done things right. initial_layout = Layout.generate_trivial_layout(*qc.qubits) @@ -1532,29 +1571,31 @@ def _visit_block(circuit, qubit_mapping=None): def test_random_circuit_no_control_flow(self, size): """Test that transpiled random circuits without control flow are physical circuits.""" circuit = random_circuit(size, 3, measure=True, seed=12342) - tqc = transpile( - circuit, - self.backend, - routing_method="stochastic", - layout_method="dense", - seed_transpiler=12342, - ) + with self.assertWarns(DeprecationWarning): + tqc = transpile( + circuit, + self.backend, + routing_method="stochastic", + layout_method="dense", + seed_transpiler=12342, + ) self.assert_valid_circuit(tqc) @data(*range(1, 27)) def test_random_circuit_no_control_flow_target(self, size): """Test that transpiled random circuits without control flow are physical circuits.""" circuit = random_circuit(size, 3, measure=True, seed=12342) - tqc = transpile( - circuit, - routing_method="stochastic", - layout_method="dense", - seed_transpiler=12342, - target=GenericBackendV2( - num_qubits=27, - coupling_map=MUMBAI_CMAP, - ).target, - ) + with self.assertWarns(DeprecationWarning): + tqc = transpile( + circuit, + routing_method="stochastic", + layout_method="dense", + seed_transpiler=12342, + target=GenericBackendV2( + num_qubits=27, + coupling_map=MUMBAI_CMAP, + ).target, + ) self.assert_valid_circuit(tqc) @data(*range(4, 27)) @@ -1569,14 +1610,15 @@ def test_random_circuit_for_loop(self, size): circuit.append(for_block, [1, 0, 2]) circuit.measure_all() - tqc = transpile( - circuit, - self.backend, - basis_gates=list(self.basis_gates), - routing_method="stochastic", - layout_method="dense", - seed_transpiler=12342, - ) + with self.assertWarns(DeprecationWarning): + tqc = transpile( + circuit, + self.backend, + basis_gates=list(self.basis_gates), + routing_method="stochastic", + layout_method="dense", + seed_transpiler=12342, + ) self.assert_valid_circuit(tqc) @data(*range(6, 27)) @@ -1598,14 +1640,15 @@ def test_random_circuit_if_else(self, size): with else_: circuit.append(else_block, [2, 5], clbit_indices[: else_block.num_clbits]) - tqc = transpile( - circuit, - self.backend, - basis_gates=list(self.basis_gates), - routing_method="stochastic", - layout_method="dense", - seed_transpiler=12342, - ) + with self.assertWarns(DeprecationWarning): + tqc = transpile( + circuit, + self.backend, + basis_gates=list(self.basis_gates), + routing_method="stochastic", + layout_method="dense", + seed_transpiler=12342, + ) self.assert_valid_circuit(tqc) diff --git a/test/python/visualization/test_dag_drawer.py b/test/python/visualization/test_dag_drawer.py index d789b1e70682..56138c12cf00 100644 --- a/test/python/visualization/test_dag_drawer.py +++ b/test/python/visualization/test_dag_drawer.py @@ -46,6 +46,7 @@ def test_dag_drawer_invalid_style(self): dag_drawer(self.dag, style="multicolor") @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") def test_dag_drawer_checks_filename_correct_format(self): """filename must contain name and extension""" with self.assertRaisesRegex( diff --git a/test/python/visualization/test_utils.py b/test/python/visualization/test_utils.py index 1dca43b6beb2..1ef87994d001 100644 --- a/test/python/visualization/test_utils.py +++ b/test/python/visualization/test_utils.py @@ -48,18 +48,18 @@ def test_get_layered_instructions(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(self.circuit) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],))], - [("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],))}, + {("cx", (self.qr2[1], self.qr2[0]), ()), ("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr1[0], self.qr1[1], self.qr2[0], self.qr2[1]], qregs) self.assertEqual([self.cr1[0], self.cr1[1], self.cr2[0], self.cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_reverse_bits(self): @@ -69,18 +69,18 @@ def test_get_layered_instructions_reverse_bits(self): ) exp = [ - [("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())], - [("measure", (self.qr2[0],), (self.cr2[0],))], - [("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())], - [("cx", (self.qr1[1], self.qr1[0]), ())], - [("measure", (self.qr2[1],), (self.cr2[1],))], - [("measure", (self.qr1[1],), (self.cr1[1],))], + {("cx", (self.qr2[0], self.qr2[1]), ()), ("cx", (self.qr1[0], self.qr1[1]), ())}, + {("measure", (self.qr2[0],), (self.cr2[0],))}, + {("measure", (self.qr1[0],), (self.cr1[0],)), ("cx", (self.qr2[1], self.qr2[0]), ())}, + {("cx", (self.qr1[1], self.qr1[0]), ())}, + {("measure", (self.qr2[1],), (self.cr2[1],))}, + {("measure", (self.qr1[1],), (self.cr1[1],))}, ] self.assertEqual([self.qr2[1], self.qr2[0], self.qr1[1], self.qr1[0]], qregs) self.assertEqual([self.cr2[1], self.cr2[0], self.cr1[1], self.cr1[0]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_remove_idle_wires(self): @@ -103,18 +103,18 @@ def test_get_layered_instructions_remove_idle_wires(self): (qregs, cregs, layered_ops) = _utils._get_layered_instructions(circuit, idle_wires=False) exp = [ - [("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())], - [("measure", (qr2[0],), (cr2[0],))], - [("measure", (qr1[0],), (cr1[0],))], - [("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())], - [("measure", (qr2[1],), (cr2[1],))], - [("measure", (qr1[1],), (cr1[1],))], + {("cx", (qr2[0], qr2[1]), ()), ("cx", (qr1[0], qr1[1]), ())}, + {("measure", (qr2[0],), (cr2[0],))}, + {("measure", (qr1[0],), (cr1[0],))}, + {("cx", (qr2[1], qr2[0]), ()), ("cx", (qr1[1], qr1[0]), ())}, + {("measure", (qr2[1],), (cr2[1],))}, + {("measure", (qr1[1],), (cr1[1],))}, ] self.assertEqual([qr1[0], qr1[1], qr2[0], qr2[1]], qregs) self.assertEqual([cr1[0], cr1[1], cr2[0], cr2[1]], cregs) self.assertEqual( - exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_simple(self): @@ -136,15 +136,15 @@ def test_get_layered_instructions_left_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], + }, + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_simple(self): @@ -166,15 +166,15 @@ def test_get_layered_instructions_right_justification_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())], - [ + {("cx", (Qubit(QuantumRegister(4, "q"), 0), Qubit(QuantumRegister(4, "q"), 3)), ())}, + { ("h", (Qubit(QuantumRegister(4, "q"), 1),), ()), ("h", (Qubit(QuantumRegister(4, "q"), 2),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_left_justification_less_simple(self): @@ -215,33 +215,33 @@ def test_get_layered_instructions_left_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="left") l_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 1),), ())}, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("u2", (Qubit(QuantumRegister(5, "q"), 0),), ())}, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - l_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + l_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_right_justification_less_simple(self): @@ -282,35 +282,35 @@ def test_get_layered_instructions_right_justification_less_simple(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc, justify="right") r_exp = [ - [ + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [ + }, + { ( "measure", (Qubit(QuantumRegister(5, "q"), 0),), (Clbit(ClassicalRegister(1, "c1"), 0),), ) - ], - [ + }, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], - [("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())], - [ + }, + {("cx", (Qubit(QuantumRegister(5, "q"), 1), Qubit(QuantumRegister(5, "q"), 0)), ())}, + { ("u2", (Qubit(QuantumRegister(5, "q"), 0),), ()), ("u2", (Qubit(QuantumRegister(5, "q"), 1),), ()), - ], + }, ] self.assertEqual( - r_exp, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + r_exp, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) def test_get_layered_instructions_op_with_cargs(self): @@ -335,25 +335,25 @@ def test_get_layered_instructions_op_with_cargs(self): (_, _, layered_ops) = _utils._get_layered_instructions(qc) expected = [ - [("h", (Qubit(QuantumRegister(2, "q"), 0),), ())], - [ + {("h", (Qubit(QuantumRegister(2, "q"), 0),), ())}, + { ( "measure", (Qubit(QuantumRegister(2, "q"), 0),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], - [ + }, + { ( "add_circ", (Qubit(QuantumRegister(2, "q"), 1),), (Clbit(ClassicalRegister(2, "c"), 0),), ) - ], + }, ] self.assertEqual( - expected, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] + expected, [{(op.name, op.qargs, op.cargs) for op in ops} for ops in layered_ops] ) @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") diff --git a/test/qpy_compat/process_version.sh b/test/qpy_compat/process_version.sh index 56743c613316..3fc34557b69e 100755 --- a/test/qpy_compat/process_version.sh +++ b/test/qpy_compat/process_version.sh @@ -45,9 +45,9 @@ if [[ ! -d qpy_$version ]] ; then echo "Building venv for qiskit-terra $version" python -m venv $version if [[ ${parts[0]} -eq 0 ]] ; then - ./$version/bin/pip install "qiskit-terra==$version" + ./$version/bin/pip install -c qpy_test_constraints.txt "qiskit-terra==$version" else - ./$version/bin/pip install "qiskit==$version" + ./$version/bin/pip install -c qpy_test_constraints.txt "qiskit==$version" fi mkdir qpy_$version pushd qpy_$version diff --git a/test/qpy_compat/qpy_test_constraints.txt b/test/qpy_compat/qpy_test_constraints.txt new file mode 100644 index 000000000000..03f798b3c01a --- /dev/null +++ b/test/qpy_compat/qpy_test_constraints.txt @@ -0,0 +1,2 @@ +numpy===1.24.4 +scipy===1.10.1 diff --git a/test/qpy_compat/run_tests.sh b/test/qpy_compat/run_tests.sh index 4fc6bc5b91fd..3810773ec0f3 100755 --- a/test/qpy_compat/run_tests.sh +++ b/test/qpy_compat/run_tests.sh @@ -20,7 +20,7 @@ export PYTHONHASHSEED=$(python -S -c "import random; print(random.randint(1, 429 echo "PYTHONHASHSEED=$PYTHONHASHSEED" python -m venv qiskit_venv -qiskit_venv/bin/pip install ../.. +qiskit_venv/bin/pip install -c ../../constraints.txt ../.. parallel bash ./process_version.sh ::: `git tag --sort=-creatordate` diff --git a/test/randomized/test_synthesis.py b/test/randomized/test_synthesis.py index 9f0619a0c296..5e500b78dafc 100644 --- a/test/randomized/test_synthesis.py +++ b/test/randomized/test_synthesis.py @@ -23,8 +23,8 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( two_qubit_cnot_decompose, TwoQubitBasisDecomposer, - Ud, ) +from qiskit._accelerate.two_qubit_decompose import Ud class TestSynthesis(CheckDecompositions): diff --git a/test/utils/base.py b/test/utils/base.py index 78d5aceb58f2..0038f7772ebf 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -134,7 +134,7 @@ def setUpClass(cls): warnings.filterwarnings( "ignore", # If "default", it floods the CI output category=DeprecationWarning, - message=r"The class ``qiskit\.providers\.models\..*`", + message=r".*qiskit\.providers\.models.*", module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", ) @@ -156,6 +156,14 @@ def setUpClass(cls): module="qiskit_aer", ) + # Safe to remove once `FakeBackend` is removed (2.0) + warnings.filterwarnings( + "ignore", # If "default", it floods the CI output + category=DeprecationWarning, + message=r".*from_backend using V1 based backend is deprecated as of Aer 0.15*", + module="qiskit.providers.fake_provider.fake_backend", + ) + allow_DeprecationWarning_message = [ r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", ] diff --git a/test/utils/decorators.py b/test/utils/decorators.py index d3abb37bbef1..1b1df0efc9c1 100644 --- a/test/utils/decorators.py +++ b/test/utils/decorators.py @@ -33,7 +33,7 @@ def slow_test(func): @functools.wraps(func) def _wrapper(*args, **kwargs): - if "run_slow" in os.environ.get("QISKIT_TESTS", ""): + if "run_slow" not in os.environ.get("QISKIT_TESTS", ""): raise unittest.SkipTest("Skipping slow tests") return func(*args, **kwargs) diff --git a/test/visual/mpl/graph/references/16_qubit_gate_map.png b/test/visual/mpl/graph/references/16_qubit_gate_map.png index c76baabf1522..722b1db91523 100644 Binary files a/test/visual/mpl/graph/references/16_qubit_gate_map.png and b/test/visual/mpl/graph/references/16_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/27_qubit_gate_map.png b/test/visual/mpl/graph/references/27_qubit_gate_map.png index 73ef08c1c3a8..59ed9d8d5cfc 100644 Binary files a/test/visual/mpl/graph/references/27_qubit_gate_map.png and b/test/visual/mpl/graph/references/27_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/5_qubit_gate_map.png b/test/visual/mpl/graph/references/5_qubit_gate_map.png index 3d89cdaf9950..557901f7e9be 100644 Binary files a/test/visual/mpl/graph/references/5_qubit_gate_map.png and b/test/visual/mpl/graph/references/5_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/65_qubit_gate_map.png b/test/visual/mpl/graph/references/65_qubit_gate_map.png index 1e99ced00400..182d7c333bbb 100644 Binary files a/test/visual/mpl/graph/references/65_qubit_gate_map.png and b/test/visual/mpl/graph/references/65_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/7_qubit_gate_map.png b/test/visual/mpl/graph/references/7_qubit_gate_map.png index 1413da0a7c47..4a0ed4fe2d51 100644 Binary files a/test/visual/mpl/graph/references/7_qubit_gate_map.png and b/test/visual/mpl/graph/references/7_qubit_gate_map.png differ diff --git a/test/visual/mpl/graph/references/bloch_multivector.png b/test/visual/mpl/graph/references/bloch_multivector.png index 5b72a9d04896..e3036bc2ccb9 100644 Binary files a/test/visual/mpl/graph/references/bloch_multivector.png and b/test/visual/mpl/graph/references/bloch_multivector.png differ diff --git a/test/visual/mpl/graph/references/bloch_multivector_figsize_improvements.png b/test/visual/mpl/graph/references/bloch_multivector_figsize_improvements.png index 80e73131a39b..c5a082e8039a 100644 Binary files a/test/visual/mpl/graph/references/bloch_multivector_figsize_improvements.png and b/test/visual/mpl/graph/references/bloch_multivector_figsize_improvements.png differ diff --git a/test/visual/mpl/graph/references/figsize.png b/test/visual/mpl/graph/references/figsize.png index 3d89cdaf9950..557901f7e9be 100644 Binary files a/test/visual/mpl/graph/references/figsize.png and b/test/visual/mpl/graph/references/figsize.png differ diff --git a/test/visual/mpl/graph/references/font_color.png b/test/visual/mpl/graph/references/font_color.png index a53f93fa4fe2..3e132fff5be4 100644 Binary files a/test/visual/mpl/graph/references/font_color.png and b/test/visual/mpl/graph/references/font_color.png differ diff --git a/test/visual/mpl/graph/references/hinton.png b/test/visual/mpl/graph/references/hinton.png index 3be80df31480..70521af917c5 100644 Binary files a/test/visual/mpl/graph/references/hinton.png and b/test/visual/mpl/graph/references/hinton.png differ diff --git a/test/visual/mpl/graph/references/histogram.png b/test/visual/mpl/graph/references/histogram.png index 3fa51ec644ed..8167908c1ba3 100644 Binary files a/test/visual/mpl/graph/references/histogram.png and b/test/visual/mpl/graph/references/histogram.png differ diff --git a/test/visual/mpl/graph/references/histogram_2_sets_with_rest.png b/test/visual/mpl/graph/references/histogram_2_sets_with_rest.png index fe11f5811931..fc6d8497b867 100644 Binary files a/test/visual/mpl/graph/references/histogram_2_sets_with_rest.png and b/test/visual/mpl/graph/references/histogram_2_sets_with_rest.png differ diff --git a/test/visual/mpl/graph/references/histogram_color.png b/test/visual/mpl/graph/references/histogram_color.png index 34f7d449616a..57a1cbabc1c0 100644 Binary files a/test/visual/mpl/graph/references/histogram_color.png and b/test/visual/mpl/graph/references/histogram_color.png differ diff --git a/test/visual/mpl/graph/references/histogram_desc_value_sort.png b/test/visual/mpl/graph/references/histogram_desc_value_sort.png index b119ae2deed4..71f87047479a 100644 Binary files a/test/visual/mpl/graph/references/histogram_desc_value_sort.png and b/test/visual/mpl/graph/references/histogram_desc_value_sort.png differ diff --git a/test/visual/mpl/graph/references/histogram_hamming.png b/test/visual/mpl/graph/references/histogram_hamming.png index 95bfe0ab1049..0fdca0e5342f 100644 Binary files a/test/visual/mpl/graph/references/histogram_hamming.png and b/test/visual/mpl/graph/references/histogram_hamming.png differ diff --git a/test/visual/mpl/graph/references/histogram_legend.png b/test/visual/mpl/graph/references/histogram_legend.png index 77fbe9c5dc0f..0cb218e12528 100644 Binary files a/test/visual/mpl/graph/references/histogram_legend.png and b/test/visual/mpl/graph/references/histogram_legend.png differ diff --git a/test/visual/mpl/graph/references/histogram_multiple_colors.png b/test/visual/mpl/graph/references/histogram_multiple_colors.png index 7edbfc07bab5..0ee70ac4360c 100644 Binary files a/test/visual/mpl/graph/references/histogram_multiple_colors.png and b/test/visual/mpl/graph/references/histogram_multiple_colors.png differ diff --git a/test/visual/mpl/graph/references/histogram_title.png b/test/visual/mpl/graph/references/histogram_title.png index c4c6859c8f2e..84e486d45e48 100644 Binary files a/test/visual/mpl/graph/references/histogram_title.png and b/test/visual/mpl/graph/references/histogram_title.png differ diff --git a/test/visual/mpl/graph/references/histogram_value_sort.png b/test/visual/mpl/graph/references/histogram_value_sort.png index e44d08e0d9f9..5f9c93e205ff 100644 Binary files a/test/visual/mpl/graph/references/histogram_value_sort.png and b/test/visual/mpl/graph/references/histogram_value_sort.png differ diff --git a/test/visual/mpl/graph/references/histogram_with_rest.png b/test/visual/mpl/graph/references/histogram_with_rest.png index 69633c1e4fdb..e1fe47fe2ba2 100644 Binary files a/test/visual/mpl/graph/references/histogram_with_rest.png and b/test/visual/mpl/graph/references/histogram_with_rest.png differ diff --git a/test/visual/mpl/graph/references/line_color.png b/test/visual/mpl/graph/references/line_color.png index 392c42fbd3ae..a7813e0ea07e 100644 Binary files a/test/visual/mpl/graph/references/line_color.png and b/test/visual/mpl/graph/references/line_color.png differ diff --git a/test/visual/mpl/graph/references/paulivec.png b/test/visual/mpl/graph/references/paulivec.png index 813a387265b1..0a0912c5cb3d 100644 Binary files a/test/visual/mpl/graph/references/paulivec.png and b/test/visual/mpl/graph/references/paulivec.png differ diff --git a/test/visual/mpl/graph/references/qsphere.png b/test/visual/mpl/graph/references/qsphere.png index f344accd2856..d687e7c8871d 100644 Binary files a/test/visual/mpl/graph/references/qsphere.png and b/test/visual/mpl/graph/references/qsphere.png differ diff --git a/test/visual/mpl/graph/references/qubit_color.png b/test/visual/mpl/graph/references/qubit_color.png index c4669adfa799..789e1c71278e 100644 Binary files a/test/visual/mpl/graph/references/qubit_color.png and b/test/visual/mpl/graph/references/qubit_color.png differ diff --git a/test/visual/mpl/graph/references/qubit_labels.png b/test/visual/mpl/graph/references/qubit_labels.png index 9ddc259a367c..d2f020048168 100644 Binary files a/test/visual/mpl/graph/references/qubit_labels.png and b/test/visual/mpl/graph/references/qubit_labels.png differ diff --git a/test/visual/mpl/graph/references/qubit_size.png b/test/visual/mpl/graph/references/qubit_size.png index fa317e6b9d14..f8f7a660f53f 100644 Binary files a/test/visual/mpl/graph/references/qubit_size.png and b/test/visual/mpl/graph/references/qubit_size.png differ diff --git a/test/visual/mpl/graph/references/state_city.png b/test/visual/mpl/graph/references/state_city.png index 990c433c9162..ecbdb3c3d2bc 100644 Binary files a/test/visual/mpl/graph/references/state_city.png and b/test/visual/mpl/graph/references/state_city.png differ diff --git a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py index 20fae107d30d..7100602f72a5 100644 --- a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py +++ b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py @@ -113,7 +113,7 @@ def test_plot_bloch_multivector(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) def test_plot_state_hinton(self): """test plot_state_hinton""" @@ -437,7 +437,7 @@ def test_plot_7_qubit_gate_map(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) def test_plot_16_qubit_gate_map(self): """Test plot_gate_map using 16 qubit backend""" @@ -455,7 +455,7 @@ def test_plot_16_qubit_gate_map(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) def test_plot_27_qubit_gate_map(self): """Test plot_gate_map using 27 qubit backend""" @@ -527,7 +527,7 @@ def test_qubit_size(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) def test_qubit_color(self): """Test qubit_color parameter of plot_gate_map""" @@ -545,7 +545,7 @@ def test_qubit_color(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) def test_qubit_labels(self): """Test qubit_labels parameter of plot_gate_map""" @@ -658,7 +658,7 @@ def test_plot_bloch_multivector_figsize_improvements(self): FAILURE_DIFF_DIR, FAILURE_PREFIX, ) - self.assertGreaterEqual(ratio, 0.99) + self.assertGreaterEqual(ratio, 0.99, msg=fname) if __name__ == "__main__": diff --git a/tools/build_standard_commutations.py b/tools/build_standard_commutations.py index 0e1fcdf1797d..31f1fe03822b 100644 --- a/tools/build_standard_commutations.py +++ b/tools/build_standard_commutations.py @@ -19,12 +19,70 @@ import itertools from functools import lru_cache from typing import List -from qiskit.circuit.commutation_checker import _get_relative_placement, _order_operations from qiskit.circuit import Gate, CommutationChecker import qiskit.circuit.library.standard_gates as stdg from qiskit.dagcircuit import DAGOpNode +@lru_cache(maxsize=10**3) +def _persistent_id(op_name: str) -> int: + """Returns an integer id of a string that is persistent over different python executions (note that + hash() can not be used, i.e. its value can change over two python executions) + Args: + op_name (str): The string whose integer id should be determined. + Return: + The integer id of the input string. + """ + return int.from_bytes(bytes(op_name, encoding="utf-8"), byteorder="big", signed=True) + + +def _order_operations(op1, qargs1, cargs1, op2, qargs2, cargs2): + """Orders two operations in a canonical way that is persistent over + @different python versions and executions + Args: + op1: first operation. + qargs1: first operation's qubits. + cargs1: first operation's clbits. + op2: second operation. + qargs2: second operation's qubits. + cargs2: second operation's clbits. + Return: + The input operations in a persistent, canonical order. + """ + op1_tuple = (op1, qargs1, cargs1) + op2_tuple = (op2, qargs2, cargs2) + least_qubits_op, most_qubits_op = ( + (op1_tuple, op2_tuple) if op1.num_qubits < op2.num_qubits else (op2_tuple, op1_tuple) + ) + # prefer operation with the least number of qubits as first key as this results in shorter keys + if op1.num_qubits != op2.num_qubits: + return least_qubits_op, most_qubits_op + else: + return ( + (op1_tuple, op2_tuple) + if _persistent_id(op1.name) < _persistent_id(op2.name) + else (op2_tuple, op1_tuple) + ) + + +def _get_relative_placement(first_qargs, second_qargs) -> tuple: + """Determines the relative qubit placement of two gates. Note: this is NOT symmetric. + + Args: + first_qargs (DAGOpNode): first gate + second_qargs (DAGOpNode): second gate + + Return: + A tuple that describes the relative qubit placement: E.g. + _get_relative_placement(CX(0, 1), CX(1, 2)) would return (None, 0) as there is no overlap on + the first qubit of the first gate but there is an overlap on the second qubit of the first gate, + i.e. qubit 0 of the second gate. Likewise, + _get_relative_placement(CX(1, 2), CX(0, 1)) would return (1, None) + """ + qubits_g2 = {q_g1: i_g1 for i_g1, q_g1 in enumerate(second_qargs)} + return tuple(qubits_g2.get(q_g0, None) for q_g0 in first_qargs) + + @lru_cache(None) def _get_unparameterizable_gates() -> List[Gate]: """Retrieve a list of non-parmaterized gates with up to 3 qubits, using the python inspection module diff --git a/tox.ini b/tox.ini index 89dc84d1758a..a0c5665695d2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.0 -envlist = py38, py39, py310, py311, py312, lint-incr +envlist = py39, py310, py311, py312, lint-incr isolated_build = true [testenv] @@ -15,7 +15,7 @@ setenv = QISKIT_SUPRESS_PACKAGING_WARNINGS=Y QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE -passenv = +passenv = RUSTUP_TOOLCHAIN RAYON_NUM_THREADS OMP_NUM_THREADS