diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 1887a88655d..b167df3f1c8 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -10,7 +10,7 @@ parameters: jobs: - job: "MacOS_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test macOS Python ${{ parameters.pythonVersion }}" - pool: {vmImage: 'macOS-11'} + pool: {vmImage: 'macOS-13'} variables: QISKIT_SUPPRESS_PACKAGING_WARNINGS: Y diff --git a/.binder/postBuild b/.binder/postBuild index 9517953258f..cb06527f280 100644 --- a/.binder/postBuild +++ b/.binder/postBuild @@ -7,7 +7,7 @@ # - pylatexenc: for MPL drawer # - pillow: for image comparison # - appmode: jupyter extension for executing the notebook -# - seaborn: visualisation pacakge required for some graphs +# - seaborn: visualization pacakge required for some graphs pip install matplotlib pylatexenc pillow appmode seaborn pip install . diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index bcc86d63fcf..88fd919e8ad 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,6 +1,6 @@ name: Backport metadata -# Mergify manages the opening of the backport PR, this workflow is just to extend its behaviour to +# Mergify manages the opening of the backport PR, this workflow is just to extend its behavior to # do useful things like copying across the tagged labels and milestone from the base PR. on: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index bdceb20c300..b32a96c3b42 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -14,14 +14,15 @@ jobs: name: Miri runs-on: ubuntu-latest env: - RUSTUP_TOOLCHAIN: nightly + RUSTUP_TOOLCHAIN: nightly-2024-05-24 steps: - uses: actions/checkout@v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@master with: + toolchain: nightly-2024-05-24 components: miri - name: Prepare Miri diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000000..20e40dec982 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,54 @@ +--- +name: Tests +on: + push: + branches: [ main, 'stable/*' ] + pull_request: + branches: [ main, 'stable/*' ] + merge_group: + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} + cancel-in-progress: true +jobs: + tests: + if: github.repository_owner == 'Qiskit' + name: macOS-arm64-tests-Python-${{ matrix.python-version }} + runs-on: macOS-14 + strategy: + fail-fast: false + matrix: + # Normally we test min and max version but we can't run python 3.8 or + # 3.9 on arm64 until actions/setup-python#808 is resolved + python-version: ["3.10", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.70 + if: matrix.python-version == '3.10' + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: arm64 + - name: 'Install dependencies' + run: | + python -m pip install -U -r requirements.txt -c constraints.txt + python -m pip install -U -r requirements-dev.txt -c constraints.txt + python -m pip install -c constraints.txt -e . + if: matrix.python-version == '3.10' + env: + QISKIT_NO_CACHE_GATES: 1 + - name: 'Install dependencies' + run: | + python -m pip install -U -r requirements.txt -c constraints.txt + python -m pip install -U -r requirements-dev.txt -c constraints.txt + python -m pip install -c constraints.txt -e . + if: matrix.python-version == '3.12' + - name: 'Install optionals' + run: | + python -m pip install -r requirements-optional.txt -c constraints.txt + python tools/report_numpy_state.py + if: matrix.python-version == '3.10' + - name: 'Run tests' + run: stestr run diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 67104433a3d..7c29f1376e4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -12,18 +12,25 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-11, windows-latest] + os: [ubuntu-latest, macos-11, windows-latest, macos-14] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.10' + if: matrix.os != 'macos-14' + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: '3.10' + architecture: arm64 + if: matrix.os == 'macos-14' - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: 'bash ./tools/build_pgo.sh /tmp/pgo-data/merged.profdata' CIBW_BEFORE_BUILD_WINDOWS: 'bash ./tools/build_pgo.sh /tmp/pgo-data/merged.profdata && cp /tmp/pgo-data/merged.profdata ~/.' @@ -34,13 +41,13 @@ jobs: with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }} - build_wheels_32bit: - name: Build wheels 32bit + build_wheels_macos_arm_py38: + name: Build wheels on macOS arm runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [macos-11] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -51,21 +58,25 @@ jobs: with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' + 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 }}-32 - build_wheels_macos_arm: - name: Build wheels on macOS arm + name: wheels-${{ matrix.os }}-arm + build_wheels_32bit: + name: Build wheels 32bit runs-on: ${{ matrix.os }} - environment: release strategy: fail-fast: false matrix: - os: [macos-11] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -73,25 +84,23 @@ jobs: with: python-version: '3.10' - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin - 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]))')" + CIBW_SKIP: 'pp* cp36-* cp37-* *musllinux* *amd64 *x86_64' - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl - name: wheels-${{ matrix.os }}-arm + name: wheels-${{ matrix.os }}-32 upload_shared_wheels: name: Upload shared build wheels runs-on: ubuntu-latest environment: release permissions: id-token: write - needs: ["build_wheels", "build_wheels_macos_arm", "build_wheels_32bit"] + needs: ["build_wheels", "build_wheels_32bit", "build_wheels_macos_arm_py38"] steps: - uses: actions/download-artifact@v4 with: @@ -124,7 +133,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: s390x CIBW_TEST_SKIP: "cp*" @@ -158,7 +167,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: ppc64le CIBW_TEST_SKIP: "cp*" @@ -192,7 +201,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_LINUX: aarch64 - uses: actions/upload-artifact@v4 diff --git a/.mergify.yml b/.mergify.yml index 87a3438930f..10b6d202225 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,4 +6,4 @@ pull_request_rules: actions: backport: branches: - - stable/1.0 + - stable/1.1 diff --git a/CITATION.bib b/CITATION.bib index a00798d9baa..deac2ef200e 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -1,6 +1,9 @@ -@misc{Qiskit, - author = {{Qiskit contributors}}, - title = {Qiskit: An Open-source Framework for Quantum Computing}, - year = {2023}, - doi = {10.5281/zenodo.2573505} +@misc{qiskit2024, + title={Quantum computing with {Q}iskit}, + author={Javadi-Abhari, Ali and Treinish, Matthew and Krsulich, Kevin and Wood, Christopher J. and Lishman, Jake and Gacon, Julien and Martiel, Simon and Nation, Paul D. and Bishop, Lev S. and Cross, Andrew W. and Johnson, Blake R. and Gambetta, Jay M.}, + year={2024}, + doi={10.48550/arXiv.2405.08810}, + eprint={2405.08810}, + archivePrefix={arXiv}, + primaryClass={quant-ph} } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ea3dc9f60f..7076c1571b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,18 @@ Note that in order to run `python setup.py ...` commands you need have build dependency packages installed in your environment, which are listed in the `pyproject.toml` file under the `[build-system]` section. +### Compile time options + +When building qiskit from source there are options available to control how +Qiskit is built. Right now the only option is if you set the environment +variable `QISKIT_NO_CACHE_GATES=1` this will disable runtime caching of +Python gate objects when accessing them from a `QuantumCircuit` or `DAGCircuit`. +This makes a tradeoff between runtime performance for Python access and memory +overhead. Caching gates will result in better runtime for users of Python at +the cost of increased memory consumption. If you're working with any custom +transpiler passes written in python or are otherwise using a workflow that +repeatedly accesses the `operation` attribute of a `CircuitInstruction` or `op` +attribute of `DAGOpNode` enabling caching is recommended. ## Issues and pull requests @@ -183,8 +195,8 @@ please ensure that: If your pull request is adding a new class, function, or module that is intended to be user facing ensure that you've also added those to a documentation `autosummary` index to include it in the api documentation. -3. If it makes sense for your change that you have added new tests that - cover the changes. +3. If you are of the opinion that the modifications you made warrant additional tests, + feel free to include them 4. Ensure that if your change has an end user facing impact (new feature, deprecation, removal etc) that you have added a reno release note for that change and that the PR is tagged for the changelog. @@ -520,7 +532,7 @@ we used in our CI systems more closely. ### Snapshot Testing for Visualizations -If you are working on code that makes changes to any matplotlib visualisations +If you are working on code that makes changes to any matplotlib visualizations you will need to check that your changes don't break any snapshot tests, and add new tests where necessary. You can do this as follows: @@ -531,7 +543,7 @@ the snapshot tests (note this may take some time to finish loading). 3. Each test result provides a set of 3 images (left: reference image, middle: your test result, right: differences). In the list of tests the passed tests are collapsed and failed tests are expanded. If a test fails, you will see a situation like this: Screenshot_2021-03-26_at_14 13 54 -4. Fix any broken tests. Working on code for one aspect of the visualisations +4. Fix any broken tests. Working on code for one aspect of the visualizations can sometimes result in minor changes elsewhere to spacing etc. In these cases you just need to update the reference images as follows: - download the mismatched images (link at top of Jupyter Notebook output) diff --git a/Cargo.lock b/Cargo.lock index 6859c622967..6ce26b3baea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -106,7 +117,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -265,27 +276,27 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "equator" -version = "0.1.10" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b0a88aa91d0ad2b9684e4479aed31a17d3f9051bdbbc634bd2c01bc5a5eee8" +checksum = "c35da53b5a021d2484a7cc49b2ac7f2d840f8236a286f84202369bd338d761ea" dependencies = [ "equator-macro", ] [[package]] name = "equator-macro" -version = "0.1.9" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d08acb9849f7fb4401564f251be5a526829183a3645a90197dea8e786cf3ae" +checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -296,9 +307,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.18.2" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e547492d9b55c4ea882584e691ed092228981e337d0c800bc721301d7e61e40a" +checksum = "41543c4de4bfb32efdffdd75cbcca5ef41b800e8a811ea4a41fb9393c6ef3bc0" dependencies = [ "bytemuck", "coe-rs", @@ -310,6 +321,7 @@ dependencies = [ "libm", "matrixcompare", "matrixcompare-core", + "nano-gemm", "npyz", "num-complex", "num-traits", @@ -323,9 +335,9 @@ dependencies = [ [[package]] name = "faer-entity" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ea5c06233193392c614a46aa3bbe3de29c1404692c8053abd9c2765a1cd159" +checksum = "ab968a02be27be95de0f1ad0af901b865fa0866b6a9b553a6cc9cf7f19c2ce71" dependencies = [ "bytemuck", "coe-rs", @@ -338,9 +350,9 @@ dependencies = [ [[package]] name = "faer-ext" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f67e0c5be50b08c57b59f1cf78a1c8399f6816f4e1a2e0801470ff58dad23a3" +checksum = "4cf30f6ae73f372c0e0cf7556c44e50f1eee0a714d71396091613d68c43625c9" dependencies = [ "faer", "ndarray", @@ -355,9 +367,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "gemm" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" +checksum = "e400f2ffd14e7548356236c35dc39cad6666d833a852cb8a8f3f28029359bb03" dependencies = [ "dyn-stack", "gemm-c32", @@ -375,9 +387,9 @@ dependencies = [ [[package]] name = "gemm-c32" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" +checksum = "10dc4a6176c8452d60eac1a155b454c91c668f794151a303bf3c75ea2874812d" dependencies = [ "dyn-stack", "gemm-common", @@ -390,9 +402,9 @@ dependencies = [ [[package]] name = "gemm-c64" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" +checksum = "cc2032ce2c0bb150da0256338759a6fb01ca056f6dfe28c4d14af32d7f878f6f" dependencies = [ "dyn-stack", "gemm-common", @@ -405,9 +417,9 @@ dependencies = [ [[package]] name = "gemm-common" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" +checksum = "90fd234fc525939654f47b39325fd5f55e552ceceea9135f3aa8bdba61eabef6" dependencies = [ "bytemuck", "dyn-stack", @@ -425,9 +437,9 @@ dependencies = [ [[package]] name = "gemm-f16" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" +checksum = "3fc3652651f96a711d46b8833e1fac27a864be4bdfa81a374055f33ddd25c0c6" dependencies = [ "dyn-stack", "gemm-common", @@ -443,9 +455,9 @@ dependencies = [ [[package]] name = "gemm-f32" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" +checksum = "acbc51c44ae3defd207e6d9416afccb3c4af1e7cef5e4960e4c720ac4d6f998e" dependencies = [ "dyn-stack", "gemm-common", @@ -458,9 +470,9 @@ dependencies = [ [[package]] name = "gemm-f64" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" +checksum = "3f37fc86e325c2415a4d0cab8324a0c5371ec06fc7d2f9cb1636fcfc9536a8d8" dependencies = [ "dyn-stack", "gemm-common", @@ -509,14 +521,17 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", "rayon", ] @@ -533,16 +548,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -550,7 +555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "rayon", ] @@ -589,6 +594,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "jod-thread" version = "0.1.2" @@ -597,9 +611,9 @@ checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -609,9 +623,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -673,6 +687,76 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "nano-gemm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f563548d38f390ef9893e4883ec38c1fb312f569e98d76bededdd91a3b41a043" +dependencies = [ + "equator", + "nano-gemm-c32", + "nano-gemm-c64", + "nano-gemm-codegen", + "nano-gemm-core", + "nano-gemm-f32", + "nano-gemm-f64", + "num-complex", +] + +[[package]] +name = "nano-gemm-c32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40449e57a5713464c3a1208c4c3301c8d29ee1344711822cf022bc91373a91b" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-c64" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743a6e6211358fba85d1009616751e4107da86f4c95b24e684ce85f25c25b3bf" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-codegen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963bf7c7110d55430169dc74c67096375491ed580cd2ef84842550ac72e781fa" + +[[package]] +name = "nano-gemm-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3fc4f83ae8861bad79dc3c016bd6b0220da5f9de302e07d3112d16efc24aa6" + +[[package]] +name = "nano-gemm-f32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3681b7ce35658f79da94b7f62c60a005e29c373c7111ed070e3bf64546a8bb" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + +[[package]] +name = "nano-gemm-f64" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1e619ed04d801809e1f63e61b669d380c4119e8b0cdd6ed184c6b111f046d8" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + [[package]] name = "ndarray" version = "0.15.6" @@ -701,23 +785,23 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", + "rand", ] [[package]] @@ -731,9 +815,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -762,9 +846,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oq3_lexer" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e867d2797100b8068715e26566a5567c598424d7eddf7118c6b38bc3b15633" +checksum = "0de2f0f9d48042c12f82b2550808378718627e108fc3f6adf63e02e5293541a3" dependencies = [ "unicode-properties", "unicode-xid", @@ -772,9 +856,9 @@ dependencies = [ [[package]] name = "oq3_parser" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf260dea71b56b405d091d476748c1f9b0a4d22b4ec9af416e002e2df25613f9" +checksum = "e69b215426a4a2a023fd62cca4436c633ba0ab39ee260aca875ac60321b3704b" dependencies = [ "drop_bomb", "oq3_lexer", @@ -783,12 +867,12 @@ dependencies = [ [[package]] name = "oq3_semantics" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ba220b91ff849190d53b296711774f761b7e06744b16a9c8f19fc2fb37de47" +checksum = "3e15e9cee54e92fb1b3aaa42556b0bd76c8c1c10912a7d6798f43dfc3afdcb0d" dependencies = [ "boolenum", - "hashbrown 0.14.3", + "hashbrown 0.12.3", "oq3_source_file", "oq3_syntax", "rowan", @@ -796,9 +880,9 @@ dependencies = [ [[package]] name = "oq3_source_file" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a81fd0c1c100ad8d7a23711c897791d693c3f5b1f3d044cb8c5770766f819c" +checksum = "4f65243cc4807c600c544a21db6c17544c53aa2bc034b3eccf388251cc6530e7" dependencies = [ "ariadne", "oq3_syntax", @@ -806,13 +890,13 @@ dependencies = [ [[package]] name = "oq3_syntax" -version = "0.0.7" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7da2ef9a591d77eee43e972e79fc95c218545e5e79b93738d20479d8d7627ec" +checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" dependencies = [ "cov-mark", "either", - "indexmap 2.2.6", + "indexmap", "itertools 0.10.5", "once_cell", "oq3_lexer", @@ -820,6 +904,7 @@ dependencies = [ "ra_ap_stdx", "rowan", "rustc-hash", + "rustversion", "smol_str", "triomphe", "xshell", @@ -827,9 +912,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -837,15 +922,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -856,9 +941,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -867,9 +952,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -877,22 +962,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "pest_meta" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -901,12 +986,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -923,12 +1008,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "priority-queue" -version = "1.4.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", - "indexmap 1.9.3", + "equivalent", + "indexmap", ] [[package]] @@ -966,9 +1052,9 @@ dependencies = [ [[package]] name = "pulp" -version = "0.18.10" +version = "0.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14989307e408d9f4245d4fda09a7b144a08114ba124e26cab60ab83dc98db10" +checksum = "0ec8d02258294f59e4e223b41ad7e81c874aa6b15bc4ced9ba3965826da0eed5" dependencies = [ "bytemuck", "libm", @@ -985,7 +1071,7 @@ checksum = "d315b3197b780e4873bc0e11251cb56a33f65a6032a3d39b8d1405c255513766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1008,8 +1094,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", - "hashbrown 0.14.3", - "indexmap 2.2.6", + "hashbrown 0.14.5", + "indexmap", "indoc", "libc", "memoffset", @@ -1053,7 +1139,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1066,19 +1152,20 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "qiskit-accelerate" -version = "1.1.0" +version = "1.2.0" dependencies = [ - "ahash", + "ahash 0.8.11", "approx", "faer", "faer-ext", - "hashbrown 0.14.3", - "indexmap 2.2.6", + "hashbrown 0.14.5", + "indexmap", + "itertools 0.13.0", "ndarray", "num-bigint", "num-complex", @@ -1093,19 +1180,25 @@ dependencies = [ "rayon", "rustworkx-core", "smallvec", + "thiserror", ] [[package]] name = "qiskit-circuit" -version = "1.1.0" +version = "1.2.0" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", + "ndarray", + "num-complex", + "numpy", "pyo3", + "smallvec", + "thiserror", ] [[package]] name = "qiskit-pyext" -version = "1.1.0" +version = "1.2.0" dependencies = [ "pyo3", "qiskit-accelerate", @@ -1116,19 +1209,19 @@ dependencies = [ [[package]] name = "qiskit-qasm2" -version = "1.1.0" +version = "1.2.0" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pyo3", "qiskit-circuit", ] [[package]] name = "qiskit-qasm3" -version = "1.1.0" +version = "1.2.0" dependencies = [ - "hashbrown 0.14.3", - "indexmap 2.2.6", + "hashbrown 0.14.5", + "indexmap", "oq3_semantics", "pyo3", ] @@ -1265,11 +1358,11 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -1279,7 +1372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" dependencies = [ "countme", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "memoffset", "rustc-hash", "text-size", @@ -1291,16 +1384,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + [[package]] name = "rustworkx-core" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +checksum = "c2b9aa5926b35dd3029530aef27eac0926b544c78f8e8f1aad4d37854b132fe9" dependencies = [ - "ahash", + "ahash 0.8.11", "fixedbitset", - "hashbrown 0.14.3", - "indexmap 2.2.6", + "hashbrown 0.14.5", + "indexmap", + "ndarray", "num-traits", "petgraph", "priority-queue", @@ -1333,22 +1433,22 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1390,9 +1490,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -1427,22 +1527,22 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1477,9 +1577,9 @@ checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -1533,11 +1633,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1594,21 +1694,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.5" @@ -1631,12 +1716,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" @@ -1649,12 +1728,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" @@ -1667,12 +1740,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1691,12 +1758,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" @@ -1709,12 +1770,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" @@ -1727,12 +1782,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" @@ -1745,12 +1794,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" @@ -1795,5 +1838,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] diff --git a/Cargo.toml b/Cargo.toml index 9c4af6260be..a6ccf60f7f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "1.1.0" +version = "1.2.0" edition = "2021" rust-version = "1.70" # Keep in sync with README.md and rust-toolchain.toml. license = "Apache-2.0" @@ -16,6 +16,12 @@ license = "Apache-2.0" [workspace.dependencies] indexmap.version = "2.2.6" hashbrown.version = "0.14.0" +num-complex = "0.4" +ndarray = "^0.15.6" +numpy = "0.21.0" +smallvec = "1.13" +thiserror = "1.0" + # 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 diff --git a/SECURITY.md b/SECURITY.md index 1a8e9cea671..45e6a9d6f51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,13 +15,13 @@ We provide more detail on [the release and support schedule of Qiskit in our doc ## Reporting a Vulnerability To report vulnerabilities, you can privately report a potential security issue -via the Github security vulnerabilities feature. This can be done here: +via the GitHub security vulnerabilities feature. This can be done here: https://github.com/Qiskit/qiskit/security/advisories Please do **not** open a public issue about a potential security vulnerability. -You can find more details on the security vulnerability feature in the Github +You can find more details on the security vulnerability feature in the GitHub documentation here: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability diff --git a/constraints.txt b/constraints.txt index d3985581d36..6681de226d9 100644 --- a/constraints.txt +++ b/constraints.txt @@ -3,6 +3,10 @@ # https://github.com/Qiskit/qiskit-terra/issues/10345 for current details. scipy<1.11; python_version<'3.12' +# Temporary pin to avoid CI issues caused by scipy 1.14.0 +# See https://github.com/Qiskit/qiskit/issues/12655 for current details. +scipy==1.13.1; python_version=='3.12' + # z3-solver from 4.12.3 onwards upped the minimum macOS API version for its # wheels to 11.7. The Azure VM images contain pre-built CPythons, of which at # least CPython 3.8 was compiled for an older macOS, so does not match a diff --git a/crates/README.md b/crates/README.md index cbe58afa07d..d72247bc61d 100644 --- a/crates/README.md +++ b/crates/README.md @@ -29,11 +29,11 @@ This would be a particular problem for defining the circuit object and using it ## Developer notes -### Beware of initialisation order +### Beware of initialization order -The Qiskit C extension `qiskit._accelerate` needs to be initialised in a single go. -It is the lowest part of the Python package stack, so it cannot rely on importing other parts of the Python library at initialisation time (except for exceptions through PyO3's `import_exception!` mechanism). -This is because, unlike pure-Python modules, the initialisation of `_accelerate` cannot be done partially, and many components of Qiskit import their accelerators from `_accelerate`. +The Qiskit C extension `qiskit._accelerate` needs to be initialized in a single go. +It is the lowest part of the Python package stack, so it cannot rely on importing other parts of the Python library at initialization time (except for exceptions through PyO3's `import_exception!` mechanism). +This is because, unlike pure-Python modules, the initialization of `_accelerate` cannot be done partially, and many components of Qiskit import their accelerators from `_accelerate`. In general, this should not be too onerous a requirement, but if you violate it, you might see Rust panics on import, and PyO3 should wrap that up into an exception. You might be able to track down the Rust source of the import cycle by running the import with the environment variable `RUST_BACKTRACE=full`. diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 05ca3f5b639..9d602478399 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -11,20 +11,22 @@ doctest = false [dependencies] rayon = "1.10" -numpy = "0.21.0" +numpy.workspace = true rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" ahash = "0.8.11" num-traits = "0.2" -num-complex = "0.4" +num-complex.workspace = true num-bigint = "0.4" -rustworkx-core = "0.14" -faer = "0.18.2" +rustworkx-core = "0.15" +faer = "0.19.1" +itertools = "0.13.0" qiskit-circuit.workspace = true +thiserror.workspace = true [dependencies.smallvec] -version = "1.13" +workspace = true features = ["union"] [dependencies.pyo3] @@ -32,7 +34,7 @@ workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] [dependencies.ndarray] -version = "^0.15.6" +workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] @@ -48,9 +50,9 @@ workspace = true features = ["rayon"] [dependencies.faer-ext] -version = "0.1.0" +version = "0.2.0" features = ["ndarray"] [dependencies.pulp] -version = "0.18.10" +version = "0.18.21" features = ["macro"] diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index e311c129b11..9c179397d64 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -20,10 +20,7 @@ use numpy::ndarray::{aview2, Array2, ArrayView2}; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; use smallvec::SmallVec; -static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; /// Return the matrix Operator resulting from a block of Instructions. #[pyfunction] diff --git a/crates/accelerate/src/dense_layout.rs b/crates/accelerate/src/dense_layout.rs index 7cb54140761..9529742d7e6 100644 --- a/crates/accelerate/src/dense_layout.rs +++ b/crates/accelerate/src/dense_layout.rs @@ -15,8 +15,8 @@ use ahash::RandomState; use hashbrown::{HashMap, HashSet}; use indexmap::IndexSet; use ndarray::prelude::*; +use numpy::IntoPyArray; use numpy::PyReadonlyArray2; -use numpy::ToPyArray; use rayon::prelude::*; use pyo3::prelude::*; @@ -108,10 +108,35 @@ pub fn best_subset( use_error: bool, symmetric_coupling_map: bool, error_matrix: PyReadonlyArray2, -) -> PyResult<(PyObject, PyObject, PyObject)> { +) -> (PyObject, PyObject, PyObject) { let coupling_adj_mat = coupling_adjacency.as_array(); - let coupling_shape = coupling_adj_mat.shape(); let err = error_matrix.as_array(); + let [rows, cols, best_map] = best_subset_inner( + num_qubits, + coupling_adj_mat, + num_meas, + num_cx, + use_error, + symmetric_coupling_map, + err, + ); + ( + rows.into_pyarray_bound(py).into(), + cols.into_pyarray_bound(py).into(), + best_map.into_pyarray_bound(py).into(), + ) +} + +pub fn best_subset_inner( + num_qubits: usize, + coupling_adj_mat: ArrayView2, + num_meas: usize, + num_cx: usize, + use_error: bool, + symmetric_coupling_map: bool, + err: ArrayView2, +) -> [Vec; 3] { + let coupling_shape = coupling_adj_mat.shape(); let avg_meas_err = err.diag().mean().unwrap(); let map_fn = |k| -> SubsetResult { @@ -172,7 +197,7 @@ pub fn best_subset( SubsetResult { count: 0, map: Vec::new(), - error: std::f64::INFINITY, + error: f64::INFINITY, subgraph: Vec::new(), } }; @@ -216,11 +241,7 @@ pub fn best_subset( let rows: Vec = new_cmap.iter().map(|edge| edge[0]).collect(); let cols: Vec = new_cmap.iter().map(|edge| edge[1]).collect(); - Ok(( - rows.to_pyarray_bound(py).into(), - cols.to_pyarray_bound(py).into(), - best_map.to_pyarray_bound(py).into(), - )) + [rows, cols, best_map] } #[pymodule] diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 1fd5fd7834f..01725269bb8 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -21,9 +21,9 @@ use std::f64::consts::PI; use std::ops::Deref; use std::str::FromStr; -use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::PyString; +use pyo3::types::{PyList, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; @@ -31,7 +31,8 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; -use qiskit_circuit::SliceOrInt; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; +use qiskit_circuit::util::c64; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -96,46 +97,15 @@ impl OneQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: Vec<(String, SmallVec<[f64; 3]>)> = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=-1 - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } @@ -855,16 +825,16 @@ pub fn params_xyx(unitary: PyReadonlyArray2) -> [f64; 4] { fn params_xzx_inner(umat: ArrayView2) -> [f64; 4] { let det = det_one_qubit(umat); - let phase = (Complex64::new(0., -1.) * det.ln()).re / 2.; + let phase = det.ln().im / 2.; let sqrt_det = det.sqrt(); let mat_zyz = arr2(&[ [ - Complex64::new((umat[[0, 0]] / sqrt_det).re, (umat[[1, 0]] / sqrt_det).im), - Complex64::new((umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), + c64((umat[[0, 0]] / sqrt_det).re, (umat[[1, 0]] / sqrt_det).im), + c64((umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), ], [ - Complex64::new(-(umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), - Complex64::new((umat[[0, 0]] / sqrt_det).re, -(umat[[1, 0]] / sqrt_det).im), + c64(-(umat[[1, 0]] / sqrt_det).re, (umat[[0, 0]] / sqrt_det).im), + c64((umat[[0, 0]] / sqrt_det).re, -(umat[[1, 0]] / sqrt_det).im), ], ]); let [theta, phi, lam, phase_zxz] = params_zxz_inner(mat_zyz.view()); diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs new file mode 100644 index 00000000000..ceaba2946b3 --- /dev/null +++ b/crates/accelerate/src/isometry.rs @@ -0,0 +1,363 @@ +// 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::ops::BitAnd; + +use approx::abs_diff_eq; +use num_complex::{Complex64, ComplexFloat}; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use hashbrown::HashSet; +use itertools::Itertools; +use ndarray::prelude::*; +use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; + +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; +use qiskit_circuit::util::C_ZERO; + +/// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or +/// basis_state=1 respectively +#[pyfunction] +pub fn reverse_qubit_state( + py: Python, + state: [Complex64; 2], + basis_state: usize, + epsilon: f64, +) -> PyObject { + reverse_qubit_state_inner(&state, basis_state, epsilon) + .into_pyarray_bound(py) + .into() +} + +#[inline(always)] +fn l2_norm(vec: &[Complex64]) -> f64 { + vec.iter() + .fold(0., |acc, elem| acc + elem.norm_sqr()) + .sqrt() +} + +fn reverse_qubit_state_inner( + state: &[Complex64; 2], + basis_state: usize, + epsilon: f64, +) -> Array2 { + let r = l2_norm(state); + let r_inv = 1. / r; + if r < epsilon { + Array2::eye(2) + } else if basis_state == 0 { + array![ + [state[0].conj() * r_inv, state[1].conj() * r_inv], + [-state[1] * r_inv, state[0] * r_inv], + ] + } else { + array![ + [-state[1] * r_inv, state[0] * r_inv], + [state[0].conj() * r_inv, state[1].conj() * r_inv], + ] + } +} + +/// This method finds the single-qubit gates for a UCGate to disentangle a qubit: +/// we consider the n-qubit state v[:,0] starting with k zeros (in the computational basis). +/// The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). + +#[pyfunction] +pub fn find_squs_for_disentangling( + py: Python, + v: PyReadonlyArray2, + k: usize, + s: usize, + epsilon: f64, + n: usize, +) -> Vec { + let v = v.as_array(); + let k_prime = 0; + let i_start = if b(k, s + 1) == 0 { + a(k, s + 1) + } else { + a(k, s + 1) + 1 + }; + let mut output: Vec> = (0..i_start).map(|_| Array2::eye(2)).collect(); + let mut squs: Vec> = (i_start..2_usize.pow((n - s - 1) as u32)) + .map(|i| { + reverse_qubit_state_inner( + &[ + v[[2 * i * 2_usize.pow(s as u32) + b(k, s), k_prime]], + v[[(2 * i + 1) * 2_usize.pow(s as u32) + b(k, s), k_prime]], + ], + k_s(k, s), + epsilon, + ) + }) + .collect(); + output.append(&mut squs); + output + .into_iter() + .map(|x| x.into_pyarray_bound(py).into()) + .collect() +} + +#[pyfunction] +pub fn apply_ucg( + py: Python, + m: PyReadonlyArray2, + k: usize, + single_qubit_gates: Vec>, +) -> PyObject { + let mut m = m.as_array().to_owned(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + let spacing: usize = 2_usize.pow(num_qubits - k as u32 - 1); + for j in 0..2_usize.pow(num_qubits - 1) { + let i = (j / spacing) * spacing + j; + let gate_index = i / (2_usize.pow(num_qubits - k as u32)); + for col in 0..num_col { + let gate = single_qubit_gates[gate_index].as_array(); + let a = m[[i, col]]; + let b = m[[i + spacing, col]]; + m[[i, col]] = gate[[0, 0]] * a + gate[[0, 1]] * b; + m[[i + spacing, col]] = gate[[1, 0]] * a + gate[[1, 1]] * b; + } + } + m.into_pyarray_bound(py).into() +} + +#[inline(always)] +fn bin_to_int(bin: &[u8]) -> usize { + bin.iter() + .fold(0_usize, |acc, digit| (acc << 1) + *digit as usize) +} + +#[pyfunction] +pub fn apply_diagonal_gate( + py: Python, + m: PyReadonlyArray2, + action_qubit_labels: Vec, + diag: PyReadonlyArray1, +) -> PyResult { + let diag = diag.as_slice()?; + let mut m = m.as_array().to_owned(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + for state in std::iter::repeat([0_u8, 1_u8]) + .take(num_qubits as usize) + .multi_cartesian_product() + { + let diag_index = action_qubit_labels + .iter() + .fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); + let i = bin_to_int(&state); + for j in 0..num_col { + m[[i, j]] = diag[diag_index] * m[[i, j]] + } + } + Ok(m.into_pyarray_bound(py).into()) +} + +#[pyfunction] +pub fn apply_diagonal_gate_to_diag( + mut m_diagonal: Vec, + action_qubit_labels: Vec, + diag: PyReadonlyArray1, + num_qubits: usize, +) -> PyResult> { + let diag = diag.as_slice()?; + if m_diagonal.is_empty() { + return Ok(m_diagonal); + } + for state in std::iter::repeat([0_u8, 1_u8]) + .take(num_qubits) + .multi_cartesian_product() + .take(m_diagonal.len()) + { + let diag_index = action_qubit_labels + .iter() + .fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); + let i = bin_to_int(&state); + m_diagonal[i] *= diag[diag_index] + } + Ok(m_diagonal) +} + +/// Helper method for _apply_multi_controlled_gate. This constructs the basis states the MG gate +/// is acting on for a specific state state_free of the qubits we neither control nor act on +fn construct_basis_states( + state_free: &[u8], + control_set: &HashSet, + target_label: usize, +) -> [usize; 2] { + let size = state_free.len() + control_set.len() + 1; + let mut e1: usize = 0; + let mut e2: usize = 0; + let mut j = 0; + for i in 0..size { + e1 <<= 1; + e2 <<= 1; + if control_set.contains(&i) { + e1 += 1; + e2 += 1; + } else if i == target_label { + e2 += 1; + } else { + e1 += state_free[j] as usize; + e2 += state_free[j] as usize; + j += 1 + } + } + [e1, e2] +} + +#[pyfunction] +pub fn apply_multi_controlled_gate( + py: Python, + m: PyReadonlyArray2, + control_labels: Vec, + target_label: usize, + gate: PyReadonlyArray2, +) -> PyObject { + let mut m = m.as_array().to_owned(); + let gate = gate.as_array(); + let shape = m.shape(); + let num_qubits = shape[0].ilog2(); + let num_col = shape[1]; + let free_qubits = num_qubits as usize - control_labels.len() - 1; + let control_set: HashSet = control_labels.into_iter().collect(); + if free_qubits == 0 { + let [e1, e2] = construct_basis_states(&[], &control_set, target_label); + for i in 0..num_col { + let temp: Vec<_> = gate + .dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) + .into_iter() + .take(2) + .collect(); + m[[e1, i]] = temp[0]; + m[[e2, i]] = temp[1]; + } + return m.into_pyarray_bound(py).into(); + } + for state_free in std::iter::repeat([0_u8, 1_u8]) + .take(free_qubits) + .multi_cartesian_product() + { + let [e1, e2] = construct_basis_states(&state_free, &control_set, target_label); + for i in 0..num_col { + let temp: Vec<_> = gate + .dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) + .into_iter() + .take(2) + .collect(); + m[[e1, i]] = temp[0]; + m[[e2, i]] = temp[1]; + } + } + m.into_pyarray_bound(py).into() +} + +#[pyfunction] +pub fn ucg_is_identity_up_to_global_phase( + single_qubit_gates: Vec>, + epsilon: f64, +) -> bool { + let global_phase: Complex64 = if single_qubit_gates[0].as_array()[[0, 0]].abs() >= epsilon { + single_qubit_gates[0].as_array()[[0, 0]].finv() + } else { + return false; + }; + for raw_gate in single_qubit_gates { + let gate = raw_gate.as_array(); + if !abs_diff_eq!( + gate.mapv(|x| x * global_phase), + aview2(&ONE_QUBIT_IDENTITY), + epsilon = 1e-8 // Default tolerance from numpy for allclose() + ) { + return false; + } + } + true +} + +#[pyfunction] +fn diag_is_identity_up_to_global_phase(diag: Vec, epsilon: f64) -> bool { + let global_phase: Complex64 = if diag[0].abs() >= epsilon { + diag[0].finv() + } else { + return false; + }; + for d in diag { + if (global_phase * d - 1.0).abs() >= epsilon { + return false; + } + } + true +} + +#[pyfunction] +pub fn merge_ucgate_and_diag( + py: Python, + single_qubit_gates: Vec>, + diag: Vec, +) -> Vec { + single_qubit_gates + .iter() + .enumerate() + .map(|(i, raw_gate)| { + let gate = raw_gate.as_array(); + let res = aview2(&[[diag[2 * i], C_ZERO], [C_ZERO, diag[2 * i + 1]]]).dot(&gate); + res.into_pyarray_bound(py).into() + }) + .collect() +} + +#[inline(always)] +#[pyfunction] +fn k_s(k: usize, s: usize) -> usize { + if k == 0 { + 0 + } else { + let filter = 1 << s; + k.bitand(filter) >> s + } +} + +#[inline(always)] +#[pyfunction] +fn a(k: usize, s: usize) -> usize { + k / 2_usize.pow(s as u32) +} + +#[inline(always)] +#[pyfunction] +fn b(k: usize, s: usize) -> usize { + k - (a(k, s) * 2_usize.pow(s as u32)) +} + +#[pymodule] +pub fn isometry(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(diag_is_identity_up_to_global_phase))?; + m.add_wrapped(wrap_pyfunction!(find_squs_for_disentangling))?; + m.add_wrapped(wrap_pyfunction!(reverse_qubit_state))?; + m.add_wrapped(wrap_pyfunction!(apply_ucg))?; + m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate))?; + m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate_to_diag))?; + m.add_wrapped(wrap_pyfunction!(apply_multi_controlled_gate))?; + m.add_wrapped(wrap_pyfunction!(ucg_is_identity_up_to_global_phase))?; + m.add_wrapped(wrap_pyfunction!(merge_ucgate_and_diag))?; + m.add_wrapped(wrap_pyfunction!(a))?; + m.add_wrapped(wrap_pyfunction!(b))?; + m.add_wrapped(wrap_pyfunction!(k_s))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index bb7621dce34..dcfbdc9f187 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -19,6 +19,7 @@ pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; @@ -27,7 +28,9 @@ pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; pub mod stochastic_swap; +pub mod synthesis; pub mod two_qubit_decompose; +pub mod uc_gate; pub mod utils; pub mod vf2_layout; diff --git a/crates/accelerate/src/nlayout.rs b/crates/accelerate/src/nlayout.rs index 1a0b73b25fe..b3709d2804b 100644 --- a/crates/accelerate/src/nlayout.rs +++ b/crates/accelerate/src/nlayout.rs @@ -107,8 +107,8 @@ impl NLayout { physical_qubits: usize, ) -> Self { let mut res = NLayout { - virt_to_phys: vec![PhysicalQubit(std::u32::MAX); virtual_qubits], - phys_to_virt: vec![VirtualQubit(std::u32::MAX); physical_qubits], + virt_to_phys: vec![PhysicalQubit(u32::MAX); virtual_qubits], + phys_to_virt: vec![VirtualQubit(u32::MAX); physical_qubits], }; for (virt, phys) in qubit_indices { res.virt_to_phys[virt.index()] = phys; @@ -184,7 +184,7 @@ impl NLayout { #[staticmethod] pub fn from_virtual_to_physical(virt_to_phys: Vec) -> PyResult { - let mut phys_to_virt = vec![VirtualQubit(std::u32::MAX); virt_to_phys.len()]; + let mut phys_to_virt = vec![VirtualQubit(u32::MAX); virt_to_phys.len()]; for (virt, phys) in virt_to_phys.iter().enumerate() { phys_to_virt[phys.index()] = VirtualQubit(virt.try_into()?); } diff --git a/crates/accelerate/src/pauli_exp_val.rs b/crates/accelerate/src/pauli_exp_val.rs index 29f741f6cf4..8ee4b019b3e 100644 --- a/crates/accelerate/src/pauli_exp_val.rs +++ b/crates/accelerate/src/pauli_exp_val.rs @@ -19,6 +19,7 @@ use pyo3::wrap_pyfunction; use rayon::prelude::*; use crate::getenv_use_multiple_threads; +use qiskit_circuit::util::c64; const PARALLEL_THRESHOLD: usize = 19; @@ -32,7 +33,7 @@ pub fn fast_sum_with_simd(simd: S, values: &[f64]) -> f64 { sum + tail.iter().sum::() } -/// Compute the pauli expectatation value of a statevector without x +/// Compute the pauli expectation value of a statevector without x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, /)")] pub fn expval_pauli_no_x( @@ -63,7 +64,7 @@ pub fn expval_pauli_no_x( } } -/// Compute the pauli expectatation value of a statevector with x +/// Compute the pauli expectation value of a statevector with x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, x_mask, phase, x_max, /)")] pub fn expval_pauli_with_x( @@ -88,7 +89,7 @@ pub fn expval_pauli_with_x( let index_0 = ((i << 1) & mask_u) | (i & mask_l); let index_1 = index_0 ^ x_mask; let val_0 = (phase - * Complex64::new( + * c64( data_arr[index_1].re * data_arr[index_0].re + data_arr[index_1].im * data_arr[index_0].im, data_arr[index_1].im * data_arr[index_0].re @@ -96,7 +97,7 @@ pub fn expval_pauli_with_x( )) .re; let val_1 = (phase - * Complex64::new( + * c64( data_arr[index_0].re * data_arr[index_1].re + data_arr[index_0].im * data_arr[index_1].im, data_arr[index_0].im * data_arr[index_1].re @@ -121,7 +122,7 @@ pub fn expval_pauli_with_x( } } -/// Compute the pauli expectatation value of a density matrix without x +/// Compute the pauli expectation value of a density matrix without x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, /)")] pub fn density_expval_pauli_no_x( @@ -153,7 +154,7 @@ pub fn density_expval_pauli_no_x( } } -/// Compute the pauli expectatation value of a density matrix with x +/// Compute the pauli expectation value of a density matrix with x #[pyfunction] #[pyo3(text_signature = "(data, num_qubits, z_mask, x_mask, phase, x_max, /)")] pub fn density_expval_pauli_with_x( diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index 1cb539d9598..a1e5e9ce641 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -15,6 +15,7 @@ use pyo3::prelude::*; use pyo3::Python; use hashbrown::HashSet; +use ndarray::prelude::*; use numpy::{IntoPyArray, PyArray, PyReadonlyArray2}; use rand::prelude::*; use rand_pcg::Pcg64Mcg; @@ -29,6 +30,8 @@ use super::sabre_dag::SabreDAG; use super::swap_map::SwapMap; use super::{Heuristic, NodeBlockResults, SabreResult}; +use crate::dense_layout::best_subset_inner; + #[pyfunction] #[pyo3(signature = (dag, neighbor_table, distance_matrix, heuristic, max_iterations, num_swap_trials, num_random_trials, seed=None, partial_layouts=vec![]))] pub fn sabre_layout_and_routing( @@ -52,6 +55,12 @@ pub fn sabre_layout_and_routing( let mut starting_layouts: Vec>> = (0..num_random_trials).map(|_| vec![]).collect(); starting_layouts.append(&mut partial_layouts); + // Run a dense layout trial + starting_layouts.push(compute_dense_starting_layout( + dag.num_qubits, + &target, + run_in_parallel, + )); let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), None => Pcg64Mcg::from_entropy(), @@ -208,3 +217,26 @@ fn layout_trial( .collect(); (initial_layout, final_permutation, sabre_result) } + +fn compute_dense_starting_layout( + num_qubits: usize, + target: &RoutingTargetView, + run_in_parallel: bool, +) -> Vec> { + let mut adj_matrix = target.distance.to_owned(); + if run_in_parallel { + adj_matrix.par_mapv_inplace(|x| if x == 1. { 1. } else { 0. }); + } else { + adj_matrix.mapv_inplace(|x| if x == 1. { 1. } else { 0. }); + } + let [_rows, _cols, map] = best_subset_inner( + num_qubits, + adj_matrix.view(), + 0, + 0, + false, + true, + aview2(&[[0.]]), + ); + map.into_iter().map(|x| Some(x as u32)).collect() +} diff --git a/crates/accelerate/src/sabre/sabre_dag.rs b/crates/accelerate/src/sabre/sabre_dag.rs index aa35a5d7942..d783f384462 100644 --- a/crates/accelerate/src/sabre/sabre_dag.rs +++ b/crates/accelerate/src/sabre/sabre_dag.rs @@ -27,7 +27,7 @@ pub struct DAGNode { } /// A DAG representation of the logical circuit to be routed. This represents the same dataflow -/// dependences as the Python-space [DAGCircuit], but without any information about _what_ the +/// dependencies as the Python-space [DAGCircuit], but without any information about _what_ the /// operations being performed are. Note that all the qubit references here are to "virtual" /// qubits, that is, the qubits are those specified by the user. This DAG does not need to be /// full-width on the hardware. diff --git a/crates/accelerate/src/sampled_exp_val.rs b/crates/accelerate/src/sampled_exp_val.rs index b51ca3c98f0..0b8836a9416 100644 --- a/crates/accelerate/src/sampled_exp_val.rs +++ b/crates/accelerate/src/sampled_exp_val.rs @@ -18,6 +18,7 @@ use pyo3::prelude::*; use pyo3::wrap_pyfunction; use crate::pauli_exp_val::fast_sum; +use qiskit_circuit::util::c64; const OPER_TABLE_SIZE: usize = (b'Z' as usize) + 1; const fn generate_oper_table() -> [[f64; 2]; OPER_TABLE_SIZE] { @@ -81,7 +82,7 @@ pub fn sampled_expval_complex( let out: Complex64 = oper_strs .into_iter() .enumerate() - .map(|(idx, string)| coeff_arr[idx] * Complex64::new(bitstring_expval(&dist, string), 0.)) + .map(|(idx, string)| coeff_arr[idx] * c64(bitstring_expval(&dist, string), 0.)) .sum(); Ok(out.re) } diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 5d6a82df794..8a51d8ee781 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -23,6 +23,7 @@ use hashbrown::HashMap; use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; use num_complex::Complex64; use num_traits::Zero; +use qiskit_circuit::util::{c64, C_ONE, C_ZERO}; use rayon::prelude::*; use crate::rayon_ext::*; @@ -141,7 +142,9 @@ impl ZXPaulis { phases: &Bound>, coeffs: &Bound>, ) -> PyResult { - let &[num_ops, num_qubits] = x.shape() else { unreachable!("PyArray2 must be 2D") }; + let &[num_ops, num_qubits] = x.shape() else { + unreachable!("PyArray2 must be 2D") + }; if z.shape() != [num_ops, num_qubits] { return Err(PyValueError::new_err(format!( "'x' and 'z' have different shapes: {:?} and {:?}", @@ -255,9 +258,9 @@ impl<'py> ZXPaulisView<'py> { let ys = (xs & zs).count_ones(); match (phase as u32 + ys) % 4 { 0 => coeff, - 1 => Complex64::new(coeff.im, -coeff.re), - 2 => Complex64::new(-coeff.re, -coeff.im), - 3 => Complex64::new(-coeff.im, coeff.re), + 1 => c64(coeff.im, -coeff.re), + 2 => c64(-coeff.re, -coeff.im), + 3 => c64(-coeff.im, coeff.re), _ => unreachable!(), } }) @@ -309,10 +312,10 @@ impl MatrixCompressedPaulis { .zip(self.z_like.drain(..)) .zip(self.coeffs.drain(..)) { - *hash_table.entry(key).or_insert(Complex64::new(0.0, 0.0)) += coeff; + *hash_table.entry(key).or_insert(C_ZERO) += coeff; } for ((x, z), coeff) in hash_table { - if coeff == Complex64::new(0.0, 0.0) { + if coeff.is_zero() { continue; } self.x_like.push(x); @@ -345,7 +348,7 @@ pub fn decompose_dense( let mut coeffs = vec![]; if num_qubits > 0 { decompose_dense_inner( - Complex64::new(1.0, 0.0), + C_ONE, num_qubits, &[], operator.as_array(), @@ -419,7 +422,7 @@ fn decompose_dense_inner( ) { if num_qubits == 0 { // It would be safe to `return` here, but if it's unreachable then LLVM is allowed to - // optimise out this branch entirely in release mode, which is good for a ~2% speedup. + // optimize out this branch entirely in release mode, which is good for a ~2% speedup. unreachable!("should not call this with an empty operator") } // Base recursion case. @@ -527,10 +530,10 @@ fn to_matrix_dense_inner(paulis: &MatrixCompressedPaulis, parallel: bool) -> Vec out }; let write_row = |(i_row, row): (usize, &mut [Complex64])| { - // Doing the initialisation here means that when we're in parallel contexts, we do the + // Doing the initialization here means that when we're in parallel contexts, we do the // zeroing across the whole threadpool. This also seems to give a speed-up in serial // contexts, but I don't understand that. ---Jake - row.fill(Complex64::new(0.0, 0.0)); + row.fill(C_ZERO); for ((&x_like, &z_like), &coeff) in paulis .x_like .iter() @@ -665,7 +668,7 @@ macro_rules! impl_to_matrix_sparse { ((i_row as $uint_ty) ^ (paulis.x_like[a] as $uint_ty)) .cmp(&((i_row as $uint_ty) ^ (paulis.x_like[b] as $uint_ty))) }); - let mut running = Complex64::new(0.0, 0.0); + let mut running = C_ZERO; let mut prev_index = i_row ^ (paulis.x_like[order[0]] as usize); for (x_like, z_like, coeff) in order .iter() @@ -719,7 +722,7 @@ macro_rules! impl_to_matrix_sparse { // The parallel overhead from splitting a subtask is fairly high (allocating and // potentially growing a couple of vecs), so we're trading off some of Rayon's ability - // to keep threads busy by subdivision with minimising overhead; we're setting the + // to keep threads busy by subdivision with minimizing overhead; we're setting the // chunk size such that the iterator will have as many elements as there are threads. let num_threads = rayon::current_num_threads(); let chunk_size = (side + num_threads - 1) / num_threads; @@ -736,7 +739,7 @@ macro_rules! impl_to_matrix_sparse { // Since we compressed the Paulis by summing equal elements, we're // lower-bounded on the number of elements per row by this value, up to // cancellations. This should be a reasonable trade-off between sometimes - // expandin the vector and overallocation. + // expanding the vector and overallocation. let mut values = Vec::::with_capacity(chunk_size * (num_ops + 1) / 2); let mut indices = Vec::<$int_ty>::with_capacity(chunk_size * (num_ops + 1) / 2); @@ -746,7 +749,7 @@ macro_rules! impl_to_matrix_sparse { (i_row as $uint_ty ^ paulis.x_like[a] as $uint_ty) .cmp(&(i_row as $uint_ty ^ paulis.x_like[b] as $uint_ty)) }); - let mut running = Complex64::new(0.0, 0.0); + let mut running = C_ZERO; let mut prev_index = i_row ^ (paulis.x_like[order[0]] as usize); for (x_like, z_like, coeff) in order .iter() @@ -842,11 +845,11 @@ mod tests { // Deliberately using multiples of small powers of two so the floating-point addition // of them is associative. coeffs: vec![ - Complex64::new(0.25, 0.5), - Complex64::new(0.125, 0.25), - Complex64::new(0.375, 0.125), - Complex64::new(-0.375, 0.0625), - Complex64::new(-0.5, -0.25), + c64(0.25, 0.5), + c64(0.125, 0.25), + c64(0.375, 0.125), + c64(-0.375, 0.0625), + c64(-0.5, -0.25), ], } } diff --git a/crates/accelerate/src/stochastic_swap.rs b/crates/accelerate/src/stochastic_swap.rs index bc13325d8d9..d4e3890b9cc 100644 --- a/crates/accelerate/src/stochastic_swap.rs +++ b/crates/accelerate/src/stochastic_swap.rs @@ -112,10 +112,10 @@ fn swap_trial( let mut new_cost: f64; let mut dist: f64; - let mut optimal_start = PhysicalQubit::new(std::u32::MAX); - let mut optimal_end = PhysicalQubit::new(std::u32::MAX); - let mut optimal_start_qubit = VirtualQubit::new(std::u32::MAX); - let mut optimal_end_qubit = VirtualQubit::new(std::u32::MAX); + let mut optimal_start = PhysicalQubit::new(u32::MAX); + let mut optimal_end = PhysicalQubit::new(u32::MAX); + let mut optimal_start_qubit = VirtualQubit::new(u32::MAX); + let mut optimal_end_qubit = VirtualQubit::new(u32::MAX); let mut scale = Array2::zeros((num_qubits, num_qubits)); @@ -270,7 +270,7 @@ pub fn swap_trials( // unless force threads is set. let run_in_parallel = getenv_use_multiple_threads(); - let mut best_depth = std::usize::MAX; + let mut best_depth = usize::MAX; let mut best_edges: Option = None; let mut best_layout: Option = None; if run_in_parallel { diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs new file mode 100644 index 00000000000..2fa158ea761 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -0,0 +1,190 @@ +// 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::QiskitError; +use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; +use pyo3::prelude::*; + +mod utils; + +#[pyfunction] +#[pyo3(signature = (mat, ncols=None, full_elim=false))] +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// Modifies the matrix mat in-place, and returns the permutation perm that was done +/// on the rows during the process. perm[0 : rank] represents the indices of linearly +/// independent rows in the original matrix. +/// Args: +/// mat: a boolean matrix with n rows and m columns +/// ncols: the number of columns for the gaussian elimination, +/// if ncols=None, then the elimination is done over all the columns +/// full_elim: whether to do a full elimination, or partial (upper triangular form) +/// Returns: +/// perm: the permutation perm that was done on the rows during the process +fn gauss_elimination_with_perm( + py: Python, + mut mat: PyReadwriteArray2, + ncols: Option, + full_elim: Option, +) -> PyResult { + let matmut = mat.as_array_mut(); + let perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim); + Ok(perm.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat, ncols=None, full_elim=false))] +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// This function modifies the input matrix in-place. +/// Args: +/// mat: a boolean matrix with n rows and m columns +/// ncols: the number of columns for the gaussian elimination, +/// if ncols=None, then the elimination is done over all the columns +/// full_elim: whether to do a full elimination, or partial (upper triangular form) +fn gauss_elimination( + mut mat: PyReadwriteArray2, + ncols: Option, + full_elim: Option, +) { + let matmut = mat.as_array_mut(); + let _perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim); +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Given a boolean matrix mat after Gaussian elimination, computes its rank +/// (i.e. simply the number of nonzero rows) +/// Args: +/// mat: a boolean matrix after gaussian elimination +/// Returns: +/// rank: the rank of the matrix +fn compute_rank_after_gauss_elim(py: Python, mat: PyReadonlyArray2) -> PyResult { + let view = mat.as_array(); + let rank = utils::compute_rank_after_gauss_elim_inner(view); + Ok(rank.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Given a boolean matrix mat computes its rank +/// Args: +/// mat: a boolean matrix +/// Returns: +/// rank: the rank of the matrix +fn compute_rank(py: Python, mat: PyReadonlyArray2) -> PyResult { + let rank = utils::compute_rank_inner(mat.as_array()); + Ok(rank.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (mat, verify=false))] +/// Given a boolean matrix mat, tries to calculate its inverse matrix +/// Args: +/// mat: a boolean square matrix. +/// verify: if True asserts that the multiplication of mat and its inverse is the identity matrix. +/// Returns: +/// the inverse matrix. +/// Raises: +/// QiskitError: if the matrix is not square or not invertible. +pub fn calc_inverse_matrix( + py: Python, + mat: PyReadonlyArray2, + verify: Option, +) -> PyResult>> { + let view = mat.as_array(); + let invmat = + utils::calc_inverse_matrix_inner(view, verify.is_some()).map_err(QiskitError::new_err)?; + Ok(invmat.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat1, mat2))] +/// Binary matrix multiplication +/// Args: +/// mat1: a boolean matrix +/// mat2: a boolean matrix +/// Returns: +/// a boolean matrix which is the multiplication of mat1 and mat2 +/// Raises: +/// QiskitError: if the dimensions of mat1 and mat2 do not match +pub fn binary_matmul( + py: Python, + mat1: PyReadonlyArray2, + mat2: PyReadonlyArray2, +) -> PyResult>> { + let view1 = mat1.as_array(); + let view2 = mat2.as_array(); + let result = utils::binary_matmul_inner(view1, view2).map_err(QiskitError::new_err)?; + Ok(result.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat, ctrl, trgt))] +/// Perform ROW operation on a matrix mat +fn row_op(mut mat: PyReadwriteArray2, ctrl: usize, trgt: usize) { + let matmut = mat.as_array_mut(); + utils::_add_row_or_col(matmut, &false, ctrl, trgt) +} + +#[pyfunction] +#[pyo3(signature = (mat, ctrl, trgt))] +/// Perform COL operation on a matrix mat (in the inverse direction) +fn col_op(mut mat: PyReadwriteArray2, ctrl: usize, trgt: usize) { + let matmut = mat.as_array_mut(); + utils::_add_row_or_col(matmut, &true, trgt, ctrl) +} + +#[pyfunction] +#[pyo3(signature = (num_qubits, seed=None))] +/// Generate a random invertible n x n binary matrix. +/// Args: +/// num_qubits: the matrix size. +/// seed: a random seed. +/// Returns: +/// np.ndarray: A random invertible binary matrix of size num_qubits. +fn random_invertible_binary_matrix( + py: Python, + num_qubits: usize, + seed: Option, +) -> PyResult>> { + let matrix = utils::random_invertible_binary_matrix_inner(num_qubits, seed); + Ok(matrix.into_pyarray_bound(py).unbind()) +} + +#[pyfunction] +#[pyo3(signature = (mat))] +/// Check that a binary matrix is invertible. +/// Args: +/// mat: a binary matrix. +/// Returns: +/// bool: True if mat in invertible and False otherwise. +fn check_invertible_binary_matrix(py: Python, mat: PyReadonlyArray2) -> PyResult { + let view = mat.as_array(); + let out = utils::check_invertible_binary_matrix_inner(view); + Ok(out.to_object(py)) +} + +#[pymodule] +pub fn linear(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(gauss_elimination_with_perm))?; + m.add_wrapped(wrap_pyfunction!(gauss_elimination))?; + m.add_wrapped(wrap_pyfunction!(compute_rank_after_gauss_elim))?; + m.add_wrapped(wrap_pyfunction!(compute_rank))?; + m.add_wrapped(wrap_pyfunction!(calc_inverse_matrix))?; + m.add_wrapped(wrap_pyfunction!(row_op))?; + m.add_wrapped(wrap_pyfunction!(col_op))?; + m.add_wrapped(wrap_pyfunction!(binary_matmul))?; + m.add_wrapped(wrap_pyfunction!(random_invertible_binary_matrix))?; + m.add_wrapped(wrap_pyfunction!(check_invertible_binary_matrix))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/linear/utils.rs b/crates/accelerate/src/synthesis/linear/utils.rs new file mode 100644 index 00000000000..b4dbf499308 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/utils.rs @@ -0,0 +1,200 @@ +// 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::{concatenate, s, Array2, ArrayView2, ArrayViewMut2, Axis}; +use rand::{Rng, SeedableRng}; +use rand_pcg::Pcg64Mcg; + +/// Binary matrix multiplication +pub fn binary_matmul_inner( + mat1: ArrayView2, + mat2: ArrayView2, +) -> Result, String> { + let n1_rows = mat1.nrows(); + let n1_cols = mat1.ncols(); + let n2_rows = mat2.nrows(); + let n2_cols = mat2.ncols(); + if n1_cols != n2_rows { + return Err(format!( + "Cannot multiply matrices with inappropriate dimensions {}, {}", + n1_cols, n2_rows + )); + } + + 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) + })) +} + +/// Gauss elimination of a matrix mat with m rows and n columns. +/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols] +/// Returns the matrix mat, and the permutation perm that was done on the rows during the process. +/// perm[0 : rank] represents the indices of linearly independent rows in the original matrix. +pub fn gauss_elimination_with_perm_inner( + mut mat: ArrayViewMut2, + ncols: Option, + full_elim: Option, +) -> Vec { + let (m, mut n) = (mat.nrows(), mat.ncols()); // no. of rows and columns + if let Some(ncols_val) = ncols { + n = usize::min(n, ncols_val); // no. of active columns + } + let mut perm: Vec = Vec::from_iter(0..m); + + let mut r = 0; // current rank + let k = 0; // current pivot column + let mut new_k = 0; + while (r < m) && (k < n) { + let mut is_non_zero = false; + let mut new_r = r; + for j in k..n { + new_k = k; + for i in r..m { + if mat[(i, j)] { + is_non_zero = true; + new_k = j; + new_r = i; + break; + } + } + if is_non_zero { + break; + } + } + if !is_non_zero { + return perm; // A is in the canonical form + } + + if new_r != r { + let temp_r = mat.slice_mut(s![r, ..]).to_owned(); + let temp_new_r = mat.slice_mut(s![new_r, ..]).to_owned(); + mat.slice_mut(s![r, ..]).assign(&temp_new_r); + mat.slice_mut(s![new_r, ..]).assign(&temp_r); + perm.swap(r, new_r); + } + + // Copy source row to avoid trying multiple borrows at once + let row0 = mat.row(r).to_owned(); + mat.axis_iter_mut(Axis(0)) + .enumerate() + .filter(|(i, row)| { + (full_elim == Some(true) && (*i < r) && row[new_k]) + || (*i > r && *i < m && row[new_k]) + }) + .for_each(|(_i, mut row)| { + row.zip_mut_with(&row0, |x, &y| *x ^= y); + }); + + r += 1; + } + perm +} + +/// Given a boolean matrix A after Gaussian elimination, computes its rank +/// (i.e. simply the number of nonzero rows) +pub fn compute_rank_after_gauss_elim_inner(mat: ArrayView2) -> usize { + let rank: usize = mat + .axis_iter(Axis(0)) + .map(|row| row.fold(false, |out, val| out | *val) as usize) + .sum(); + rank +} + +/// Given a boolean matrix mat computes its rank +pub fn compute_rank_inner(mat: ArrayView2) -> usize { + let mut temp_mat = mat.to_owned(); + gauss_elimination_with_perm_inner(temp_mat.view_mut(), None, Some(false)); + let rank = compute_rank_after_gauss_elim_inner(temp_mat.view()); + rank +} + +/// Given a square boolean matrix mat, tries to compute its inverse. +pub fn calc_inverse_matrix_inner( + mat: ArrayView2, + verify: bool, +) -> Result, String> { + if mat.shape()[0] != mat.shape()[1] { + return Err("Matrix to invert is a non-square matrix.".to_string()); + } + let n = mat.shape()[0]; + + // concatenate the matrix and identity + let identity_matrix: Array2 = Array2::from_shape_fn((n, n), |(i, j)| i == j); + let mut mat1 = concatenate(Axis(1), &[mat.view(), identity_matrix.view()]).unwrap(); + + gauss_elimination_with_perm_inner(mat1.view_mut(), None, Some(true)); + + let r = compute_rank_after_gauss_elim_inner(mat1.slice(s![.., 0..n])); + if r < n { + return Err("The matrix is not invertible.".to_string()); + } + + let invmat = mat1.slice(s![.., n..2 * n]).to_owned(); + + if verify { + let mat2 = binary_matmul_inner(mat, (&invmat).into())?; + let identity_matrix: Array2 = Array2::from_shape_fn((n, n), |(i, j)| i == j); + if mat2.ne(&identity_matrix) { + return Err("The inverse matrix is not correct.".to_string()); + } + } + + Ok(invmat) +} + +/// Mutate a matrix inplace by adding the value of the ``ctrl`` row to the +/// ``target`` row. If ``add_cols`` is true, add columns instead of rows. +pub fn _add_row_or_col(mut mat: ArrayViewMut2, add_cols: &bool, ctrl: usize, trgt: usize) { + // get the two rows (or columns) + let info = if *add_cols { + (s![.., ctrl], s![.., trgt]) + } else { + (s![ctrl, ..], s![trgt, ..]) + }; + let (row0, mut row1) = mat.multi_slice_mut(info); + + // add them inplace + row1.zip_mut_with(&row0, |x, &y| *x ^= y); +} + +/// Generate a random invertible n x n binary matrix. +pub fn random_invertible_binary_matrix_inner(num_qubits: usize, seed: Option) -> Array2 { + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + + let mut matrix = Array2::from_elem((num_qubits, num_qubits), false); + + loop { + for value in matrix.iter_mut() { + *value = rng.gen_bool(0.5); + } + + let rank = compute_rank_inner(matrix.view()); + if rank == num_qubits { + break; + } + } + matrix +} + +/// Check that a binary matrix is invertible. +pub fn check_invertible_binary_matrix_inner(mat: ArrayView2) -> bool { + if mat.nrows() != mat.ncols() { + return false; + } + let rank = compute_rank_inner(mat); + rank == mat.nrows() +} diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs new file mode 100644 index 00000000000..db28751437f --- /dev/null +++ b/crates/accelerate/src/synthesis/mod.rs @@ -0,0 +1,24 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +mod linear; +mod permutation; + +use pyo3::prelude::*; +use pyo3::wrap_pymodule; + +#[pymodule] +pub fn synthesis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + m.add_wrapped(wrap_pymodule!(linear::linear))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs new file mode 100644 index 00000000000..bf0ff97848f --- /dev/null +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -0,0 +1,68 @@ +// 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::PyArrayLike1; +use smallvec::smallvec; + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; + +mod utils; + +/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + utils::validate_permutation(&view)?; + Ok(py.None()) +} + +/// Finds inverse of a permutation pattern. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let inverse_i64: Vec = utils::invert(&view).iter().map(|&x| x as i64).collect(); + Ok(inverse_i64.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _synth_permutation_basic(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let num_qubits = view.len(); + CircuitData::from_standard_gates( + py, + num_qubits as u32, + utils::get_ordered_swap(&view).iter().map(|(i, j)| { + ( + StandardGate::SwapGate, + smallvec![], + smallvec![Qubit(*i as u32), Qubit(*j as u32)], + ) + }), + Param::Float(0.0), + ) +} + +#[pymodule] +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)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/permutation/utils.rs b/crates/accelerate/src/synthesis/permutation/utils.rs new file mode 100644 index 00000000000..a78088bfbfa --- /dev/null +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -0,0 +1,86 @@ +// 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::{Array1, ArrayView1}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use std::vec::Vec; + +pub fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { + let n = pattern.len(); + let mut seen: Vec = vec![false; n]; + + for &x in pattern { + if x < 0 { + return Err(PyValueError::new_err( + "Invalid permutation: input contains a negative number.", + )); + } + + if x as usize >= n { + return Err(PyValueError::new_err(format!( + "Invalid permutation: input has length {} and contains {}.", + n, x + ))); + } + + if seen[x as usize] { + return Err(PyValueError::new_err(format!( + "Invalid permutation: input contains {} more than once.", + x + ))); + } + + seen[x as usize] = true; + } + + Ok(()) +} + +pub fn invert(pattern: &ArrayView1) -> Array1 { + let mut inverse: Array1 = Array1::zeros(pattern.len()); + pattern.iter().enumerate().for_each(|(ii, &jj)| { + inverse[jj as usize] = ii; + }); + inverse +} + +/// Sorts the input permutation by iterating through the permutation list +/// and putting each element to its correct position via a SWAP (if it's not +/// at the correct position already). If ``n`` is the length of the input +/// permutation, this requires at most ``n`` SWAPs. +/// +/// More precisely, if the input permutation is a cycle of length ``m``, +/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); +/// if the input permutation consists of several disjoint cycles, then each cycle +/// is essentially treated independently. +pub fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { + let mut permutation: Vec = pattern.iter().map(|&x| x as usize).collect(); + let mut index_map = invert(pattern); + + let n = permutation.len(); + let mut swaps: Vec<(i64, i64)> = Vec::with_capacity(n); + for ii in 0..n { + let val = permutation[ii]; + if val == ii { + continue; + } + let jj = index_map[ii]; + swaps.push((ii as i64, jj as i64)); + (permutation[ii], permutation[jj]) = (permutation[jj], permutation[ii]); + index_map[val] = jj; + index_map[ii] = ii; + } + + swaps[..].reverse(); + swaps +} diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7dcb273ac16..37061d5159f 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -21,10 +21,6 @@ use approx::{abs_diff_eq, relative_eq}; use num_complex::{Complex, Complex64, ComplexFloat}; use num_traits::Zero; -use pyo3::exceptions::{PyIndexError, PyValueError}; -use pyo3::prelude::*; -use pyo3::wrap_pyfunction; -use pyo3::Python; use smallvec::{smallvec, SmallVec}; use std::f64::consts::{FRAC_1_SQRT_2, PI}; use std::ops::Deref; @@ -37,7 +33,11 @@ use ndarray::prelude::*; use ndarray::Zip; use numpy::PyReadonlyArray2; use numpy::{IntoPyArray, ToPyArray}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; +use pyo3::types::PyList; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ @@ -51,72 +51,29 @@ use rand::prelude::*; use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; -use qiskit_circuit::SliceOrInt; +use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; +use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; -const PI2: f64 = PI / 2.0; -const PI4: f64 = PI / 4.0; +const PI2: f64 = PI / 2.; +const PI4: f64 = PI / 4.; const PI32: f64 = 3.0 * PI2; const TWO_PI: f64 = 2.0 * PI; const C1: c64 = c64 { re: 1.0, im: 0.0 }; -static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; - -static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [ - [ - Complex64::new(1.0, 0.), - Complex64::new(0., 1.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 1.), - Complex64::new(1.0, 0.0), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 1.), - Complex64::new(-1., 0.), - ], - [ - Complex64::new(1., 0.), - Complex64::new(0., -1.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], +static B_NON_NORMALIZED: GateArray2Q = [ + [C_ONE, IM, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, IM, C_ONE], + [C_ZERO, C_ZERO, IM, C_M_ONE], + [C_ONE, M_IM, C_ZERO, C_ZERO], ]; -static B_NON_NORMALIZED_DAGGER: [[Complex64; 4]; 4] = [ - [ - Complex64::new(0.5, 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0.5, 0.0), - ], - [ - Complex64::new(0., -0.5), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.5), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., -0.5), - Complex64::new(0., -0.5), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0.5, 0.), - Complex64::new(-0.5, 0.), - Complex64::new(0., 0.), - ], +static B_NON_NORMALIZED_DAGGER: GateArray2Q = [ + [c64(0.5, 0.), C_ZERO, C_ZERO, c64(0.5, 0.)], + [c64(0., -0.5), C_ZERO, C_ZERO, c64(0., 0.5)], + [C_ZERO, c64(0., -0.5), c64(0., -0.5), C_ZERO], + [C_ZERO, c64(0.5, 0.), c64(-0.5, 0.), C_ZERO], ]; enum MagicBasisTransform { @@ -297,7 +254,7 @@ fn __num_basis_gates(basis_b: f64, basis_fidelity: f64, unitary: MatRef) -> c64::new(4.0 * c.cos(), 0.0), c64::new(4.0, 0.0), ]; - // The originial Python had `np.argmax`, which returns the lowest index in case two or more + // The original Python had `np.argmax`, which returns the lowest index in case two or more // values have a common maximum value. // `max_by` and `min_by` return the highest and lowest indices respectively, in case of ties. // So to reproduce `np.argmax`, we use `min_by` and switch the order of the @@ -322,77 +279,26 @@ fn closest_partial_swap(a: f64, b: f64, c: f64) -> f64 { fn rx_matrix(theta: f64) -> Array2 { let half_theta = theta / 2.; - let cos = Complex64::new(half_theta.cos(), 0.); - let isin = Complex64::new(0., -half_theta.sin()); + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., -half_theta.sin()); array![[cos, isin], [isin, cos]] } fn ry_matrix(theta: f64) -> Array2 { let half_theta = theta / 2.; - let cos = Complex64::new(half_theta.cos(), 0.); - let sin = Complex64::new(half_theta.sin(), 0.); + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); array![[cos, -sin], [sin, cos]] } fn rz_matrix(theta: f64) -> Array2 { - let ilam2 = Complex64::new(0., 0.5 * theta); - array![ - [(-ilam2).exp(), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), ilam2.exp()] - ] + let ilam2 = c64(0., 0.5 * theta); + array![[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } -static HGATE: [[Complex64; 2]; 2] = [ - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(-FRAC_1_SQRT_2, 0.), - ], -]; - -static CXGATE: [[Complex64; 4]; 4] = [ - [ - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], -]; - -static SXGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0.5, 0.5), Complex64::new(0.5, -0.5)], - [Complex64::new(0.5, -0.5), Complex64::new(0.5, 0.5)], -]; - -static XGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(1., 0.), Complex64::new(0., 0.)], -]; - fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); - let phase = Complex64::new(0., global_phase).exp(); + let phase = c64(0., global_phase).exp(); let mut matrix = Array2::from_diag(&arr1(&[phase, phase, phase, phase])); sequence .iter() @@ -402,10 +308,10 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2< // sequence. If we get a different gate this is getting called // by something else and is invalid. let gate_matrix = match inst.0.as_ref() { - "sx" => aview2(&SXGATE).to_owned(), + "sx" => aview2(&SX_GATE).to_owned(), "rz" => rz_matrix(inst.1[0]), - "cx" => aview2(&CXGATE).to_owned(), - "x" => aview2(&XGATE).to_owned(), + "cx" => aview2(&CX_GATE).to_owned(), + "x" => aview2(&X_GATE).to_owned(), _ => unreachable!("Undefined gate"), }; (gate_matrix, &inst.2) @@ -427,7 +333,6 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2< } const DEFAULT_FIDELITY: f64 = 1.0 - 1.0e-9; -const C1_IM: Complex64 = Complex64::new(0.0, 1.0); #[derive(Clone, Debug, Copy)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose")] @@ -552,18 +457,9 @@ impl TwoQubitWeylDecomposition { } } -static IPZ: [[Complex64; 2]; 2] = [ - [C1_IM, Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(0., -1.)], -]; -static IPY: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(-1., 0.), Complex64::new(0., 0.)], -]; -static IPX: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), C1_IM], - [C1_IM, Complex64::new(0., 0.)], -]; +static IPZ: GateArray1Q = [[IM, C_ZERO], [C_ZERO, M_IM]]; +static IPY: GateArray1Q = [[C_ZERO, C_ONE], [C_M_ONE, C_ZERO]]; +static IPX: GateArray1Q = [[C_ZERO, IM], [IM, C_ZERO]]; #[pymethods] impl TwoQubitWeylDecomposition { @@ -639,7 +535,7 @@ impl TwoQubitWeylDecomposition { // M2 is a symmetric complex matrix. We need to decompose it as M2 = P D P^T where // P ∈ SO(4), D is diagonal with unit-magnitude elements. // - // We can't use raw `eig` directly because it isn't guaranteed to give us real or othogonal + // We can't use raw `eig` directly because it isn't guaranteed to give us real or orthogonal // eigenvectors. Instead, since `M2` is complex-symmetric, // M2 = A + iB // for real-symmetric `A` and `B`, and as @@ -723,7 +619,7 @@ impl TwoQubitWeylDecomposition { temp.diag_mut() .iter_mut() .enumerate() - .for_each(|(index, x)| *x = (C1_IM * d[index]).exp()); + .for_each(|(index, x)| *x = (IM * d[index]).exp()); let k1 = magic_basis_transform(u_p.dot(&p).dot(&temp).view(), MagicBasisTransform::Into); let k2 = magic_basis_transform(p.t(), MagicBasisTransform::Into); @@ -789,7 +685,7 @@ impl TwoQubitWeylDecomposition { let is_close = |ap: f64, bp: f64, cp: f64| -> bool { let [da, db, dc] = [a - ap, b - bp, c - cp]; let tr = 4. - * Complex64::new( + * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ); @@ -1068,13 +964,13 @@ impl TwoQubitWeylDecomposition { b - specialized.b, -c - specialized.c, ]; - 4. * Complex64::new( + 4. * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ) } else { let [da, db, dc] = [a - specialized.a, b - specialized.b, c - specialized.c]; - 4. * Complex64::new( + 4. * c64( da.cos() * db.cos() * dc.cos(), da.sin() * db.sin() * dc.sin(), ) @@ -1235,46 +1131,15 @@ impl TwoQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: TwoQubitSequenceVec = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=- - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } @@ -1481,7 +1346,7 @@ impl TwoQubitBasisDecomposer { } else { euler_matrix_q0 = rz_matrix(euler_q0[0][2] + euler_q0[1][0]).dot(&euler_matrix_q0); } - euler_matrix_q0 = aview2(&HGATE).dot(&euler_matrix_q0); + euler_matrix_q0 = aview2(&H_GATE).dot(&euler_matrix_q0); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q0.view(), 0); let rx_0 = rx_matrix(euler_q1[0][0]); @@ -1489,7 +1354,7 @@ impl TwoQubitBasisDecomposer { let rx_1 = rx_matrix(euler_q1[0][2] + euler_q1[1][0]); let mut euler_matrix_q1 = rz.dot(&rx_0); euler_matrix_q1 = rx_1.dot(&euler_matrix_q1); - euler_matrix_q1 = aview2(&HGATE).dot(&euler_matrix_q1); + euler_matrix_q1 = aview2(&H_GATE).dot(&euler_matrix_q1); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q1.view(), 1); gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); @@ -1550,12 +1415,12 @@ impl TwoQubitBasisDecomposer { return None; } gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); - let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rx_matrix(euler_q0[3][1]).dot(&euler_matrix); euler_matrix = rz_matrix(euler_q0[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 0); - let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rz_matrix(euler_q1[3][1]).dot(&euler_matrix); euler_matrix = rx_matrix(euler_q1[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 1); @@ -1649,20 +1514,14 @@ impl TwoQubitBasisDecomposer { } } -static K12R_ARR: [[Complex64; 2]; 2] = [ - [ - Complex64::new(0., FRAC_1_SQRT_2), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(-FRAC_1_SQRT_2, 0.), - Complex64::new(0., -FRAC_1_SQRT_2), - ], +static K12R_ARR: GateArray1Q = [ + [c64(0., FRAC_1_SQRT_2), c64(FRAC_1_SQRT_2, 0.)], + [c64(-FRAC_1_SQRT_2, 0.), c64(0., -FRAC_1_SQRT_2)], ]; -static K12L_ARR: [[Complex64; 2]; 2] = [ - [Complex64::new(0.5, 0.5), Complex64::new(0.5, 0.5)], - [Complex64::new(-0.5, 0.5), Complex64::new(0.5, -0.5)], +static K12L_ARR: GateArray1Q = [ + [c64(0.5, 0.5), c64(0.5, 0.5)], + [c64(-0.5, 0.5), c64(0.5, -0.5)], ]; fn decomp0_inner(target: &TwoQubitWeylDecomposition) -> SmallVec<[Array2; 8]> { @@ -1702,90 +1561,71 @@ impl TwoQubitBasisDecomposer { // Create some useful matrices U1, U2, U3 are equivalent to the basis, // expand as Ui = Ki1.Ubasis.Ki2 let b = basis_decomposer.b; - let temp = Complex64::new(0.5, -0.5); + let temp = c64(0.5, -0.5); let k11l = array![ - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., -b).exp()), - temp * Complex64::new(0., -b).exp() - ], - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()), - temp * -(Complex64::new(0., b).exp()) - ], + [temp * (M_IM * c64(0., -b).exp()), temp * c64(0., -b).exp()], + [temp * (M_IM * c64(0., b).exp()), temp * -(c64(0., b).exp())], ]; let k11r = array![ [ - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * Complex64::new(0., -b).exp()), - FRAC_1_SQRT_2 * -Complex64::new(0., -b).exp() + FRAC_1_SQRT_2 * (IM * c64(0., -b).exp()), + FRAC_1_SQRT_2 * -c64(0., -b).exp() ], [ - FRAC_1_SQRT_2 * Complex64::new(0., b).exp(), - FRAC_1_SQRT_2 * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()) + FRAC_1_SQRT_2 * c64(0., b).exp(), + FRAC_1_SQRT_2 * (M_IM * c64(0., b).exp()) ], ]; let k12l = aview2(&K12L_ARR); let k12r = aview2(&K12R_ARR); let k32l_k21l = array![ [ - FRAC_1_SQRT_2 * Complex64::new(1., (2. * b).cos()), - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * (2. * b).sin()) + FRAC_1_SQRT_2 * c64(1., (2. * b).cos()), + FRAC_1_SQRT_2 * (IM * (2. * b).sin()) ], [ - FRAC_1_SQRT_2 * (Complex64::new(0., 1.) * (2. * b).sin()), - FRAC_1_SQRT_2 * Complex64::new(1., -(2. * b).cos()) + FRAC_1_SQRT_2 * (IM * (2. * b).sin()), + FRAC_1_SQRT_2 * c64(1., -(2. * b).cos()) ], ]; - let temp = Complex64::new(0.5, 0.5); + let temp = c64(0.5, 0.5); let k21r = array![ [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., -2. * b).exp()), - temp * Complex64::new(0., -2. * b).exp() + temp * (M_IM * c64(0., -2. * b).exp()), + temp * c64(0., -2. * b).exp() ], [ - temp * (Complex64::new(0., 1.) * Complex64::new(0., 2. * b).exp()), - temp * Complex64::new(0., 2. * b).exp() + temp * (IM * c64(0., 2. * b).exp()), + temp * c64(0., 2. * b).exp() ], ]; - const K22L_ARR: [[Complex64; 2]; 2] = [ - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(-FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], + const K22L_ARR: GateArray1Q = [ + [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], + [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], ]; let k22l = aview2(&K22L_ARR); - let k22r_arr: [[Complex64; 2]; 2] = [ - [Complex64::zero(), Complex64::new(1., 0.)], - [Complex64::new(-1., 0.), Complex64::zero()], - ]; + let k22r_arr: GateArray1Q = [[Complex64::zero(), C_ONE], [C_M_ONE, Complex64::zero()]]; let k22r = aview2(&k22r_arr); let k31l = array![ [ - FRAC_1_SQRT_2 * Complex64::new(0., -b).exp(), - FRAC_1_SQRT_2 * Complex64::new(0., -b).exp() + FRAC_1_SQRT_2 * c64(0., -b).exp(), + FRAC_1_SQRT_2 * c64(0., -b).exp() ], [ - FRAC_1_SQRT_2 * -Complex64::new(0., b).exp(), - FRAC_1_SQRT_2 * Complex64::new(0., b).exp() + FRAC_1_SQRT_2 * -c64(0., b).exp(), + FRAC_1_SQRT_2 * c64(0., b).exp() ], ]; - let temp = Complex64::new(0., 1.); let k31r = array![ - [temp * Complex64::new(0., b).exp(), Complex64::zero()], - [Complex64::zero(), temp * -Complex64::new(0., -b).exp()], + [IM * c64(0., b).exp(), Complex64::zero()], + [Complex64::zero(), M_IM * c64(0., -b).exp()], ]; - let temp = Complex64::new(0.5, 0.5); + let temp = c64(0.5, 0.5); let k32r = array![ + [temp * c64(0., b).exp(), temp * -c64(0., -b).exp()], [ - temp * Complex64::new(0., b).exp(), - temp * -Complex64::new(0., -b).exp() - ], - [ - temp * (Complex64::new(0., -1.) * Complex64::new(0., b).exp()), - temp * (Complex64::new(0., -1.) * Complex64::new(0., -b).exp()) + temp * (M_IM * c64(0., b).exp()), + temp * (M_IM * c64(0., -b).exp()) ], ]; let k1ld = transpose_conjugate(basis_decomposer.K1l.view()); @@ -1845,11 +1685,11 @@ impl TwoQubitBasisDecomposer { fn traces(&self, target: &TwoQubitWeylDecomposition) -> [Complex64; 4] { [ - 4. * Complex64::new( + 4. * c64( target.a.cos() * target.b.cos() * target.c.cos(), target.a.sin() * target.b.sin() * target.c.sin(), ), - 4. * Complex64::new( + 4. * c64( (PI4 - target.a).cos() * (self.basis_decomposer.b - target.b).cos() * target.c.cos(), @@ -1857,8 +1697,8 @@ impl TwoQubitBasisDecomposer { * (self.basis_decomposer.b - target.b).sin() * target.c.sin(), ), - Complex64::new(4. * target.c.cos(), 0.), - Complex64::new(4., 0.), + c64(4. * target.c.cos(), 0.), + c64(4., 0.), ] } diff --git a/crates/accelerate/src/uc_gate.rs b/crates/accelerate/src/uc_gate.rs new file mode 100644 index 00000000000..21fd7fa0465 --- /dev/null +++ b/crates/accelerate/src/uc_gate.rs @@ -0,0 +1,163 @@ +// 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 num_complex::{Complex64, ComplexFloat}; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; +use std::f64::consts::{FRAC_1_SQRT_2, PI}; + +use faer_ext::{IntoFaerComplex, IntoNdarrayComplex}; +use ndarray::prelude::*; +use numpy::{IntoPyArray, PyReadonlyArray2}; + +use crate::euler_one_qubit_decomposer::det_one_qubit; +use qiskit_circuit::util::{c64, C_ZERO, IM}; + +const EPS: f64 = 1e-10; + +// These constants are the non-zero elements of an RZ gate's unitary with an +// angle of pi / 2 +const RZ_PI2_11: Complex64 = c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2); +const RZ_PI2_00: Complex64 = c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2); + +/// This method implements the decomposition given in equation (3) in +/// https://arxiv.org/pdf/quant-ph/0410066.pdf. +/// +/// The decomposition is used recursively to decompose uniformly controlled gates. +/// +/// a,b = single qubit unitaries +/// v,u,r = outcome of the decomposition given in the reference mentioned above +/// +/// (see there for the details). +fn demultiplex_single_uc( + a: ArrayView2, + b: ArrayView2, +) -> [Array2; 3] { + let x = a.dot(&b.mapv(|x| x.conj()).t()); + let det_x = det_one_qubit(x.view()); + let x11 = x[[0, 0]] / det_x.sqrt(); + let phi = det_x.arg(); + + let r1 = (IM / 2. * (PI / 2. - phi / 2. - x11.arg())).exp(); + let r2 = (IM / 2. * (PI / 2. - phi / 2. + x11.arg() + PI)).exp(); + + let r = array![[r1, C_ZERO], [C_ZERO, r2],]; + + let decomp = r + .dot(&x) + .dot(&r) + .view() + .into_faer_complex() + .complex_eigendecomposition(); + let mut u: Array2 = decomp.u().into_ndarray_complex().to_owned(); + let s = decomp.s().column_vector(); + let mut diag: Array1 = + Array1::from_shape_fn(u.shape()[0], |i| s[i].to_num_complex()); + + // If d is not equal to diag(i,-i), then we put it into this "standard" form + // (see eq. (13) in https://arxiv.org/pdf/quant-ph/0410066.pdf) by interchanging + // the eigenvalues and eigenvectors + if (diag[0] + IM).abs() < EPS { + diag = diag.slice(s![..;-1]).to_owned(); + u = u.slice(s![.., ..;-1]).to_owned(); + } + diag.mapv_inplace(|x| x.sqrt()); + let d = Array2::from_diag(&diag); + let v = d + .dot(&u.mapv(|x| x.conj()).t()) + .dot(&r.mapv(|x| x.conj()).t()) + .dot(&b); + [v, u, r] +} + +#[pyfunction] +pub fn dec_ucg_help( + py: Python, + sq_gates: Vec>, + num_qubits: u32, +) -> (Vec, PyObject) { + let mut single_qubit_gates: Vec> = sq_gates + .into_iter() + .map(|x| x.as_array().to_owned()) + .collect(); + let mut diag: Array1 = Array1::ones(2_usize.pow(num_qubits)); + let num_controls = num_qubits - 1; + for dec_step in 0..num_controls { + let num_ucgs = 2_usize.pow(dec_step); + // The decomposition works recursively and the followign loop goes over the different + // UCGates that arise in the decomposition + for ucg_index in 0..num_ucgs { + let len_ucg = 2_usize.pow(num_controls - dec_step); + for i in 0..len_ucg / 2 { + let shift = ucg_index * len_ucg; + let a = single_qubit_gates[shift + i].view(); + let b = single_qubit_gates[shift + len_ucg / 2 + i].view(); + // Apply the decomposition for UCGates given in equation (3) in + // https://arxiv.org/pdf/quant-ph/0410066.pdf + // to demultiplex one control of all the num_ucgs uniformly-controlled gates + // with log2(len_ucg) uniform controls + let [v, u, r] = demultiplex_single_uc(a, b); + // replace the single-qubit gates with v,u (the already existing ones + // are not needed any more) + single_qubit_gates[shift + i] = v; + single_qubit_gates[shift + len_ucg / 2 + i] = u; + // Now we decompose the gates D as described in Figure 4 in + // https://arxiv.org/pdf/quant-ph/0410066.pdf and merge some of the gates + // into the UCGates and the diagonal at the end of the circuit + + // Remark: The Rz(pi/2) rotation acting on the target qubit and the Hadamard + // gates arising in the decomposition of D are ignored for the moment (they will + // be added together with the C-NOT gates at the end of the decomposition + // (in the method dec_ucg())) + let r_conj_t = r.mapv(|x| x.conj()).t().to_owned(); + if ucg_index < num_ucgs - 1 { + // Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and + // merge the UC-Rz rotation with the following UCGate, + // which hasn't been decomposed yet + let k = shift + len_ucg + i; + + single_qubit_gates[k] = single_qubit_gates[k].dot(&r_conj_t); + single_qubit_gates[k].mapv_inplace(|x| x * RZ_PI2_00); + let k = k + len_ucg / 2; + single_qubit_gates[k] = single_qubit_gates[k].dot(&r); + single_qubit_gates[k].mapv_inplace(|x| x * RZ_PI2_11); + } else { + // Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and merge + // the trailing UC-Rz rotation into a diagonal gate at the end of the circuit + for ucg_index_2 in 0..num_ucgs { + let shift_2 = ucg_index_2 * len_ucg; + let k = 2 * (i + shift_2); + diag[k] *= r_conj_t[[0, 0]] * RZ_PI2_00; + diag[k + 1] *= r_conj_t[[1, 1]] * RZ_PI2_00; + let k = len_ucg + k; + diag[k] *= r[[0, 0]] * RZ_PI2_11; + diag[k + 1] *= r[[1, 1]] * RZ_PI2_11; + } + } + } + } + } + ( + single_qubit_gates + .into_iter() + .map(|x| x.into_pyarray_bound(py).into()) + .collect(), + diag.into_pyarray_bound(py).into(), + ) +} + +#[pymodule] +pub fn uc_gate(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(dec_ucg_help))?; + Ok(()) +} diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc3..50160c7bac1 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -11,4 +11,18 @@ doctest = false [dependencies] hashbrown.workspace = true -pyo3.workspace = true +num-complex.workspace = true +ndarray.workspace = true +numpy.workspace = true +thiserror.workspace = true + +[dependencies.pyo3] +workspace = true +features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] + +[dependencies.smallvec] +workspace = true +features = ["union"] + +[features] +cache_pygates = [] diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b9375c9f99d..bbb4e54651a 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -4,3 +4,66 @@ The Rust-based data structures for circuits. This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well. This crate is a very low part of the Rust stack, if not the very lowest. + +The data model exposed by this crate is as follows. + +## CircuitData + +The core representation of a quantum circuit in Rust is the `CircuitData` struct. This containts the list +of instructions that are comprising the circuit. Each element in this list is modeled by a +`CircuitInstruction` struct. The `CircuitInstruction` contains the operation object and it's operands. +This includes the parameters and bits. It also contains the potential mutable state of the Operation representation from the legacy Python data model; namely `duration`, `unit`, `condition`, and `label`. +In the future we'll be able to remove all of that except for label. + +At rest a `CircuitInstruction` is compacted into a `PackedInstruction` which caches reused qargs +in the instructions to reduce the memory overhead of `CircuitData`. The `PackedInstruction` objects +get unpacked back to `CircuitInstruction` when accessed for a more convienent working form. + +Additionally the `CircuitData` contains a `param_table` field which is used to track parameterized +instructions that are using python defined `ParameterExpression` objects for any parameters and also +a global phase field which is used to track the global phase of the circuit. + +## Operation Model + +In the circuit crate all the operations used in a `CircuitInstruction` are part of the `OperationType` +enum. The `OperationType` enum has four variants which are used to define the different types of +operation objects that can be on a circuit: + + - `StandardGate`: a rust native representation of a member of the Qiskit standard gate library. This is + an `enum` that enumerates all the gates in the library and statically defines all the gate properties + except for gates that take parameters, + - `PyGate`: A struct that wraps a gate outside the standard library defined in Python. This struct wraps + a `Gate` instance (or subclass) as a `PyObject`. The static properties of this object (such as name, + number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the matrix or definition are accessed by calling back into Python to get them from the stored + `PyObject` + - `PyInstruction`: A struct that wraps an instruction defined in Python. This struct wraps an + `Instruction` instance (or subclass) as a `PyObject`. The static properties of this object (such as + name, number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the definition are accessed by calling back into Python to get them from the stored `PyObject`. As + the primary difference between `Gate` and `Instruction` in the python data model are that `Gate` is a + specialized `Instruction` subclass that represents unitary operations the primary difference between + this and `PyGate` are that `PyInstruction` will always return `None` when it's matrix is accessed. + - `PyOperation`: A struct that wraps an operation defined in Python. This struct wraps an `Operation` + instance (or subclass) as a `PyObject`. The static properties of this object (such as name, number + of qubits, etc) are stored in Rust for performance. As `Operation` is the base abstract interface + definition of what can be put on a circuit this is mostly just a container for custom Python objects. + Anything that's operating on a bare operation will likely need to access it via the `PyObject` + manually because the interface doesn't define many standard properties outside of what's cached in + the struct. + +There is also an `Operation` trait defined which defines the common access pattern interface to these +4 types along with the `OperationType` parent. This trait defines methods to access the standard data +model attributes of operations in Qiskit. This includes things like the name, number of qubits, the matrix, the definition, etc. + +## ParamTable + +The `ParamTable` struct is used to track which circuit instructions are using `ParameterExpression` +objects for any of their parameters. The Python space `ParameterExpression` is comprised of a symengine +symbolic expression that defines operations using `Parameter` objects. Each `Parameter` is modeled by +a uuid and a name to uniquely identify it. The parameter table maps the `Parameter` objects to the +`CircuitInstruction` in the `CircuitData` that are using them. The `Parameter` comprised of 3 `HashMaps` internally that map the uuid (as `u128`, which is accesible in Python by using `uuid.int`) to the `ParamEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. + +The `ParamEntry` is just a `HashSet` of 2-tuples with usize elements. The two usizes represent the instruction index in the `CircuitData` and the index of the `CircuitInstruction.params` field of +a give instruction where the given `Parameter` is used in the circuit. If the instruction index is +`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`. diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs new file mode 100644 index 00000000000..40540f9df5a --- /dev/null +++ b/crates/circuit/src/bit_data.rs @@ -0,0 +1,201 @@ +// 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::BitType; +use hashbrown::HashMap; +use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyList; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; + +/// Private wrapper for Python-side Bit instances that implements +/// [Hash] and [Eq], allowing them to be used in Rust hash-based +/// sets and maps. +/// +/// Python's `hash()` is called on the wrapped Bit instance during +/// construction and returned from Rust's [Hash] trait impl. +/// The impl of [PartialEq] first compares the native Py pointers +/// to determine equality. If these are not equal, only then does +/// it call `repr()` on both sides, which has a significant +/// performance advantage. +#[derive(Clone, Debug)] +struct BitAsKey { + /// Python's `hash()` of the wrapped instance. + hash: isize, + /// The wrapped instance. + bit: PyObject, +} + +impl BitAsKey { + pub fn new(bit: &Bound) -> Self { + BitAsKey { + // This really shouldn't fail, but if it does, + // we'll just use 0. + hash: bit.hash().unwrap_or(0), + bit: bit.clone().unbind(), + } + } +} + +impl Hash for BitAsKey { + fn hash(&self, state: &mut H) { + state.write_isize(self.hash); + } +} + +impl PartialEq for BitAsKey { + fn eq(&self, other: &Self) -> bool { + self.bit.is(&other.bit) + || Python::with_gil(|py| { + self.bit + .bind(py) + .repr() + .unwrap() + .eq(other.bit.bind(py).repr().unwrap()) + .unwrap() + }) + } +} + +impl Eq for BitAsKey {} + +#[derive(Clone, Debug)] +pub(crate) struct BitData { + /// The public field name (i.e. `qubits` or `clbits`). + description: String, + /// Registered Python bits. + bits: Vec, + /// Maps Python bits to native type. + indices: HashMap, + /// The bits registered, cached as a PyList. + cached: Py, +} + +pub(crate) 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, + BitType: From, +{ + pub fn new(py: Python<'_>, description: String) -> Self { + BitData { + description, + bits: Vec::new(), + indices: HashMap::new(), + cached: PyList::empty_bound(py).unbind(), + } + } + + /// Gets the number of bits. + pub fn len(&self) -> usize { + self.bits.len() + } + + /// Gets a reference to the underlying vector of Python bits. + #[inline] + pub fn bits(&self) -> &Vec { + &self.bits + } + + /// Gets a reference to the cached Python list, maintained by + /// this instance. + #[inline] + pub fn cached(&self) -> &Py { + &self.cached + } + + /// Finds the native bit index of the given Python bit. + #[inline] + pub fn find(&self, bit: &Bound) -> Option { + self.indices.get(&BitAsKey::new(bit)).copied() + } + + /// Map the provided Python bits to their native indices. + /// An error is returned if any bit is not registered. + pub fn map_bits<'py>( + &self, + bits: impl IntoIterator>, + ) -> Result, BitNotFoundError<'py>> { + let v: Result, _> = bits + .into_iter() + .map(|b| { + self.indices + .get(&BitAsKey::new(&b)) + .copied() + .ok_or_else(|| BitNotFoundError(b)) + }) + .collect(); + v.map(|x| x.into_iter()) + } + + /// Map the provided native indices to the corresponding Python + /// bit instances. + /// Panics if any of the indices are out of range. + pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator> { + let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect(); + v.into_iter() + } + + /// Gets the Python bit corresponding to the given native + /// bit index. + #[inline] + pub fn get(&self, index: T) -> Option<&PyObject> { + self.bits.get(>::from(index) as usize) + } + + /// Adds a new Python bit. + 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) + )); + } + let idx: BitType = self.bits.len().try_into().map_err(|_| { + PyRuntimeError::new_err(format!( + "The number of {} in the circuit has exceeded the maximum capacity", + self.description + )) + })?; + if self + .indices + .try_insert(BitAsKey::new(bit), idx.into()) + .is_ok() + { + self.bits.push(bit.into_py(py)); + self.cached.bind(py).append(bit)?; + } else if strict { + return Err(PyValueError::new_err(format!( + "Existing bit {:?} cannot be re-added in strict mode.", + bit + ))); + } + Ok(()) + } + + /// Called during Python garbage collection, only!. + /// Note: INVALIDATES THIS INSTANCE. + pub fn dispose(&mut self) { + self.indices.clear(); + self.bits.clear(); + } +} diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 07bab2c17c9..10e0691021a 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1,6 +1,6 @@ // This code is part of Qiskit. // -// (C) Copyright IBM 2023 +// (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 @@ -10,76 +10,28 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::circuit_instruction::CircuitInstruction; -use crate::intern_context::{BitType, IndexType, InternContext}; -use crate::SliceOrInt; +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; -use hashbrown::HashMap; -use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; -use pyo3::prelude::*; -use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; -use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; -use std::hash::{Hash, Hasher}; - -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -struct PackedInstruction { - /// The Python-side operation instance. - op: PyObject, - /// The index under which the interner has stored `qubits`. - qubits_id: IndexType, - /// The index under which the interner has stored `clbits`. - clbits_id: IndexType, -} - -/// Private wrapper for Python-side Bit instances that implements -/// [Hash] and [Eq], allowing them to be used in Rust hash-based -/// sets and maps. -/// -/// Python's `hash()` is called on the wrapped Bit instance during -/// construction and returned from Rust's [Hash] trait impl. -/// The impl of [PartialEq] first compares the native Py pointers -/// to determine equality. If these are not equal, only then does -/// it call `repr()` on both sides, which has a significant -/// performance advantage. -#[derive(Clone, Debug)] -struct BitAsKey { - /// Python's `hash()` of the wrapped instance. - hash: isize, - /// The wrapped instance. - bit: PyObject, -} - -impl BitAsKey { - fn new(bit: &Bound) -> PyResult { - Ok(BitAsKey { - hash: bit.hash()?, - bit: bit.into_py(bit.py()), - }) - } -} - -impl Hash for BitAsKey { - fn hash(&self, state: &mut H) { - state.write_isize(self.hash); - } -} +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, + PackedInstruction, +}; +use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::operations::{Operation, OperationType, Param, StandardGate}; +use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; +use crate::slice::{PySequenceIndex, SequenceIndex}; +use crate::{Clbit, Qubit}; -impl PartialEq for BitAsKey { - fn eq(&self, other: &Self) -> bool { - self.bit.is(&other.bit) - || Python::with_gil(|py| { - self.bit - .bind(py) - .repr() - .unwrap() - .eq(other.bit.bind(py).repr().unwrap()) - .unwrap() - }) - } -} +use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyList, PySet, PyTuple, PyType}; +use pyo3::{intern, PyTraverseError, PyVisit}; -impl Eq for BitAsKey {} +use hashbrown::{HashMap, HashSet}; +use smallvec::SmallVec; /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning @@ -136,45 +88,269 @@ impl Eq for BitAsKey {} pub struct CircuitData { /// The packed instruction listing. data: Vec, - /// The intern context used to intern instruction bits. - intern_context: InternContext, - /// The qubits registered (e.g. through :meth:`~.CircuitData.add_qubit`). - qubits_native: Vec, - /// The clbits registered (e.g. through :meth:`~.CircuitData.add_clbit`). - clbits_native: Vec, - /// Map of :class:`.Qubit` instances to their index in - /// :attr:`.CircuitData.qubits`. - qubit_indices_native: HashMap, - /// Map of :class:`.Clbit` instances to their index in - /// :attr:`.CircuitData.clbits`. - clbit_indices_native: HashMap, - /// The qubits registered, cached as a ``list[Qubit]``. - qubits: Py, - /// The clbits registered, cached as a ``list[Clbit]``. - clbits: Py, + /// The cache used to intern instruction bits. + qargs_interner: IndexedInterner>, + /// The cache used to intern instruction bits. + cargs_interner: IndexedInterner>, + /// Qubits registered in the circuit. + qubits: BitData, + /// Clbits registered in the circuit. + clbits: BitData, + param_table: ParamTable, + #[pyo3(get)] + global_phase: Param, +} + +impl CircuitData { + /// 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 + /// synthesis without needing to involve Python. + /// + /// This can be connected with the Python space + /// QuantumCircuit.from_circuit_data() constructor to build a full + /// QuantumCircuit from Rust. + /// + /// # Arguments + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * num_qubits: The number of qubits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * instructions: An iterator of the standard gate params and qubits to + /// add to the circuit + /// * global_phase: The global phase to use for the circuit + pub fn from_standard_gates( + py: Python, + num_qubits: u32, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator, SmallVec<[Qubit; 2]>)>, + { + let instruction_iter = instructions.into_iter(); + let mut res = CircuitData { + data: Vec::with_capacity(instruction_iter.size_hint().0), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_cls = QUBIT.get_bound(py); + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + for (operation, params, qargs) in instruction_iter { + let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); + let clbits = PyTuple::empty_bound(py).unbind(); + let inst = res.pack_owned( + py, + &CircuitInstruction { + operation: OperationType::Standard(operation), + qubits, + clbits, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: None, + }, + )?; + res.data.push(inst); + } + Ok(res) + } + + fn handle_manual_params( + &mut self, + py: Python, + inst_index: usize, + params: &[(usize, Vec)], + ) -> PyResult { + let mut new_param = false; + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, raw_param_objs) in params { + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract::(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in atomic_parameters.iter() { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear() + } + Ok(new_param) + } + + /// Add an instruction's entries to the parameter table + fn update_param_table( + &mut self, + py: Python, + inst_index: usize, + params: Option)>>, + ) -> PyResult { + if let Some(params) = params { + return self.handle_manual_params(py, inst_index, ¶ms); + } + // Update the parameter table + let mut new_param = false; + let inst_params = &self.data[inst_index].params; + if !inst_params.is_empty() { + let params: Vec<(usize, PyObject)> = inst_params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + let list_builtin = BUILTIN_LIST.get_bound(py); + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in &atomic_parameters { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear(); + } + } + } + Ok(new_param) + } + + /// Remove an index's entries from the parameter table. + fn remove_from_parameter_table(&mut self, py: Python, inst_index: usize) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + if inst_index == GLOBAL_PHASE_INDEX { + if let Param::ParameterExpression(global_phase) = &self.global_phase { + let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + for (param_index, param_obj) in raw_param_objs.iter().enumerate() { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name: String = param_obj.getattr(py, intern!(py, "name"))?.extract(py)?; + self.param_table + .discard_references(uuid, inst_index, param_index, name); + } + } + } else if !self.data[inst_index].params.is_empty() { + let params: Vec<(usize, PyObject)> = self.data[inst_index] + .params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + let mut atomic_parameters: HashSet<(u128, String)> = + HashSet::with_capacity(params.len()); + for x in raw_param_objs { + let uuid = x + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name = x.getattr(py, intern!(py, "name"))?.extract(py)?; + atomic_parameters.insert((uuid, name)); + } + for (uuid, name) in atomic_parameters { + self.param_table + .discard_references(uuid, inst_index, *param_index, name); + } + } + } + } + Ok(()) + } + + fn reindex_parameter_table(&mut self, py: Python) -> PyResult<()> { + self.param_table.clear(); + + for inst_index in 0..self.data.len() { + self.update_param_table(py, inst_index, None)?; + } + // Technically we could keep the global phase entry directly if it exists, but we're + // the incremental cost is minimal after reindexing everything. + self.global_phase(py, self.global_phase.clone())?; + Ok(()) + } + + pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { + let packed = self.pack(value)?; + let new_index = self.data.len(); + self.data.push(packed); + self.update_param_table(py, new_index, None) + } } #[pymethods] impl CircuitData { #[new] - #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))] + #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))] pub fn new( py: Python<'_>, qubits: Option<&Bound>, clbits: Option<&Bound>, data: Option<&Bound>, reserve: usize, + global_phase: Param, ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), - intern_context: InternContext::new(), - qubits_native: Vec::new(), - clbits_native: Vec::new(), - qubit_indices_native: HashMap::new(), - clbit_indices_native: HashMap::new(), - qubits: PyList::empty_bound(py).unbind(), - clbits: PyList::empty_bound(py).unbind(), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase: Param::Float(0.), }; + self_.global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { self_.add_qubit(py, &bit?, true)?; @@ -197,8 +373,8 @@ impl CircuitData { let args = { let self_ = self_.borrow(); ( - self_.qubits.clone_ref(py), - self_.clbits.clone_ref(py), + self_.qubits.cached().clone_ref(py), + self_.clbits.cached().clone_ref(py), None::<()>, self_.data.len(), ) @@ -217,7 +393,17 @@ impl CircuitData { /// list(:class:`.Qubit`): The current sequence of registered qubits. #[getter] pub fn qubits(&self, py: Python<'_>) -> Py { - self.qubits.clone_ref(py) + self.qubits.cached().clone_ref(py) + } + + /// Return the number of qubits. This is equivalent to the length of the list returned by + /// :meth:`.CircuitData.qubits` + /// + /// Returns: + /// int: The number of qubits. + #[getter] + pub fn num_qubits(&self) -> usize { + self.qubits.len() } /// Returns the current sequence of registered :class:`.Clbit` @@ -232,7 +418,26 @@ impl CircuitData { /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter] pub fn clbits(&self, py: Python<'_>) -> Py { - self.clbits.clone_ref(py) + self.clbits.cached().clone_ref(py) + } + + /// Return the number of clbits. This is equivalent to the length of the list returned by + /// :meth:`.CircuitData.clbits`. + /// + /// Returns: + /// int: The number of clbits. + #[getter] + pub fn num_clbits(&self) -> usize { + self.clbits.len() + } + + /// Return the width of the circuit. This is the number of qubits plus the + /// number of clbits. + /// + /// Returns: + /// int: The width of the circuit. + pub fn width(&self) -> usize { + self.num_qubits() + self.num_clbits() } /// Registers a :class:`.Qubit` instance. @@ -246,31 +451,7 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_qubit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.qubits_native.len() != self.qubits.bind(bit.py()).len() { - return Err(PyRuntimeError::new_err(concat!( - "This circuit's 'qubits' list has become out of sync with the circuit data.", - " Did something modify it?" - ))); - } - let idx: BitType = self.qubits_native.len().try_into().map_err(|_| { - PyRuntimeError::new_err( - "The number of qubits in the circuit has exceeded the maximum capacity", - ) - })?; - if self - .qubit_indices_native - .try_insert(BitAsKey::new(bit)?, idx) - .is_ok() - { - self.qubits_native.push(bit.into_py(py)); - self.qubits.bind(py).append(bit)?; - } else if strict { - return Err(PyValueError::new_err(format!( - "Existing bit {:?} cannot be re-added in strict mode.", - bit - ))); - } - Ok(()) + self.qubits.add(py, bit, strict) } /// Registers a :class:`.Clbit` instance. @@ -284,47 +465,70 @@ impl CircuitData { /// was provided. #[pyo3(signature = (bit, *, strict=true))] pub fn add_clbit(&mut self, py: Python, bit: &Bound, strict: bool) -> PyResult<()> { - if self.clbits_native.len() != self.clbits.bind(bit.py()).len() { - return Err(PyRuntimeError::new_err(concat!( - "This circuit's 'clbits' list has become out of sync with the circuit data.", - " Did something modify it?" - ))); - } - let idx: BitType = self.clbits_native.len().try_into().map_err(|_| { - PyRuntimeError::new_err( - "The number of clbits in the circuit has exceeded the maximum capacity", - ) - })?; - if self - .clbit_indices_native - .try_insert(BitAsKey::new(bit)?, idx) - .is_ok() - { - self.clbits_native.push(bit.into_py(py)); - self.clbits.bind(py).append(bit)?; - } else if strict { - return Err(PyValueError::new_err(format!( - "Existing bit {:?} cannot be re-added in strict mode.", - bit - ))); - } - Ok(()) + self.clbits.add(py, bit, strict) } /// Performs a shallow copy. /// /// Returns: /// CircuitData: The shallow copy. - pub fn copy(&self, py: Python<'_>) -> PyResult { + #[pyo3(signature = (copy_instructions=true, deepcopy=false))] + pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { let mut res = CircuitData::new( py, - Some(self.qubits.bind(py)), - Some(self.clbits.bind(py)), + Some(self.qubits.cached().bind(py)), + Some(self.clbits.cached().bind(py)), None, 0, + self.global_phase.clone(), )?; - res.intern_context = self.intern_context.clone(); - res.data = self.data.clone(); + res.qargs_interner = self.qargs_interner.clone(); + res.cargs_interner = self.cargs_interner.clone(); + res.data.clone_from(&self.data); + res.param_table.clone_from(&self.param_table); + + if deepcopy { + let deepcopy = py + .import_bound(intern!(py, "copy"))? + .getattr(intern!(py, "deepcopy"))?; + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => {} + OperationType::Gate(ref mut op) => { + op.gate = deepcopy.call1((&op.gate,))?.unbind(); + } + OperationType::Instruction(ref mut op) => { + op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + } + OperationType::Operation(ref mut op) => { + op.operation = deepcopy.call1((&op.operation,))?.unbind(); + } + }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } + } + } else if copy_instructions { + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => {} + OperationType::Gate(ref mut op) => { + op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; + } + OperationType::Instruction(ref mut op) => { + op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; + } + OperationType::Operation(ref mut op) => { + op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; + } + }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } + } + } Ok(res) } @@ -347,11 +551,11 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.intern_context.lookup(inst.qubits_id).iter() { - qubits.add(self.qubits_native[*b as usize].clone_ref(py))?; + for b in self.qargs_interner.intern(inst.qubits_id).value.iter() { + qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.intern_context.lookup(inst.clbits_id).iter() { - clbits.add(self.clbits_native[*b as usize].clone_ref(py))?; + for b in self.cargs_interner.intern(inst.clbits_id).value.iter() { + clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -366,7 +570,7 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - func.call1((inst.op.bind(py),))?; + func.call1((inst.unpack_py_op(py)?,))?; } Ok(()) } @@ -380,7 +584,7 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - func.call1((index, inst.op.bind(py)))?; + func.call1((index, inst.unpack_py_op(py)?))?; } Ok(()) } @@ -388,6 +592,12 @@ impl CircuitData { /// Invokes callable ``func`` with each instruction's operation, /// replacing the operation with the result. /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// /// Args: /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their @@ -395,7 +605,55 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - inst.op = func.call1((inst.op.bind(py),))?.into_py(py); + let py_op = { + if let OperationType::Standard(op) = inst.op { + match inst.extra_attrs.as_deref() { + None + | Some(ExtraInstructionAttributes { + condition: None, .. + }) => op.into_py(py), + _ => inst.unpack_py_op(py)?, + } + } else { + inst.unpack_py_op(py)? + } + }; + let result: OperationInput = func.call1((py_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = Some(new_op); + } + } + } } Ok(()) } @@ -458,36 +716,26 @@ impl CircuitData { qubits: Option<&Bound>, clbits: Option<&Bound>, ) -> PyResult<()> { - let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; - if temp.qubits_native.len() < self.qubits_native.len() { - return Err(PyValueError::new_err(format!( - "Replacement 'qubits' of size {:?} must contain at least {:?} bits.", - temp.qubits_native.len(), - self.qubits_native.len(), - ))); - } - if temp.clbits_native.len() < self.clbits_native.len() { - return Err(PyValueError::new_err(format!( - "Replacement 'clbits' of size {:?} must contain at least {:?} bits.", - temp.clbits_native.len(), - self.clbits_native.len(), - ))); - } + let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?; if qubits.is_some() { + if temp.num_qubits() < self.num_qubits() { + return Err(PyValueError::new_err(format!( + "Replacement 'qubits' of size {:?} must contain at least {:?} bits.", + temp.num_qubits(), + self.num_qubits(), + ))); + } std::mem::swap(&mut temp.qubits, &mut self.qubits); - std::mem::swap(&mut temp.qubits_native, &mut self.qubits_native); - std::mem::swap( - &mut temp.qubit_indices_native, - &mut self.qubit_indices_native, - ); } if clbits.is_some() { + if temp.num_clbits() < self.num_clbits() { + return Err(PyValueError::new_err(format!( + "Replacement 'clbits' of size {:?} must contain at least {:?} bits.", + temp.num_clbits(), + self.num_clbits(), + ))); + } std::mem::swap(&mut temp.clbits, &mut self.clbits); - std::mem::swap(&mut temp.clbits_native, &mut self.clbits_native); - std::mem::swap( - &mut temp.clbit_indices_native, - &mut self.clbit_indices_native, - ); } Ok(()) } @@ -497,147 +745,143 @@ impl CircuitData { } // Note: we also rely on this to make us iterable! - pub fn __getitem__(&self, py: Python, index: &Bound) -> PyResult { - // Internal helper function to get a specific - // instruction by index. - fn get_at( - self_: &CircuitData, - py: Python<'_>, - index: isize, - ) -> PyResult> { - let index = self_.convert_py_index(index)?; - if let Some(inst) = self_.data.get(index) { - self_.unpack(py, inst) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - - if index.is_exact_instance_of::() { - let slice = self.convert_py_slice(index.downcast_exact::()?)?; - let result = slice - .into_iter() - .map(|i| get_at(self, py, i)) - .collect::>>()?; - Ok(result.into_py(py)) - } else { - Ok(get_at(self, py, index.extract()?)?.into_py(py)) + pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult { + // 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_id); + let clbits = self.cargs_interner.intern(inst.clbits_id); + CircuitInstruction::new( + py, + inst.op.clone(), + self.qubits.map_indices(qubits.value), + self.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), + ) + .into_py(py) + }; + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => Ok(get_single(index)), + indices => Ok(PyList::new_bound(py, indices.iter().map(get_single)).into_py(py)), } } - pub fn __delitem__(&mut self, index: SliceOrInt) -> PyResult<()> { - match index { - SliceOrInt::Slice(slice) => { - let slice = { - let mut s = self.convert_py_slice(&slice)?; - if s.len() > 1 && s.first().unwrap() < s.last().unwrap() { - // Reverse the order so we're sure to delete items - // at the back first (avoids messing up indices). - s.reverse() - } - s - }; - for i in slice.into_iter() { - self.__delitem__(SliceOrInt::Int(i))?; - } - Ok(()) - } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - if self.data.get(index).is_some() { - self.data.remove(index); - Ok(()) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - } + pub fn __delitem__(&mut self, py: Python, index: PySequenceIndex) -> PyResult<()> { + self.delitem(py, index.with_len(self.data.len())?) } - pub fn __setitem__( + pub fn setitem_no_param_table_update( &mut self, - py: Python<'_>, - index: SliceOrInt, - value: &Bound, + index: usize, + value: PyRef, ) -> PyResult<()> { - match index { - SliceOrInt::Slice(slice) => { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - let slice = self.convert_py_slice(&slice)?; - let values = value.iter()?.collect::>>>()?; - if indices.step != 1 && slice.len() != values.len() { - // A replacement of a different length when step isn't exactly '1' - // would result in holes. - return Err(PyValueError::new_err(format!( - "attempt to assign sequence of size {:?} to extended slice of size {:?}", - values.len(), - slice.len(), - ))); - } + let mut packed = self.pack(value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } - for (i, v) in slice.iter().zip(values.iter()) { - self.__setitem__(py, SliceOrInt::Int(*i), v)?; - } + pub fn __setitem__(&mut self, index: PySequenceIndex, value: &Bound) -> PyResult<()> { + fn set_single(slf: &mut CircuitData, index: usize, value: &Bound) -> PyResult<()> { + let py = value.py(); + let mut packed = slf.pack(value.downcast::()?.borrow())?; + slf.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut slf.data[index]); + slf.update_param_table(py, index, None)?; + Ok(()) + } - if slice.len() > values.len() { - // Delete any extras. - let slice = PySlice::new_bound( + let py = value.py(); + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => set_single(self, index, value), + indices @ SequenceIndex::PosRange { + start, + stop, + step: 1, + } => { + // `list` allows setting a slice with step +1 to an arbitrary length. + let values = value.iter()?.collect::>>()?; + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; + } + if indices.len() > values.len() { + self.delitem( py, - indices.start + values.len() as isize, - indices.stop, - 1isize, - ); - self.__delitem__(SliceOrInt::Slice(slice))?; + SequenceIndex::PosRange { + start: start + values.len(), + stop, + step: 1, + }, + )? } else { - // Insert any extra values. - for v in values.iter().skip(slice.len()).rev() { - let v: PyRef = v.extract()?; - self.insert(py, indices.stop, v)?; + for value in values[indices.len()..].iter().rev() { + self.insert(stop as isize, value.downcast()?.borrow())?; } } - Ok(()) } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - let value: PyRef = value.extract()?; - let mut packed = self.pack(py, value)?; - std::mem::swap(&mut packed, &mut self.data[index]); - Ok(()) + indices => { + let values = value.iter()?.collect::>>()?; + if indices.len() == values.len() { + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; + } + Ok(()) + } else { + Err(PyValueError::new_err(format!( + "attempt to assign sequence of size {:?} to extended slice of size {:?}", + values.len(), + indices.len(), + ))) + } } } } - pub fn insert( - &mut self, - py: Python<'_>, - index: isize, - value: PyRef, - ) -> PyResult<()> { - let index = self.convert_py_index_clamped(index); - let packed = self.pack(py, value)?; + pub fn insert(&mut self, mut index: isize, value: PyRef) -> PyResult<()> { + // `list.insert` has special-case extra clamping logic for its index argument. + let index = { + if index < 0 { + // This can't exceed `isize::MAX` because `self.data[0]` is larger than a byte. + index += self.data.len() as isize; + } + if index < 0 { + 0 + } else if index as usize > self.data.len() { + self.data.len() + } else { + index as usize + } + }; + let py = value.py(); + let packed = self.pack(value)?; self.data.insert(index, packed); + if index == self.data.len() - 1 { + self.update_param_table(py, index, None)?; + } else { + self.reindex_parameter_table(py)?; + } Ok(()) } - pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { - let index = - index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); - let item = self.__getitem__(py, index.bind(py))?; - self.__delitem__(index.bind(py).extract()?)?; + pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { + let index = index.unwrap_or(PySequenceIndex::Int(-1)); + let native_index = index.with_len(self.data.len())?; + let item = self.__getitem__(py, index)?; + self.delitem(py, native_index)?; Ok(item) } - pub fn append(&mut self, py: Python<'_>, value: PyRef) -> PyResult<()> { - let packed = self.pack(py, value)?; + pub fn append( + &mut self, + py: Python<'_>, + value: &Bound, + params: Option)>>, + ) -> PyResult { + let packed = self.pack(value.try_borrow()?)?; + let new_index = self.data.len(); self.data.push(packed); - Ok(()) + self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { @@ -647,41 +891,56 @@ impl CircuitData { self.data.reserve(other.data.len()); for inst in other.data.iter() { let qubits = other - .intern_context - .lookup(inst.qubits_id) + .qargs_interner + .intern(inst.qubits_id) + .value .iter() .map(|b| { - Ok(self.qubit_indices_native - [&BitAsKey::new(other.qubits_native[*b as usize].bind(py))?]) + Ok(self + .qubits + .find(other.qubits.get(*b).unwrap().bind(py)) + .unwrap()) }) - .collect::>>()?; + .collect::>>()?; let clbits = other - .intern_context - .lookup(inst.clbits_id) + .cargs_interner + .intern(inst.clbits_id) + .value .iter() .map(|b| { - Ok(self.clbit_indices_native - [&BitAsKey::new(other.clbits_native[*b as usize].bind(py))?]) + Ok(self + .clbits + .find(other.clbits.get(*b).unwrap().bind(py)) + .unwrap()) }) - .collect::>>()?; - + .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))?; self.data.push(PackedInstruction { - op: inst.op.clone_ref(py), - qubits_id: self.intern_context.intern(qubits)?, - clbits_id: self.intern_context.intern(clbits)?, + op: inst.op.clone(), + qubits_id: qubits_id.index, + clbits_id: clbits_id.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }); + self.update_param_table(py, new_index, None)?; } return Ok(()); } - for v in itr.iter()? { - self.append(py, v?.extract()?)?; + self.append_inner(py, v?.extract()?)?; } Ok(()) } pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { std::mem::take(&mut self.data); + self.param_table.clear(); Ok(()) } @@ -719,10 +978,7 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for packed in self.data.iter() { - visit.call(&packed.op)?; - } - for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) { + for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { visit.call(bit)?; } @@ -730,126 +986,194 @@ impl CircuitData { // There's no need to visit the native Rust data // structures used for internal tracking: the only Python // references they contain are to the bits in these lists! - visit.call(&self.qubits)?; - visit.call(&self.clbits)?; + visit.call(self.qubits.cached())?; + visit.call(self.clbits.cached())?; Ok(()) } fn __clear__(&mut self) { // Clear anything that could have a reference cycle. self.data.clear(); - self.qubits_native.clear(); - self.clbits_native.clear(); - self.qubit_indices_native.clear(); - self.clbit_indices_native.clear(); + self.qubits.dispose(); + self.clbits.dispose(); } -} -impl CircuitData { - /// Converts a Python slice to a `Vec` of indices into - /// the instruction listing, [CircuitData.data]. - fn convert_py_slice(&self, slice: &Bound) -> PyResult> { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - if indices.step > 0 { - Ok((indices.start..indices.stop) - .step_by(indices.step as usize) - .collect()) - } else { - let mut out = Vec::with_capacity(indices.slicelength as usize); - let mut x = indices.start; - while x > indices.stop { - out.push(x); - x += indices.step; + #[setter] + pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + self.remove_from_parameter_table(py, GLOBAL_PHASE_INDEX)?; + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); } - Ok(out) - } - } + Param::ParameterExpression(angle) => { + let temp: PyObject = angle.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; - /// Converts a Python index to an index into the instruction listing, - /// or one past its end. - /// If the resulting index would be < 0, clamps to 0. - /// If the resulting index would be > len(data), clamps to len(data). - fn convert_py_index_clamped(&self, index: isize) -> usize { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index + for (param_index, param_obj) in raw_param_objs.into_iter().enumerate() { + let param_uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + match self.param_table.table.get_mut(¶m_uuid) { + Some(entry) => entry.add(GLOBAL_PHASE_INDEX, param_index), + None => { + let new_entry = ParamEntry::new(GLOBAL_PHASE_INDEX, param_index); + self.param_table.insert(py, param_obj, new_entry)?; + } + }; + } + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyValueError::new_err("Invalid type for global phase")), }; - std::cmp::min(std::cmp::max(0, index), self.data.len() as isize) as usize + Ok(()) } - /// Converts a Python index to an index into the instruction listing. - fn convert_py_index(&self, index: isize) -> PyResult { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index - }; + /// Get the global_phase sentinel value + #[classattr] + pub const fn global_phase_param_index() -> usize { + GLOBAL_PHASE_INDEX + } + + // Below are functions to interact with the parameter table. These methods + // are done to avoid needing to deal with shared references and provide + // an entry point via python through an owned CircuitData object. + pub fn num_params(&self) -> usize { + self.param_table.table.len() + } + + pub fn get_param_from_name(&self, py: Python, name: String) -> Option { + self.param_table.get_param_from_name(py, name) + } - if index < 0 || index >= self.data.len() as isize { - return Err(PyIndexError::new_err(format!( - "Index {:?} is out of bounds.", - index, - ))); + pub fn get_params_unsorted(&self, py: Python) -> PyResult> { + Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) + } + + pub fn pop_param( + &mut self, + py: Python, + uuid: u128, + name: String, + default: PyObject, + ) -> PyObject { + match self.param_table.pop(uuid, name) { + Some(res) => res.into_py(py), + None => default.clone_ref(py), } - Ok(index as usize) } - /// Returns a [PackedInstruction] containing the original operation - /// of `elem` and [InternContext] indices of its `qubits` and `clbits` - /// fields. - fn pack( + pub fn _get_param(&self, py: Python, uuid: u128) -> PyObject { + self.param_table.table[&uuid].clone().into_py(py) + } + + pub fn contains_param(&self, uuid: u128) -> bool { + self.param_table.table.contains_key(&uuid) + } + + pub fn add_new_parameter( &mut self, - py: Python<'_>, - inst: PyRef, - ) -> PyResult { - let mut interned_bits = - |indices: &HashMap, bits: &Bound| -> PyResult { - let args = bits - .into_iter() - .map(|b| { - let key = BitAsKey::new(&b)?; - indices.get(&key).copied().ok_or_else(|| { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - b - )) - }) - }) - .collect::>>()?; - self.intern_context.intern(args) - }; + py: Python, + param: PyObject, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + self.param_table.insert( + py, + param.clone_ref(py), + ParamEntry::new(inst_index, param_index), + )?; + Ok(()) + } + + pub fn update_parameter_entry( + &mut self, + uuid: u128, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + match self.param_table.table.get_mut(&uuid) { + Some(entry) => { + entry.add(inst_index, param_index); + Ok(()) + } + None => Err(PyIndexError::new_err(format!( + "Invalid parameter uuid: {:?}", + uuid + ))), + } + } + + pub fn _get_entry_count(&self, py: Python, param_obj: PyObject) -> PyResult { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + Ok(self.param_table.table[&uuid].index_ids.len()) + } + + pub fn num_nonlocal_gates(&self) -> usize { + self.data + .iter() + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) + .count() + } +} + +impl CircuitData { + /// Native internal driver of `__delitem__` that uses a Rust-space version of the + /// `SequenceIndex`. This assumes that the `SequenceIndex` contains only in-bounds indices, and + /// panics if not. + fn delitem(&mut self, py: Python, indices: SequenceIndex) -> PyResult<()> { + // We need to delete in reverse order so we don't invalidate higher indices with a deletion. + for index in indices.descending() { + self.data.remove(index); + } + if !indices.is_empty() { + self.reindex_parameter_table(py)?; + } + Ok(()) + } + + fn pack(&mut self, inst: PyRef) -> PyResult { + let py = inst.py(); + 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()), + )?; Ok(PackedInstruction { - op: inst.operation.clone_ref(py), - qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, - clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, + op: inst.operation.clone(), + qubits_id: qubits.index, + clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(inst.py_op.clone()), }) } - fn unpack(&self, py: Python<'_>, inst: &PackedInstruction) -> PyResult> { - Py::new( - py, - CircuitInstruction { - operation: inst.op.clone_ref(py), - qubits: PyTuple::new_bound( - py, - self.intern_context - .lookup(inst.qubits_id) - .iter() - .map(|i| self.qubits_native[*i as usize].clone_ref(py)) - .collect::>(), - ) - .unbind(), - clbits: PyTuple::new_bound( - py, - self.intern_context - .lookup(inst.clbits_id) - .iter() - .map(|i| self.clbits_native[*i as usize].clone_ref(py)) - .collect::>(), - ) - .unbind(), - }, - ) + fn pack_owned(&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()), + )?; + Ok(PackedInstruction { + op: inst.operation.clone(), + qubits_id: qubits.index, + clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(inst.py_op.clone()), + }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 48b0c4d20ee..74302b526d5 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -10,10 +10,102 @@ // 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::RefCell; + use pyo3::basic::CompareOp; +use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PyTuple}; -use pyo3::{PyObject, PyResult}; +use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; +use pyo3::{intern, IntoPy, PyObject, PyResult}; +use smallvec::{smallvec, SmallVec}; + +use crate::imports::{ + get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, +}; +use crate::interner::Index; +use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; + +/// 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`. +#[derive(Debug, Clone)] +pub struct ExtraInstructionAttributes { + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Private type used to store instructions with interned arg lists. +#[derive(Clone, Debug)] +pub(crate) struct PackedInstruction { + /// The Python-side operation instance. + pub op: OperationType, + /// The index under which the interner has stored `qubits`. + pub qubits_id: Index, + /// The index under which the interner has stored `clbits`. + pub clbits_id: Index, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + + #[cfg(feature = "cache_pygates")] + /// This is hidden in a `RefCell` because, while that has additional memory-usage implications + /// while we're still building with the feature enabled, we intend to remove the feature in the + /// future, and hiding the cache within a `RefCell` lets us keep the cache transparently in our + /// interfaces, without needing various functions to unnecessarily take `&mut` references. + pub py_op: RefCell>, +} + +impl PackedInstruction { + /// 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. + /// + /// A standard-gate operation object returned by this function is disconnected from the + /// containing circuit; updates to its label, duration, unit and condition will not be + /// propagated back. + pub fn unpack_py_op(&self, py: Python) -> PyResult> { + #[cfg(feature = "cache_pygates")] + { + if let Some(cached_op) = self.py_op.borrow().as_ref() { + return Ok(cached_op.clone_ref(py)); + } + } + let (label, duration, unit, condition) = match self.extra_attrs.as_deref() { + Some(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + }) => ( + label.as_deref(), + duration.as_ref(), + unit.as_deref(), + condition.as_ref(), + ), + None => (None, None, None, None), + }; + let out = operation_type_and_data_to_py( + py, + &self.op, + &self.params, + label, + duration, + unit, + condition, + )?; + #[cfg(feature = "cache_pygates")] + { + if let Ok(mut cell) = self.py_op.try_borrow_mut() { + cell.get_or_insert_with(|| out.clone_ref(py)); + } + } + Ok(out) + } +} /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. @@ -47,30 +139,90 @@ use pyo3::{PyObject, PyResult}; /// mutations of the object do not invalidate the types, nor the restrictions placed on it by /// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence /// of distinct items, with no duplicates. -#[pyclass( - freelist = 20, - sequence, - get_all, - module = "qiskit._accelerate.circuit" -)] +#[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")] #[derive(Clone, Debug)] pub struct CircuitInstruction { - /// The logical operation that this instruction represents an execution of. - pub operation: PyObject, + pub operation: OperationType, /// A sequence of the qubits that the operation is applied to. + #[pyo3(get)] pub qubits: Py, /// A sequence of the classical bits that this operation reads from or writes to. + #[pyo3(get)] pub clbits: Py, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} + +/// This enum is for backwards compatibility if a user was doing something from +/// Python like CircuitInstruction(SXGate(), [qr[0]], []) by passing a python +/// gate object directly to a CircuitInstruction. In this case we need to +/// create a rust side object from the pyobject in CircuitInstruction.new() +/// With the `Object` variant which will convert the python object to a rust +/// `OperationType` +#[derive(FromPyObject, Debug)] +pub enum OperationInput { + Standard(StandardGate), + Gate(PyGate), + Instruction(PyInstruction), + Operation(PyOperation), + Object(PyObject), +} + +impl CircuitInstruction { + pub fn new( + py: Python, + operation: OperationType, + qubits: impl IntoIterator, + clbits: impl IntoIterator, + params: SmallVec<[Param; 3]>, + extra_attrs: Option>, + ) -> Self + where + T1: ToPyObject, + T2: ToPyObject, + U1: ExactSizeIterator, + U2: ExactSizeIterator, + { + CircuitInstruction { + operation, + qubits: PyTuple::new_bound(py, qubits).unbind(), + clbits: PyTuple::new_bound(py, clbits).unbind(), + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + } + } +} + +impl From for OperationInput { + fn from(value: OperationType) -> Self { + match value { + OperationType::Standard(op) => Self::Standard(op), + OperationType::Gate(gate) => Self::Gate(gate), + OperationType::Instruction(inst) => Self::Instruction(inst), + OperationType::Operation(op) => Self::Operation(op), + } + } } #[pymethods] impl CircuitInstruction { + #[allow(clippy::too_many_arguments)] #[new] - pub fn new( + #[pyo3(signature = (operation, qubits=None, clbits=None, params=smallvec![], label=None, duration=None, unit=None, condition=None))] + pub fn py_new( py: Python<'_>, - operation: PyObject, + operation: OperationInput, qubits: Option<&Bound>, clbits: Option<&Bound>, + params: SmallVec<[Param; 3]>, + label: Option, + duration: Option, + unit: Option, + condition: Option, ) -> PyResult { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { match seq { @@ -95,11 +247,136 @@ impl CircuitInstruction { } } - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - }) + let extra_attrs = + if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { + Some(Box::new(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + })) + } else { + None + }; + + match operation { + OperationInput::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Object(old_op) => { + let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?; + let extra_attrs = if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })) + } else { + None + }; + + match op.operation { + OperationType::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + } + } + } } /// Returns a shallow copy. @@ -110,44 +387,127 @@ impl CircuitInstruction { self.clone() } + /// The logical operation that this instruction represents an execution of. + #[cfg(not(feature = "cache_pygates"))] + #[getter] + pub fn operation(&self, py: Python) -> PyResult { + operation_type_to_py(py, self) + } + + #[cfg(feature = "cache_pygates")] + #[getter] + pub fn operation(&mut self, py: Python) -> PyResult { + Ok(match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: /// CircuitInstruction: A new instance with the given fields replaced. + #[allow(clippy::too_many_arguments)] pub fn replace( &self, py: Python<'_>, - operation: Option, + operation: Option, qubits: Option<&Bound>, clbits: Option<&Bound>, + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, ) -> PyResult { - CircuitInstruction::new( + let operation = operation.unwrap_or_else(|| self.operation.clone().into()); + + let params = match params { + Some(params) => params, + None => self.params.clone(), + }; + + let label = match label { + Some(label) => Some(label), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.label.clone(), + None => None, + }, + }; + let duration = match duration { + Some(duration) => Some(duration), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.duration.clone(), + None => None, + }, + }; + + let unit: Option = match unit { + Some(unit) => Some(unit), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.unit.clone(), + None => None, + }, + }; + + let condition: Option = match condition { + Some(condition) => Some(condition), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.condition.clone(), + None => None, + }, + }; + + CircuitInstruction::py_new( py, - operation.unwrap_or_else(|| self.operation.clone_ref(py)), + operation, Some(qubits.unwrap_or_else(|| self.qubits.bind(py))), Some(clbits.unwrap_or_else(|| self.clbits.bind(py))), + params, + label, + duration, + unit, + condition, ) } - fn __getstate__(&self, py: Python<'_>) -> PyObject { - ( - self.operation.bind(py), + fn __getstate__(&self, py: Python<'_>) -> PyResult { + Ok(( + operation_type_to_py(py, self)?, self.qubits.bind(py), self.clbits.bind(py), ) - .into_py(py) + .into_py(py)) } - fn __setstate__(&mut self, _py: Python<'_>, state: &Bound) -> PyResult<()> { - self.operation = state.get_item(0)?.extract()?; + fn __setstate__(&mut self, py: Python<'_>, state: &Bound) -> PyResult<()> { + let op = convert_py_to_operation_type(py, state.get_item(0)?.into())?; + self.operation = op.operation; + self.params = op.params; self.qubits = state.get_item(1)?.extract()?; self.clbits = state.get_item(2)?.extract()?; + if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + self.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })); + } Ok(()) } pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { Ok(( - self.operation.bind(py), + operation_type_to_py(py, self)?, self.qubits.bind(py), self.clbits.bind(py), ) @@ -164,7 +524,7 @@ impl CircuitInstruction { , clbits={}\ )", type_name, - r.operation.bind(py).repr()?, + operation_type_to_py(py, &r)?, r.qubits.bind(py).repr()?, r.clbits.bind(py).repr()? )) @@ -176,27 +536,67 @@ impl CircuitInstruction { // the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated // like that via unpacking or similar. That means that the `parameters` field is completely // absent, and the qubits and clbits must be converted to lists. - pub fn _legacy_format<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + #[cfg(not(feature = "cache_pygates"))] + pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult> { + let op = operation_type_to_py(py, self)?; + + Ok(PyTuple::new_bound( py, [ - self.operation.bind(py), - &self.qubits.bind(py).to_list(), - &self.clbits.bind(py).to_list(), + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), ], - ) + )) } + #[cfg(feature = "cache_pygates")] + pub fn _legacy_format<'py>(&mut self, py: Python<'py>) -> PyResult> { + let op = match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }; + Ok(PyTuple::new_bound( + py, + [ + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), + ], + )) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { - Ok(self._legacy_format(py).as_any().get_item(key)?.into_py(py)) + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } + #[cfg(feature = "cache_pygates")] + pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __iter__(&self, py: Python<'_>) -> PyResult { - Ok(self._legacy_format(py).as_any().iter()?.into_py(py)) + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) + } + + #[cfg(feature = "cache_pygates")] + pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } - pub fn __len__(&self) -> usize { - 3 + pub fn __len__(&self, py: Python) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(3) } pub fn __richcmp__( @@ -219,16 +619,94 @@ impl CircuitInstruction { let other: PyResult> = other.extract(); return other.map_or(Ok(Some(false)), |v| { let v = v.try_borrow()?; + let op_eq = match &self_.operation { + OperationType::Standard(op) => { + if let OperationType::Standard(other) = &v.operation { + if op != other { + false + } else { + let other_params = &v.params; + let mut out = true; + for (param_a, param_b) in self_.params.iter().zip(other_params) + { + match param_a { + Param::Float(val_a) => { + if let Param::Float(val_b) = param_b { + if val_a != val_b { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::ParameterExpression(val_a) => { + if let Param::ParameterExpression(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::Obj(val_a) => { + if let Param::Obj(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + } + } + out + } + } else { + false + } + } + OperationType::Gate(op) => { + if let OperationType::Gate(other) = &v.operation { + op.gate.bind(py).eq(other.gate.bind(py))? + } else { + false + } + } + OperationType::Instruction(op) => { + if let OperationType::Instruction(other) = &v.operation { + op.instruction.bind(py).eq(other.instruction.bind(py))? + } else { + false + } + } + OperationType::Operation(op) => { + if let OperationType::Operation(other) = &v.operation { + op.operation.bind(py).eq(other.operation.bind(py))? + } else { + false + } + } + }; + Ok(Some( self_.clbits.bind(py).eq(v.clbits.bind(py))? && self_.qubits.bind(py).eq(v.qubits.bind(py))? - && self_.operation.bind(py).eq(v.operation.bind(py))?, + && op_eq, )) }); } if other.is_instance_of::() { - return Ok(Some(self_._legacy_format(py).eq(other)?)); + #[cfg(feature = "cache_pygates")] + let mut self_ = self_.clone(); + let legacy_format = self_._legacy_format(py)?; + return Ok(Some(legacy_format.eq(other)?)); } Ok(None) @@ -247,3 +725,245 @@ impl CircuitInstruction { } } } + +/// Take a reference to a `CircuitInstruction` and convert the operation +/// inside that to a python side object. +pub(crate) fn operation_type_to_py( + py: Python, + circuit_inst: &CircuitInstruction, +) -> PyResult { + let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { + None => (None, None, None, None), + Some(extra_attrs) => ( + extra_attrs.label.as_deref(), + extra_attrs.duration.as_ref(), + extra_attrs.unit.as_deref(), + extra_attrs.condition.as_ref(), + ), + }; + operation_type_and_data_to_py( + py, + &circuit_inst.operation, + &circuit_inst.params, + label, + duration, + unit, + condition, + ) +} + +/// Take an OperationType and the other mutable state fields from a +/// rust instruction representation and return a PyObject representing +/// a Python side full-fat Qiskit operation as a PyObject. This is typically +/// used by accessor functions that need to return an operation to Qiskit, such +/// as accesing `CircuitInstruction.operation`. +pub(crate) fn operation_type_and_data_to_py( + py: Python, + operation: &OperationType, + params: &[Param], + label: Option<&str>, + duration: Option<&PyObject>, + unit: Option<&str>, + condition: Option<&PyObject>, +) -> PyResult { + match &operation { + OperationType::Standard(op) => { + let gate_class: &PyObject = &get_std_gate_class(py, *op)?; + + let args = if params.is_empty() { + PyTuple::empty_bound(py) + } else { + PyTuple::new_bound(py, params) + }; + let kwargs = [ + ("label", label.to_object(py)), + ("unit", unit.to_object(py)), + ("duration", duration.to_object(py)), + ] + .into_py_dict_bound(py); + let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; + if condition.is_some() { + out = out.call_method0(py, "to_mutable")?; + out.setattr(py, "condition", condition.to_object(py))?; + } + Ok(out) + } + OperationType::Gate(gate) => Ok(gate.gate.clone_ref(py)), + OperationType::Instruction(inst) => Ok(inst.instruction.clone_ref(py)), + OperationType::Operation(op) => Ok(op.operation.clone_ref(py)), + } +} + +/// A container struct that contains the output from the Python object to +/// conversion to construct a CircuitInstruction object +#[derive(Debug)] +pub(crate) struct OperationTypeConstruct { + pub operation: OperationType, + pub params: SmallVec<[Param; 3]>, + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Convert an inbound Python object for a Qiskit operation and build a rust +/// representation of that operation. This will map it to appropriate variant +/// of operation type based on class +pub(crate) fn convert_py_to_operation_type( + py: Python, + py_op: PyObject, +) -> PyResult { + let attr = intern!(py, "_standard_gate"); + let py_op_bound = py_op.clone_ref(py).into_bound(py); + // Get PyType from either base_class if it exists, or if not use the + // class/type info from the pyobject + let binding = py_op_bound.getattr(intern!(py, "base_class")).ok(); + let op_obj = py_op_bound.get_type(); + let raw_op_type: Py = match binding { + Some(base_class) => base_class.downcast()?.clone().unbind(), + None => op_obj.unbind(), + }; + let op_type: Bound = raw_op_type.into_bound(py); + let mut standard: Option = match op_type.getattr(attr) { + Ok(stdgate) => stdgate.extract().ok().unwrap_or_default(), + Err(_) => None, + }; + // If the input instruction is a standard gate and a singleton instance + // we should check for mutable state. A mutable instance should be treated + // as a custom gate not a standard gate because it has custom properties. + // + // In the futuer we can revisit this when we've dropped `duration`, `unit`, + // and `condition` from the api as we should own the label in the + // `CircuitInstruction`. The other piece here is for controlled gates there + // is the control state, so for `SingletonControlledGates` we'll still need + // this check. + if standard.is_some() { + let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; + if mutable + && (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))? + || py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?) + { + standard = None; + } + } + if let Some(op) = standard { + let base_class = op_type.to_object(py); + populate_std_gate_map(py, op, base_class); + return Ok(OperationTypeConstruct { + operation: OperationType::Standard(op), + params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?, + label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?, + duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?, + unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?, + condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, + }); + } + if op_type.is_subclass(GATE.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyGate { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + gate: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Gate(out_op), + params, + label, + duration, + unit, + condition, + }); + } + if op_type.is_subclass(INSTRUCTION.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyInstruction { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + instruction: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Instruction(out_op), + params, + label, + duration, + unit, + condition, + }); + } + + if op_type.is_subclass(OPERATION.get_bound(py))? { + let params = match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.extract(py)?, + Err(_) => smallvec![], + }; + let label = None; + let duration = None; + let unit = None; + let condition = None; + let out_op = PyOperation { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.downcast_bound::(py)?.len() as u32, + Err(_) => 0, + }, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + operation: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Operation(out_op), + params, + label, + duration, + unit, + condition, + }); + } + Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) +} + +/// Issue a Python `DeprecationWarning` about using the legacy tuple-like interface to +/// `CircuitInstruction`. +/// +/// Beware the `stacklevel` here doesn't work quite the same way as it does in Python as Rust-space +/// calls are completely transparent to Python. +#[inline] +fn warn_on_legacy_circuit_instruction_iteration(py: Python) -> PyResult<()> { + WARNINGS_WARN + .get_bound(py) + .call1(( + intern!( + py, + concat!( + "Treating CircuitInstruction as an iterable is deprecated legacy behavior", + " since Qiskit 1.2, and will be removed in Qiskit 2.0.", + " Instead, use the `operation`, `qubits` and `clbits` named attributes." + ) + ), + py.get_type_bound::(), + // Stack level. Compared to Python-space calls to `warn`, this is unusually low + // beacuse all our internal call structure is now Rust-space and invisible to Python. + 1, + )) + .map(|_| ()) +} diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs new file mode 100644 index 00000000000..c8b6a4c8b08 --- /dev/null +++ b/crates/circuit/src/dag_node.rs @@ -0,0 +1,323 @@ +// 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::circuit_instruction::{ + convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, + ExtraInstructionAttributes, +}; +use crate::operations::Operation; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; +use pyo3::{intern, PyObject, PyResult}; + +/// 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, +} + +#[pymethods] +impl DAGNode { + #[new] + #[pyo3(signature=(nid=-1))] + fn new(nid: isize) -> Self { + DAGNode { _node_id: nid } + } + + fn __getstate__(&self) -> isize { + self._node_id + } + + fn __setstate__(&mut self, nid: isize) { + self._node_id = nid; + } + + fn __lt__(&self, other: &DAGNode) -> bool { + self._node_id < other._node_id + } + + fn __gt__(&self, other: &DAGNode) -> bool { + self._node_id > other._node_id + } + + fn __str__(_self: &Bound) -> String { + format!("{}", _self.as_ptr() as usize) + } + + fn __hash__(&self, py: Python) -> PyResult { + self._node_id.into_py(py).bind(py).hash() + } +} + +/// Object to represent an Instruction at a node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGOpNode { + pub instruction: CircuitInstruction, + #[pyo3(get)] + pub sort_key: PyObject, +} + +#[pymethods] +impl DAGOpNode { + #[new] + fn new( + py: Python, + op: PyObject, + 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() + } + } + } + None => qargs.str()?.into_any(), + }; + let res = convert_py_to_operation_type(py, op.clone_ref(py))?; + + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; + + Ok(( + DAGOpNode { + instruction: CircuitInstruction { + operation: res.operation, + qubits: qargs.unbind(), + clbits: cargs.unbind(), + params: res.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(op), + }, + sort_key: sort_key.unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyResult { + let state = (slf.as_ref()._node_id, &slf.sort_key); + Ok(( + py.get_type_bound::(), + ( + operation_type_to_py(py, &slf.instruction)?, + &slf.instruction.qubits, + &slf.instruction.clbits, + ), + 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; + slf.sort_key = sort_key; + Ok(()) + } + + #[getter] + fn get_op(&self, py: Python) -> PyResult { + operation_type_to_py(py, &self.instruction) + } + + #[setter] + fn set_op(&mut self, py: Python, op: PyObject) -> PyResult<()> { + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + self.instruction.params = res.params; + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; + self.instruction.extra_attrs = extra_attrs; + Ok(()) + } + + #[getter] + fn get_qargs(&self, py: Python) -> Py { + self.instruction.qubits.clone_ref(py) + } + + #[setter] + fn set_qargs(&mut self, qargs: Py) { + self.instruction.qubits = qargs; + } + + #[getter] + fn get_cargs(&self, py: Python) -> Py { + self.instruction.clbits.clone_ref(py) + } + + #[setter] + fn set_cargs(&mut self, cargs: Py) { + self.instruction.clbits = cargs; + } + + /// Returns the Instruction name corresponding to the op for this node + #[getter] + fn get_name(&self, py: Python) -> PyObject { + self.instruction.operation.name().to_object(py) + } + + /// Sets the Instruction name corresponding to the op for this node + #[setter] + fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { + let op = operation_type_to_py(py, &self.instruction)?; + op.bind(py).setattr(intern!(py, "name"), new_name)?; + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + Ok(()) + } + + /// Returns a representation of the DAGOpNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!( + "DAGOpNode(op={}, qargs={}, cargs={})", + operation_type_to_py(py, &self.instruction)? + .bind(py) + .repr()?, + self.instruction.qubits.bind(py).repr()?, + self.instruction.clbits.bind(py).repr()? + )) + } +} + +/// Object to represent an incoming wire node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGInNode { + #[pyo3(get)] + wire: PyObject, + #[pyo3(get)] + sort_key: PyObject, +} + +#[pymethods] +impl DAGInNode { + #[new] + fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok(( + DAGInNode { + wire, + sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyObject { + let state = (slf.as_ref()._node_id, &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; + slf.sort_key = sort_key; + Ok(()) + } + + /// Returns a representation of the DAGInNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!("DAGInNode(wire={})", self.wire.bind(py).repr()?)) + } +} + +/// Object to represent an outgoing wire node in the DAGCircuit. +#[pyclass(module = "qiskit._accelerate.circuit", extends=DAGNode)] +pub struct DAGOutNode { + #[pyo3(get)] + wire: PyObject, + #[pyo3(get)] + sort_key: PyObject, +} + +#[pymethods] +impl DAGOutNode { + #[new] + fn new(py: Python, wire: PyObject) -> PyResult<(Self, DAGNode)> { + Ok(( + DAGOutNode { + wire, + sort_key: PyList::empty_bound(py).str()?.into_any().unbind(), + }, + DAGNode { _node_id: -1 }, + )) + } + + fn __reduce__(slf: PyRef, py: Python) -> PyObject { + let state = (slf.as_ref()._node_id, &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; + slf.sort_key = sort_key; + Ok(()) + } + + /// Returns a representation of the DAGOutNode + fn __repr__(&self, py: Python) -> PyResult { + Ok(format!("DAGOutNode(wire={})", self.wire.bind(py).repr()?)) + } +} diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs new file mode 100644 index 00000000000..46585ff6da6 --- /dev/null +++ b/crates/circuit/src/gate_matrix.rs @@ -0,0 +1,442 @@ +// 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. + +use std::f64::consts::FRAC_1_SQRT_2; + +use crate::util::{ + c64, GateArray0Q, GateArray1Q, GateArray2Q, GateArray3Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM, +}; + +pub static ONE_QUBIT_IDENTITY: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_ONE]]; + +#[inline] +pub fn r_gate(theta: f64, phi: f64) -> GateArray1Q { + let half_theta = theta / 2.; + let cost = c64(half_theta.cos(), 0.); + let sint = half_theta.sin(); + let cosphi = phi.cos(); + let sinphi = phi.sin(); + [ + [cost, c64(-sint * sinphi, -sint * cosphi)], + [c64(sint * sinphi, -sint * cosphi), cost], + ] +} + +#[inline] +pub fn rx_gate(theta: f64) -> GateArray1Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., -half_theta.sin()); + [[cos, isin], [isin, cos]] +} + +#[inline] +pub fn ry_gate(theta: f64) -> GateArray1Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [[cos, -sin], [sin, cos]] +} + +#[inline] +pub fn rz_gate(theta: f64) -> GateArray1Q { + let ilam2 = c64(0., 0.5 * theta); + [[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] +} + +pub static H_GATE: GateArray1Q = [ + [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], + [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], +]; + +pub static CX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], +]; + +pub static SX_GATE: GateArray1Q = [ + [c64(0.5, 0.5), c64(0.5, -0.5)], + [c64(0.5, -0.5), c64(0.5, 0.5)], +]; + +pub static SXDG_GATE: GateArray1Q = [ + [c64(0.5, -0.5), c64(0.5, 0.5)], + [c64(0.5, 0.5), c64(0.5, -0.5)], +]; + +pub static X_GATE: GateArray1Q = [[C_ZERO, C_ONE], [C_ONE, C_ZERO]]; + +pub static Z_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_M_ONE]]; + +pub static Y_GATE: GateArray1Q = [[C_ZERO, M_IM], [IM, C_ZERO]]; + +pub static CZ_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_M_ONE], +]; + +pub static CY_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, M_IM], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, IM, C_ZERO, C_ZERO], +]; + +pub static CCX_GATE: GateArray3Q = [ + [ + C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], +]; + +pub static ECR_GATE: GateArray2Q = [ + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(0., FRAC_1_SQRT_2), + ], + [ + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(0., -FRAC_1_SQRT_2), + C_ZERO, + ], + [ + C_ZERO, + c64(0., FRAC_1_SQRT_2), + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + ], + [ + c64(0., -FRAC_1_SQRT_2), + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + ], +]; + +pub static SWAP_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], +]; +pub static ISWAP_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, IM, C_ZERO], + [C_ZERO, IM, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], +]; + +pub static S_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, IM]]; + +pub static SDG_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, M_IM]]; + +pub static T_GATE: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)]]; + +pub static TDG_GATE: GateArray1Q = [ + [C_ONE, C_ZERO], + [C_ZERO, c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], +]; + +pub static CH_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + ], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(-FRAC_1_SQRT_2, 0.), + ], +]; + +pub static CS_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, IM], +]; + +pub static CSDG_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, M_IM], +]; + +pub static CSX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, c64(0.5, 0.5), C_ZERO, c64(0.5, -0.5)], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, c64(0.5, -0.5), C_ZERO, c64(0.5, 0.5)], +]; + +pub static CSWAP_GATE: GateArray3Q = [ + [ + C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, + ], +]; + +pub static DCX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], +]; + +#[inline] +pub fn crx_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., half_theta.sin()); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -isin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, -isin, C_ZERO, cos], + ] +} + +#[inline] +pub fn cry_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -sin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, sin, C_ZERO, cos], + ] +} + +#[inline] +pub fn crz_gate(theta: f64) -> GateArray2Q { + let i_half_theta = c64(0., theta / 2.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], + ] +} + +#[inline] +pub fn global_phase_gate(theta: f64) -> GateArray0Q { + [[c64(0., theta).exp()]] +} + +#[inline] +pub fn phase_gate(lam: f64) -> GateArray1Q { + [[C_ONE, C_ZERO], [C_ZERO, c64(0., lam).exp()]] +} + +#[inline] +pub fn u_gate(theta: f64, phi: f64, lam: f64) -> GateArray1Q { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), (-c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} + +#[inline] +pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [ + c64(cos, 0.), + C_ZERO, + C_ZERO, + c64(0., -sin) * c64(0., -beta).exp(), + ], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [ + c64(0., -sin) * c64(0., beta).exp(), + C_ZERO, + C_ZERO, + c64(cos, 0.), + ], + ] +} + +#[inline] +pub fn u1_gate(lam: f64) -> GateArray1Q { + [[C_ONE, C_ZERO], [C_ZERO, c64(0., lam).exp()]] +} + +#[inline] +pub fn u2_gate(phi: f64, lam: f64) -> GateArray1Q { + [ + [ + c64(FRAC_1_SQRT_2, 0.), + (-c64(0., lam).exp()) * FRAC_1_SQRT_2, + ], + [ + c64(0., phi).exp() * FRAC_1_SQRT_2, + c64(0., phi + lam).exp() * FRAC_1_SQRT_2, + ], + ] +} + +#[inline] +pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> GateArray1Q { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), -(c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} + +#[inline] +pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [ + C_ZERO, + c64(cos, 0.), + c64(0., -sin) * c64(0., -beta).exp(), + C_ZERO, + ], + [ + C_ZERO, + c64(0., -sin) * c64(0., beta).exp(), + c64(cos, 0.), + C_ZERO, + ], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], + ] +} + +#[inline] +pub fn cp_gate(lam: f64) -> GateArray2Q { + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, c64(0., lam).exp()], + ] +} + +#[inline] +pub fn rxx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csinm = c64(0., -sint); + + [ + [ccos, C_ZERO, C_ZERO, csinm], + [C_ZERO, ccos, csinm, C_ZERO], + [C_ZERO, csinm, ccos, C_ZERO], + [csinm, C_ZERO, C_ZERO, ccos], + ] +} + +#[inline] +pub fn ryy_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + + [ + [ccos, C_ZERO, C_ZERO, csin], + [C_ZERO, ccos, -csin, C_ZERO], + [C_ZERO, -csin, ccos, C_ZERO], + [csin, C_ZERO, C_ZERO, ccos], + ] +} + +#[inline] +pub fn rzz_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let exp_it2 = c64(cost, sint); + let exp_mit2 = c64(cost, -sint); + + [ + [exp_mit2, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, exp_it2, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, exp_it2, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, exp_mit2], + ] +} + +#[inline] +pub fn rzx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + + [ + [ccos, C_ZERO, -csin, C_ZERO], + [C_ZERO, ccos, C_ZERO, csin], + [-csin, C_ZERO, ccos, C_ZERO], + [C_ZERO, csin, C_ZERO, ccos], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs new file mode 100644 index 00000000000..53fee34f486 --- /dev/null +++ b/crates/circuit/src/imports.rs @@ -0,0 +1,247 @@ +// 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. + +// This module contains objects imported from Python that are reused. These are +// typically data model classes that are used to identify an object, or for +// python side casting + +use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; + +use crate::operations::{StandardGate, STANDARD_GATE_SIZE}; + +/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object +/// that is lazily imported. +pub struct ImportOnceCell { + module: &'static str, + object: &'static str, + cell: GILOnceCell>, +} + +impl ImportOnceCell { + const fn new(module: &'static str, object: &'static str) -> Self { + Self { + module, + object, + cell: GILOnceCell::new(), + } + } + + /// Get the underlying GIL-independent reference to the contained object, importing if + /// required. + #[inline] + pub fn get(&self, py: Python) -> &Py { + self.cell.get_or_init(py, || { + py.import_bound(self.module) + .unwrap() + .getattr(self.object) + .unwrap() + .unbind() + }) + } + + /// Get a GIL-bound reference to the contained object, importing if required. + #[inline] + pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> { + self.get(py).bind(py) + } +} + +pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); +pub static INSTRUCTION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); +pub static GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.gate", "Gate"); +pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); +pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static PARAMETER_EXPRESSION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); +pub static QUANTUM_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); +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 WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); + +/// 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 +/// when a gate is added directly via the StandardGate path and there isn't a Python object +/// to poll the _standard_gate attribute for. +/// +/// NOTE: the order here is significant, the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +// TODO: replace placeholders with actual implementation +static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ + // ZGate = 0 + ["qiskit.circuit.library.standard_gates.z", "ZGate"], + // YGate = 1 + ["qiskit.circuit.library.standard_gates.y", "YGate"], + // XGate = 2 + ["qiskit.circuit.library.standard_gates.x", "XGate"], + // CZGate = 3 + ["qiskit.circuit.library.standard_gates.z", "CZGate"], + // CYGate = 4 + ["qiskit.circuit.library.standard_gates.y", "CYGate"], + // CXGate = 5 + ["qiskit.circuit.library.standard_gates.x", "CXGate"], + // CCXGate = 6 + ["qiskit.circuit.library.standard_gates.x", "CCXGate"], + // RXGate = 7 + ["qiskit.circuit.library.standard_gates.rx", "RXGate"], + // RYGate = 8 + ["qiskit.circuit.library.standard_gates.ry", "RYGate"], + // RZGate = 9 + ["qiskit.circuit.library.standard_gates.rz", "RZGate"], + // ECRGate = 10 + ["qiskit.circuit.library.standard_gates.ecr", "ECRGate"], + // SwapGate = 11 + ["qiskit.circuit.library.standard_gates.swap", "SwapGate"], + // SXGate = 12 + ["qiskit.circuit.library.standard_gates.sx", "SXGate"], + // GlobalPhaseGate = 13 + [ + "qiskit.circuit.library.standard_gates.global_phase", + "GlobalPhaseGate", + ], + // IGate = 14 + ["qiskit.circuit.library.standard_gates.i", "IGate"], + // HGate = 15 + ["qiskit.circuit.library.standard_gates.h", "HGate"], + // PhaseGate = 16 + ["qiskit.circuit.library.standard_gates.p", "PhaseGate"], + // UGate = 17 + ["qiskit.circuit.library.standard_gates.u", "UGate"], + // SGate = 18 + ["qiskit.circuit.library.standard_gates.s", "SGate"], + // SdgGate = 19 + ["qiskit.circuit.library.standard_gates.s", "SdgGate"], + // TGate = 20 + ["qiskit.circuit.library.standard_gates.t", "TGate"], + // TdgGate = 21 + ["qiskit.circuit.library.standard_gates.t", "TdgGate"], + // SXdgGate = 22 + ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], + // iSWAPGate = 23 + ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], + // XXMinusYYGate = 24 + [ + "qiskit.circuit.library.standard_gates.xx_minus_yy", + "XXMinusYYGate", + ], + // XXPlusYYGate = 25 + [ + "qiskit.circuit.library.standard_gates.xx_plus_yy", + "XXPlusYYGate", + ], + // U1Gate = 26 + ["qiskit.circuit.library.standard_gates.u1", "U1Gate"], + // U2Gate = 27 + ["qiskit.circuit.library.standard_gates.u2", "U2Gate"], + // U3Gate = 28 + ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], + // CRXGate = 29 + ["qiskit.circuit.library.standard_gates.rx", "CRXGate"], + // CRYGate = 30 + ["qiskit.circuit.library.standard_gates.ry", "CRYGate"], + // CRZGate = 31 + ["qiskit.circuit.library.standard_gates.rz", "CRZGate"], + // RGate 32 + ["qiskit.circuit.library.standard_gates.r", "RGate"], + // CHGate = 33 + ["qiskit.circuit.library.standard_gates.h", "CHGate"], + // CPhaseGate = 34 + ["qiskit.circuit.library.standard_gates.p", "CPhaseGate"], + // CSGate = 35 + ["qiskit.circuit.library.standard_gates.s", "CSGate"], + // CSdgGate = 36 + ["qiskit.circuit.library.standard_gates.s", "CSdgGate"], + // CSXGate = 37 + ["qiskit.circuit.library.standard_gates.sx", "CSXGate"], + // CSwapGate = 38 + ["qiskit.circuit.library.standard_gates.swap", "CSwapGate"], + // CUGate = 39 + ["qiskit.circuit.library.standard_gates.u", "CUGate"], + // CU1Gate = 40 + ["qiskit.circuit.library.standard_gates.u1", "CU1Gate"], + // CU3Gate = 41 + ["qiskit.circuit.library.standard_gates.u3", "CU3Gate"], + // C3XGate = 42 + ["placeholder", "placeholder"], + // C3SXGate = 43 + ["placeholder", "placeholder"], + // C4XGate = 44 + ["placeholder", "placeholder"], + // DCXGate = 45 + ["qiskit.circuit.library.standard_gates.dcx", "DCXGate"], + // CCZGate = 46 + ["placeholder", "placeholder"], + // RCCXGate = 47 + ["placeholder", "placeholder"], + // RC3XGate = 48 + ["placeholder", "placeholder"], + // RXXGate = 49 + ["qiskit.circuit.library.standard_gates.rxx", "RXXGate"], + // RYYGate = 50 + ["qiskit.circuit.library.standard_gates.ryy", "RYYGate"], + // RZZGate = 51 + ["qiskit.circuit.library.standard_gates.rzz", "RZZGate"], + // RZXGate = 52 + ["qiskit.circuit.library.standard_gates.rzx", "RZXGate"], +]; + +/// A mapping from the enum variant in crate::operations::StandardGate to the python object for the +/// class that matches it. This is typically used when we need to convert from the internal rust +/// representation to a Python object for a python user to interact with. +/// +/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +static mut STDGATE_PYTHON_GATES: GILOnceCell<[Option; STANDARD_GATE_SIZE]> = + GILOnceCell::new(); + +#[inline] +pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObject) { + let gate_map = unsafe { + match STDGATE_PYTHON_GATES.get_mut() { + Some(gate_map) => gate_map, + None => { + let array: [Option; STANDARD_GATE_SIZE] = std::array::from_fn(|_| None); + STDGATE_PYTHON_GATES.set(py, array).unwrap(); + STDGATE_PYTHON_GATES.get_mut().unwrap() + } + } + }; + let gate_cls = &gate_map[rs_gate as usize]; + if gate_cls.is_none() { + gate_map[rs_gate as usize] = Some(py_gate.clone_ref(py)); + } +} + +#[inline] +pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { + let gate_map = + unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || std::array::from_fn(|_| None)) }; + let gate = &gate_map[rs_gate as usize]; + let populate = gate.is_none(); + let out_gate = match gate { + Some(gate) => gate.clone_ref(py), + None => { + let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize]; + py.import_bound(py_mod)?.getattr(py_class)?.unbind() + } + }; + if populate { + populate_std_gate_map(py, rs_gate, out_gate.clone_ref(py)); + } + Ok(out_gate) +} diff --git a/crates/circuit/src/intern_context.rs b/crates/circuit/src/intern_context.rs deleted file mode 100644 index 0c8b596e6dd..00000000000 --- a/crates/circuit/src/intern_context.rs +++ /dev/null @@ -1,71 +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. - -use hashbrown::HashMap; -use pyo3::exceptions::PyRuntimeError; -use pyo3::PyResult; -use std::sync::Arc; - -pub type IndexType = u32; -pub type BitType = u32; - -/// A Rust-only data structure (not a pyclass!) for interning -/// `Vec`. -/// -/// Takes ownership of vectors given to [InternContext.intern] -/// and returns an [IndexType] index that can be used to look up -/// an _equivalent_ sequence by reference via [InternContext.lookup]. -#[derive(Clone, Debug)] -pub struct InternContext { - slots: Vec>>, - slot_lookup: HashMap>, IndexType>, -} - -impl InternContext { - pub fn new() -> Self { - InternContext { - slots: Vec::new(), - slot_lookup: HashMap::new(), - } - } - - /// Takes `args` by reference and returns an index that can be used - /// to obtain a reference to an equivalent sequence of `BitType` by - /// calling [CircuitData.lookup]. - pub fn intern(&mut self, args: Vec) -> PyResult { - if let Some(slot_idx) = self.slot_lookup.get(&args) { - return Ok(*slot_idx); - } - - let args = Arc::new(args); - let slot_idx: IndexType = self - .slots - .len() - .try_into() - .map_err(|_| PyRuntimeError::new_err("InternContext capacity exceeded!"))?; - self.slots.push(args.clone()); - self.slot_lookup.insert_unique_unchecked(args, slot_idx); - Ok(slot_idx) - } - - /// Returns the sequence corresponding to `slot_idx`, which must - /// be a value returned by [InternContext.intern]. - pub fn lookup(&self, slot_idx: IndexType) -> &[BitType] { - self.slots.get(slot_idx as usize).unwrap() - } -} - -impl Default for InternContext { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs new file mode 100644 index 00000000000..f22bb80ae05 --- /dev/null +++ b/crates/circuit/src/interner.rs @@ -0,0 +1,133 @@ +// 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. + +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), +} + +impl From for InternerKey { + fn from(value: Index) -> Self { + InternerKey::Index(value) + } +} + +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) + } +} + +pub struct CacheFullError; + +impl From for PyErr { + fn from(_: CacheFullError) -> Self { + PyRuntimeError::new_err("The bit operands cache is full!") + } +} + +/// 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>; + + fn intern(self, index: Index) -> Self::Output { + let value = self.entries.get(index.0 as usize).unwrap(); + InternerValue { + index, + value: value.as_ref(), + } + } +} + +impl<'a, T> Interner for &'a mut IndexedInterner +where + T: Eq + Hash, +{ + type Key = InternerKey; + type Output = Result, CacheFullError>; + + 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, + }) + } + } + } + } +} + +impl IndexedInterner { + pub fn new() -> Self { + IndexedInterner { + entries: Vec::new(), + index_lookup: HashMap::new(), + } + } +} + +impl Default for IndexedInterner { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index cd560bad738..9f0a8017bf2 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -1,6 +1,6 @@ // This code is part of Qiskit. // -// (C) Copyright IBM 2023 +// (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 @@ -12,24 +12,60 @@ pub mod circuit_data; pub mod circuit_instruction; -pub mod intern_context; +pub mod dag_node; +pub mod gate_matrix; +pub mod imports; +pub mod operations; +pub mod parameter_table; +pub mod slice; +pub mod util; + +mod bit_data; +mod interner; use pyo3::prelude::*; -use pyo3::types::PySlice; - -/// A private enumeration type used to extract arguments to pymethod -/// that may be either an index or a slice -#[derive(FromPyObject)] -pub enum SliceOrInt<'a> { - // The order here defines the order the variants are tried in the FromPyObject` derivation. - // `Int` is _much_ more common, so that should be first. - Int(isize), - Slice(Bound<'a, PySlice>), + +pub type BitType = u32; +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Qubit(pub BitType); +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Clbit(pub BitType); + +impl From for Qubit { + fn from(value: BitType) -> Self { + Qubit(value) + } +} + +impl From for BitType { + fn from(value: Qubit) -> Self { + value.0 + } +} + +impl From for Clbit { + fn from(value: BitType) -> Self { + Clbit(value) + } +} + +impl From for BitType { + fn from(value: Clbit) -> Self { + value.0 + } } #[pymodule] 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::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs new file mode 100644 index 00000000000..df15b4abb41 --- /dev/null +++ b/crates/circuit/src/operations.rs @@ -0,0 +1,1631 @@ +// 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 crate::circuit_data::CircuitData; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::{gate_matrix, Qubit}; + +use ndarray::{aview2, Array2}; +use num_complex::Complex64; +use numpy::IntoPyArray; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::{intern, IntoPy, Python}; +use smallvec::smallvec; + +/// Valid types for an operation field in a CircuitInstruction +/// +/// These are basically the types allowed in a QuantumCircuit +#[derive(FromPyObject, Clone, Debug)] +pub enum OperationType { + Standard(StandardGate), + Instruction(PyInstruction), + Gate(PyGate), + Operation(PyOperation), +} + +impl Operation for OperationType { + fn name(&self) -> &str { + match self { + Self::Standard(op) => op.name(), + Self::Gate(op) => op.name(), + Self::Instruction(op) => op.name(), + Self::Operation(op) => op.name(), + } + } + + fn num_qubits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_qubits(), + Self::Gate(op) => op.num_qubits(), + Self::Instruction(op) => op.num_qubits(), + Self::Operation(op) => op.num_qubits(), + } + } + fn num_clbits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_clbits(), + Self::Gate(op) => op.num_clbits(), + Self::Instruction(op) => op.num_clbits(), + Self::Operation(op) => op.num_clbits(), + } + } + + fn num_params(&self) -> u32 { + match self { + Self::Standard(op) => op.num_params(), + Self::Gate(op) => op.num_params(), + Self::Instruction(op) => op.num_params(), + Self::Operation(op) => op.num_params(), + } + } + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::Standard(op) => op.matrix(params), + Self::Gate(op) => op.matrix(params), + Self::Instruction(op) => op.matrix(params), + Self::Operation(op) => op.matrix(params), + } + } + + fn control_flow(&self) -> bool { + match self { + Self::Standard(op) => op.control_flow(), + Self::Gate(op) => op.control_flow(), + Self::Instruction(op) => op.control_flow(), + Self::Operation(op) => op.control_flow(), + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::Standard(op) => op.definition(params), + Self::Gate(op) => op.definition(params), + Self::Instruction(op) => op.definition(params), + Self::Operation(op) => op.definition(params), + } + } + + fn standard_gate(&self) -> Option { + match self { + Self::Standard(op) => op.standard_gate(), + Self::Gate(op) => op.standard_gate(), + Self::Instruction(op) => op.standard_gate(), + Self::Operation(op) => op.standard_gate(), + } + } + + fn directive(&self) -> bool { + match self { + Self::Standard(op) => op.directive(), + Self::Gate(op) => op.directive(), + Self::Instruction(op) => op.directive(), + Self::Operation(op) => op.directive(), + } + } +} + +/// Trait for generic circuit operations these define the common attributes +/// needed for something to be addable to the circuit struct +pub trait Operation { + fn name(&self) -> &str; + fn num_qubits(&self) -> u32; + fn num_clbits(&self) -> u32; + fn num_params(&self) -> u32; + fn control_flow(&self) -> bool; + fn matrix(&self, params: &[Param]) -> Option>; + fn definition(&self, params: &[Param]) -> Option; + fn standard_gate(&self) -> Option; + fn directive(&self) -> bool; +} + +#[derive(Clone, Debug)] +pub enum Param { + ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +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()))? + || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? + { + Param::ParameterExpression(b.clone().unbind()) + } else if let Ok(val) = b.extract::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }, + ) + } +} + +impl IntoPy for Param { + fn into_py(self, py: Python) -> PyObject { + match &self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +impl ToPyObject for Param { + fn to_object(&self, py: Python) -> PyObject { + match self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[pyclass(module = "qiskit._accelerate.circuit")] +pub enum StandardGate { + ZGate = 0, + YGate = 1, + XGate = 2, + CZGate = 3, + CYGate = 4, + CXGate = 5, + CCXGate = 6, + RXGate = 7, + RYGate = 8, + RZGate = 9, + ECRGate = 10, + SwapGate = 11, + SXGate = 12, + GlobalPhaseGate = 13, + IGate = 14, + HGate = 15, + PhaseGate = 16, + UGate = 17, + SGate = 18, + SdgGate = 19, + TGate = 20, + TdgGate = 21, + SXdgGate = 22, + ISwapGate = 23, + XXMinusYYGate = 24, + XXPlusYYGate = 25, + U1Gate = 26, + U2Gate = 27, + U3Gate = 28, + CRXGate = 29, + CRYGate = 30, + CRZGate = 31, + RGate = 32, + CHGate = 33, + CPhaseGate = 34, + CSGate = 35, + CSdgGate = 36, + CSXGate = 37, + CSwapGate = 38, + CUGate = 39, + CU1Gate = 40, + CU3Gate = 41, + C3XGate = 42, + C3SXGate = 43, + C4XGate = 44, + DCXGate = 45, + CCZGate = 46, + RCCXGate = 47, + RC3XGate = 48, + RXXGate = 49, + RYYGate = 50, + RZZGate = 51, + RZXGate = 52, +} + +// TODO: replace all 34s (placeholders) with actual number +static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 + 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 + 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, // 20-29 + 2, 2, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 2, 2, 34, 34, 34, 2, 34, 34, 34, 2, // 40-49 + 2, 2, 2, // 50-52 +]; + +// TODO: replace all 34s (placeholders) with actual number +static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 + 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 + 0, 0, 0, 0, 2, 2, 1, 2, 3, 1, // 20-29 + 1, 1, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 1, 3, 34, 34, 34, 0, 34, 34, 34, 1, // 40-49 + 1, 1, 1, // 50-52 +]; + +static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ + "z", // 0 + "y", // 1 + "x", // 2 + "cz", // 3 + "cy", // 4 + "cx", // 5 + "ccx", // 6 + "rx", // 7 + "ry", // 8 + "rz", // 9 + "ecr", // 10 + "swap", // 11 + "sx", // 12 + "global_phase", // 13 + "id", // 14 + "h", // 15 + "p", // 16 + "u", // 17 + "s", // 18 + "sdg", // 19 + "t", // 20 + "tdg", // 21 + "sxdg", // 22 + "iswap", // 23 + "xx_minus_yy", // 24 + "xx_plus_yy", // 25 + "u1", // 26 + "u2", // 27 + "u3", // 28 + "crx", // 29 + "cry", // 30 + "crz", // 31 + "r", // 32 + "ch", // 33 + "cp", // 34 + "cs", // 35 + "csdg", // 36 + "csx", // 37 + "cswap", // 38 + "cu", // 39 + "cu1", // 40 + "cu3", // 41 + "c3x", // 42 + "c3sx", // 43 + "c4x", // 44 + "dcx", // 45 + "ccz", // 46 + "rccx", // 47 + "rc3x", // 48 + "rxx", // 49 + "ryy", // 50 + "rzz", // 51 + "rzx", // 52 +]; + +#[pymethods] +impl StandardGate { + pub fn copy(&self) -> Self { + *self + } + + // These pymethods are for testing: + pub fn _to_matrix(&self, py: Python, params: Vec) -> Option { + self.matrix(¶ms) + .map(|x| x.into_pyarray_bound(py).into()) + } + + pub fn _num_params(&self) -> u32 { + self.num_params() + } + + pub fn _get_definition(&self, params: Vec) -> Option { + self.definition(¶ms) + } + + #[getter] + pub fn get_num_qubits(&self) -> u32 { + self.num_qubits() + } + + #[getter] + pub fn get_num_clbits(&self) -> u32 { + self.num_clbits() + } + + #[getter] + pub fn get_num_params(&self) -> u32 { + self.num_params() + } + + #[getter] + pub fn get_name(&self) -> &str { + self.name() + } +} + +// This must be kept up-to-date with `StandardGate` when adding or removing +// gates from the enum +// +// Remove this when std::mem::variant_count() is stabilized (see +// https://github.com/rust-lang/rust/issues/73662 ) +pub const STANDARD_GATE_SIZE: usize = 53; + +impl Operation for StandardGate { + fn name(&self) -> &str { + STANDARD_GATE_NAME[*self as usize] + } + + fn num_qubits(&self) -> u32 { + STANDARD_GATE_NUM_QUBITS[*self as usize] + } + + fn num_params(&self) -> u32 { + STANDARD_GATE_NUM_PARAMS[*self as usize] + } + + fn num_clbits(&self) -> u32 { + 0 + } + + fn control_flow(&self) -> bool { + false + } + + fn directive(&self) -> bool { + false + } + + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::ZGate => match params { + [] => Some(aview2(&gate_matrix::Z_GATE).to_owned()), + _ => None, + }, + Self::YGate => match params { + [] => Some(aview2(&gate_matrix::Y_GATE).to_owned()), + _ => None, + }, + Self::XGate => match params { + [] => Some(aview2(&gate_matrix::X_GATE).to_owned()), + _ => None, + }, + Self::CZGate => match params { + [] => Some(aview2(&gate_matrix::CZ_GATE).to_owned()), + _ => None, + }, + Self::CYGate => match params { + [] => Some(aview2(&gate_matrix::CY_GATE).to_owned()), + _ => None, + }, + Self::CXGate => match params { + [] => Some(aview2(&gate_matrix::CX_GATE).to_owned()), + _ => None, + }, + Self::CCXGate => match params { + [] => Some(aview2(&gate_matrix::CCX_GATE).to_owned()), + _ => None, + }, + Self::RXGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()), + _ => None, + }, + Self::RYGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()), + _ => None, + }, + Self::RZGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), + _ => None, + }, + Self::CRXGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::crx_gate(*theta)).to_owned()), + _ => None, + }, + Self::CRYGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::cry_gate(*theta)).to_owned()), + _ => None, + }, + Self::CRZGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::crz_gate(*theta)).to_owned()), + _ => None, + }, + Self::ECRGate => match params { + [] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()), + _ => None, + }, + Self::SwapGate => match params { + [] => Some(aview2(&gate_matrix::SWAP_GATE).to_owned()), + _ => None, + }, + Self::SXGate => match params { + [] => Some(aview2(&gate_matrix::SX_GATE).to_owned()), + _ => None, + }, + Self::SXdgGate => match params { + [] => Some(aview2(&gate_matrix::SXDG_GATE).to_owned()), + _ => None, + }, + Self::GlobalPhaseGate => match params { + [Param::Float(theta)] => { + Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) + } + _ => None, + }, + Self::IGate => match params { + [] => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()), + _ => None, + }, + Self::HGate => match params { + [] => Some(aview2(&gate_matrix::H_GATE).to_owned()), + _ => None, + }, + Self::PhaseGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()), + _ => None, + }, + Self::UGate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, + Self::SGate => match params { + [] => Some(aview2(&gate_matrix::S_GATE).to_owned()), + _ => None, + }, + Self::SdgGate => match params { + [] => Some(aview2(&gate_matrix::SDG_GATE).to_owned()), + _ => None, + }, + Self::TGate => match params { + [] => Some(aview2(&gate_matrix::T_GATE).to_owned()), + _ => None, + }, + Self::TdgGate => match params { + [] => Some(aview2(&gate_matrix::TDG_GATE).to_owned()), + _ => None, + }, + Self::ISwapGate => match params { + [] => Some(aview2(&gate_matrix::ISWAP_GATE).to_owned()), + _ => None, + }, + Self::XXMinusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_minus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, + Self::XXPlusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_plus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, + Self::U1Gate => match params[0] { + Param::Float(val) => Some(aview2(&gate_matrix::u1_gate(val)).to_owned()), + _ => None, + }, + Self::U2Gate => match params { + [Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u2_gate(*phi, *lam)).to_owned()) + } + _ => None, + }, + Self::U3Gate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u3_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, + Self::CHGate => match params { + [] => Some(aview2(&gate_matrix::CH_GATE).to_owned()), + _ => None, + }, + Self::CPhaseGate => match params { + [Param::Float(lam)] => Some(aview2(&gate_matrix::cp_gate(*lam)).to_owned()), + _ => None, + }, + Self::CSGate => match params { + [] => Some(aview2(&gate_matrix::CS_GATE).to_owned()), + _ => None, + }, + Self::CSdgGate => match params { + [] => Some(aview2(&gate_matrix::CSDG_GATE).to_owned()), + _ => None, + }, + Self::CSXGate => match params { + [] => Some(aview2(&gate_matrix::CSX_GATE).to_owned()), + _ => None, + }, + Self::CSwapGate => match params { + [] => Some(aview2(&gate_matrix::CSWAP_GATE).to_owned()), + _ => None, + }, + Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::RGate => match params { + [Param::Float(theta), Param::Float(phi)] => { + Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) + } + _ => None, + }, + Self::DCXGate => match params { + [] => Some(aview2(&gate_matrix::DCX_GATE).to_owned()), + _ => None, + }, + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rxx_gate(theta)).to_owned()), + _ => None, + }, + Self::RYYGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::ryy_gate(theta)).to_owned()), + _ => None, + }, + Self::RZZGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzz_gate(theta)).to_owned()), + _ => None, + }, + Self::RZXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzx_gate(theta)).to_owned()), + _ => None, + }, + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::ZGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::YGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![ + Param::Float(PI), + Param::Float(PI / 2.), + Param::Float(PI / 2.), + ], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CYGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SdgGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::SGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CXGate => None, + Self::CCXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q2 = smallvec![Qubit(2)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + let q0_2 = smallvec![Qubit(0), Qubit(2)]; + let q1_2 = smallvec![Qubit(1), Qubit(2)]; + Some( + CircuitData::from_standard_gates( + py, + 3, + [ + (Self::HGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q1_2.clone()), + (Self::TdgGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q0_2.clone()), + (Self::TGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q1_2), + (Self::TdgGate, smallvec![], q2.clone()), + (Self::CXGate, smallvec![], q0_2), + (Self::TGate, smallvec![], q1.clone()), + (Self::TGate, smallvec![], q2.clone()), + (Self::HGate, smallvec![], q2), + (Self::CXGate, smallvec![], q0_1.clone()), + (Self::TGate, smallvec![], q0), + (Self::TdgGate, smallvec![], q1), + (Self::CXGate, smallvec![], q0_1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RXGate => todo!("Add when we have R"), + Self::RYGate => todo!("Add when we have R"), + Self::RZGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![theta.clone()], + smallvec![Qubit(0)], + )], + multiply_param(theta, -0.5, py), + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CRXGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::PhaseGate, + smallvec![Param::Float(PI / 2.)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::UGate, + smallvec![ + multiply_param(theta, -0.5, py), + Param::Float(0.0), + Param::Float(0.0) + ], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::UGate, + smallvec![ + multiply_param(theta, 0.5, py), + Param::Float(-PI / 2.), + Param::Float(0.0) + ], + smallvec![Qubit(1)], + ), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), + Self::CRYGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RYGate, + smallvec![multiply_param(theta, 0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), + Self::CRZGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RZGate, + smallvec![multiply_param(theta, 0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ( + Self::RZGate, + smallvec![multiply_param(theta, -0.5, py)], + smallvec![Qubit(1)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + Param::Float(0.0), + ) + .expect("Unexpected Qiskit Python bug!"), + ) + }), + Self::ECRGate => todo!("Add when we have RZX"), + Self::SwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SXGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [ + (Self::SdgGate, smallvec![], smallvec![Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::SdgGate, smallvec![], smallvec![Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SXdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [ + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates(py, 0, [], params[0].clone()) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::IGate => None, + Self::HGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::PhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(0.), Param::Float(0.), params[0].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::UGate => None, + Self::U1Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::U2Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::U3Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI / 2.)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(-PI / 2.)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::TGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::TdgGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(-PI / 4.)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::ISwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SGate, smallvec![], smallvec![Qubit(0)]), + (Self::SGate, smallvec![], smallvec![Qubit(1)]), + (Self::HGate, smallvec![], smallvec![Qubit(0)]), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + (Self::HGate, smallvec![], smallvec![Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XXMinusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RZGate, + smallvec![multiply_param(beta, -1.0, py)], + q1.clone(), + ), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q0.clone()), + (Self::SXGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q0.clone()), + (Self::SGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, 0.5, py)], + q0.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::SdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q0.clone()), + (Self::SXdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q0), + (Self::RZGate, smallvec![beta.clone()], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XXPlusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q1_0 = smallvec![Qubit(1), Qubit(0)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RZGate, smallvec![beta.clone()], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q1.clone()), + (Self::SXGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q1.clone()), + (Self::SGate, smallvec![], q0.clone()), + (Self::CXGate, smallvec![], q1_0.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q0.clone(), + ), + (Self::CXGate, smallvec![], q1_0), + (Self::SdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI / 2.)], q1.clone()), + (Self::SXdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI / 2.)], q1), + (Self::RZGate, smallvec![multiply_param(beta, -1.0, py)], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CHGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::TGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::TdgGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::SdgGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CPhaseGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q0, + ), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q1, + ), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(-PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSdgGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CPhaseGate, smallvec![Param::Float(PI / 2.)], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 3, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ( + Self::CCXGate, + smallvec![], + smallvec![Qubit(0), Qubit(1), Qubit(2)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RGate => Python::with_gil(|py| -> Option { + let theta_expr = clone_param(¶ms[0], py); + let phi_expr1 = add_param(¶ms[1], -PI / 2., py); + let phi_expr2 = multiply_param(&phi_expr1, -1.0, py); + let defparams = smallvec![theta_expr, phi_expr1, phi_expr2]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [(Self::UGate, defparams, smallvec![Qubit(0)])], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CUGate => todo!(), + Self::CU1Gate => todo!(), + Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::DCXGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q0.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + (Self::HGate, smallvec![], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q0.clone()), + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q0), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1), + (Self::CXGate, smallvec![], q0_q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + } + } + + fn standard_gate(&self) -> Option { + Some(*self) + } +} + +const FLOAT_ZERO: Param = Param::Float(0.0); + +// Return explicitly requested copy of `param`, handling +// each variant separately. +fn clone_param(param: &Param, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta), + Param::ParameterExpression(theta) => Param::ParameterExpression(theta.clone_ref(py)), + Param::Obj(_) => unreachable!(), + } +} + +fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta * mult), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__rmul__"), (mult,)) + .expect("Multiplication of Parameter expression by float failed."), + ), + Param::Obj(_) => unreachable!(), + } +} + +fn add_param(param: &Param, summand: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta + summand), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (summand,)) + .expect("Sum of Parameter expression and float failed."), + ), + Param::Obj(_) => unreachable!(), + } +} + +/// This class is used to wrap a Python side Instruction that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyInstruction { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub instruction: PyObject, +} + +#[pymethods] +impl PyInstruction { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, instruction: PyObject) -> Self { + PyInstruction { + qubits, + clbits, + params, + op_name, + instruction, + } + } +} + +impl Operation for PyInstruction { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.instruction.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.instruction.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} + +/// This class is used to wrap a Python side Gate that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyGate { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub gate: PyObject, +} + +#[pymethods] +impl PyGate { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, gate: PyObject) -> Self { + PyGate { + qubits, + clbits, + params, + op_name, + gate, + } + } +} + +impl Operation for PyGate { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + Python::with_gil(|py| -> Option> { + match self.gate.getattr(py, intern!(py, "to_matrix")) { + Ok(to_matrix) => { + let res: Option = to_matrix.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let array: PyReadonlyArray2 = x.extract(py).ok()?; + Some(array.as_array().to_owned()) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + 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, + }, + Err(_) => None, + } + }) + } + fn directive(&self) -> bool { + false + } +} + +/// This class is used to wrap a Python side Operation that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyOperation { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub operation: PyObject, +} + +#[pymethods] +impl PyOperation { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, operation: PyObject) -> Self { + PyOperation { + qubits, + clbits, + params, + op_name, + operation, + } + } +} + +impl Operation for PyOperation { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + None + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.operation.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs new file mode 100644 index 00000000000..48c779eed3a --- /dev/null +++ b/crates/circuit/src/parameter_table.rs @@ -0,0 +1,173 @@ +// 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::{import_exception, intern, PyObject}; + +import_exception!(qiskit.circuit.exceptions, CircuitError); + +use hashbrown::{HashMap, HashSet}; + +/// The index value in a `ParamEntry` that indicates the global phase. +pub const GLOBAL_PHASE_INDEX: usize = usize::MAX; + +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamEntryKeys { + keys: Vec<(usize, usize)>, + iter_pos: usize, +} + +#[pymethods] +impl ParamEntryKeys { + fn __iter__(slf: PyRef) -> Py { + slf.into() + } + + fn __next__(mut slf: PyRefMut) -> Option<(usize, usize)> { + if slf.iter_pos < slf.keys.len() { + let res = Some(slf.keys[slf.iter_pos]); + slf.iter_pos += 1; + res + } else { + None + } + } +} + +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamEntry { + /// Mapping of tuple of instruction index (in CircuitData) and parameter index to the actual + /// parameter object + pub index_ids: HashSet<(usize, usize)>, +} + +impl ParamEntry { + pub fn add(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.insert((inst_index, param_index)); + } + + pub fn discard(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.remove(&(inst_index, param_index)); + } +} + +#[pymethods] +impl ParamEntry { + #[new] + pub fn new(inst_index: usize, param_index: usize) -> Self { + ParamEntry { + index_ids: HashSet::from([(inst_index, param_index)]), + } + } + + pub fn __len__(&self) -> usize { + self.index_ids.len() + } + + pub fn __contains__(&self, key: (usize, usize)) -> bool { + self.index_ids.contains(&key) + } + + pub fn __iter__(&self) -> ParamEntryKeys { + ParamEntryKeys { + keys: self.index_ids.iter().copied().collect(), + iter_pos: 0, + } + } +} + +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamTable { + /// Mapping of parameter uuid (as an int) to the Parameter Entry + pub table: HashMap, + /// Mapping of parameter name to uuid as an int + pub names: HashMap, + /// Mapping of uuid to a parameter object + pub uuid_map: HashMap, +} + +impl ParamTable { + pub fn insert(&mut self, py: Python, parameter: PyObject, entry: ParamEntry) -> PyResult<()> { + let uuid: u128 = parameter + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name: String = parameter.getattr(py, intern!(py, "name"))?.extract(py)?; + + if self.names.contains_key(&name) && !self.table.contains_key(&uuid) { + return Err(CircuitError::new_err(format!( + "Name conflict on adding parameter: {}", + name + ))); + } + self.table.insert(uuid, entry); + self.names.insert(name, uuid); + self.uuid_map.insert(uuid, parameter); + Ok(()) + } + + pub fn discard_references( + &mut self, + uuid: u128, + inst_index: usize, + param_index: usize, + name: String, + ) { + if let Some(refs) = self.table.get_mut(&uuid) { + if refs.__len__() == 1 { + self.table.remove(&uuid); + self.names.remove(&name); + self.uuid_map.remove(&uuid); + } else { + refs.discard(inst_index, param_index); + } + } + } +} + +#[pymethods] +impl ParamTable { + #[new] + pub fn new() -> Self { + ParamTable { + table: HashMap::new(), + names: HashMap::new(), + uuid_map: HashMap::new(), + } + } + + pub fn clear(&mut self) { + self.table.clear(); + self.names.clear(); + self.uuid_map.clear(); + } + + pub fn pop(&mut self, key: u128, name: String) -> Option { + self.names.remove(&name); + self.uuid_map.remove(&key); + self.table.remove(&key) + } + + fn set(&mut self, uuid: u128, name: String, param: PyObject, refs: ParamEntry) { + self.names.insert(name, uuid); + self.table.insert(uuid, refs); + self.uuid_map.insert(uuid, param); + } + + pub fn get_param_from_name(&self, py: Python, name: String) -> Option { + self.names + .get(&name) + .map(|x| self.uuid_map.get(x).map(|y| y.clone_ref(py)))? + } +} diff --git a/crates/circuit/src/slice.rs b/crates/circuit/src/slice.rs new file mode 100644 index 00000000000..056adff0a28 --- /dev/null +++ b/crates/circuit/src/slice.rs @@ -0,0 +1,375 @@ +// 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 thiserror::Error; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::types::PySlice; + +use self::sealed::{Descending, SequenceIndexIter}; + +/// A Python-space indexer for the standard `PySequence` type; a single integer or a slice. +/// +/// These come in as `isize`s from Python space, since Python typically allows negative indices. +/// Use `with_len` to specialize the index to a valid Rust-space indexer into a collection of the +/// given length. +pub enum PySequenceIndex<'py> { + Int(isize), + Slice(Bound<'py, PySlice>), +} + +impl<'py> FromPyObject<'py> for PySequenceIndex<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + // `slice` can't be subclassed in Python, so it's safe (and faster) to check for it exactly. + // The `downcast_exact` check is just a pointer comparison, so while `slice` is the less + // common input, doing that first has little-to-no impact on the speed of the `isize` path, + // while the reverse makes `slice` inputs significantly slower. + if let Ok(slice) = ob.downcast_exact::() { + return Ok(Self::Slice(slice.clone())); + } + Ok(Self::Int(ob.extract()?)) + } +} + +impl<'py> PySequenceIndex<'py> { + /// Specialize this index to a collection of the given `len`, returning a Rust-native type. + pub fn with_len(&self, len: usize) -> Result { + match self { + PySequenceIndex::Int(index) => { + let index = if *index >= 0 { + let index = *index as usize; + if index >= len { + return Err(PySequenceIndexError::OutOfRange); + } + index + } else { + len.checked_sub(index.unsigned_abs()) + .ok_or(PySequenceIndexError::OutOfRange)? + }; + Ok(SequenceIndex::Int(index)) + } + PySequenceIndex::Slice(slice) => { + let indices = slice + .indices(len as ::std::os::raw::c_long) + .map_err(PySequenceIndexError::from)?; + if indices.step > 0 { + Ok(SequenceIndex::PosRange { + start: indices.start as usize, + stop: indices.stop as usize, + step: indices.step as usize, + }) + } else { + Ok(SequenceIndex::NegRange { + // `indices.start` can be negative if the collection length is 0. + start: (indices.start >= 0).then_some(indices.start as usize), + // `indices.stop` can be negative if the 0 index should be output. + stop: (indices.stop >= 0).then_some(indices.stop as usize), + step: indices.step.unsigned_abs(), + }) + } + } + } + } +} + +/// Error type for problems encountered when calling methods on `PySequenceIndex`. +#[derive(Error, Debug)] +pub enum PySequenceIndexError { + #[error("index out of range")] + OutOfRange, + #[error(transparent)] + InnerPy(#[from] PyErr), +} +impl From for PyErr { + fn from(value: PySequenceIndexError) -> PyErr { + match value { + PySequenceIndexError::OutOfRange => PyIndexError::new_err("index out of range"), + PySequenceIndexError::InnerPy(inner) => inner, + } + } +} + +/// Rust-native version of a Python sequence-like indexer. +/// +/// Typically this is constructed by a call to `PySequenceIndex::with_len`, which guarantees that +/// all the indices will be in bounds for a collection of the given length. +/// +/// This splits the positive- and negative-step versions of the slice in two so it can be translated +/// more easily into static dispatch. This type can be converted into several types of iterator. +#[derive(Clone, Copy, Debug)] +pub enum SequenceIndex { + Int(usize), + PosRange { + start: usize, + stop: usize, + step: usize, + }, + NegRange { + start: Option, + stop: Option, + step: usize, + }, +} + +impl SequenceIndex { + /// The number of indices this refers to. + pub fn len(&self) -> usize { + match self { + Self::Int(_) => 1, + Self::PosRange { start, stop, step } => { + let gap = stop.saturating_sub(*start); + gap / *step + (gap % *step != 0) as usize + } + Self::NegRange { start, stop, step } => 'arm: { + let Some(start) = start else { break 'arm 0 }; + let gap = stop + .map(|stop| start.saturating_sub(stop)) + .unwrap_or(*start + 1); + gap / step + (gap % step != 0) as usize + } + } + } + + pub fn is_empty(&self) -> bool { + // This is just to keep clippy happy; the length is already fairly inexpensive to calculate. + self.len() == 0 + } + + /// Get an iterator over the indices. This will be a single-item iterator for the case of + /// `Self::Int`, but you probably wanted to destructure off that case beforehand anyway. + pub fn iter(&self) -> SequenceIndexIter { + match self { + Self::Int(value) => SequenceIndexIter::Int(Some(*value)), + Self::PosRange { start, step, .. } => SequenceIndexIter::PosRange { + lowest: *start, + step: *step, + indices: 0..self.len(), + }, + Self::NegRange { start, step, .. } => SequenceIndexIter::NegRange { + // We can unwrap `highest` to an arbitrary value if `None`, because in that case the + // `len` is 0 and the iterator will not yield any objects. + highest: start.unwrap_or_default(), + step: *step, + indices: 0..self.len(), + }, + } + } + + // Get an iterator over the contained indices that is guaranteed to iterate from the highest + // index to the lowest. + pub fn descending(&self) -> Descending { + Descending(self.iter()) + } +} + +impl IntoIterator for SequenceIndex { + type Item = usize; + type IntoIter = SequenceIndexIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +// Private module to make it impossible to construct or inspect the internals of the iterator types +// from outside this file, while still allowing them to be used. +mod sealed { + /// Custom iterator for indices for Python sequence-likes. + /// + /// In the range types, the `indices ` are `Range` objects that run from 0 to the length of the + /// iterator. In theory, we could generate the iterators ourselves, but that ends up with a lot of + /// boilerplate. + #[derive(Clone, Debug)] + pub enum SequenceIndexIter { + Int(Option), + PosRange { + lowest: usize, + step: usize, + indices: ::std::ops::Range, + }, + NegRange { + highest: usize, + // The step of the iterator, but note that this is a negative range, so the forwards method + // steps downwards from `upper` towards `lower`. + step: usize, + indices: ::std::ops::Range, + }, + } + impl Iterator for SequenceIndexIter { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next().map(|idx| *highest - idx * *step), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Int(None) => (0, Some(0)), + Self::Int(Some(_)) => (1, Some(1)), + Self::PosRange { indices, .. } | Self::NegRange { indices, .. } => { + indices.size_hint() + } + } + } + } + impl DoubleEndedIterator for SequenceIndexIter { + #[inline] + fn next_back(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next_back().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next_back().map(|idx| *highest - idx * *step), + } + } + } + impl ExactSizeIterator for SequenceIndexIter {} + + pub struct Descending(pub SequenceIndexIter); + impl Iterator for Descending { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => self.0.next(), + SequenceIndexIter::PosRange { .. } => self.0.next_back(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + } + impl DoubleEndedIterator for Descending { + #[inline] + fn next_back(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => { + self.0.next_back() + } + SequenceIndexIter::PosRange { .. } => self.0.next(), + } + } + } + impl ExactSizeIterator for Descending {} +} + +#[cfg(test)] +mod test { + use super::*; + + /// Get a set of test parametrisations for iterator methods. The second argument is the + /// expected values from a normal forward iteration. + fn index_iterator_cases() -> impl Iterator)> { + let pos = |start, stop, step| SequenceIndex::PosRange { start, stop, step }; + let neg = |start, stop, step| SequenceIndex::NegRange { start, stop, step }; + + [ + (SequenceIndex::Int(3), vec![3]), + (pos(0, 5, 2), vec![0, 2, 4]), + (pos(2, 10, 1), vec![2, 3, 4, 5, 6, 7, 8, 9]), + (pos(1, 15, 3), vec![1, 4, 7, 10, 13]), + (neg(Some(3), None, 1), vec![3, 2, 1, 0]), + (neg(Some(3), None, 2), vec![3, 1]), + (neg(Some(2), Some(0), 1), vec![2, 1]), + (neg(Some(2), Some(0), 2), vec![2]), + (neg(Some(2), Some(0), 3), vec![2]), + (neg(Some(10), Some(2), 3), vec![10, 7, 4]), + (neg(None, None, 1), vec![]), + (neg(None, None, 3), vec![]), + ] + .into_iter() + } + + /// Test that the index iterator's implementation of `ExactSizeIterator` is correct. + #[test] + fn index_iterator() { + for (index, forwards) in index_iterator_cases() { + // We're testing that all the values are the same, and the `size_hint` is correct at + // every single point. + let mut actual = Vec::new(); + let mut sizes = Vec::new(); + let mut iter = index.iter(); + loop { + sizes.push(iter.size_hint().0); + if let Some(next) = iter.next() { + actual.push(next); + } else { + break; + } + } + assert_eq!( + actual, forwards, + "values for {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, forwards, + ); + let expected_sizes = (0..=forwards.len()).rev().collect::>(); + assert_eq!( + sizes, expected_sizes, + "sizes for {:?}\nActual : {:?}\nExpected: {:?}", + index, sizes, expected_sizes, + ); + } + } + + /// Test that the index iterator's implementation of `DoubleEndedIterator` is correct. + #[test] + fn reversed_index_iterator() { + for (index, forwards) in index_iterator_cases() { + let actual = index.iter().rev().collect::>(); + let expected = forwards.into_iter().rev().collect::>(); + assert_eq!( + actual, expected, + "reversed {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } + + /// Test that `descending` produces its values in reverse-sorted order. + #[test] + fn descending() { + for (index, mut expected) in index_iterator_cases() { + let actual = index.descending().collect::>(); + expected.sort_by(|left, right| right.cmp(left)); + assert_eq!( + actual, expected, + "descending {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } +} diff --git a/crates/circuit/src/util.rs b/crates/circuit/src/util.rs new file mode 100644 index 00000000000..11562b0a48c --- /dev/null +++ b/crates/circuit/src/util.rs @@ -0,0 +1,48 @@ +// 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 num_complex::Complex64; + +// This is a very conservative version of an abbreviation for constructing new Complex64. +// A couple of alternatives to this function are +// `c64, V: Into>(re: T, im: V) -> Complex64` +// Disadvantages are: +// 1. Some people don't like that this allows things like `c64(1, 0)`. Presumably, +// they prefer a more explicit construction. +// 2. This will not work in `const` and `static` constructs. +// Another alternative is +// macro_rules! c64 { +// ($re: expr, $im: expr $(,)*) => { +// Complex64::new($re as f64, $im as f64) +// }; +// Advantages: This allows things like `c64!(1, 2.0)`, including in +// `static` and `const` constructs. +// Disadvantages: +// 1. Three characters `c64!` rather than two `c64`. +// 2. Some people prefer the opposite of the advantages, i.e. more explicitness. +/// Create a new [`Complex`] +#[inline(always)] +pub const fn c64(re: f64, im: f64) -> Complex64 { + Complex64::new(re, im) +} + +pub type GateArray0Q = [[Complex64; 1]; 1]; +pub type GateArray1Q = [[Complex64; 2]; 2]; +pub type GateArray2Q = [[Complex64; 4]; 4]; +pub type GateArray3Q = [[Complex64; 8]; 8]; + +// Use prefix `C_` to distinguish from real, for example +pub const C_ZERO: Complex64 = c64(0., 0.); +pub const C_ONE: Complex64 = c64(1., 0.); +pub const C_M_ONE: Complex64 = c64(-1., 0.); +pub const IM: Complex64 = c64(0., 1.); +pub const M_IM: Complex64 = c64(0., -1.); diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index daaf19e1f6a..413165e84b1 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] # crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the # feature a default, and run `cargo test --no-default-features` to turn it off. default = ["pyo3/extension-module"] +cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"] [dependencies] pyo3.workspace = true diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index b7c89872bf8..72f0d759099 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -15,10 +15,11 @@ use pyo3::wrap_pymodule; 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, 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, - stochastic_swap::stochastic_swap, two_qubit_decompose::two_qubit_decompose, utils::utils, + error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, + 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, stochastic_swap::stochastic_swap, synthesis::synthesis, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, vf2_layout::vf2_layout, }; @@ -31,15 +32,18 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(dense_layout))?; m.add_wrapped(wrap_pymodule!(error_map))?; m.add_wrapped(wrap_pymodule!(euler_one_qubit_decomposer))?; + m.add_wrapped(wrap_pymodule!(isometry))?; m.add_wrapped(wrap_pymodule!(nlayout))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(pauli_expval))?; + m.add_wrapped(wrap_pymodule!(synthesis))?; m.add_wrapped(wrap_pymodule!(results))?; m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op))?; m.add_wrapped(wrap_pymodule!(stochastic_swap))?; m.add_wrapped(wrap_pymodule!(two_qubit_decompose))?; + m.add_wrapped(wrap_pymodule!(uc_gate))?; m.add_wrapped(wrap_pymodule!(utils))?; m.add_wrapped(wrap_pymodule!(vf2_layout))?; Ok(()) diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs index f7faad0c629..fe78b290e0f 100644 --- a/crates/qasm2/src/expr.rs +++ b/crates/qasm2/src/expr.rs @@ -104,7 +104,7 @@ impl From for Op { } } -/// An atom of the operator-precendence expression parsing. This is a stripped-down version of the +/// An atom of the operator-precedence expression parsing. This is a stripped-down version of the /// [Token] and [TokenType] used in the main parser. We can use a data enum here because we do not /// need all the expressive flexibility in expecting and accepting many different token types as /// we do in the main parser; it does not significantly harm legibility to simply do @@ -233,7 +233,7 @@ fn binary_power(op: Op) -> (u8, u8) { /// A subparser used to do the operator-precedence part of the parsing for individual parameter /// expressions. The main parser creates a new instance of this struct for each expression it /// expects, and the instance lives only as long as is required to parse that expression, because -/// it takes temporary resposibility for the [TokenStream] that backs the main parser. +/// it takes temporary responsibility for the [TokenStream] that backs the main parser. pub struct ExprParser<'a> { pub tokens: &'a mut Vec, pub context: &'a mut TokenContext, @@ -501,8 +501,13 @@ impl<'a> ExprParser<'a> { | TokenType::Sin | TokenType::Sqrt | TokenType::Tan => Ok(Some(Atom::Function(token.ttype.into()))), - TokenType::Real => Ok(Some(Atom::Const(token.real(self.context)))), - TokenType::Integer => Ok(Some(Atom::Const(token.int(self.context) as f64))), + // This deliberately parses an _integer_ token as a float, since all OpenQASM 2.0 + // integers can be interpreted as floats, and doing that allows us to gracefully handle + // cases where a huge float would overflow a `usize`. Never mind that in such a case, + // there's almost certainly precision loss from the floating-point representing + // having insufficient mantissa digits to faithfully represent the angle mod 2pi; + // that's not our fault in the parser. + TokenType::Real | TokenType::Integer => Ok(Some(Atom::Const(token.real(self.context)))), TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))), TokenType::Id => { let id = token.text(self.context); @@ -698,6 +703,11 @@ impl<'a> ExprParser<'a> { /// Parse a single expression completely. This is the only public entry point to the /// operator-precedence parser. + /// + /// .. note:: + /// + /// This evaluates in a floating-point context, including evaluating integer tokens, since + /// the only places that expressions are valid in OpenQASM 2 is during gate applications. pub fn parse_expression(&mut self, cause: &Token) -> PyResult { self.eval_expression(0, cause) } diff --git a/crates/qasm2/src/lex.rs b/crates/qasm2/src/lex.rs index 024681b877f..551fd2b7af4 100644 --- a/crates/qasm2/src/lex.rs +++ b/crates/qasm2/src/lex.rs @@ -21,7 +21,7 @@ //! keyword; the spec technically says that any real number is valid, but in reality that leads to //! weirdness like `200.0e-2` being a valid version specifier. We do things with a custom //! context-dependent match after seeing an `OPENQASM` token, to avoid clashes with the general -//! real-number tokenisation. +//! real-number tokenization. use hashbrown::HashMap; use pyo3::prelude::PyResult; @@ -30,7 +30,7 @@ use std::path::Path; use crate::error::{message_generic, Position, QASM2ParseError}; -/// Tokenised version information data. This is more structured than the real number suggested by +/// Tokenized version information data. This is more structured than the real number suggested by /// the specification. #[derive(Clone, Debug)] pub struct Version { @@ -262,9 +262,9 @@ impl Token { } /// If the token is a real number, this method can be called to evaluate its value. Panics if - /// the token is not a real number. + /// the token is not a float or an integer. pub fn real(&self, context: &TokenContext) -> f64 { - if self.ttype != TokenType::Real { + if !(self.ttype == TokenType::Real || self.ttype == TokenType::Integer) { panic!() } context.text[self.index].parse().unwrap() @@ -353,7 +353,7 @@ impl TokenStream { line_buffer: Vec::with_capacity(80), done: false, // The first line is numbered "1", and the first column is "0". The counts are - // initialised like this so the first call to `next_byte` can easily detect that it + // initialized like this so the first call to `next_byte` can easily detect that it // needs to extract the next line. line: 0, col: 0, diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index e4c74984112..f7eceb6aeef 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -1630,7 +1630,7 @@ impl State { /// Update the parser state with the definition of a particular gate. This does not emit any /// bytecode because not all gate definitions need something passing to Python. For example, - /// the Python parser initialises its state including the built-in gates `U` and `CX`, and + /// the Python parser initializes its state including the built-in gates `U` and `CX`, and /// handles the `qelib1.inc` include specially as well. fn define_gate( &mut self, diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index a8e20d13d58..4dd0d977786 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -13,4 +13,4 @@ doctest = false pyo3.workspace = true indexmap.workspace = true hashbrown.workspace = true -oq3_semantics = "0.0.7" +oq3_semantics = "0.6.0" diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index 154cd391252..f5cf2fd4efc 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -69,7 +69,7 @@ impl BuilderState { Err(QASM3ImporterError::new_err("cannot handle consts")) } else if decl.initializer().is_some() { Err(QASM3ImporterError::new_err( - "cannot handle initialised bits", + "cannot handle initialized bits", )) } else { self.add_clbit(py, name_id.clone()) @@ -80,7 +80,7 @@ impl BuilderState { Err(QASM3ImporterError::new_err("cannot handle consts")) } else if decl.initializer().is_some() { Err(QASM3ImporterError::new_err( - "cannot handle initialised registers", + "cannot handle initialized registers", )) } else { match dims { @@ -226,42 +226,32 @@ impl BuilderState { self.qc.append(py, instruction).map(|_| ()) } - fn define_gate( - &mut self, - _py: Python, - ast_symbols: &SymbolTable, - decl: &asg::GateDeclaration, - ) -> PyResult<()> { - let name_id = decl - .name() - .as_ref() - .map_err(|err| QASM3ImporterError::new_err(format!("internal error: {:?}", err)))?; - let name_symbol = &ast_symbols[name_id]; - let pygate = self.pygates.get(name_symbol.name()).ok_or_else(|| { - QASM3ImporterError::new_err(format!( - "can't handle non-built-in gate: '{}'", - name_symbol.name() - )) - })?; - let defined_num_params = decl.params().as_ref().map_or(0, Vec::len); - let defined_num_qubits = decl.qubits().len(); - if pygate.num_params() != defined_num_params { - return Err(QASM3ImporterError::new_err(format!( - "given constructor for '{}' expects {} parameters, but is defined as taking {}", - pygate.name(), - pygate.num_params(), - defined_num_params, - ))); - } - if pygate.num_qubits() != defined_num_qubits { - return Err(QASM3ImporterError::new_err(format!( - "given constructor for '{}' expects {} qubits, but is defined as taking {}", - pygate.name(), - pygate.num_qubits(), - defined_num_qubits, - ))); + // Map gates in the symbol table to Qiskit gates in the standard library. + // Encountering any gates not in the standard library results in raising an exception. + // Gates mapped via CustomGates will not raise an exception. + fn map_gate_ids(&mut self, _py: Python, ast_symbols: &SymbolTable) -> PyResult<()> { + for (name, name_id, defined_num_params, defined_num_qubits) in ast_symbols.gates() { + let pygate = self.pygates.get(name).ok_or_else(|| { + QASM3ImporterError::new_err(format!("can't handle non-built-in gate: '{}'", name)) + })?; + if pygate.num_params() != defined_num_params { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} parameters, but is defined as taking {}", + pygate.name(), + pygate.num_params(), + defined_num_params, + ))); + } + if pygate.num_qubits() != defined_num_qubits { + return Err(QASM3ImporterError::new_err(format!( + "given constructor for '{}' expects {} qubits, but is defined as taking {}", + pygate.name(), + pygate.num_qubits(), + defined_num_qubits, + ))); + } + self.symbols.gates.insert(name_id.clone(), pygate.clone()); } - self.symbols.gates.insert(name_id.clone(), pygate.clone()); Ok(()) } @@ -377,37 +367,45 @@ pub fn convert_asg( pygates: gate_constructors, module, }; + + state.map_gate_ids(py, ast_symbols)?; + for statement in program.stmts().iter() { match statement { asg::Stmt::GateCall(call) => state.call_gate(py, ast_symbols, call)?, asg::Stmt::DeclareClassical(decl) => state.declare_classical(py, ast_symbols, decl)?, asg::Stmt::DeclareQuantum(decl) => state.declare_quantum(py, ast_symbols, decl)?, - asg::Stmt::GateDeclaration(decl) => state.define_gate(py, ast_symbols, decl)?, + // We ignore gate definitions because the only information we can currently use + // from them is extracted with `SymbolTable::gates` via `map_gate_ids`. + asg::Stmt::GateDefinition(_) => (), asg::Stmt::Barrier(barrier) => state.apply_barrier(py, ast_symbols, barrier)?, asg::Stmt::Assignment(assignment) => state.assign(py, ast_symbols, assignment)?, - asg::Stmt::Alias + asg::Stmt::Alias(_) | asg::Stmt::AnnotatedStmt(_) | asg::Stmt::Block(_) | asg::Stmt::Box | asg::Stmt::Break | asg::Stmt::Cal | asg::Stmt::Continue - | asg::Stmt::Def + | asg::Stmt::DeclareHardwareQubit(_) | asg::Stmt::DefCal - | asg::Stmt::Delay + | asg::Stmt::DefStmt(_) + | asg::Stmt::Delay(_) | asg::Stmt::End | asg::Stmt::ExprStmt(_) | asg::Stmt::Extern - | asg::Stmt::For + | asg::Stmt::ForStmt(_) | asg::Stmt::GPhaseCall(_) - | asg::Stmt::IODeclaration | asg::Stmt::If(_) | asg::Stmt::Include(_) + | asg::Stmt::InputDeclaration(_) + | asg::Stmt::ModifiedGPhaseCall(_) | asg::Stmt::NullStmt | asg::Stmt::OldStyleDeclaration + | asg::Stmt::OutputDeclaration(_) | asg::Stmt::Pragma(_) - | asg::Stmt::Reset - | asg::Stmt::Return + | asg::Stmt::Reset(_) + | asg::Stmt::SwitchCaseStmt(_) | asg::Stmt::While(_) => { return Err(QASM3ImporterError::new_err(format!( "this statement is not yet handled during OpenQASM 3 import: {:?}", diff --git a/crates/qasm3/src/circuit.rs b/crates/qasm3/src/circuit.rs index 747980819a0..fdd92c43c0b 100644 --- a/crates/qasm3/src/circuit.rs +++ b/crates/qasm3/src/circuit.rs @@ -16,7 +16,6 @@ use pyo3::types::{PyList, PyString, PyTuple, PyType}; use crate::error::QASM3ImporterError; pub trait PyRegister { - fn bit(&self, py: Python, index: usize) -> PyResult>; // This really should be // fn iter<'a>(&'a self, py: Python<'a>) -> impl Iterator; // or at a minimum @@ -39,15 +38,6 @@ macro_rules! register_type { } impl PyRegister for $name { - /// Get an individual bit from the register. - fn bit(&self, py: Python, index: usize) -> PyResult> { - // Unfortunately, `PyList::get_item_unchecked` isn't usable with the stable ABI. - self.items - .bind(py) - .get_item(index) - .map(|item| item.into_py(py)) - } - fn bit_list<'a>(&'a self, py: Python<'a>) -> &Bound<'a, PyList> { self.items.bind(py) } @@ -291,7 +281,7 @@ impl PyCircuitModule { /// Circuit construction context object to provide an easier Rust-space interface for us to /// construct the Python :class:`.QuantumCircuit`. The idea of doing this from Rust space like /// this is that we might steadily be able to move more and more of it into being native Rust as -/// the Rust-space APIs around the internal circuit data stabilise. +/// the Rust-space APIs around the internal circuit data stabilize. pub struct PyCircuit(Py); impl PyCircuit { diff --git a/crates/qasm3/src/expr.rs b/crates/qasm3/src/expr.rs index d16bd53add0..64afe58991c 100644 --- a/crates/qasm3/src/expr.rs +++ b/crates/qasm3/src/expr.rs @@ -71,7 +71,7 @@ fn eval_const_int(_py: Python, _ast_symbols: &SymbolTable, expr: &asg::TExpr) -> match expr.expression() { asg::Expr::Literal(asg::Literal::Int(lit)) => Ok(*lit.value() as isize), expr => Err(QASM3ImporterError::new_err(format!( - "unhandled expression type for constant-integer evaluatation: {:?}", + "unhandled expression type for constant-integer evaluation: {:?}", expr ))), } @@ -244,11 +244,11 @@ pub fn eval_qarg( qarg: &asg::GateOperand, ) -> PyResult { match qarg { - asg::GateOperand::Identifier(iden) => broadcast_bits_for_identifier( + asg::GateOperand::Identifier(symbol) => broadcast_bits_for_identifier( py, &our_symbols.qubits, &our_symbols.qregs, - iden.symbol().as_ref().unwrap(), + symbol.as_ref().unwrap(), ), asg::GateOperand::IndexedIdentifier(indexed) => { let iden_symbol = indexed.identifier().as_ref().unwrap(); diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 89a2a6bb9a6..8581d56ace7 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -1,41 +1,90 @@ .. module:: qiskit +.. + Within each section, the modules should be ordered alphabetically by + module name (not RST filename). ============= API Reference ============= +Circuit construction: + .. toctree:: :maxdepth: 1 circuit - circuit_library + qiskit.circuit.QuantumCircuit circuit_classical - circuit_singleton - compiler - visualization classicalfunction + circuit_library + circuit_singleton + +Quantum information: + +.. toctree:: + :maxdepth: 1 + + quantum_info + +Transpilation: + +.. toctree:: + :maxdepth: 1 + converters - assembler dagcircuit passmanager + synthesis + qiskit.synthesis.unitary.aqc + transpiler + transpiler_passes + transpiler_synthesis_plugins + transpiler_preset + transpiler_plugins + +Primitives and providers: + +.. toctree:: + :maxdepth: 1 + + primitives providers providers_basic_provider providers_fake_provider providers_models - pulse - scheduler - synthesis - primitives + +Results and visualizations: + +.. toctree:: + :maxdepth: 1 + + result + visualization + +Serialization: + +.. toctree:: + :maxdepth: 1 + qasm2 qasm3 - qobj qpy - quantum_info - result - transpiler - transpiler_passes - transpiler_preset - transpiler_plugins - transpiler_synthesis_plugins - utils + +Pulse-level programming: + +.. toctree:: + :maxdepth: 1 + + pulse + scheduler + +Other: + +.. toctree:: + :maxdepth: 1 + + assembler + compiler exceptions + qobj + utils diff --git a/docs/apidoc/qiskit.circuit.QuantumCircuit.rst b/docs/apidoc/qiskit.circuit.QuantumCircuit.rst new file mode 100644 index 00000000000..1fa9cb5a7d9 --- /dev/null +++ b/docs/apidoc/qiskit.circuit.QuantumCircuit.rst @@ -0,0 +1,17 @@ +.. _qiskit-circuit-quantumcircuit: + +============================== +:class:`.QuantumCircuit` class +============================== + +.. + This is so big it gets its own page in the toctree, and because we + don't want it to use autosummary. + +.. currentmodule:: qiskit.circuit + +.. autoclass:: qiskit.circuit.QuantumCircuit + :no-members: + :no-inherited-members: + :no-special-members: + :class-doc-from: class diff --git a/docs/apidoc/qiskit.synthesis.unitary.aqc.rst b/docs/apidoc/qiskit.synthesis.unitary.aqc.rst new file mode 100644 index 00000000000..5f3219a40db --- /dev/null +++ b/docs/apidoc/qiskit.synthesis.unitary.aqc.rst @@ -0,0 +1,6 @@ +.. _qiskit-synthesis_unitary_aqc: + +.. automodule:: qiskit.synthesis.unitary.aqc + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/conf.py b/docs/conf.py index 4a79b543ed7..f6bf2faa9a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,9 +30,9 @@ author = "Qiskit Development Team" # The short X.Y version -version = "1.1" +version = "1.2" # The full version, including alpha/beta/rc tags -release = "1.1.0" +release = "1.2.0" language = "en" @@ -114,9 +114,9 @@ autosummary_generate = True autosummary_generate_overwrite = False -# The pulse library contains some names that differ only in capitalisation, during the changeover +# 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 -# capitalisation, this causes problems when the documentation is built on an OS/filesystem that is +# 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 = { @@ -178,7 +178,7 @@ def linkcode_resolve(domain, info): if "qiskit" not in module_name: return None - try: + try: module = importlib.import_module(module_name) except ModuleNotFoundError: return None diff --git a/pyproject.toml b/pyproject.toml index 40c6701fd05..2f62557aa15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ target-version = ['py38', 'py39', 'py310', 'py311'] [tool.cibuildwheel] manylinux-x86_64-image = "manylinux2014" manylinux-i686-image = "manylinux2014" -skip = "pp* cp36-* cp37-* *musllinux* *win32 *i686" +skip = "pp* cp36-* cp37-* *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 @@ -209,30 +209,19 @@ disable = [ "too-many-lines", "too-many-branches", "too-many-locals", "too-many-nested-blocks", "too-many-statements", "too-many-instance-attributes", "too-many-arguments", "too-many-public-methods", "too-few-public-methods", "too-many-ancestors", "unnecessary-pass", # allow for methods with just "pass", for clarity + "unnecessary-dunder-call", # do not want to implement "no-else-return", # relax "elif" after a clause with a return "docstring-first-line-empty", # relax docstring style "import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages "nested-min-max", # this gives false equivalencies if implemented for the current lint version + "consider-using-max-builtin", "consider-using-min-builtin", # unnecessary stylistic opinion # TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, # remove from here and fix the issues. Else, move it above this section and add a comment # with the rationale - "arguments-renamed", - "broad-exception-raised", - "consider-iterating-dictionary", - "consider-using-dict-items", - "consider-using-enumerate", - "consider-using-f-string", - "no-member", - "no-value-for-parameter", + "no-member", # for dynamically created members "not-context-manager", - "superfluous-parens", - "unexpected-keyword-arg", - "unnecessary-dict-index-lookup", - "unnecessary-dunder-call", - "unnecessary-lambda-assignment", - "unspecified-encoding", - "unsupported-assignment-operation", - "use-implicit-booleaness-not-comparison", + "unnecessary-lambda-assignment", # do not want to implement + "unspecified-encoding", # do not want to implement ] enable = [ @@ -241,3 +230,12 @@ enable = [ [tool.pylint.spelling] spelling-private-dict-file = ".local-spellings" + +[tool.coverage.report] +exclude_also = [ + "def __repr__", # Printable epresentational string does not typically execute during testing + "raise NotImplementedError", # Abstract methods are not testable + "raise RuntimeError", # Exceptions for defensive programming that cannot be tested a head + "if TYPE_CHECKING:", # Code that only runs during type checks + "@abstractmethod", # Abstract methods are not testable + ] diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index 9084fa2f716..26aaba0e866 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 590a5698a77..aca555da8cb 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,wrong-import-order """Main Qiskit public functionality.""" @@ -52,34 +52,36 @@ ) -import qiskit._accelerate +from . import _accelerate import qiskit._numpy_compat # Globally define compiled submodules. The normal import mechanism will not find compiled submodules # in _accelerate because it relies on file paths, but PyO3 generates only one shared library file. # 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"] = qiskit._accelerate.circuit -sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = ( - qiskit._accelerate.convert_2q_block_matrix -) -sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout -sys.modules["qiskit._accelerate.error_map"] = qiskit._accelerate.error_map +sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit +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 +sys.modules["qiskit._accelerate.isometry"] = _accelerate.isometry +sys.modules["qiskit._accelerate.uc_gate"] = _accelerate.uc_gate sys.modules["qiskit._accelerate.euler_one_qubit_decomposer"] = ( - qiskit._accelerate.euler_one_qubit_decomposer + _accelerate.euler_one_qubit_decomposer ) -sys.modules["qiskit._accelerate.nlayout"] = qiskit._accelerate.nlayout -sys.modules["qiskit._accelerate.optimize_1q_gates"] = qiskit._accelerate.optimize_1q_gates -sys.modules["qiskit._accelerate.pauli_expval"] = qiskit._accelerate.pauli_expval -sys.modules["qiskit._accelerate.qasm2"] = qiskit._accelerate.qasm2 -sys.modules["qiskit._accelerate.qasm3"] = qiskit._accelerate.qasm3 -sys.modules["qiskit._accelerate.results"] = qiskit._accelerate.results -sys.modules["qiskit._accelerate.sabre"] = qiskit._accelerate.sabre -sys.modules["qiskit._accelerate.sampled_exp_val"] = qiskit._accelerate.sampled_exp_val -sys.modules["qiskit._accelerate.sparse_pauli_op"] = qiskit._accelerate.sparse_pauli_op -sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap -sys.modules["qiskit._accelerate.two_qubit_decompose"] = qiskit._accelerate.two_qubit_decompose -sys.modules["qiskit._accelerate.vf2_layout"] = qiskit._accelerate.vf2_layout +sys.modules["qiskit._accelerate.nlayout"] = _accelerate.nlayout +sys.modules["qiskit._accelerate.optimize_1q_gates"] = _accelerate.optimize_1q_gates +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.results"] = _accelerate.results +sys.modules["qiskit._accelerate.sabre"] = _accelerate.sabre +sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val +sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op +sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap +sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose +sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout +sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation +sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/_numpy_compat.py b/qiskit/_numpy_compat.py index a6c06671c98..9b6b466fbc9 100644 --- a/qiskit/_numpy_compat.py +++ b/qiskit/_numpy_compat.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Compatiblity helpers for the Numpy 1.x to 2.0 transition.""" +"""Compatibility helpers for the Numpy 1.x to 2.0 transition.""" import re import typing diff --git a/qiskit/assembler/__init__.py b/qiskit/assembler/__init__.py index a356501a263..45798084ea6 100644 --- a/qiskit/assembler/__init__.py +++ b/qiskit/assembler/__init__.py @@ -17,23 +17,18 @@ .. currentmodule:: qiskit.assembler -Circuit Assembler -================= +Functions +========= -.. autofunction:: assemble_circuits -Schedule Assembler -================== +.. autofunction:: assemble_circuits .. autofunction:: assemble_schedules -Disassembler -============ - .. autofunction:: disassemble -RunConfig -========= +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/assembler/assemble_circuits.py b/qiskit/assembler/assemble_circuits.py index b27fe47a02e..a3d9b6bbb54 100644 --- a/qiskit/assembler/assemble_circuits.py +++ b/qiskit/assembler/assemble_circuits.py @@ -153,9 +153,9 @@ def _assemble_circuit( conditional_reg_idx = memory_slots + max_conditional_idx conversion_bfunc = QasmQobjInstruction( name="bfunc", - mask="0x%X" % mask, + mask="0x%X" % mask, # pylint: disable=consider-using-f-string relation="==", - val="0x%X" % val, + val="0x%X" % val, # pylint: disable=consider-using-f-string register=conditional_reg_idx, ) instructions.append(conversion_bfunc) diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index c60c28ff9a5..2d5ebefa2fd 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -152,7 +152,7 @@ def _assemble_experiments( # TODO: add other experimental header items (see circuit assembler) qobj_experiment_header = qobj.QobjExperimentHeader( memory_slots=max_memory_slot + 1, # Memory slots are 0 indexed - name=sched.name or "Experiment-%d" % idx, + name=sched.name or f"Experiment-{idx}", metadata=metadata, ) @@ -306,18 +306,11 @@ def _validate_meas_map( common_next = next_inst_qubits.intersection(meas_set) if common_instr_qubits and common_next: raise QiskitError( - "Qubits {} and {} are in the same measurement grouping: {}. " + f"Qubits {common_instr_qubits} and {common_next} are in the same measurement " + f"grouping: {meas_map}. " "They must either be acquired at the same time, or disjointly" - ". Instead, they were acquired at times: {}-{} and " - "{}-{}".format( - common_instr_qubits, - common_next, - meas_map, - inst[0][0], - inst_end_time, - next_inst_time, - next_inst_time + next_inst[0][1], - ) + f". Instead, they were acquired at times: {inst[0][0]}-{inst_end_time} and " + f"{next_inst_time}-{next_inst_time + next_inst[0][1]}" ) diff --git a/qiskit/assembler/disassemble.py b/qiskit/assembler/disassemble.py index c94b108c4b2..127bbd35eb2 100644 --- a/qiskit/assembler/disassemble.py +++ b/qiskit/assembler/disassemble.py @@ -109,7 +109,7 @@ def _qobj_to_circuit_cals(qobj, pulse_lib): config = (tuple(gate["qubits"]), tuple(gate["params"])) cal = { config: pulse.Schedule( - name="{} {} {}".format(gate["name"], str(gate["params"]), str(gate["qubits"])) + name=f"{gate['name']} {str(gate['params'])} {str(gate['qubits'])}" ) } for instruction in gate["instructions"]: diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 9fbefb4c5d9..65a88519a0d 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -203,8 +203,8 @@ .. _circuit-module-api: -API overview of :mod:`qiskit.circuit` -===================================== +API overview of qiskit.circuit +============================== All objects here are described in more detail, and in their greater context in the following sections. This section provides an overview of the API elements documented here. @@ -270,7 +270,7 @@ * :class:`ContinueLoopOp`, to move immediately to the next iteration of the containing loop * :class:`ForLoopOp`, to loop over a fixed range of values * :class:`IfElseOp`, to conditionally enter one of two subcircuits - * :class:`SwitchCaseOp`, to conditionally enter one of many subcicuits + * :class:`SwitchCaseOp`, to conditionally enter one of many subcircuits * :class:`WhileLoopOp`, to repeat a subcircuit until a condition is falsified. :ref:`Circuits can include classical expressions that are evaluated in real time @@ -321,16 +321,6 @@ :class:`QuantumCircuit` class itself and the multitude of available methods on it in its class documentation. -.. - TODO: the intention is to replace this `autosummary` directive with a proper entry in the API - toctree once the `QuantumCircuit` class-level documentation has been completely rewritten into - more of this style. For now, this just ensures it gets *any* page generated. - -.. autosummary:: - :toctree: ../stubs/ - - QuantumCircuit - Internally, a :class:`QuantumCircuit` contains the qubits, classical bits, compile-time parameters, real-time variables, and other tracking information about the data it acts on and how it is parametrized. It then contains a sequence of :class:`CircuitInstruction`\ s, which contain @@ -390,7 +380,7 @@ Circuits track registers, but registers themselves impart almost no behavioral differences on circuits. The only exception is that :class:`ClassicalRegister`\ s can be implicitly cast to unsigned integers for use in conditional comparisons of :ref:`control flow operations -`. +`. Classical registers and bits were the original way of representing classical data in Qiskit, and remain the most supported currently. Longer term, the data model is moving towards a more complete @@ -433,6 +423,8 @@ circuit), but these are now discouraged and you should use the alternatives noted in those methods. +.. _circuit-operations-instructions: + Operations, instructions and gates ---------------------------------- @@ -598,17 +590,14 @@ Real-time classical computation ------------------------------- -.. note:: +.. seealso:: + :mod:`qiskit.circuit.classical` + Module-level documentation for how the variable-, expression- and type-systems work, the + objects used to represent them, and the classical operations available. - The primary documentation for real-time classical computation is in the module-level - documentation of :mod:`qiskit.circuit.classical`. - - You might also want to read about the circuit methods for working with real-time variables on - the :class:`QuantumCircuit` class page. - - .. - TODO: write a section in the QuantumCircuit-level guide about real-time-variable methods and - cross-ref to it. + :ref:`circuit-real-time-methods` + The :class:`QuantumCircuit` methods for working with these variables in the context of a + single circuit. Qiskit has rudimentary low-level support for representing real-time classical computations, which happen during the QPU execution and affect the results. We are still relatively early into hardware @@ -674,7 +663,7 @@ ParameterVector -.. _circuit-control-flow: +.. _circuit-control-flow-repr: Control flow in circuits ------------------------ @@ -718,11 +707,8 @@ The classes representations are documented here, but please note that manually constructing these classes is a low-level operation that we do not expect users to need to do frequently. - .. - TODO: make this below statement valid, and reinsert. - - Users should read :ref:`circuit-creating-control-flow` for the recommended workflows for - building control-flow-enabled circuits. + Users should read :ref:`circuit-control-flow-methods` for the recommended workflows for building + control-flow-enabled circuits. Since :class:`ControlFlowOp` subclasses are also :class:`Instruction` subclasses, this means that the way they are stored in :class:`CircuitInstruction` instances has them "applied" to a sequence of @@ -772,11 +758,8 @@ argument), but user code will typically use the control-flow builder interface, which handles this automatically. -.. - TODO: make the below sentence valid, then re-insert. - - Consult :ref:`the control-flow construction documentation ` for - more information on how to build circuits with control flow. +Consult :ref:`the control-flow construction documentation ` for more +information on how to build circuits with control flow. .. _circuit-custom-gates: @@ -833,10 +816,11 @@ ``__array__``. This is used by :meth:`Gate.to_matrix`, and has the signature: .. currentmodule:: None -.. py:method:: __array__(dtype=None, copy=None) +.. py:method:: object.__array__(dtype=None, copy=None) - Return a Numpy array representing the gate. This can use the gate's :attr:`~Instruction.params` - field, and may assume that these are numeric values (assuming the subclass expects that) and not + Return a Numpy array representing the gate. This can use the gate's + :attr:`~qiskit.circuit.Instruction.params` field, and may assume that these are numeric + values (assuming the subclass expects that) and not :ref:`compile-time parameters `. For greatest efficiency, the returned array should default to a dtype of :class:`complex`. @@ -920,122 +904,6 @@ def __array__(self, dtype=None, copy=None): Working with circuit-level objects ================================== -Circuit properties ------------------- - -.. - TODO: rewrite this section and move it into the `QuantumCircuit` class-level overview of its - functions. - -When constructing quantum circuits, there are several properties that help quantify -the "size" of the circuits, and their ability to be run on a noisy quantum device. -Some of these, like number of qubits, are straightforward to understand, while others -like depth and number of tensor components require a bit more explanation. Here we will -explain all of these properties, and, in preparation for understanding how circuits change -when run on actual devices, highlight the conditions under which they change. - -Consider the following circuit: - -.. plot:: - :include-source: - - from qiskit import QuantumCircuit - qc = QuantumCircuit(12) - for idx in range(5): - qc.h(idx) - qc.cx(idx, idx+5) - - qc.cx(1, 7) - qc.x(8) - qc.cx(1, 9) - qc.x(7) - qc.cx(1, 11) - qc.swap(6, 11) - qc.swap(6, 9) - qc.swap(6, 10) - qc.x(6) - qc.draw('mpl') - -From the plot, it is easy to see that this circuit has 12 qubits, and a collection of -Hadamard, CNOT, X, and SWAP gates. But how to quantify this programmatically? Because we -can do single-qubit gates on all the qubits simultaneously, the number of qubits in this -circuit is equal to the **width** of the circuit: - -.. code-block:: - - qc.width() - -.. parsed-literal:: - - 12 - -We can also just get the number of qubits directly: - -.. code-block:: - - qc.num_qubits - -.. parsed-literal:: - - 12 - -.. important:: - - For a quantum circuit composed from just qubits, the circuit width is equal - to the number of qubits. This is the definition used in quantum computing. However, - for more complicated circuits with classical registers, and classically controlled gates, - this equivalence breaks down. As such, from now on we will not refer to the number of - qubits in a quantum circuit as the width. - - -It is also straightforward to get the number and type of the gates in a circuit using -:meth:`QuantumCircuit.count_ops`: - -.. code-block:: - - qc.count_ops() - -.. parsed-literal:: - - OrderedDict([('cx', 8), ('h', 5), ('x', 3), ('swap', 3)]) - -We can also get just the raw count of operations by computing the circuits -:meth:`QuantumCircuit.size`: - -.. code-block:: - - qc.size() - -.. parsed-literal:: - - 19 - -A particularly important circuit property is known as the circuit **depth**. The depth -of a quantum circuit is a measure of how many "layers" of quantum gates, executed in -parallel, it takes to complete the computation defined by the circuit. Because quantum -gates take time to implement, the depth of a circuit roughly corresponds to the amount of -time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit -is one important quantity used to measure if a quantum circuit can be run on a device. - -The depth of a quantum circuit has a mathematical definition as the longest path in a -directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for -experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar -with playing `Tetris `_. Lets see how to compute this -graphically: - -.. image:: /source_images/depth.gif - - -We can verify our graphical result using :meth:`QuantumCircuit.depth`: - -.. code-block:: - - qc.depth() - -.. parsed-literal:: - - 9 - .. _circuit-abstract-to-physical: Converting abstract circuits to physical circuits diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py index cfbdd077bda..ba42f15cddc 100644 --- a/qiskit/circuit/_classical_resource_map.py +++ b/qiskit/circuit/_classical_resource_map.py @@ -31,23 +31,26 @@ class VariableMapper(expr.ExprVisitor[expr.Expr]): call its :meth:`map_condition`, :meth:`map_target` or :meth:`map_expr` methods as appropriate, which will return the new object that should be used. - If an ``add_register`` callable is given to the initialiser, the mapper will use it to attempt + If an ``add_register`` callable is given to the initializer, the mapper will use it to attempt to add new aliasing registers to the outer circuit object, if there is not already a suitable register for the mapping available in the circuit. If this parameter is not given, a ``ValueError`` will be raised instead. The given ``add_register`` callable may choose to raise its own exception.""" - __slots__ = ("target_cregs", "register_map", "bit_map", "add_register") + __slots__ = ("target_cregs", "register_map", "bit_map", "var_map", "add_register") def __init__( self, target_cregs: typing.Iterable[ClassicalRegister], bit_map: typing.Mapping[Bit, Bit], + var_map: typing.Mapping[expr.Var, expr.Var] | None = None, + *, add_register: typing.Callable[[ClassicalRegister], None] | None = None, ): self.target_cregs = tuple(target_cregs) self.register_map = {} self.bit_map = bit_map + self.var_map = var_map or {} self.add_register = add_register def _map_register(self, theirs: ClassicalRegister) -> ClassicalRegister: @@ -70,12 +73,12 @@ def _map_register(self, theirs: ClassicalRegister) -> ClassicalRegister: def map_condition(self, condition, /, *, allow_reorder=False): """Map the given ``condition`` so that it only references variables in the destination - circuit (as given to this class on initialisation). + circuit (as given to this class on initialization). If ``allow_reorder`` is ``True``, then when a legacy condition (the two-tuple form) is made on a register that has a counterpart in the destination with all the same (mapped) bits but in a different order, then that register will be used and the value suitably modified to - make the equality condition work. This is maintaining legacy (tested) behaviour of + make the equality condition work. This is maintaining legacy (tested) behavior of :meth:`.DAGCircuit.compose`; nowhere else does this, and in general this would require *far* more complex classical rewriting than Terra needs to worry about in the full expression era. """ @@ -88,7 +91,7 @@ def map_condition(self, condition, /, *, allow_reorder=False): return (self.bit_map[target], value) if not allow_reorder: return (self._map_register(target), value) - # This is maintaining the legacy behaviour of `DAGCircuit.compose`. We don't attempt to + # This is maintaining the legacy behavior of `DAGCircuit.compose`. We don't attempt to # speed-up this lookup with a cache, since that would just make the more standard cases more # annoying to deal with. mapped_bits_order = [self.bit_map[bit] for bit in target] @@ -111,7 +114,7 @@ def map_condition(self, condition, /, *, allow_reorder=False): def map_target(self, target, /): """Map the real-time variables in a ``target`` of a :class:`.SwitchCaseOp` to the new - circuit, as defined in the ``circuit`` argument of the initialiser of this class.""" + circuit, as defined in the ``circuit`` argument of the initializer of this class.""" if isinstance(target, Clbit): return self.bit_map[target] if isinstance(target, ClassicalRegister): @@ -127,9 +130,7 @@ def visit_var(self, node, /): return expr.Var(self.bit_map[node.var], node.type) if isinstance(node.var, ClassicalRegister): return expr.Var(self._map_register(node.var), node.type) - # Defensive against the expansion of the variable system; we don't want to silently do the - # wrong thing (which would be `return node` without mapping, right now). - raise RuntimeError(f"unhandled variable in 'compose': {node}") # pragma: no cover + return self.var_map.get(node, node) def visit_value(self, node, /): return expr.Value(node.value, node.type) @@ -142,3 +143,6 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): return expr.Cast(node.operand.accept(self), node.type, implicit=node.implicit) + + def visit_index(self, node, /): + return expr.Index(node.target.accept(self), node.index.accept(self), node.type) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 9884062c5f5..00f1c2e0676 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -43,9 +43,9 @@ real-time variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. .. autoclass:: Var - :members: var, name + :members: var, name, new -Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes +Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes with associated types. .. autoclass:: Value @@ -62,6 +62,12 @@ :members: Op :member-order: bysource +Bit-like types (unsigned integers) can be indexed by integer types, represented by :class:`Index`. +The result is a single bit. The resulting expression has an associated memory location (and so can +be used as an lvalue for :class:`.Store`, etc) if the target is also an lvalue. + +.. autoclass:: Index + When constructing expressions, one must ensure that the types are valid for the operation. Attempts to construct expressions with invalid types will raise a regular Python ``TypeError``. @@ -122,6 +128,13 @@ .. autofunction:: less_equal .. autofunction:: greater .. autofunction:: greater_equal +.. autofunction:: shift_left +.. autofunction:: shift_right + +You can index into unsigned integers and bit-likes using another unsigned integer of any width. +This includes in storing operations, if the target of the index is writeable. + +.. autofunction:: index Qiskit's legacy method for specifying equality conditions for use in conditionals is to use a two-tuple of a :class:`.Clbit` or :class:`.ClassicalRegister` and an integer. This represents an @@ -174,6 +187,7 @@ "Cast", "Unary", "Binary", + "Index", "ExprVisitor", "iter_vars", "structurally_equivalent", @@ -185,6 +199,8 @@ "bit_and", "bit_or", "bit_xor", + "shift_left", + "shift_right", "logic_and", "logic_or", "equal", @@ -193,10 +209,11 @@ "less_equal", "greater", "greater_equal", + "index", "lift_legacy_condition", ] -from .expr import Expr, Var, Value, Cast, Unary, Binary +from .expr import Expr, Var, Value, Cast, Unary, Binary, Index from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue from .constructors import ( lift, @@ -214,5 +231,8 @@ less_equal, greater, greater_equal, + shift_left, + shift_right, + index, lift_legacy_condition, ) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 64a19a2aee2..de3875eef90 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -37,7 +37,7 @@ import typing -from .expr import Expr, Var, Value, Unary, Binary, Cast +from .expr import Expr, Var, Value, Unary, Binary, Cast, Index from ..types import CastKind, cast_kind from .. import types @@ -471,3 +471,86 @@ def greater_equal(left: typing.Any, right: typing.Any, /) -> Expr: Uint(3)) """ return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) + + +def _shift_like( + op: Binary.Op, left: typing.Any, right: typing.Any, type: types.Type | None +) -> Expr: + if type is not None and type.kind is not types.Uint: + raise TypeError(f"type '{type}' is not a valid bitshift operand type") + if isinstance(left, Expr): + left = _coerce_lossless(left, type) if type is not None else left + else: + left = lift(left, type) + right = lift(right) + if left.type.kind != types.Uint or right.type.kind != types.Uint: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + return Binary(op, left, right, left.type) + + +def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: + """Create a 'bitshift left' expression node from the given two values, resolving any implicit + casts and lifting the values into :class:`Value` nodes if required. + + If ``type`` is given, the ``left`` operand will be coerced to it (if possible). + + Examples: + Shift the value of a standalone variable left by some amount:: + + >>> from qiskit.circuit.classical import expr, types + >>> a = expr.Var.new("a", types.Uint(8)) + >>> expr.shift_left(a, 4) + Binary(Binary.Op.SHIFT_LEFT, \ +Var(, Uint(8), name='a'), \ +Value(4, Uint(3)), \ +Uint(8)) + + Shift an integer literal by a variable amount, coercing the type of the literal:: + + >>> expr.shift_left(3, a, types.Uint(16)) + Binary(Binary.Op.SHIFT_LEFT, \ +Value(3, Uint(16)), \ +Var(, Uint(8), name='a'), \ +Uint(16)) + """ + return _shift_like(Binary.Op.SHIFT_LEFT, left, right, type) + + +def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: + """Create a 'bitshift right' expression node from the given values, resolving any implicit casts + and lifting the values into :class:`Value` nodes if required. + + If ``type`` is given, the ``left`` operand will be coerced to it (if possible). + + Examples: + Shift the value of a classical register right by some amount:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.shift_right(ClassicalRegister(8, "a"), 4) + Binary(Binary.Op.SHIFT_RIGHT, \ +Var(ClassicalRegister(8, "a"), Uint(8)), \ +Value(4, Uint(3)), \ +Uint(8)) + """ + return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) + + +def index(target: typing.Any, index: typing.Any, /) -> Expr: + """Index into the ``target`` with the given integer ``index``, lifting the values into + :class:`Value` nodes if required. + + This can be used as the target of a :class:`.Store`, if the ``target`` is itself an lvalue. + + Examples: + Index into a classical register with a literal:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.index(ClassicalRegister(8, "a"), 3) + Index(Var(ClassicalRegister(8, "a"), Uint(8)), Value(3, Uint(2)), Bool()) + """ + target, index = lift(target), lift(index) + if target.type.kind is not types.Uint or index.type.kind is not types.Uint: + raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") + return Index(target, index, types.Bool()) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index c22870e51fe..586b06ec9db 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -53,7 +53,7 @@ class Expr(abc.ABC): expressions, and it does not make sense to add more outside of Qiskit library code. All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and - should not call the parent initialiser.""" + should not call the parent initializer.""" __slots__ = ("type",) @@ -193,7 +193,7 @@ def __copy__(self): return self def __deepcopy__(self, memo): - # ... as are all my consituent parts. + # ... as are all my constituent parts. return self @@ -241,7 +241,7 @@ class Op(enum.Enum): # If adding opcodes, remember to add helper constructor functions in `constructors.py`. # The opcode integers should be considered a public interface; they are used by - # serialisation formats that may transfer data between different versions of Qiskit. + # serialization formats that may transfer data between different versions of Qiskit. BIT_NOT = 1 """Bitwise negation. ``~operand``.""" LOGIC_NOT = 2 @@ -300,11 +300,16 @@ class Op(enum.Enum): The binary mathematical relations :data:`EQUAL`, :data:`NOT_EQUAL`, :data:`LESS`, :data:`LESS_EQUAL`, :data:`GREATER` and :data:`GREATER_EQUAL` take unsigned integers (with an implicit cast to make them the same width), and return a Boolean. + + The bitshift operations :data:`SHIFT_LEFT` and :data:`SHIFT_RIGHT` can take bit-like + container types (e.g. unsigned integers) as the left operand, and any integer type as the + right-hand operand. In all cases, the output bit width is the same as the input, and zeros + fill in the "exposed" spaces. """ # If adding opcodes, remember to add helper constructor functions in `constructors.py` # The opcode integers should be considered a public interface; they are used by - # serialisation formats that may transfer data between different versions of Qiskit. + # serialization formats that may transfer data between different versions of Qiskit. BIT_AND = 1 """Bitwise "and". ``lhs & rhs``.""" BIT_OR = 2 @@ -327,6 +332,10 @@ class Op(enum.Enum): """Numeric greater than. ``lhs > rhs``.""" GREATER_EQUAL = 11 """Numeric greater than or equal to. ``lhs >= rhs``.""" + SHIFT_LEFT = 12 + """Zero-padding bitshift to the left. ``lhs << rhs``.""" + SHIFT_RIGHT = 13 + """Zero-padding bitshift to the right. ``lhs >> rhs``.""" def __str__(self): return f"Binary.{super().__str__()}" @@ -354,3 +363,35 @@ def __eq__(self, other): def __repr__(self): return f"Binary({self.op}, {self.left}, {self.right}, {self.type})" + + +@typing.final +class Index(Expr): + """An indexing expression. + + Args: + target: The object being indexed. + index: The expression doing the indexing. + type: The resolved type of the result. + """ + + __slots__ = ("target", "index") + + def __init__(self, target: Expr, index: Expr, type: types.Type): + self.target = target + self.index = index + self.type = type + + def accept(self, visitor, /): + return visitor.visit_index(self) + + def __eq__(self, other): + return ( + isinstance(other, Index) + and self.type == other.type + and self.target == other.target + and self.index == other.index + ) + + def __repr__(self): + return f"Index({self.target}, {self.index}, {self.type})" diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index c0c1a5894af..be7e9311c37 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -29,7 +29,7 @@ class ExprVisitor(typing.Generic[_T_co]): """Base class for visitors to the :class:`Expr` tree. Subclasses should override whichever of - the ``visit_*`` methods that they are able to handle, and should be organised such that + the ``visit_*`` methods that they are able to handle, and should be organized such that non-existent methods will never be called.""" # The method names are self-explanatory and docstrings would just be noise. @@ -55,6 +55,9 @@ def visit_binary(self, node: expr.Binary, /) -> _T_co: # pragma: no cover def visit_cast(self, node: expr.Cast, /) -> _T_co: # pragma: no cover return self.visit_generic(node) + def visit_index(self, node: expr.Index, /) -> _T_co: # pragma: no cover + return self.visit_generic(node) + class _VarWalkerImpl(ExprVisitor[typing.Iterable[expr.Var]]): __slots__ = () @@ -75,6 +78,10 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): yield from node.operand.accept(self) + def visit_index(self, node, /): + yield from node.target.accept(self) + yield from node.index.accept(self) + _VAR_WALKER = _VarWalkerImpl() @@ -164,6 +171,16 @@ def visit_cast(self, node, /): self.other = self.other.operand return node.operand.accept(self) + def visit_index(self, node, /): + if self.other.__class__ is not node.__class__ or self.other.type != node.type: + return False + other = self.other + self.other = other.target + if not node.target.accept(self): + return False + self.other = other.index + return node.index.accept(self) + def structurally_equivalent( left: expr.Expr, @@ -235,6 +252,9 @@ def visit_binary(self, node, /): def visit_cast(self, node, /): return False + def visit_index(self, node, /): + return node.target.accept(self) + _IS_LVALUE = _IsLValueImpl() diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 93ab90e3216..ae38a0d97fb 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -40,20 +40,21 @@ .. autoclass:: Bool .. autoclass:: Uint -Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single +Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. Working with types ================== -There are some functions on these types exposed here as well. These are mostly expected to be used -only in manipulations of the expression tree; users who are building expressions using the +There are some additional functions on these types documented in the subsequent sections. +These are mostly expected to be used only in manipulations of the expression tree; +users who are building expressions using the :ref:`user-facing construction interface ` should not need to use these. Partial ordering of types -------------------------- +========================= The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as ":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the @@ -78,7 +79,7 @@ Casting between types ---------------------- +===================== It is common to need to cast values of one type to another type. The casting rules for this are embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 04266aefd41..d20e7b5fd74 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -29,7 +29,7 @@ class _Singleton(type): - """Metaclass to make the child, which should take zero initialisation arguments, a singleton + """Metaclass to make the child, which should take zero initialization arguments, a singleton object.""" def _get_singleton_instance(cls): @@ -76,7 +76,7 @@ def __deepcopy__(self, _memo): def __setstate__(self, state): _dict, slots = state for slot, value in slots.items(): - # We need to overcome the type's enforcement of immutability post initialisation. + # We need to overcome the type's enforcement of immutability post initialization. super().__setattr__(slot, value) diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py index a2268acfe2d..a072d910f97 100644 --- a/qiskit/circuit/classicalfunction/__init__.py +++ b/qiskit/circuit/classicalfunction/__init__.py @@ -81,6 +81,7 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1: Decorator for a classical function that returns a `ClassicalFunction` object. +.. autofunction:: classical_function ClassicalFunction ----------------- diff --git a/qiskit/circuit/classicalfunction/boolean_expression.py b/qiskit/circuit/classicalfunction/boolean_expression.py index 0f4a53494af..e517f36db02 100644 --- a/qiskit/circuit/classicalfunction/boolean_expression.py +++ b/qiskit/circuit/classicalfunction/boolean_expression.py @@ -116,7 +116,7 @@ def from_dimacs_file(cls, filename: str): expr_obj = cls.__new__(cls) if not isfile(filename): - raise FileNotFoundError("The file %s does not exists." % filename) + raise FileNotFoundError(f"The file {filename} does not exists.") expr_obj._tweedledum_bool_expression = BoolFunction.from_dimacs_file(filename) num_qubits = ( diff --git a/qiskit/circuit/classicalfunction/classical_function_visitor.py b/qiskit/circuit/classicalfunction/classical_function_visitor.py index dfe8b956b09..be89e8ee7f8 100644 --- a/qiskit/circuit/classicalfunction/classical_function_visitor.py +++ b/qiskit/circuit/classicalfunction/classical_function_visitor.py @@ -83,7 +83,7 @@ def bit_binop(self, op, values): """Uses ClassicalFunctionVisitor.bitops to extend self._network""" bitop = ClassicalFunctionVisitor.bitops.get(type(op)) if not bitop: - raise ClassicalFunctionParseError("Unknown binop.op %s" % op) + raise ClassicalFunctionParseError(f"Unknown binop.op {op}") binop = getattr(self._network, bitop) left_type, left_signal = values[0] @@ -112,19 +112,19 @@ def visit_UnaryOp(self, node): operand_type, operand_signal = self.visit(node.operand) if operand_type != "Int1": raise ClassicalFunctionCompilerTypeError( - "UntaryOp.op %s only support operation on Int1s for now" % node.op + f"UntaryOp.op {node.op} only support operation on Int1s for now" ) bitop = ClassicalFunctionVisitor.bitops.get(type(node.op)) if not bitop: raise ClassicalFunctionCompilerTypeError( - "UntaryOp.op %s does not operate with Int1 type " % node.op + f"UntaryOp.op {node.op} does not operate with Int1 type " ) return "Int1", getattr(self._network, bitop)(operand_signal) def visit_Name(self, node): """Reduce variable names.""" if node.id not in self.scopes[-1]: - raise ClassicalFunctionParseError("out of scope: %s" % node.id) + raise ClassicalFunctionParseError(f"out of scope: {node.id}") return self.scopes[-1][node.id] def generic_visit(self, node): @@ -143,7 +143,7 @@ def generic_visit(self, node): ), ): return super().generic_visit(node) - raise ClassicalFunctionParseError("Unknown node: %s" % type(node)) + raise ClassicalFunctionParseError(f"Unknown node: {type(node)}") def extend_scope(self, args_node: _ast.arguments) -> None: """Add the arguments to the scope""" diff --git a/qiskit/circuit/classicalfunction/utils.py b/qiskit/circuit/classicalfunction/utils.py index 237a8b83853..75dcd3e20a7 100644 --- a/qiskit/circuit/classicalfunction/utils.py +++ b/qiskit/circuit/classicalfunction/utils.py @@ -47,7 +47,7 @@ def _convert_tweedledum_operator(op): if op.kind() == "py_operator": return op.py_op() else: - raise RuntimeError("Unrecognized operator: %s" % op.kind()) + raise RuntimeError(f"Unrecognized operator: {op.kind()}") # TODO: need to deal with cbits too! if op.num_controls() > 0: diff --git a/qiskit/circuit/classicalregister.py b/qiskit/circuit/classicalregister.py index 7a21e6b2fa5..802d8c602e2 100644 --- a/qiskit/circuit/classicalregister.py +++ b/qiskit/circuit/classicalregister.py @@ -43,7 +43,7 @@ def __init__(self, register=None, index=None): super().__init__(register, index) else: raise CircuitError( - "Clbit needs a ClassicalRegister and %s was provided" % type(register).__name__ + f"Clbit needs a ClassicalRegister and {type(register).__name__} was provided" ) diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index bfb0d905387..e80910aac3e 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -127,7 +127,7 @@ def unify_circuit_resources(circuits: Iterable[QuantumCircuit]) -> Iterable[Quan This function will preferentially try to mutate its inputs if they share an ordering, but if not, it will rebuild two new circuits. This is to avoid coupling too tightly to the inner class; there is no real support for deleting or re-ordering bits within a :obj:`.QuantumCircuit` - context, and we don't want to rely on the *current* behaviour of the private APIs, since they + context, and we don't want to rely on the *current* behavior of the private APIs, since they are very liable to change. No matter the method used, circuits with unified bits and registers are returned. """ diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c6c95d27f92..ab464a50ca6 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -13,7 +13,7 @@ """Builder types for the basic control-flow constructs.""" # This file is in circuit.controlflow rather than the root of circuit because the constructs here -# are only intended to be localised to constructing the control flow instructions. We anticipate +# are only intended to be localized to constructing the control flow instructions. We anticipate # having a far more complete builder of all circuits, with more classical control and creation, in # the future. @@ -57,7 +57,9 @@ def instructions(self) -> Sequence[CircuitInstruction]: """Indexable view onto the :class:`.CircuitInstruction`s backing this scope.""" @abc.abstractmethod - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate=False + ) -> CircuitInstruction: """Low-level 'append' primitive; this may assume that the qubits, clbits and operation are all valid for the circuit. @@ -204,7 +206,7 @@ class InstructionPlaceholder(Instruction, abc.ABC): When appending a placeholder instruction into a circuit scope, you should create the placeholder, and then ask it what resources it should be considered as using from the start by calling :meth:`.InstructionPlaceholder.placeholder_instructions`. This set will be a subset of - the final resources it asks for, but it is used for initialising resources that *must* be + the final resources it asks for, but it is used for initializing resources that *must* be supplied, such as the bits used in the conditions of placeholder ``if`` statements. .. warning:: @@ -358,7 +360,7 @@ def __init__( which use a classical register as their condition. allow_jumps: Whether this builder scope should allow ``break`` and ``continue`` statements within it. This is intended to help give sensible error messages when - dangerous behaviour is encountered, such as using ``break`` inside an ``if`` context + dangerous behavior is encountered, such as using ``break`` inside an ``if`` context manager that is not within a ``for`` manager. This can only be safe if the user is going to place the resulting :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that uses *exactly* the same set of resources. We cannot verify this from within the @@ -393,7 +395,7 @@ def clbits(self): def allow_jumps(self): """Whether this builder scope should allow ``break`` and ``continue`` statements within it. - This is intended to help give sensible error messages when dangerous behaviour is + This is intended to help give sensible error messages when dangerous behavior is encountered, such as using ``break`` inside an ``if`` context manager that is not within a ``for`` manager. This can only be safe if the user is going to place the resulting :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that uses *exactly* the same set of @@ -420,7 +422,9 @@ def _raise_on_jump(operation): " because it is not in a loop." ) - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate: bool = False + ) -> CircuitInstruction: if self._forbidden_message is not None: raise CircuitError(self._forbidden_message) if not self._allow_jumps: diff --git a/qiskit/circuit/controlflow/control_flow.py b/qiskit/circuit/controlflow/control_flow.py index 51b3709db6b..2085f760ebc 100644 --- a/qiskit/circuit/controlflow/control_flow.py +++ b/qiskit/circuit/controlflow/control_flow.py @@ -22,6 +22,7 @@ if typing.TYPE_CHECKING: from qiskit.circuit import QuantumCircuit + from qiskit.circuit.classical import expr class ControlFlowOp(Instruction, ABC): @@ -72,3 +73,12 @@ def map_block(block: QuantumCircuit) -> QuantumCircuit: Returns: New :class:`ControlFlowOp` with replaced blocks. """ + + def iter_captured_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterator over the unique captured variables in all blocks of this construct.""" + seen = set() + for block in self.blocks: + for var in block.iter_captured_vars(): + if var not in seen: + seen.add(var) + yield var diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 121d4c681f2..dd639c65f4b 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -199,7 +199,7 @@ def __init__( super().__init__( "if_else", len(self.__resources.qubits), len(self.__resources.clbits), [], label=label ) - # Set the condition after super().__init__() has initialised it to None. + # Set the condition after super().__init__() has initialized it to None. self.condition = validate_condition(condition) def with_false_block(self, false_block: ControlFlowBuilderBlock) -> "IfElsePlaceholder": @@ -236,7 +236,7 @@ def registers(self): def _calculate_placeholder_resources(self) -> InstructionResources: """Get the placeholder resources (see :meth:`.placeholder_resources`). - This is a separate function because we use the resources during the initialisation to + This is a separate function because we use the resources during the initialization to determine how we should set our ``num_qubits`` and ``num_clbits``, so we implement the public version as a cache access for efficiency. """ diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 446230c3c3c..6df8c4ef62a 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -94,7 +94,7 @@ def __init__( it's easier for things like `assign_parameters`, which need to touch each circuit object exactly once, to function.""" self._label_spec: List[Tuple[Union[int, Literal[CASE_DEFAULT]], ...]] = [] - """List of the normalised jump value specifiers. This is a list of tuples, where each tuple + """List of the normalized jump value specifiers. This is a list of tuples, where each tuple contains the values, and the indexing is the same as the values of `_case_map` and `_params`.""" self._params = [] diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index a333125a5a2..25e7a6f3356 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -32,7 +32,7 @@ def __init__(self, duration, unit="dt"): unit: the unit of the duration. Must be ``"dt"`` or an SI-prefixed seconds unit. """ if unit not in {"s", "ms", "us", "ns", "ps", "dt"}: - raise CircuitError("Unknown unit %s is specified." % unit) + raise CircuitError(f"Unknown unit {unit} is specified.") super().__init__("delay", 1, 0, params=[duration], unit=unit) diff --git a/qiskit/circuit/duration.py b/qiskit/circuit/duration.py index 6acb230baad..fdf6e99e611 100644 --- a/qiskit/circuit/duration.py +++ b/qiskit/circuit/duration.py @@ -35,8 +35,8 @@ def duration_in_dt(duration_in_sec: float, dt_in_sec: float) -> int: rounding_error = abs(duration_in_sec - res * dt_in_sec) if rounding_error > 1e-15: warnings.warn( - "Duration is rounded to %d [dt] = %e [s] from %e [s]" - % (res, res * dt_in_sec, duration_in_sec), + f"Duration is rounded to {res:d} [dt] = {res * dt_in_sec:e} [s] " + f"from {duration_in_sec:e} [s]", UserWarning, ) return res diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 45921c3f229..17912517d24 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -249,7 +249,7 @@ def _build_basis_graph(self): ) node_map[decomp_basis] = decomp_basis_node - label = "{}\n{}".format(str(params), str(decomp) if num_qubits <= 5 else "...") + label = f"{str(params)}\n{str(decomp) if num_qubits <= 5 else '...'}" graph.add_edge( node_map[basis], node_map[decomp_basis], @@ -273,8 +273,8 @@ def _raise_if_param_mismatch(gate_params, circuit_parameters): if set(gate_parameters) != circuit_parameters: raise CircuitError( "Cannot add equivalence between circuit and gate " - "of different parameters. Gate params: {}. " - "Circuit params: {}.".format(gate_parameters, circuit_parameters) + f"of different parameters. Gate params: {gate_parameters}. " + f"Circuit params: {circuit_parameters}." ) @@ -282,10 +282,8 @@ def _raise_if_shape_mismatch(gate, circuit): if gate.num_qubits != circuit.num_qubits or gate.num_clbits != circuit.num_clbits: raise CircuitError( "Cannot add equivalence between circuit and gate " - "of different shapes. Gate: {} qubits and {} clbits. " - "Circuit: {} qubits and {} clbits.".format( - gate.num_qubits, gate.num_clbits, circuit.num_qubits, circuit.num_clbits - ) + f"of different shapes. Gate: {gate.num_qubits} qubits and {gate.num_clbits} clbits. " + f"Circuit: {circuit.num_qubits} qubits and {circuit.num_clbits} clbits." ) diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 13252677586..d2c88f40bdb 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -177,7 +177,7 @@ def _broadcast_3_or_more_args(qargs: list) -> Iterator[tuple[list, list]]: for arg in zip(*qargs): yield list(arg), [] else: - raise CircuitError("Not sure how to combine these qubit arguments:\n %s\n" % qargs) + raise CircuitError(f"Not sure how to combine these qubit arguments:\n {qargs}\n") def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, list]]: """Validation and handling of the arguments and its relationship. @@ -236,7 +236,7 @@ def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, elif len(qargs) >= 3: return Gate._broadcast_3_or_more_args(qargs) else: - raise CircuitError("This gate cannot handle %i arguments" % len(qargs)) + raise CircuitError(f"This gate cannot handle {len(qargs)} arguments") def validate_parameter(self, parameter): """Gate parameters should be int, float, or ParameterExpression""" diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e339cb8d94b..1b5fca3f738 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -58,6 +58,7 @@ class Instruction(Operation): # Class attribute to treat like barrier for transpiler, unroller, drawer # NOTE: Using this attribute may change in the future (See issue # 5811) _directive = False + _standard_gate = None def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None): """Create a new instruction. @@ -80,7 +81,7 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt raise CircuitError("num_qubits and num_clbits must be integer.") if num_qubits < 0 or num_clbits < 0: raise CircuitError( - "bad instruction dimensions: %d qubits, %d clbits." % num_qubits, num_clbits + f"bad instruction dimensions: {num_qubits} qubits, {num_clbits} clbits." ) self._name = name self._num_qubits = num_qubits @@ -114,12 +115,12 @@ def base_class(self) -> Type[Instruction]: The "base class" of an instruction is the lowest class in its inheritance tree that the object should be considered entirely compatible with for _all_ circuit applications. This typically means that the subclass is defined purely to offer some sort of programmer - convenience over the base class, and the base class is the "true" class for a behavioural + convenience over the base class, and the base class is the "true" class for a behavioral perspective. In particular, you should *not* override :attr:`base_class` if you are defining a custom version of an instruction that will be implemented differently by - hardware, such as an alternative measurement strategy, or a version of a parametrised gate + hardware, such as an alternative measurement strategy, or a version of a parametrized gate with a particular set of parameters for the purposes of distinguishing it in a - :class:`.Target` from the full parametrised gate. + :class:`.Target` from the full parametrized gate. This is often exactly equivalent to ``type(obj)``, except in the case of singleton instances of standard-library instructions. These singleton instances are special subclasses of their @@ -221,8 +222,9 @@ def __repr__(self) -> str: str: A representation of the Instruction instance with the name, number of qubits, classical bits and params( if any ) """ - return "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - self.name, self.num_qubits, self.num_clbits, self.params + return ( + f"Instruction(name='{self.name}', num_qubits={self.num_qubits}, " + f"num_clbits={self.num_clbits}, params={self.params})" ) def soft_compare(self, other: "Instruction") -> bool: @@ -455,7 +457,7 @@ def inverse(self, annotated: bool = False): return AnnotatedOperation(self, InverseModifier()) if self.definition is None: - raise CircuitError("inverse() not implemented for %s." % self.name) + raise CircuitError(f"inverse() not implemented for {self.name}.") from qiskit.circuit import Gate # pylint: disable=cyclic-import diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index ac3d9fabd64..576d5dee826 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -140,13 +140,12 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc ) if self._requester is not None: classical = self._requester(classical) - for instruction in self._instructions: + for idx, instruction in enumerate(self._instructions): if isinstance(instruction, CircuitInstruction): updated = instruction.operation.c_if(classical, val) - if updated is not instruction.operation: - raise CircuitError( - "SingletonGate instances can only be added to InstructionSet via _add_ref" - ) + self._instructions[idx] = instruction.replace( + operation=updated, condition=updated.condition + ) else: data, idx = instruction instruction = data[idx] diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 5f21967e482..a9ae005d982 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -129,35 +129,19 @@ Standard Directives =================== -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - Directives are operations to the quantum stack that are meant to be interpreted by the backend or the transpiler. In general, the transpiler or backend might optionally ignore them if there is no implementation for them. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Barrier +* :class:`qiskit.circuit.Barrier` Standard Operations =================== Operations are non-reversible changes in the quantum state of the circuit. -.. - This summary table deliberately does not generate toctree entries; these directives are "owned" - by ``qiskit.circuit``. - -.. autosummary:: - - Measure - Reset +* :class:`qiskit.circuit.Measure` +* :class:`qiskit.circuit.Reset` Generalized Gates ================= diff --git a/qiskit/circuit/library/arithmetic/linear_amplitude_function.py b/qiskit/circuit/library/arithmetic/linear_amplitude_function.py index 0825f3f4e0a..a79670eef65 100644 --- a/qiskit/circuit/library/arithmetic/linear_amplitude_function.py +++ b/qiskit/circuit/library/arithmetic/linear_amplitude_function.py @@ -119,7 +119,7 @@ def __init__( self._image = image self._rescaling_factor = rescaling_factor - # do rescalings + # do rescaling a, b = domain c, d = image diff --git a/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py b/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py index a40767154bc..bc80ef77861 100644 --- a/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/linear_pauli_rotations.py @@ -153,7 +153,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py index a27c57ef28f..cc34d3631f5 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py +++ b/qiskit/circuit/library/arithmetic/piecewise_chebyshev.py @@ -122,7 +122,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py index 509433af557..3d84e64ccb1 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_linear_pauli_rotations.py @@ -202,7 +202,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) if len(self.breakpoints) != len(self.slopes) or len(self.breakpoints) != len(self.offsets): diff --git a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py index f604e16f469..741b920e368 100644 --- a/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/piecewise_polynomial_pauli_rotations.py @@ -218,8 +218,8 @@ def evaluate(self, x: float) -> float: """ y = 0 - for i in range(0, len(self.breakpoints)): - y = y + (x >= self.breakpoints[i]) * (np.poly1d(self.mapped_coeffs[i][::-1])(x)) + for i, breakpt in enumerate(self.breakpoints): + y = y + (x >= breakpt) * (np.poly1d(self.mapped_coeffs[i][::-1])(x)) return y @@ -237,7 +237,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) if len(self.breakpoints) != len(self.coeffs) + 1: diff --git a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py index 13fb8229881..4f04a04dd52 100644 --- a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py @@ -248,7 +248,7 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: if raise_on_failure: raise CircuitError( "Not enough qubits in the circuit, need at least " - "{}.".format(self.num_state_qubits + 1) + f"{self.num_state_qubits + 1}." ) return valid diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 3d1f5c77f44..16cc0e3dbaf 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -17,7 +17,7 @@ from qiskit._accelerate.circuit import CircuitData from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit.parametertable import ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView class BlueprintCircuit(QuantumCircuit, ABC): @@ -68,7 +68,6 @@ def _build(self) -> None: def _invalidate(self) -> None: """Invalidate the current circuit build.""" self._data = CircuitData(self._data.qubits, self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -88,7 +87,6 @@ def qregs(self, qregs): self._ancillas = [] self._qubit_indices = {} self._data = CircuitData(clbits=self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -122,17 +120,37 @@ def parameters(self) -> ParameterView: self._build() return super().parameters - def _append(self, instruction, _qargs=None, _cargs=None): + def _append(self, instruction, _qargs=None, _cargs=None, *, _standard_gate=False): if not self._is_built: self._build() - return super()._append(instruction, _qargs, _cargs) + return super()._append(instruction, _qargs, _cargs, _standard_gate=_standard_gate) def compose( - self, other, qubits=None, clbits=None, front=False, inplace=False, wrap=False, *, copy=True + self, + other, + qubits=None, + clbits=None, + front=False, + inplace=False, + wrap=False, + *, + copy=True, + var_remap=None, + inline_captures=False, ): if not self._is_built: self._build() - return super().compose(other, qubits, clbits, front, inplace, wrap, copy=copy) + return super().compose( + other, + qubits, + clbits, + front, + inplace, + wrap, + copy=copy, + var_remap=var_remap, + inline_captures=False, + ) def inverse(self, annotated: bool = False): if not self._is_built: @@ -180,10 +198,10 @@ def num_connected_components(self, unitary_only=False): self._build() return super().num_connected_components(unitary_only=unitary_only) - def copy_empty_like(self, name=None): + def copy_empty_like(self, name=None, *, vars_mode="alike"): if not self._is_built: self._build() - cpy = super().copy_empty_like(name=name) + cpy = super().copy_empty_like(name=name, vars_mode=vars_mode) # The base `copy_empty_like` will typically trigger code that `BlueprintCircuit` treats as # an "invalidation", so we have to manually restore properties deleted by that that # `copy_empty_like` is supposed to propagate. diff --git a/qiskit/circuit/library/data_preparation/initializer.py b/qiskit/circuit/library/data_preparation/initializer.py index 394f863191d..0e38f067403 100644 --- a/qiskit/circuit/library/data_preparation/initializer.py +++ b/qiskit/circuit/library/data_preparation/initializer.py @@ -36,6 +36,14 @@ class Initialize(Instruction): the :class:`~.library.StatePreparation` class. Note that ``Initialize`` is an :class:`~.circuit.Instruction` and not a :class:`.Gate` since it contains a reset instruction, which is not unitary. + + The initial state is prepared based on the :class:`~.library.Isometry` synthesis described in [1]. + + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. + """ def __init__( diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index b05287ca049..03bbc031ec6 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -97,7 +97,7 @@ class PauliFeatureMap(NLocal): >>> from qiskit.circuit.library import EfficientSU2 >>> prep = PauliFeatureMap(3, reps=3, paulis=['Z', 'YY', 'ZXZ']) >>> wavefunction = EfficientSU2(3) - >>> classifier = prep.compose(wavefunction + >>> classifier = prep.compose(wavefunction) >>> classifier.num_parameters 27 >>> classifier.count_ops() diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 3284b8dd4f5..4e466e6d99c 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Prepare a quantum state from the state where all qubits are 0.""" -import cmath from typing import Union, Optional import math @@ -21,11 +20,10 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.gate import Gate -from qiskit.circuit.library.standard_gates.x import CXGate, XGate +from qiskit.circuit.library.standard_gates.x import XGate from qiskit.circuit.library.standard_gates.h import HGate from qiskit.circuit.library.standard_gates.s import SGate, SdgGate -from qiskit.circuit.library.standard_gates.ry import RYGate -from qiskit.circuit.library.standard_gates.rz import RZGate +from qiskit.circuit.library.generalized_gates import Isometry from qiskit.circuit.exceptions import CircuitError from qiskit.quantum_info.states.statevector import ( Statevector, @@ -73,13 +71,13 @@ def __init__( Raises: QiskitError: ``num_qubits`` parameter used when ``params`` is not an integer - When a Statevector argument is passed the state is prepared using a recursive - initialization algorithm, including optimizations, from [1], as well - as some additional optimizations including removing zero rotations and double cnots. + When a Statevector argument is passed the state is prepared based on the + :class:`~.library.Isometry` synthesis described in [1]. - **References:** - [1] Shende, Bullock, Markov. Synthesis of Quantum Logic Circuits (2004) - [`https://arxiv.org/abs/quant-ph/0406176v5`] + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ self._params_arg = params @@ -121,7 +119,7 @@ def _define(self): elif self._from_int: self.definition = self._define_from_int() else: - self.definition = self._define_synthesis() + self.definition = self._define_synthesis_isom() def _define_from_label(self): q = QuantumRegister(self.num_qubits, "q") @@ -158,8 +156,8 @@ def _define_from_int(self): # Raise if number of bits is greater than num_qubits if len(intstr) > self.num_qubits: raise QiskitError( - "StatePreparation integer has %s bits, but this exceeds the" - " number of qubits in the circuit, %s." % (len(intstr), self.num_qubits) + f"StatePreparation integer has {len(intstr)} bits, but this exceeds the" + f" number of qubits in the circuit, {self.num_qubits}." ) for qubit, bit in enumerate(intstr): @@ -170,29 +168,18 @@ def _define_from_int(self): # we don't need to invert anything return initialize_circuit - def _define_synthesis(self): - """Calculate a subcircuit that implements this initialization + def _define_synthesis_isom(self): + """Calculate a subcircuit that implements this initialization via isometry""" + q = QuantumRegister(self.num_qubits, "q") + initialize_circuit = QuantumCircuit(q, name="init_def") - Implements a recursive initialization algorithm, including optimizations, - from "Synthesis of Quantum Logic Circuits" Shende, Bullock, Markov - https://arxiv.org/abs/quant-ph/0406176v5 - - Additionally implements some extra optimizations: remove zero rotations and - double cnots. - """ - # call to generate the circuit that takes the desired vector to zero - disentangling_circuit = self._gates_to_uncompute() + isom = Isometry(self._params_arg, 0, 0) + initialize_circuit.append(isom, q[:]) # invert the circuit to create the desired vector from zero (assuming # the qubits are in the zero state) - if self._inverse is False: - initialize_instr = disentangling_circuit.to_instruction().inverse() - else: - initialize_instr = disentangling_circuit.to_instruction() - - q = QuantumRegister(self.num_qubits, "q") - initialize_circuit = QuantumCircuit(q, name="init_def") - initialize_circuit.append(initialize_instr, q[:]) + if self._inverse is True: + return initialize_circuit.inverse() return initialize_circuit @@ -227,9 +214,9 @@ def broadcast_arguments(self, qargs, cargs): if self.num_qubits != len(flat_qargs): raise QiskitError( - "StatePreparation parameter vector has %d elements, therefore expects %s " - "qubits. However, %s were provided." - % (2**self.num_qubits, self.num_qubits, len(flat_qargs)) + f"StatePreparation parameter vector has {2**self.num_qubits}" + f" elements, therefore expects {self.num_qubits} " + f"qubits. However, {len(flat_qargs)} were provided." ) yield flat_qargs, [] @@ -241,8 +228,8 @@ def validate_parameter(self, parameter): if parameter in ["0", "1", "+", "-", "l", "r"]: return parameter raise CircuitError( - "invalid param label {} for instruction {}. Label should be " - "0, 1, +, -, l, or r ".format(type(parameter), self.name) + f"invalid param label {type(parameter)} for instruction {self.name}. Label should be " + "0, 1, +, -, l, or r " ) # StatePreparation instruction parameter can be int, float, and complex. @@ -256,169 +243,6 @@ def validate_parameter(self, parameter): def _return_repeat(self, exponent: float) -> "Gate": return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) - def _gates_to_uncompute(self): - """Call to create a circuit with gates that take the desired vector to zero. - - Returns: - QuantumCircuit: circuit to take self.params vector to :math:`|{00\\ldots0}\\rangle` - """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q, name="disentangler") - - # kick start the peeling loop, and disentangle one-by-one from LSB to MSB - remaining_param = self.params - - for i in range(self.num_qubits): - # work out which rotations must be done to disentangle the LSB - # qubit (we peel away one qubit at a time) - ( - remaining_param, - thetas, - phis, - ) = StatePreparation._rotations_to_disentangle(remaining_param) - - # perform the required rotations to decouple the LSB qubit (so that - # it can be "factored" out, leaving a shorter amplitude vector to peel away) - - add_last_cnot = True - if np.linalg.norm(phis) != 0 and np.linalg.norm(thetas) != 0: - add_last_cnot = False - - if np.linalg.norm(phis) != 0: - rz_mult = self._multiplex(RZGate, phis, last_cnot=add_last_cnot) - circuit.append(rz_mult.to_instruction(), q[i : self.num_qubits]) - - if np.linalg.norm(thetas) != 0: - ry_mult = self._multiplex(RYGate, thetas, last_cnot=add_last_cnot) - circuit.append(ry_mult.to_instruction().reverse_ops(), q[i : self.num_qubits]) - circuit.global_phase -= np.angle(sum(remaining_param)) - return circuit - - @staticmethod - def _rotations_to_disentangle(local_param): - """ - Static internal method to work out Ry and Rz rotation angles used - to disentangle the LSB qubit. - These rotations make up the block diagonal matrix U (i.e. multiplexor) - that disentangles the LSB. - - [[Ry(theta_1).Rz(phi_1) 0 . . 0], - [0 Ry(theta_2).Rz(phi_2) . 0], - . - . - 0 0 Ry(theta_2^n).Rz(phi_2^n)]] - """ - remaining_vector = [] - thetas = [] - phis = [] - - param_len = len(local_param) - - for i in range(param_len // 2): - # Ry and Rz rotations to move bloch vector from 0 to "imaginary" - # qubit - # (imagine a qubit state signified by the amplitudes at index 2*i - # and 2*(i+1), corresponding to the select qubits of the - # multiplexor being in state |i>) - (remains, add_theta, add_phi) = StatePreparation._bloch_angles( - local_param[2 * i : 2 * (i + 1)] - ) - - remaining_vector.append(remains) - - # rotations for all imaginary qubits of the full vector - # to move from where it is to zero, hence the negative sign - thetas.append(-add_theta) - phis.append(-add_phi) - - return remaining_vector, thetas, phis - - @staticmethod - def _bloch_angles(pair_of_complex): - """ - Static internal method to work out rotation to create the passed-in - qubit from the zero vector. - """ - [a_complex, b_complex] = pair_of_complex - # Force a and b to be complex, as otherwise numpy.angle might fail. - a_complex = complex(a_complex) - b_complex = complex(b_complex) - mag_a = abs(a_complex) - final_r = math.sqrt(mag_a**2 + abs(b_complex) ** 2) - if final_r < _EPS: - theta = 0 - phi = 0 - final_r = 0 - final_t = 0 - else: - theta = 2 * math.acos(mag_a / final_r) - a_arg = cmath.phase(a_complex) - b_arg = cmath.phase(b_complex) - final_t = a_arg + b_arg - phi = b_arg - a_arg - - return final_r * cmath.exp(1.0j * final_t / 2), theta, phi - - def _multiplex(self, target_gate, list_of_angles, last_cnot=True): - """ - Return a recursive implementation of a multiplexor circuit, - where each instruction itself has a decomposition based on - smaller multiplexors. - - The LSB is the multiplexor "data" and the other bits are multiplexor "select". - - Args: - target_gate (Gate): Ry or Rz gate to apply to target qubit, multiplexed - over all other "select" qubits - list_of_angles (list[float]): list of rotation angles to apply Ry and Rz - last_cnot (bool): add the last cnot if last_cnot = True - - Returns: - DAGCircuit: the circuit implementing the multiplexor's action - """ - list_len = len(list_of_angles) - local_num_qubits = int(math.log2(list_len)) + 1 - - q = QuantumRegister(local_num_qubits) - circuit = QuantumCircuit(q, name="multiplex" + str(local_num_qubits)) - - lsb = q[0] - msb = q[local_num_qubits - 1] - - # case of no multiplexing: base case for recursion - if local_num_qubits == 1: - circuit.append(target_gate(list_of_angles[0]), [q[0]]) - return circuit - - # calc angle weights, assuming recursion (that is the lower-level - # requested angles have been correctly implemented by recursion - angle_weight = np.kron([[0.5, 0.5], [0.5, -0.5]], np.identity(2 ** (local_num_qubits - 2))) - - # calc the combo angles - list_of_angles = angle_weight.dot(np.array(list_of_angles)).tolist() - - # recursive step on half the angles fulfilling the above assumption - multiplex_1 = self._multiplex(target_gate, list_of_angles[0 : (list_len // 2)], False) - circuit.append(multiplex_1.to_instruction(), q[0:-1]) - - # attach CNOT as follows, thereby flipping the LSB qubit - circuit.append(CXGate(), [msb, lsb]) - - # implement extra efficiency from the paper of cancelling adjacent - # CNOTs (by leaving out last CNOT and reversing (NOT inverting) the - # second lower-level multiplex) - multiplex_2 = self._multiplex(target_gate, list_of_angles[(list_len // 2) :], False) - if list_len > 1: - circuit.append(multiplex_2.to_instruction().reverse_ops(), q[0:-1]) - else: - circuit.append(multiplex_2.to_instruction(), q[0:-1]) - - # attach a final CNOT - if last_cnot: - circuit.append(CXGate(), [msb, lsb]) - - return circuit - class UniformSuperpositionGate(Gate): r"""Implements a uniform superposition state. diff --git a/qiskit/circuit/library/generalized_gates/isometry.py b/qiskit/circuit/library/generalized_gates/isometry.py index 1294feb2634..e6b4f6fb21c 100644 --- a/qiskit/circuit/library/generalized_gates/isometry.py +++ b/qiskit/circuit/library/generalized_gates/isometry.py @@ -21,7 +21,6 @@ from __future__ import annotations -import itertools import math import numpy as np from qiskit.circuit.exceptions import CircuitError @@ -30,6 +29,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_isometry +from qiskit._accelerate import isometry as isometry_rs from .diagonal import Diagonal from .uc import UCGate @@ -45,10 +45,10 @@ class Isometry(Instruction): The decomposition is based on [1]. - **References:** - - [1] Iten et al., Quantum circuits for isometries (2016). - `Phys. Rev. A 93, 032318 `__. + References: + 1. Iten et al., Quantum circuits for isometries (2016). + `Phys. Rev. A 93, 032318 + `__. """ @@ -123,8 +123,8 @@ def _define(self): # later here instead. gate = self.inv_gate() gate = gate.inverse() - q = QuantumRegister(self.num_qubits) - iso_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + iso_circuit = QuantumCircuit(q, name="isometry") iso_circuit.append(gate, q[:]) self.definition = iso_circuit @@ -139,8 +139,8 @@ def _gates_to_uncompute(self): Call to create a circuit with gates that take the desired isometry to the first 2^m columns of the 2^n*2^n identity matrix (see https://arxiv.org/abs/1501.06911) """ - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="isometry_to_uncompute") ( q_input, q_ancillas_for_output, @@ -157,12 +157,16 @@ def _gates_to_uncompute(self): # correspond to the firstfew columns of the identity matrix up to diag, and hence we only # have to save a list containing them. for column_index in range(2**m): - self._decompose_column(circuit, q, diag, remaining_isometry, column_index) + remaining_isometry, diag = self._decompose_column( + circuit, q, diag, remaining_isometry, column_index + ) # extract phase of the state that was sent to the basis state ket(column_index) diag.append(remaining_isometry[column_index, 0]) # remove first column (which is now stored in diag) remaining_isometry = remaining_isometry[:, 1:] - if len(diag) > 1 and not _diag_is_identity_up_to_global_phase(diag, self._epsilon): + if len(diag) > 1 and not isometry_rs.diag_is_identity_up_to_global_phase( + diag, self._epsilon + ): diagonal = Diagonal(np.conj(diag)) circuit.append(diagonal, q_input) return circuit @@ -173,7 +177,10 @@ def _decompose_column(self, circuit, q, diag, remaining_isometry, column_index): """ n = int(math.log2(self.iso_data.shape[0])) for s in range(n): - self._disentangle(circuit, q, diag, remaining_isometry, column_index, s) + remaining_isometry, diag = self._disentangle( + circuit, q, diag, remaining_isometry, column_index, s + ) + return remaining_isometry, diag def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): """ @@ -189,13 +196,19 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): n = int(math.log2(self.iso_data.shape[0])) # MCG to set one entry to zero (preparation for disentangling with UCGate): - index1 = 2 * _a(k, s + 1) * 2**s + _b(k, s + 1) - index2 = (2 * _a(k, s + 1) + 1) * 2**s + _b(k, s + 1) + index1 = 2 * isometry_rs.a(k, s + 1) * 2**s + isometry_rs.b(k, s + 1) + index2 = (2 * isometry_rs.a(k, s + 1) + 1) * 2**s + isometry_rs.b(k, s + 1) target_label = n - s - 1 # Check if a MCG is required - if _k_s(k, s) == 0 and _b(k, s + 1) != 0 and np.abs(v[index2, k_prime]) > self._epsilon: + if ( + isometry_rs.k_s(k, s) == 0 + and isometry_rs.b(k, s + 1) != 0 + and np.abs(v[index2, k_prime]) > self._epsilon + ): # Find the MCG, decompose it and apply it to the remaining isometry - gate = _reverse_qubit_state([v[index1, k_prime], v[index2, k_prime]], 0, self._epsilon) + gate = isometry_rs.reverse_qubit_state( + [v[index1, k_prime], v[index2, k_prime]], 0, self._epsilon + ) control_labels = [ i for i, x in enumerate(_get_binary_rep_as_list(k, n)) @@ -205,57 +218,49 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s): circuit, q, gate, control_labels, target_label ) # apply the MCG to the remaining isometry - _apply_multi_controlled_gate(v, control_labels, target_label, gate) + v = isometry_rs.apply_multi_controlled_gate(v, control_labels, target_label, gate) # correct for the implementation "up to diagonal" - diag_mcg_inverse = np.conj(diagonal_mcg).tolist() - _apply_diagonal_gate(v, control_labels + [target_label], diag_mcg_inverse) + diag_mcg_inverse = np.conj(diagonal_mcg).astype(complex, copy=False) + v = isometry_rs.apply_diagonal_gate( + v, control_labels + [target_label], diag_mcg_inverse + ) # update the diag according to the applied diagonal gate - _apply_diagonal_gate_to_diag(diag, control_labels + [target_label], diag_mcg_inverse, n) + diag = isometry_rs.apply_diagonal_gate_to_diag( + diag, control_labels + [target_label], diag_mcg_inverse, n + ) # UCGate to disentangle a qubit: # Find the UCGate, decompose it and apply it to the remaining isometry single_qubit_gates = self._find_squs_for_disentangling(v, k, s) - if not _ucg_is_identity_up_to_global_phase(single_qubit_gates, self._epsilon): + if not isometry_rs.ucg_is_identity_up_to_global_phase(single_qubit_gates, self._epsilon): control_labels = list(range(target_label)) diagonal_ucg = self._append_ucg_up_to_diagonal( circuit, q, single_qubit_gates, control_labels, target_label ) # merge the diagonal into the UCGate for efficient application of both together - diagonal_ucg_inverse = np.conj(diagonal_ucg).tolist() - single_qubit_gates = _merge_UCGate_and_diag(single_qubit_gates, diagonal_ucg_inverse) + diagonal_ucg_inverse = np.conj(diagonal_ucg).astype(complex, copy=False) + single_qubit_gates = isometry_rs.merge_ucgate_and_diag( + single_qubit_gates, diagonal_ucg_inverse + ) # apply the UCGate (with the merged diagonal gate) to the remaining isometry - _apply_ucg(v, len(control_labels), single_qubit_gates) + v = isometry_rs.apply_ucg(v, len(control_labels), single_qubit_gates) # update the diag according to the applied diagonal gate - _apply_diagonal_gate_to_diag( + diag = isometry_rs.apply_diagonal_gate_to_diag( diag, control_labels + [target_label], diagonal_ucg_inverse, n ) # # correct for the implementation "up to diagonal" # diag_inv = np.conj(diag).tolist() # _apply_diagonal_gate(v, control_labels + [target_label], diag_inv) + return v, diag # This method finds the single-qubit gates for a UCGate to disentangle a qubit: # we consider the n-qubit state v[:,0] starting with k zeros (in the computational basis). # The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). def _find_squs_for_disentangling(self, v, k, s): - k_prime = 0 - n = int(math.log2(self.iso_data.shape[0])) - if _b(k, s + 1) == 0: - i_start = _a(k, s + 1) - else: - i_start = _a(k, s + 1) + 1 - id_list = [np.eye(2, 2) for _ in range(i_start)] - squs = [ - _reverse_qubit_state( - [ - v[2 * i * 2**s + _b(k, s), k_prime], - v[(2 * i + 1) * 2**s + _b(k, s), k_prime], - ], - _k_s(k, s), - self._epsilon, - ) - for i in range(i_start, 2 ** (n - s - 1)) - ] - return id_list + squs + res = isometry_rs.find_squs_for_disentangling( + v, k, s, self._epsilon, n=int(math.log2(self.iso_data.shape[0])) + ) + return res # Append a UCGate up to diagonal to the circuit circ. def _append_ucg_up_to_diagonal(self, circ, q, single_qubit_gates, control_labels, target_label): @@ -338,146 +343,6 @@ def inv_gate(self): return self._inverse -# Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or -# basis_state=1 respectively -def _reverse_qubit_state(state, basis_state, epsilon): - state = np.array(state) - r = np.linalg.norm(state) - if r < epsilon: - return np.eye(2, 2) - if basis_state == 0: - m = np.array([[np.conj(state[0]), np.conj(state[1])], [-state[1], state[0]]]) / r - else: - m = np.array([[-state[1], state[0]], [np.conj(state[0]), np.conj(state[1])]]) / r - return m - - -# Methods for applying gates to matrices (should be moved to Qiskit AER) - -# Input: matrix m with 2^n rows (and arbitrary many columns). Think of the columns as states -# on n qubits. The method applies a uniformly controlled gate (UCGate) to all the columns, where -# the UCGate is specified by the inputs k and single_qubit_gates: - -# k = number of controls. We assume that the controls are on the k most significant qubits -# (and the target is on the (k+1)th significant qubit) -# single_qubit_gates = [u_0,...,u_{2^k-1}], where the u_i's are 2*2 unitaries -# (provided as numpy arrays) - -# The order of the single-qubit unitaries is such that the first unitary u_0 is applied to the -# (k+1)th significant qubit if the control qubits are in the state ket(0...00), the gate u_1 is -# applied if the control qubits are in the state ket(0...01), and so on. - -# The input matrix m and the single-qubit gates have to be of dtype=complex. - - -def _apply_ucg(m, k, single_qubit_gates): - # ToDo: Improve efficiency by parallelizing the gate application. A generalized version of - # ToDo: this method should be implemented by the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_col = m.shape[1] - spacing = 2 ** (num_qubits - k - 1) - for j in range(2 ** (num_qubits - 1)): - i = (j // spacing) * spacing + j - gate_index = i // (2 ** (num_qubits - k)) - for col in range(num_col): - m[np.array([i, i + spacing]), np.array([col, col])] = np.ndarray.flatten( - single_qubit_gates[gate_index].dot(np.array([[m[i, col]], [m[i + spacing, col]]])) - ).tolist() - return m - - -# Apply a diagonal gate with diagonal entries liste in diag and acting on qubits with labels -# action_qubit_labels to a matrix m. -# The input matrix m has to be of dtype=complex -# The qubit labels are such that label 0 corresponds to the most significant qubit, label 1 to -# the second most significant qubit, and so on ... - - -def _apply_diagonal_gate(m, action_qubit_labels, diag): - # ToDo: Improve efficiency by parallelizing the gate application. A generalized version of - # ToDo: this method should be implemented by the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_cols = m.shape[1] - basis_states = list(itertools.product([0, 1], repeat=num_qubits)) - for state in basis_states: - state_on_action_qubits = [state[i] for i in action_qubit_labels] - diag_index = _bin_to_int(state_on_action_qubits) - i = _bin_to_int(state) - for j in range(num_cols): - m[i, j] = diag[diag_index] * m[i, j] - return m - - -# Special case of the method _apply_diagonal_gate, where the input m is a diagonal matrix on the -# log2(len(m_diagonal)) least significant qubits (this method is more efficient in this case -# than _apply_diagonal_gate). The input m_diagonal is provided as a list of diagonal entries. -# The diagonal diag is applied on the qubits with labels listed in action_qubit_labels. The input -# num_qubits gives the total number of considered qubits (this input is required to interpret the -# action_qubit_labels in relation to the least significant qubits). - - -def _apply_diagonal_gate_to_diag(m_diagonal, action_qubit_labels, diag, num_qubits): - if not m_diagonal: - return m_diagonal - basis_states = list(itertools.product([0, 1], repeat=num_qubits)) - for state in basis_states[: len(m_diagonal)]: - state_on_action_qubits = [state[i] for i in action_qubit_labels] - diag_index = _bin_to_int(state_on_action_qubits) - i = _bin_to_int(state) - m_diagonal[i] *= diag[diag_index] - return m_diagonal - - -# Apply a MC single-qubit gate (given by the 2*2 unitary input: gate) with controlling on -# the qubits with label control_labels and acting on the qubit with label target_label -# to a matrix m. The input matrix m and the gate have to be of dtype=complex. The qubit labels are -# such that label 0 corresponds to the most significant qubit, label 1 to the second most -# significant qubit, and so on ... - - -def _apply_multi_controlled_gate(m, control_labels, target_label, gate): - # ToDo: This method should be integrated into the state vector simulator in Qiskit AER. - num_qubits = int(math.log2(m.shape[0])) - num_cols = m.shape[1] - control_labels.sort() - free_qubits = num_qubits - len(control_labels) - 1 - basis_states_free = list(itertools.product([0, 1], repeat=free_qubits)) - for state_free in basis_states_free: - (e1, e2) = _construct_basis_states(state_free, control_labels, target_label) - for i in range(num_cols): - m[np.array([e1, e2]), np.array([i, i])] = np.ndarray.flatten( - gate.dot(np.array([[m[e1, i]], [m[e2, i]]])) - ).tolist() - return m - - -# Helper method for _apply_multi_controlled_gate. This constructs the basis states the MG gate -# is acting on for a specific state state_free of the qubits we neither control nor act on. - - -def _construct_basis_states(state_free, control_labels, target_label): - e1 = [] - e2 = [] - j = 0 - for i in range(len(state_free) + len(control_labels) + 1): - if i in control_labels: - e1.append(1) - e2.append(1) - elif i == target_label: - e1.append(0) - e2.append(1) - else: - e1.append(state_free[j]) - e2.append(state_free[j]) - j += 1 - out1 = _bin_to_int(e1) - out2 = _bin_to_int(e2) - return out1, out2 - - -# Some helper methods: - - # Get the qubits in the list qubits corresponding to the labels listed in labels. The total number # of qubits is given by num_qubits (and determines the convention for the qubit labeling) @@ -496,14 +361,6 @@ def _reverse_qubit_oder(qubits): # Convert list of binary digits to integer -def _bin_to_int(binary_digits_as_list): - return int("".join(str(x) for x in binary_digits_as_list), 2) - - -def _ct(m): - return np.transpose(np.conjugate(m)) - - def _get_binary_rep_as_list(n, num_digits): binary_string = np.binary_repr(n).zfill(num_digits) binary = [] @@ -511,64 +368,3 @@ def _get_binary_rep_as_list(n, num_digits): for c in line: binary.append(int(c)) return binary[-num_digits:] - - -# absorb a diagonal gate into a UCGate - - -def _merge_UCGate_and_diag(single_qubit_gates, diag): - for i, gate in enumerate(single_qubit_gates): - single_qubit_gates[i] = np.array([[diag[2 * i], 0.0], [0.0, diag[2 * i + 1]]]).dot(gate) - return single_qubit_gates - - -# Helper variables/functions for the column-by-column decomposition - - -# a(k,s) and b(k,s) are positive integers such that k = a(k,s)2^s + b(k,s) -# (with the maximal choice of a(k,s)) - - -def _a(k, s): - return k // 2**s - - -def _b(k, s): - return k - (_a(k, s) * 2**s) - - -# given a binary representation of k with binary digits [k_{n-1},..,k_1,k_0], -# the method k_s(k, s) returns k_s - - -def _k_s(k, s): - if k == 0: - return 0 - else: - num_digits = s + 1 - return _get_binary_rep_as_list(k, num_digits)[0] - - -# Check if a gate of a special form is equal to the identity gate up to global phase - - -def _ucg_is_identity_up_to_global_phase(single_qubit_gates, epsilon): - if not np.abs(single_qubit_gates[0][0, 0]) < epsilon: - global_phase = 1.0 / (single_qubit_gates[0][0, 0]) - else: - return False - for gate in single_qubit_gates: - if not np.allclose(global_phase * gate, np.eye(2, 2)): - return False - return True - - -def _diag_is_identity_up_to_global_phase(diag, epsilon): - if not np.abs(diag[0]) < epsilon: - global_phase = 1.0 / (diag[0]) - else: - return False - for d in diag: - if not np.abs(global_phase * d - 1) < epsilon: - return False - return True diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index 68deaddd732..519a306c357 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2021. +# (C) Copyright IBM 2017, 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 @@ -16,7 +16,6 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError -from qiskit.synthesis.linear import check_invertible_binary_matrix from qiskit.circuit.library.generalized_gates.permutation import PermutationGate # pylint: disable=cyclic-import @@ -115,6 +114,8 @@ def __init__( # Optionally, check that the matrix is invertible if validate_input: + from qiskit.synthesis.linear import check_invertible_binary_matrix + if not check_invertible_binary_matrix(linear): raise CircuitError( "A linear function must be represented by an invertible matrix." diff --git a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py index 49d7dc36958..b95ec6f63e3 100644 --- a/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py +++ b/qiskit/circuit/library/generalized_gates/mcg_up_to_diagonal.py @@ -68,8 +68,8 @@ def __init__( def _define(self): mcg_up_diag_circuit, _ = self._dec_mcg_up_diag() gate = mcg_up_diag_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) - mcg_up_diag_circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + mcg_up_diag_circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") mcg_up_diag_circuit.append(gate, q[:]) self.definition = mcg_up_diag_circuit @@ -108,8 +108,8 @@ def _dec_mcg_up_diag(self): q=[q_target,q_controls,q_ancilla_zero,q_ancilla_dirty] """ diag = np.ones(2 ** (self.num_controls + 1)).tolist() - q = QuantumRegister(self.num_qubits) - circuit = QuantumCircuit(q) + q = QuantumRegister(self.num_qubits, "q") + circuit = QuantumCircuit(q, name="mcg_up_to_diagonal") (q_target, q_controls, q_ancillas_zero, q_ancillas_dirty) = self._define_qubit_role(q) # ToDo: Keep this threshold updated such that the lowest gate count is achieved: # ToDo: we implement the MCG with a UCGate up to diagonal if the number of controls is diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index 776c69d94f0..b2d17d2bed2 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -80,15 +80,13 @@ def __init__( name = "permutation_" + np.array_str(pattern).replace(" ", ",") - circuit = QuantumCircuit(num_qubits, name=name) - super().__init__(num_qubits, name=name) # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic - for i, j in _get_ordered_swap(pattern): - circuit.swap(i, j) + circuit = synth_permutation_basic(pattern) + circuit.name = name all_qubits = self.qubits self.append(circuit.to_gate(), all_qubits) @@ -184,10 +182,11 @@ def inverse(self, annotated: bool = False): def _qasm2_decomposition(self): # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_" - out = QuantumCircuit(self.num_qubits, name=name) - for i, j in _get_ordered_swap(self.pattern): - out.swap(i, j) + + out = synth_permutation_basic(self.pattern) + out.name = name + return out.to_gate() diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index 2d650e98466..c81494da3ee 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -21,7 +21,6 @@ from __future__ import annotations -import cmath import math import numpy as np @@ -33,14 +32,11 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError from qiskit.exceptions import QiskitError - -# pylint: disable=cyclic-import -from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer +from qiskit._accelerate import uc_gate from .diagonal import Diagonal _EPS = 1e-10 # global variable used to chop very small numbers to zero -_DECOMPOSER1Q = OneQubitEulerDecomposer("U3") class UCGate(Gate): @@ -152,10 +148,10 @@ def _dec_ucg(self): the diagonal gate is also returned. """ diag = np.ones(2**self.num_qubits).tolist() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") q_controls = q[1:] q_target = q[0] - circuit = QuantumCircuit(q) + circuit = QuantumCircuit(q, name="uc") # If there is no control, we use the ZYZ decomposition if not q_controls: circuit.unitary(self.params[0], [q]) @@ -203,99 +199,7 @@ def _dec_ucg_help(self): https://arxiv.org/pdf/quant-ph/0410066.pdf. """ single_qubit_gates = [gate.astype(complex) for gate in self.params] - diag = np.ones(2**self.num_qubits, dtype=complex) - num_contr = self.num_qubits - 1 - for dec_step in range(num_contr): - num_ucgs = 2**dec_step - # The decomposition works recursively and the following loop goes over the different - # UCGates that arise in the decomposition - for ucg_index in range(num_ucgs): - len_ucg = 2 ** (num_contr - dec_step) - for i in range(int(len_ucg / 2)): - shift = ucg_index * len_ucg - a = single_qubit_gates[shift + i] - b = single_qubit_gates[shift + len_ucg // 2 + i] - # Apply the decomposition for UCGates given in equation (3) in - # https://arxiv.org/pdf/quant-ph/0410066.pdf - # to demultiplex one control of all the num_ucgs uniformly-controlled gates - # with log2(len_ucg) uniform controls - v, u, r = self._demultiplex_single_uc(a, b) - # replace the single-qubit gates with v,u (the already existing ones - # are not needed any more) - single_qubit_gates[shift + i] = v - single_qubit_gates[shift + len_ucg // 2 + i] = u - # Now we decompose the gates D as described in Figure 4 in - # https://arxiv.org/pdf/quant-ph/0410066.pdf and merge some of the gates - # into the UCGates and the diagonal at the end of the circuit - - # Remark: The Rz(pi/2) rotation acting on the target qubit and the Hadamard - # gates arising in the decomposition of D are ignored for the moment (they will - # be added together with the C-NOT gates at the end of the decomposition - # (in the method dec_ucg())) - if ucg_index < num_ucgs - 1: - # Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and - # merge the UC-Rz rotation with the following UCGate, - # which hasn't been decomposed yet. - k = shift + len_ucg + i - single_qubit_gates[k] = single_qubit_gates[k].dot( - UCGate._ct(r) - ) * UCGate._rz(np.pi / 2).item((0, 0)) - k = k + len_ucg // 2 - single_qubit_gates[k] = single_qubit_gates[k].dot(r) * UCGate._rz( - np.pi / 2 - ).item((1, 1)) - else: - # Absorb the Rz(pi/2) rotation on the control into the UC-Rz gate and merge - # the trailing UC-Rz rotation into a diagonal gate at the end of the circuit - for ucg_index_2 in range(num_ucgs): - shift_2 = ucg_index_2 * len_ucg - k = 2 * (i + shift_2) - diag[k] = ( - diag[k] - * UCGate._ct(r).item((0, 0)) - * UCGate._rz(np.pi / 2).item((0, 0)) - ) - diag[k + 1] = ( - diag[k + 1] - * UCGate._ct(r).item((1, 1)) - * UCGate._rz(np.pi / 2).item((0, 0)) - ) - k = len_ucg + k - diag[k] *= r.item((0, 0)) * UCGate._rz(np.pi / 2).item((1, 1)) - diag[k + 1] *= r.item((1, 1)) * UCGate._rz(np.pi / 2).item((1, 1)) - return single_qubit_gates, diag - - def _demultiplex_single_uc(self, a, b): - """ - This method implements the decomposition given in equation (3) in - https://arxiv.org/pdf/quant-ph/0410066.pdf. - The decomposition is used recursively to decompose uniformly controlled gates. - a,b = single qubit unitaries - v,u,r = outcome of the decomposition given in the reference mentioned above - (see there for the details). - """ - # The notation is chosen as in https://arxiv.org/pdf/quant-ph/0410066.pdf. - x = a.dot(UCGate._ct(b)) - det_x = np.linalg.det(x) - x11 = x.item((0, 0)) / cmath.sqrt(det_x) - phi = cmath.phase(det_x) - r1 = cmath.exp(1j / 2 * (np.pi / 2 - phi / 2 - cmath.phase(x11))) - r2 = cmath.exp(1j / 2 * (np.pi / 2 - phi / 2 + cmath.phase(x11) + np.pi)) - r = np.array([[r1, 0], [0, r2]], dtype=complex) - d, u = np.linalg.eig(r.dot(x).dot(r)) - # If d is not equal to diag(i,-i), then we put it into this "standard" form - # (see eq. (13) in https://arxiv.org/pdf/quant-ph/0410066.pdf) by interchanging - # the eigenvalues and eigenvectors. - if abs(d[0] + 1j) < _EPS: - d = np.flip(d, 0) - u = np.flip(u, 1) - d = np.diag(np.sqrt(d)) - v = d.dot(UCGate._ct(u)).dot(UCGate._ct(r)).dot(b) - return v, u, r - - @staticmethod - def _ct(m): - return np.transpose(np.conjugate(m)) + return uc_gate.dec_ucg_help(single_qubit_gates, self.num_qubits) @staticmethod def _rz(alpha): diff --git a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py index 5b5633ec423..6b637f7d2b2 100644 --- a/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py +++ b/qiskit/circuit/library/generalized_gates/uc_pauli_rot.py @@ -69,7 +69,7 @@ def __init__(self, angle_list: list[float], rot_axis: str) -> None: def _define(self): ucr_circuit = self._dec_ucrot() gate = ucr_circuit.to_instruction() - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") ucr_circuit = QuantumCircuit(q) ucr_circuit.append(gate, q[:]) self.definition = ucr_circuit @@ -79,7 +79,7 @@ def _dec_ucrot(self): Finds a decomposition of a UC rotation gate into elementary gates (C-NOTs and single-qubit rotations). """ - q = QuantumRegister(self.num_qubits) + q = QuantumRegister(self.num_qubits, "q") circuit = QuantumCircuit(q) q_target = q[0] q_controls = q[1:] diff --git a/qiskit/circuit/library/generalized_gates/unitary.py b/qiskit/circuit/library/generalized_gates/unitary.py index 1fd36e52e0c..6a6623ffce5 100644 --- a/qiskit/circuit/library/generalized_gates/unitary.py +++ b/qiskit/circuit/library/generalized_gates/unitary.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 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 @@ -30,14 +30,8 @@ from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.predicates import is_unitary_matrix -# pylint: disable=cyclic-import -from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer -from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose - from .isometry import Isometry -_DECOMPOSER1Q = OneQubitEulerDecomposer("U") - if typing.TYPE_CHECKING: from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -143,13 +137,21 @@ def transpose(self): def _define(self): """Calculate a subcircuit that implements this unitary.""" if self.num_qubits == 1: + from qiskit.synthesis.one_qubit.one_qubit_decompose import OneQubitEulerDecomposer + q = QuantumRegister(1, "q") qc = QuantumCircuit(q, name=self.name) - theta, phi, lam, global_phase = _DECOMPOSER1Q.angles_and_phase(self.to_matrix()) + theta, phi, lam, global_phase = OneQubitEulerDecomposer("U").angles_and_phase( + self.to_matrix() + ) qc._append(UGate(theta, phi, lam), [q[0]], []) qc.global_phase = global_phase self.definition = qc elif self.num_qubits == 2: + from qiskit.synthesis.two_qubit.two_qubit_decompose import ( # pylint: disable=cyclic-import + two_qubit_cnot_decompose, + ) + self.definition = two_qubit_cnot_decompose(self.to_matrix()) else: from qiskit.synthesis.unitary.qsd import ( # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/graph_state.py b/qiskit/circuit/library/graph_state.py index ceefff7971d..89d1edb035f 100644 --- a/qiskit/circuit/library/graph_state.py +++ b/qiskit/circuit/library/graph_state.py @@ -74,7 +74,7 @@ def __init__(self, adjacency_matrix: list | np.ndarray) -> None: raise CircuitError("The adjacency matrix must be symmetric.") num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name="graph: %s" % (adjacency_matrix)) + circuit = QuantumCircuit(num_qubits, name=f"graph: {adjacency_matrix}") circuit.h(range(num_qubits)) for i in range(num_qubits): diff --git a/qiskit/circuit/library/hamiltonian_gate.py b/qiskit/circuit/library/hamiltonian_gate.py index 2997d01ed48..d920d787387 100644 --- a/qiskit/circuit/library/hamiltonian_gate.py +++ b/qiskit/circuit/library/hamiltonian_gate.py @@ -103,8 +103,7 @@ def __array__(self, dtype=None, copy=None): time = float(self.params[1]) except TypeError as ex: raise TypeError( - "Unable to generate Unitary matrix for " - "unbound t parameter {}".format(self.params[1]) + f"Unable to generate Unitary matrix for unbound t parameter {self.params[1]}" ) from ex arr = scipy.linalg.expm(-1j * self.params[0] * time) dtype = complex if dtype is None else dtype diff --git a/qiskit/circuit/library/hidden_linear_function.py b/qiskit/circuit/library/hidden_linear_function.py index 1140f1866f0..b68fda7f8fc 100644 --- a/qiskit/circuit/library/hidden_linear_function.py +++ b/qiskit/circuit/library/hidden_linear_function.py @@ -82,7 +82,7 @@ def __init__(self, adjacency_matrix: Union[List[List[int]], np.ndarray]) -> None raise CircuitError("The adjacency matrix must be symmetric.") num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name="hlf: %s" % adjacency_matrix) + circuit = QuantumCircuit(num_qubits, name=f"hlf: {adjacency_matrix}") circuit.h(range(num_qubits)) for i in range(num_qubits): diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index fc72a2a6c53..e27fe407e18 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -110,11 +110,11 @@ def __init__( If only one gate is provided, the same gate is applied to each qubit. If a list of gates is provided, all gates are applied to each qubit in the provided order. - entanglement: Specifies the entanglement structure. Can be a string ('full', 'linear' - , 'reverse_linear', 'circular' or 'sca'), a list of integer-pairs specifying the indices - of qubits entangled with one another, or a callable returning such a list provided with - the index of the entanglement layer. - Default to 'reverse_linear' entanglement. + entanglement: Specifies the entanglement structure. Can be a string + ('full', 'linear', 'reverse_linear', 'pairwise', 'circular', or 'sca'), + a list of integer-pairs specifying the indices of qubits entangled with one another, + or a callable returning such a list provided with the index of the entanglement layer. + Defaults to 'reverse_linear' entanglement. Note that 'reverse_linear' entanglement provides the same unitary as 'full' with fewer entangling gates. See the Examples section of :class:`~qiskit.circuit.library.TwoLocal` for more diff --git a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py index a50b48ce488..4bc6bcc58a1 100644 --- a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -22,7 +22,6 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info import Operator, Pauli, SparsePauliOp -from qiskit.synthesis.evolution import LieTrotter from .n_local import NLocal @@ -185,6 +184,8 @@ def _evolve_operator(self, operator, time): gate = HamiltonianGate(operator, time) # otherwise, use the PauliEvolutionGate else: + from qiskit.synthesis.evolution import LieTrotter + evolution = LieTrotter() if self._evolution is None else self._evolution gate = PauliEvolutionGate(operator, time, synthesis=evolution) diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 430edfd94f3..2a750195dab 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -162,7 +162,7 @@ def __init__( self._bounds: list[tuple[float | None, float | None]] | None = None self._flatten = flatten - # During the build, if a subclass hasn't overridden our parametrisation methods, we can use + # During the build, if a subclass hasn't overridden our parametrization methods, we can use # a newer fast-path method to parametrise the rotation and entanglement blocks if internally # those are just simple stdlib gates that have been promoted to circuits. We don't # precalculate the fast-path layers themselves because there's far too much that can be @@ -441,9 +441,8 @@ def ordered_parameters(self, parameters: ParameterVector | list[Parameter]) -> N ): raise ValueError( "The length of ordered parameters must be equal to the number of " - "settable parameters in the circuit ({}), but is {}".format( - self.num_parameters_settable, len(parameters) - ) + f"settable parameters in the circuit ({self.num_parameters_settable})," + f" but is {len(parameters)}" ) self._ordered_parameters = parameters self._invalidate() @@ -1094,7 +1093,7 @@ def _stdlib_gate_from_simple_block(block: QuantumCircuit) -> _StdlibGateResult | return None instruction = block.data[0] # If the single instruction isn't a standard-library gate that spans the full width of the block - # in the correct order, we're not simple. If the gate isn't fully parametrised with pure, + # in the correct order, we're not simple. If the gate isn't fully parametrized with pure, # unique `Parameter` instances (expressions are too complex) that are in order, we're not # simple. if ( diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index b79f0888938..71b090d0884 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -118,7 +118,7 @@ def _build_rotation_layer(self, circuit, param_iter, i): qubits = range(self.num_qubits) # if no gates for this layer were generated, generate them - if i not in self._gates.keys(): + if i not in self._gates: self._gates[i] = list(self._rng.choice(["rx", "ry", "rz"], self.num_qubits)) # if not enough gates exist, add more elif len(self._gates[i]) < self.num_qubits: diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index d62e12c4d94..43869c0c54c 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -97,20 +97,18 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: valid = False if raise_on_failure: raise ValueError( - "The number of qubits of the initial state {} does not match " - "the number of qubits of the cost operator {}".format( - self.initial_state.num_qubits, self.num_qubits - ) + f"The number of qubits of the initial state {self.initial_state.num_qubits}" + " does not match " + f"the number of qubits of the cost operator {self.num_qubits}" ) if self.mixer_operator is not None and self.mixer_operator.num_qubits != self.num_qubits: valid = False if raise_on_failure: raise ValueError( - "The number of qubits of the mixer {} does not match " - "the number of qubits of the cost operator {}".format( - self.mixer_operator.num_qubits, self.num_qubits - ) + f"The number of qubits of the mixer {self.mixer_operator.num_qubits}" + f" does not match " + f"the number of qubits of the cost operator {self.num_qubits}" ) return valid diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index 61b9e725cc6..f3822d53243 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -87,7 +87,7 @@ class TwoLocal(NLocal): >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True) >>> qc = QuantumCircuit(3) - >>> qc += two + >>> qc &= two >>> print(qc.decompose().draw()) ┌──────────┐┌──────────┐ ░ ░ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├─░──■──■─────░─┤ Ry(θ[6]) ├─┤ Rz(θ[9]) ├ @@ -110,7 +110,7 @@ class TwoLocal(NLocal): >>> entangler_map = [[0, 3], [0, 2]] # entangle the first and last two-way >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1) - >>> circuit = two + two + >>> circuit = two.compose(two) >>> print(circuit.decompose().draw()) # note, that the parameters are the same! q_0: ─────■───────────■───────────■───────────■────── │ │ │ │ diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index ed86d8abb9a..f6ae5fd6ebd 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -26,11 +26,11 @@ class UnitaryOverlap(QuantumCircuit): names `"p1"` (for circuit ``unitary1``) and `"p2"` (for circuit ``unitary_2``) in the output circuit. - This circuit is usually employed in computing the fidelity:: + This circuit is usually employed in computing the fidelity: - .. math:: + .. math:: - \left|\langle 0| U_2^{\dag} U_1|0\rangle\right|^{2} + \left|\langle 0| U_2^{\dag} U_1|0\rangle\right|^{2} by computing the probability of being in the all-zeros bit-string, or equivalently, the expectation value of projector :math:`|0\rangle\langle 0|`. @@ -59,7 +59,12 @@ class UnitaryOverlap(QuantumCircuit): """ def __init__( - self, unitary1: QuantumCircuit, unitary2: QuantumCircuit, prefix1="p1", prefix2="p2" + self, + unitary1: QuantumCircuit, + unitary2: QuantumCircuit, + prefix1: str = "p1", + prefix2: str = "p2", + insert_barrier: bool = False, ): """ Args: @@ -69,6 +74,7 @@ def __init__( if it is parameterized. Defaults to ``"p1"``. prefix2: The name of the parameter vector associated to ``unitary2``, if it is parameterized. Defaults to ``"p2"``. + insert_barrier: Whether to insert a barrier between the two unitaries. Raises: CircuitError: Number of qubits in ``unitary1`` and ``unitary2`` does not match. @@ -95,6 +101,8 @@ def __init__( # Generate the actual overlap circuit super().__init__(unitaries[0].num_qubits, name="UnitaryOverlap") self.compose(unitaries[0], inplace=True) + if insert_barrier: + self.barrier() self.compose(unitaries[1].inverse(), inplace=True) @@ -104,8 +112,6 @@ def _check_unitary(circuit): for instruction in circuit.data: if not isinstance(instruction.operation, (Gate, Barrier)): raise CircuitError( - ( - "One or more instructions cannot be converted to" - ' a gate. "{}" is not a gate instruction' - ).format(instruction.operation.name) + "One or more instructions cannot be converted to" + f' a gate. "{instruction.operation.name}" is not a gate instruction' ) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index c6d69789bae..b0af3fbe416 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 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 @@ -14,14 +14,16 @@ from __future__ import annotations -from typing import Union, Optional +from typing import Union, Optional, TYPE_CHECKING import numpy as np from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.synthesis.evolution import EvolutionSynthesis, LieTrotter from qiskit.quantum_info import Pauli, SparsePauliOp +if TYPE_CHECKING: + from qiskit.synthesis.evolution import EvolutionSynthesis + class PauliEvolutionGate(Gate): r"""Time-evolution of an operator consisting of Paulis. @@ -107,6 +109,8 @@ class docstring for an example. operator = _to_sparse_pauli_op(operator) if synthesis is None: + from qiskit.synthesis.evolution import LieTrotter + synthesis = LieTrotter() if label is None: diff --git a/qiskit/circuit/library/standard_gates/dcx.py b/qiskit/circuit/library/standard_gates/dcx.py index 6455bea2779..d83f2e2f9c7 100644 --- a/qiskit/circuit/library/standard_gates/dcx.py +++ b/qiskit/circuit/library/standard_gates/dcx.py @@ -15,6 +15,7 @@ from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0]]) @@ -48,6 +49,8 @@ class DCXGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.DCXGate + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new DCX gate.""" super().__init__("dcx", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 73bb1bb0389..f00c02df538 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -17,6 +17,7 @@ from qiskit.circuit._utils import with_gate_array from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key +from qiskit._accelerate.circuit import StandardGate from .rzx import RZXGate from .x import XGate @@ -84,6 +85,8 @@ class ECRGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.ECRGate + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new ECR gate.""" super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 9793dc0202a..c4619ca2785 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -850,6 +850,56 @@ def _cnot_rxx_decompose(plus_ry: bool = True, plus_rxx: bool = True): def_swap.append(inst, qargs, cargs) _sel.add_equivalence(SwapGate(), def_swap) +# SwapGate +# +# q_0: ─X─ +# │ ≡ +# q_1: ─X─ +# +# ┌──────────┐┌──────┐ ┌────┐ ┌──────┐┌──────────┐┌──────┐ +# q_0: ┤ Rz(-π/2) ├┤0 ├───┤ √X ├───┤1 ├┤ Rz(-π/2) ├┤0 ├ +# └──┬────┬──┘│ Ecr │┌──┴────┴──┐│ Ecr │└──┬────┬──┘│ Ecr │ +# q_1: ───┤ √X ├───┤1 ├┤ Rz(-π/2) ├┤0 ├───┤ √X ├───┤1 ├ +# └────┘ └──────┘└──────────┘└──────┘ └────┘ └──────┘ +# +q = QuantumRegister(2, "q") +def_swap_ecr = QuantumCircuit(q) +def_swap_ecr.rz(-pi / 2, 0) +def_swap_ecr.sx(1) +def_swap_ecr.ecr(0, 1) +def_swap_ecr.rz(-pi / 2, 1) +def_swap_ecr.sx(0) +def_swap_ecr.ecr(1, 0) +def_swap_ecr.rz(-pi / 2, 0) +def_swap_ecr.sx(1) +def_swap_ecr.ecr(0, 1) +_sel.add_equivalence(SwapGate(), def_swap_ecr) + +# SwapGate +# +# q_0: ─X─ +# │ ≡ +# q_1: ─X─ +# +# global phase: 3π/2 +# ┌────┐ ┌────┐ ┌────┐ +# q_0: ┤ √X ├─■─┤ √X ├─■─┤ √X ├─■─ +# ├────┤ │ ├────┤ │ ├────┤ │ +# q_1: ┤ √X ├─■─┤ √X ├─■─┤ √X ├─■─ +# └────┘ └────┘ └────┘ +q = QuantumRegister(2, "q") +def_swap_cz = QuantumCircuit(q, global_phase=-pi / 2) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +def_swap_cz.sx(0) +def_swap_cz.sx(1) +def_swap_cz.cz(0, 1) +_sel.add_equivalence(SwapGate(), def_swap_cz) + # iSwapGate # # ┌────────┐ ┌───┐┌───┐ ┌───┐ diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index ccd758e4724..59d6b56373d 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -20,6 +20,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class GlobalPhaseGate(Gate): @@ -36,6 +37,8 @@ class GlobalPhaseGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.GlobalPhaseGate + def __init__( self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index cc06a071a3f..c07895ebbea 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _H_ARRAY = 1 / sqrt(2) * numpy.array([[1, 1], [1, -1]], dtype=numpy.complex128) @@ -51,6 +52,8 @@ class HGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.HGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new H gate.""" super().__init__("h", 1, [], label=label, duration=duration, unit=unit) @@ -182,6 +185,8 @@ class CHGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CHGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 93523215d6f..13a98ce0df8 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -15,6 +15,7 @@ from typing import Optional from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0], [0, 1]]) @@ -45,6 +46,8 @@ class IGate(SingletonGate): └───┘ """ + _standard_gate = StandardGate.IGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Identity gate.""" super().__init__("id", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/iswap.py b/qiskit/circuit/library/standard_gates/iswap.py index 50d3a6bb347..8074990a384 100644 --- a/qiskit/circuit/library/standard_gates/iswap.py +++ b/qiskit/circuit/library/standard_gates/iswap.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate from .xx_plus_yy import XXPlusYYGate @@ -85,6 +86,8 @@ class iSwapGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.ISwapGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new iSwap gate.""" super().__init__("iswap", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 6de0307dc79..8c83aa46402 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class PhaseGate(Gate): @@ -75,6 +76,8 @@ class PhaseGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.PhaseGate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): @@ -197,6 +200,8 @@ class CPhaseGate(ControlledGate): phase difference. """ + _standard_gate = StandardGate.CPhaseGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 9d4905e2786..22c30e24bf6 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RGate(Gate): @@ -49,6 +50,8 @@ class RGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index eaa73cf87c9..cb851a740d2 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXGate(Gate): @@ -50,6 +51,8 @@ class RXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): @@ -196,6 +199,8 @@ class CRXGate(ControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CRXGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index c4e35e53d55..1c06ae05a85 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXXGate(Gate): @@ -72,6 +73,8 @@ class RXXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 633a518bca7..b60b34ffde6 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYGate(Gate): @@ -49,6 +50,8 @@ class RYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): @@ -195,6 +198,8 @@ class CRYGate(ControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CRYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 98847b7b218..91d7d8096cf 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYYGate(Gate): @@ -72,6 +73,8 @@ class RYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 3040f956834..78cf20efa5c 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -17,6 +17,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZGate(Gate): @@ -59,6 +60,8 @@ class RZGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.RZGate + def __init__( self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): @@ -213,6 +216,8 @@ class CRZGate(ControlledGate): phase difference. """ + _standard_gate = StandardGate.CRZGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 1f930ab422d..90e7b71c0a3 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZXGate(Gate): @@ -117,6 +118,8 @@ class RZXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 5ca974764d3..119dd370e20 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZZGate(Gate): @@ -84,6 +85,8 @@ class RZZGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZZGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index 6fde1c6544e..975d1cb3be8 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -20,6 +20,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _S_ARRAY = numpy.array([[1, 0], [0, 1j]]) @@ -57,6 +58,8 @@ class SGate(SingletonGate): Equivalent to a :math:`\pi/2` radian rotation about the Z axis. """ + _standard_gate = StandardGate.SGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new S gate.""" super().__init__("s", 1, [], label=label, duration=duration, unit=unit) @@ -134,6 +137,8 @@ class SdgGate(SingletonGate): Equivalent to a :math:`-\pi/2` radian rotation about the Z axis. """ + _standard_gate = StandardGate.SdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Sdg gate.""" super().__init__("sdg", 1, [], label=label, duration=duration, unit=unit) @@ -210,6 +215,8 @@ class CSGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSGate + def __init__( self, label: Optional[str] = None, @@ -296,6 +303,8 @@ class CSdgGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSdgGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 0e49783308c..5d33bc74b8d 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SWAP_ARRAY = numpy.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) @@ -58,6 +59,8 @@ class SwapGate(SingletonGate): |a, b\rangle \rightarrow |b, a\rangle """ + _standard_gate = StandardGate.SwapGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SWAP gate.""" super().__init__("swap", 2, [], label=label, duration=duration, unit=unit) @@ -213,6 +216,8 @@ class CSwapGate(SingletonControlledGate): |1, b, c\rangle \rightarrow |1, c, b\rangle """ + _standard_gate = StandardGate.CSwapGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 0c003748a66..ec3c8765314 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SX_ARRAY = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]] @@ -62,6 +63,8 @@ class SXGate(SingletonGate): """ + _standard_gate = StandardGate.SXGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SX gate.""" super().__init__("sx", 1, [], label=label, duration=duration, unit=unit) @@ -164,6 +167,8 @@ class SXdgGate(SingletonGate): = e^{-i \pi/4} \sqrt{X}^{\dagger} """ + _standard_gate = StandardGate.SXdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SXdg gate.""" super().__init__("sxdg", 1, [], label=label, duration=duration, unit=unit) @@ -261,6 +266,8 @@ class CSXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CSXGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 87a38d9d44c..e4301168ac5 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -21,6 +21,7 @@ from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) @@ -55,6 +56,8 @@ class TGate(SingletonGate): Equivalent to a :math:`\pi/4` radian rotation about the Z axis. """ + _standard_gate = StandardGate.TGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new T gate.""" super().__init__("t", 1, [], label=label, duration=duration, unit=unit) @@ -130,6 +133,8 @@ class TdgGate(SingletonGate): Equivalent to a :math:`-\pi/4` radian rotation about the Z axis. """ + _standard_gate = StandardGate.TdgGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Tdg gate.""" super().__init__("tdg", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3d631898850..07684097f8c 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class UGate(Gate): @@ -68,6 +69,8 @@ class UGate(Gate): U(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.UGate + def __init__( self, theta: ParameterValueType, @@ -180,7 +183,7 @@ def __setitem__(self, key, value): # Magic numbers: CUGate has 4 parameters, UGate has 3, with the last of CUGate's missing. if isinstance(key, slice): # We don't need to worry about the case of the slice being used to insert extra / remove - # elements because that would be "undefined behaviour" in a gate already, so we're + # elements because that would be "undefined behavior" in a gate already, so we're # within our rights to do anything at all. for i, base_key in enumerate(range(*key.indices(4))): if base_key < 0: diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index 1d59cabae1f..f141146b72d 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -19,6 +19,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int +from qiskit._accelerate.circuit import StandardGate class U1Gate(Gate): @@ -92,6 +93,8 @@ class U1Gate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.U1Gate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/u2.py b/qiskit/circuit/library/standard_gates/u2.py index c8e4de96efe..9e59cd4c5bb 100644 --- a/qiskit/circuit/library/standard_gates/u2.py +++ b/qiskit/circuit/library/standard_gates/u2.py @@ -18,6 +18,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U2Gate(Gate): @@ -86,6 +87,8 @@ class U2Gate(Gate): using two X90 pulses. """ + _standard_gate = StandardGate.U2Gate + def __init__( self, phi: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 62c1e33b962..f191609ea8f 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U3Gate(Gate): @@ -80,6 +81,8 @@ class U3Gate(Gate): U3(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.U3Gate + def __init__( self, theta: ParameterValueType, @@ -344,7 +347,7 @@ def _generate_gray_code(num_bits): result = [0] for i in range(num_bits): result += [x + 2**i for x in reversed(result)] - return [format(x, "0%sb" % num_bits) for x in result] + return [format(x, f"0{num_bits}b") for x in result] def _gray_code_chain(q, num_ctrl_qubits, gate): diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index c0eb505efba..cd4a6196382 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key 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 _X_ARRAY = [[0, 1], [1, 0]] @@ -70,6 +71,8 @@ class XGate(SingletonGate): |1\rangle \rightarrow |0\rangle """ + _standard_gate = StandardGate.XGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new X gate.""" super().__init__("x", 1, [], label=label, duration=duration, unit=unit) @@ -107,7 +110,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -212,6 +215,8 @@ class CXGate(SingletonControlledGate): `|a, b\rangle \rightarrow |a, a \oplus b\rangle` """ + _standard_gate = StandardGate.CXGate + def __init__( self, label: Optional[str] = None, @@ -250,7 +255,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -362,6 +367,8 @@ class CCXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CCXGate + def __init__( self, label: Optional[str] = None, @@ -444,7 +451,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -585,7 +592,7 @@ def __init__( Args: label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. """ from .sx import SXGate @@ -785,7 +792,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -965,7 +972,7 @@ def __init__( _singleton_lookup_key = stdlib_singleton_key(num_ctrl_qubits=4) - # seems like open controls not hapening? + # seems like open controls not happening? def _define(self): """ gate c3sqrtx a,b,c,d @@ -1029,7 +1036,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. @@ -1204,7 +1211,7 @@ def control( num_ctrl_qubits: number of control qubits. label: An optional label for the gate [Default: ``None``] ctrl_state: control state expressed as integer, - string (e.g.``'110'``), or ``None``. If ``None``, use all 1s. + string (e.g. ``'110'``), or ``None``. If ``None``, use all 1s. annotated: indicates whether the controlled gate can be implemented as an annotated gate. diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index 4bf4ab80eca..db3c3dc8915 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -27,6 +27,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class XXMinusYYGate(Gate): @@ -91,6 +92,8 @@ class XXMinusYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.XXMinusYYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index a7b62175f20..7920454d0b9 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class XXPlusYYGate(Gate): @@ -71,20 +72,24 @@ class XXPlusYYGate(Gate): q_1: ┤0 ├ └───────────────┘ - .. math:: + .. math:: - \newcommand{\rotationangle}{\frac{\theta}{2}} + \newcommand{\rotationangle}{\frac{\theta}{2}} - R_{XX+YY}(\theta, \beta)\ q_0, q_1 = - RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = - \begin{pmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos\left(\rotationangle\right) & -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ - 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & \cos\left(\rotationangle\right) & 0 \\ - 0 & 0 & 0 & 1 - \end{pmatrix} + R_{XX+YY}(\theta, \beta)\ q_0, q_1 = + RZ_1(-\beta) \cdot \exp\left(-i \frac{\theta}{2} \frac{XX+YY}{2}\right) \cdot RZ_1(\beta) = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos\left(\rotationangle\right) & + -i\sin\left(\rotationangle\right)e^{i\beta} & 0 \\ + 0 & -i\sin\left(\rotationangle\right)e^{-i\beta} & + \cos\left(\rotationangle\right) & 0 \\ + 0 & 0 & 0 & 1 + \end{pmatrix} """ + _standard_gate = StandardGate.XXPlusYYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index e69e1e2b794..d62586aa2b9 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _Y_ARRAY = [[0, -1j], [1j, 0]] @@ -70,6 +71,8 @@ class YGate(SingletonGate): |1\rangle \rightarrow -i|0\rangle """ + _standard_gate = StandardGate.YGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Y gate.""" super().__init__("y", 1, [], label=label, duration=duration, unit=unit) @@ -197,6 +200,8 @@ class CYGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CYGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 2b69595936d..19e4382cd84 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -20,6 +20,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate from .p import PhaseGate @@ -73,6 +74,8 @@ class ZGate(SingletonGate): |1\rangle \rightarrow -|1\rangle """ + _standard_gate = StandardGate.ZGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Z gate.""" super().__init__("z", 1, [], label=label, duration=duration, unit=unit) @@ -181,6 +184,8 @@ class CZGate(SingletonControlledGate): the target qubit if the control qubit is in the :math:`|1\rangle` state. """ + _standard_gate = StandardGate.CZGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/operation.py b/qiskit/circuit/operation.py index c299e130178..8856222b266 100644 --- a/qiskit/circuit/operation.py +++ b/qiskit/circuit/operation.py @@ -25,7 +25,7 @@ class Operation(ABC): :class:`~qiskit.circuit.Reset`, :class:`~qiskit.circuit.Barrier`, :class:`~qiskit.circuit.Measure`, and operators such as :class:`~qiskit.quantum_info.Clifford`. - The main purpose is to add allow abstract mathematical objects to be added directly onto + The main purpose is to allow abstract mathematical objects to be added directly onto abstract circuits, and for the exact syntheses of these to be determined later, during compilation. diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 4d0f73cf077..c7a8228dd46 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -59,10 +59,10 @@ class Parameter(ParameterExpression): bc.draw('mpl') """ - __slots__ = ("_name", "_uuid", "_hash") + __slots__ = ("_uuid", "_hash") # This `__init__` does not call the super init, because we can't construct the - # `_parameter_symbols` dictionary we need to pass to it before we're entirely initialised + # `_parameter_symbols` dictionary we need to pass to it before we're entirely initialized # anyway, because `ParameterExpression` depends heavily on the structure of `Parameter`. def __init__( @@ -79,7 +79,6 @@ def __init__( field when creating two parameters to the same thing (along with the same name) allows them to be equal. This is useful during serialization and deserialization. """ - self._name = name self._uuid = uuid4() if uuid is None else uuid symbol = symengine.Symbol(name) @@ -110,14 +109,14 @@ def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False): if allow_unknown_parameters: return self raise CircuitError( - "Cannot bind Parameters ({}) not present in " - "expression.".format([str(p) for p in parameter_map]) + f"Cannot bind Parameters ({[str(p) for p in parameter_map]}) not present in " + "expression." ) @property def name(self): """Returns the name of the :class:`Parameter`.""" - return self._name + return self._symbol_expr.name @property def uuid(self) -> UUID: @@ -143,7 +142,7 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, Parameter): - return (self._uuid, self._name) == (other._uuid, other._name) + return (self._uuid, self._symbol_expr) == (other._uuid, other._symbol_expr) elif isinstance(other, ParameterExpression): return super().__eq__(other) else: @@ -155,7 +154,7 @@ def _hash_key(self): # expression, so its full hash key is split into `(parameter_keys, symbolic_expression)`. # This method lets containing expressions get only the bits they need for equality checks in # the first value, without wasting time re-hashing individual Sympy/Symengine symbols. - return (self._name, self._uuid) + return (self._symbol_expr, self._uuid) def __hash__(self): # This is precached for performance, since it's used a lot and we are immutable. @@ -165,10 +164,10 @@ def __hash__(self): # operation attempts to put this parameter into a hashmap. def __getstate__(self): - return (self._name, self._uuid, self._symbol_expr) + return (self.name, self._uuid, self._symbol_expr) def __setstate__(self, state): - self._name, self._uuid, self._symbol_expr = state + _, self._uuid, self._symbol_expr = state self._parameter_keys = frozenset((self._hash_key(),)) self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: self._symbol_expr} diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 4f6453f90f4..16b691480d2 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -48,7 +48,7 @@ def __init__(self, symbol_map: dict, expr): expr (sympy.Expr): Expression of :class:`sympy.Symbol` s. """ # NOTE: `Parameter.__init__` does not call up to this method, since this method is dependent - # on `Parameter` instances already being initialised enough to be hashable. If changing + # on `Parameter` instances already being initialized enough to be hashable. If changing # this method, check that `Parameter.__init__` and `__setstate__` are still valid. self._parameter_symbols = symbol_map self._parameter_keys = frozenset(p._hash_key() for p in self._parameter_symbols) @@ -140,7 +140,7 @@ def bind( raise ZeroDivisionError( "Binding provided for expression " "results in division by zero " - "(Expression: {}, Bindings: {}).".format(self, parameter_values) + f"(Expression: {self}, Bindings: {parameter_values})." ) return ParameterExpression(free_parameter_symbols, bound_symbol_expr) @@ -199,8 +199,8 @@ def _raise_if_passed_unknown_parameters(self, parameters): unknown_parameters = parameters - self.parameters if unknown_parameters: raise CircuitError( - "Cannot bind Parameters ({}) not present in " - "expression.".format([str(p) for p in unknown_parameters]) + f"Cannot bind Parameters ({[str(p) for p in unknown_parameters]}) not present in " + "expression." ) def _raise_if_passed_nan(self, parameter_values): @@ -327,8 +327,11 @@ def __rsub__(self, other): def __mul__(self, other): return self._apply_operation(operator.mul, other) + def __pos__(self): + return self._apply_operation(operator.mul, 1) + def __neg__(self): - return self._apply_operation(operator.mul, -1.0) + return self._apply_operation(operator.mul, -1) def __rmul__(self, other): return self._apply_operation(operator.mul, other, reflected=True) @@ -401,8 +404,8 @@ def __complex__(self): except (TypeError, RuntimeError) as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a complex.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to a complex." ) from None raise TypeError("could not cast expression to complex") from exc @@ -413,13 +416,13 @@ def __float__(self): except (TypeError, RuntimeError) as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a float.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to a float." ) from None # In symengine, if an expression was complex at any time, its type is likely to have # stayed "complex" even when the imaginary part symbolically (i.e. exactly) - # cancelled out. Sympy tends to more aggressively recognise these as symbolically - # real. This second attempt at a cast is a way of unifying the behaviour to the + # cancelled out. Sympy tends to more aggressively recognize these as symbolically + # real. This second attempt at a cast is a way of unifying the behavior to the # more expected form for our users. cval = complex(self) if cval.imag == 0.0: @@ -433,12 +436,15 @@ def __int__(self): except RuntimeError as exc: if self.parameters: raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to an int.".format(self.parameters) + f"ParameterExpression with unbound parameters ({self.parameters}) " + "cannot be cast to an int." ) from None raise TypeError("could not cast expression to int") from exc def __hash__(self): + if not self._parameter_symbols: + # For fully bound expressions, fall back to the underlying value + return hash(self.numeric()) return hash((self._parameter_keys, self._symbol_expr)) def __copy__(self): diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index 6803126ec10..e5a41b1971c 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -12,197 +12,8 @@ """ Look-up table for variable parameters in QuantumCircuit. """ -import operator -import typing -from collections.abc import MappingView, MutableMapping, MutableSet - -class ParameterReferences(MutableSet): - """A set of instruction parameter slot references. - Items are expected in the form ``(instruction, param_index)``. Membership - testing is overridden such that items that are otherwise value-wise equal - are still considered distinct if their ``instruction``\\ s are referentially - distinct. - - In the case of the special value :attr:`.ParameterTable.GLOBAL_PHASE` for ``instruction``, the - ``param_index`` should be ``None``. - """ - - def _instance_key(self, ref): - return (id(ref[0]), ref[1]) - - def __init__(self, refs): - self._instance_ids = {} - - for ref in refs: - if not isinstance(ref, tuple) or len(ref) != 2: - raise ValueError("refs must be in form (instruction, param_index)") - k = self._instance_key(ref) - self._instance_ids[k] = ref[0] - - def __getstate__(self): - # Leave behind the reference IDs (keys of _instance_ids) since they'll - # be incorrect after unpickling on the other side. - return list(self) - - def __setstate__(self, refs): - # Recompute reference IDs for the newly unpickled instructions. - self._instance_ids = {self._instance_key(ref): ref[0] for ref in refs} - - def __len__(self): - return len(self._instance_ids) - - def __iter__(self): - for (_, idx), instruction in self._instance_ids.items(): - yield (instruction, idx) - - def __contains__(self, x) -> bool: - return self._instance_key(x) in self._instance_ids - - def __repr__(self) -> str: - return f"ParameterReferences({repr(list(self))})" - - def add(self, value): - """Adds a reference to the listing if it's not already present.""" - k = self._instance_key(value) - self._instance_ids[k] = value[0] - - def discard(self, value): - k = self._instance_key(value) - self._instance_ids.pop(k, None) - - def copy(self): - """Create a shallow copy.""" - return ParameterReferences(self) - - -class ParameterTable(MutableMapping): - """Class for tracking references to circuit parameters by specific - instruction instances. - - Keys are parameters. Values are of type :class:`~ParameterReferences`, - which overrides membership testing to be referential for instructions, - and is set-like. Elements of :class:`~ParameterReferences` - are tuples of ``(instruction, param_index)``. - """ - - __slots__ = ["_table", "_keys", "_names"] - - class _GlobalPhaseSentinel: - __slots__ = () - - def __copy__(self): - return self - - def __deepcopy__(self, memo=None): - return self - - def __reduce__(self): - return (operator.attrgetter("GLOBAL_PHASE"), (ParameterTable,)) - - def __repr__(self): - return "" - - GLOBAL_PHASE = _GlobalPhaseSentinel() - """Tracking object to indicate that a reference refers to the global phase of a circuit.""" - - def __init__(self, mapping=None): - """Create a new instance, initialized with ``mapping`` if provided. - - Args: - mapping (Mapping[Parameter, ParameterReferences]): - Mapping of parameter to the set of parameter slots that reference - it. - - Raises: - ValueError: A value in ``mapping`` is not a :class:`~ParameterReferences`. - """ - if mapping is not None: - if any(not isinstance(refs, ParameterReferences) for refs in mapping.values()): - raise ValueError("Values must be of type ParameterReferences") - self._table = mapping.copy() - else: - self._table = {} - - self._keys = set(self._table) - self._names = {x.name: x for x in self._table} - - def __getitem__(self, key): - return self._table[key] - - def __setitem__(self, parameter, refs): - """Associate a parameter with the set of parameter slots ``(instruction, param_index)`` - that reference it. - - .. note:: - - Items in ``refs`` are considered unique if their ``instruction`` is referentially - unique. See :class:`~ParameterReferences` for details. - - Args: - parameter (Parameter): the parameter - refs (Union[ParameterReferences, Iterable[(Instruction, int)]]): the parameter slots. - If this is an iterable, a new :class:`~ParameterReferences` is created from its - contents. - """ - if not isinstance(refs, ParameterReferences): - refs = ParameterReferences(refs) - - self._table[parameter] = refs - self._keys.add(parameter) - self._names[parameter.name] = parameter - - def get_keys(self): - """Return a set of all keys in the parameter table - - Returns: - set: A set of all the keys in the parameter table - """ - return self._keys - - def get_names(self): - """Return a set of all parameter names in the parameter table - - Returns: - set: A set of all the names in the parameter table - """ - return self._names.keys() - - def parameter_from_name(self, name: str, default: typing.Any = None): - """Get a :class:`.Parameter` with references in this table by its string name. - - If the parameter is not present, return the ``default`` value. - - Args: - name: The name of the :class:`.Parameter` - default: The object that should be returned if the parameter is missing. - """ - return self._names.get(name, default) - - def discard_references(self, expression, key): - """Remove all references to parameters contained within ``expression`` at the given table - ``key``. This also discards parameter entries from the table if they have no further - references. No action is taken if the object is not tracked.""" - for parameter in expression.parameters: - if (refs := self._table.get(parameter)) is not None: - if len(refs) == 1: - del self[parameter] - else: - refs.discard(key) - - def __delitem__(self, key): - del self._table[key] - self._keys.discard(key) - del self._names[key.name] - - def __iter__(self): - return iter(self._table) - - def __len__(self): - return len(self._table) - - def __repr__(self): - return f"ParameterTable({repr(self._table)})" +from collections.abc import MappingView class ParameterView(MappingView): diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py index abc8a6f60ef..7b32395e143 100644 --- a/qiskit/circuit/parametervector.py +++ b/qiskit/circuit/parametervector.py @@ -50,11 +50,10 @@ def __setstate__(self, state): class ParameterVector: """ParameterVector class to quickly generate lists of parameters.""" - __slots__ = ("_name", "_params", "_size", "_root_uuid") + __slots__ = ("_name", "_params", "_root_uuid") def __init__(self, name, length=0): self._name = name - self._size = length self._root_uuid = uuid4() root_uuid_int = self._root_uuid.int self._params = [ @@ -76,32 +75,38 @@ def index(self, value): return self._params.index(value) def __getitem__(self, key): - if isinstance(key, slice): - start, stop, step = key.indices(self._size) - return self.params[start:stop:step] - - if key > self._size: - raise IndexError(f"Index out of range: {key} > {self._size}") return self.params[key] def __iter__(self): - return iter(self.params[: self._size]) + return iter(self.params) def __len__(self): - return self._size + return len(self._params) def __str__(self): - return f"{self.name}, {[str(item) for item in self.params[: self._size]]}" + return f"{self.name}, {[str(item) for item in self.params]}" def __repr__(self): - return f"{self.__class__.__name__}(name={self.name}, length={len(self)})" + return f"{self.__class__.__name__}(name={repr(self.name)}, length={len(self)})" def resize(self, length): - """Resize the parameter vector. - - If necessary, new elements are generated. If length is smaller than before, the - previous elements are cached and not re-generated if the vector is enlarged again. + """Resize the parameter vector. If necessary, new elements are generated. + + Note that the UUID of each :class:`.Parameter` element will be generated + deterministically given the root UUID of the ``ParameterVector`` and the index + of the element. In particular, if a ``ParameterVector`` is resized to + be smaller and then later resized to be larger, the UUID of the later + generated element at a given index will be the same as the UUID of the + previous element at that index. This is to ensure that the parameter instances do not change. + + >>> from qiskit.circuit import ParameterVector + >>> pv = ParameterVector("theta", 20) + >>> elt_19 = pv[19] + >>> rv.resize(10) + >>> rv.resize(20) + >>> pv[19] == elt_19 + True """ if length > len(self._params): root_uuid_int = self._root_uuid.int @@ -111,4 +116,5 @@ def resize(self, length): for i in range(len(self._params), length) ] ) - self._size = length + else: + del self._params[length:] diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index f25dca4b03b..0019a15443e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ ) import numpy as np from qiskit._accelerate.circuit import CircuitData +from qiskit._accelerate.circuit import StandardGate, PyGate, PyInstruction, PyOperation from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -45,7 +46,7 @@ from qiskit.circuit.exceptions import CircuitError from . import _classical_resource_map from ._utils import sort_parameters -from .controlflow import ControlFlowOp +from .controlflow import ControlFlowOp, _builder_utils from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder @@ -57,7 +58,7 @@ from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit -from .parametertable import ParameterReferences, ParameterTable, ParameterView +from .parametertable import ParameterView from .parametervector import ParameterVector from .instructionset import InstructionSet from .operation import Operation @@ -106,113 +107,878 @@ BitType = TypeVar("BitType", Qubit, Clbit) +# NOTE: +# +# If you're adding methods or attributes to `QuantumCircuit`, be sure to update the class docstring +# to document them in a suitable place. The class is huge, so we do its documentation manually so +# it has at least some amount of organizational structure. + + class QuantumCircuit: - """Create a new circuit. + """Core Qiskit representation of a quantum circuit. + + .. note:: + For more details setting the :class:`QuantumCircuit` in context of all of the data + structures that go with it, how it fits into the rest of the :mod:`qiskit` package, and the + different regimes of quantum-circuit descriptions in Qiskit, see the module-level + documentation of :mod:`qiskit.circuit`. + + Circuit attributes + ================== + + :class:`QuantumCircuit` has a small number of public attributes, which are mostly older + functionality. Most of its functionality is accessed through methods. + + A small handful of the attributes are intentionally mutable, the rest are data attributes that + should be considered immutable. + + ========================= ====================================================================== + Mutable attribute Summary + ========================= ====================================================================== + :attr:`global_phase` The global phase of the circuit, measured in radians. + :attr:`metadata` Arbitrary user mapping, which Qiskit will preserve through the + transpiler, but otherwise completely ignore. + :attr:`name` An optional string name for the circuit. + ========================= ====================================================================== + + ========================= ====================================================================== + Immutable data attribute Summary + ========================= ====================================================================== + :attr:`ancillas` List of :class:`AncillaQubit`\\ s tracked by the circuit. + :attr:`calibrations` Custom user-supplied pulse calibrations for individual instructions. + :attr:`cregs` List of :class:`ClassicalRegister`\\ s tracked by the circuit. + + :attr:`clbits` List of :class:`Clbit`\\ s tracked by the circuit. + :attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the + circuit. + :attr:`duration` Total duration of the circuit, added by scheduling transpiler passes. + + :attr:`layout` Hardware layout and routing information added by the transpiler. + :attr:`num_ancillas` The number of ancilla qubits in the circuit. + :attr:`num_clbits` The number of clbits in the circuit. + :attr:`num_captured_vars` Number of captured real-time classical variables. + + :attr:`num_declared_vars` Number of locally declared real-time classical variables in the outer + circuit scope. + :attr:`num_input_vars` Number of input real-time classical variables. + :attr:`num_parameters` Number of compile-time :class:`Parameter`\\ s in the circuit. + :attr:`num_qubits` Number of qubits in the circuit. + + :attr:`num_vars` Total number of real-time classical variables in the outer circuit + scope. + :attr:`op_start_times` Start times of scheduled operations, added by scheduling transpiler + passes. + :attr:`parameters` Ordered set-like view of the compile-time :class:`Parameter`\\ s + tracked by the circuit. + :attr:`qregs` List of :class:`QuantumRegister`\\ s tracked by the circuit. + + :attr:`qubits` List of :class:`Qubit`\\ s tracked by the circuit. + :attr:`unit` The unit of the :attr:`duration` field. + ========================= ====================================================================== + + The core attribute is :attr:`data`. This is a sequence-like object that exposes the + :class:`CircuitInstruction`\\ s contained in an ordered form. You generally should not mutate + this object directly; :class:`QuantumCircuit` is only designed for append-only operations (which + should use :meth:`append`). Most operations that mutate circuits in place should be written as + transpiler passes (:mod:`qiskit.transpiler`). + + .. autoattribute:: data + + Alongside the :attr:`data`, the :attr:`global_phase` of a circuit can have some impact on its + output, if the circuit is used to describe a :class:`.Gate` that may be controlled. This is + measured in radians and is directly settable. + + .. autoattribute:: global_phase + + The :attr:`name` of a circuit becomes the name of the :class:`~.circuit.Instruction` or + :class:`.Gate` resulting from :meth:`to_instruction` and :meth:`to_gate` calls, which can be + handy for visualizations. + + .. autoattribute:: name + + You can attach arbitrary :attr:`metadata` to a circuit. No part of core Qiskit will inspect + this or change its behavior based on metadata, but it will be faithfully passed through the + transpiler, so you can tag your circuits yourself. When serializing a circuit with QPY (see + :mod:`qiskit.qpy`), the metadata will be JSON-serialized and you may need to pass a custom + serializer to handle non-JSON-compatible objects within it (see :func:`.qpy.dump` for more + detail). This field is ignored during export to OpenQASM 2 or 3. + + .. autoattribute:: metadata + + :class:`QuantumCircuit` exposes data attributes tracking its internal quantum and classical bits + and registers. These appear as Python :class:`list`\\ s, but you should treat them as + immutable; changing them will *at best* have no effect, and more likely will simply corrupt + the internal data of the :class:`QuantumCircuit`. + + .. autoattribute:: qregs + .. autoattribute:: cregs + .. autoattribute:: qubits + .. autoattribute:: ancillas + .. autoattribute:: clbits + + The :ref:`compile-time parameters ` present in instructions on + the circuit are available in :attr:`parameters`. This has a canonical order (mostly lexical, + except in the case of :class:`.ParameterVector`), which matches the order that parameters will + be assigned when using the list forms of :meth:`assign_parameters`, but also supports + :class:`set`-like constant-time membership testing. + + .. autoattribute:: parameters + + The storage of any :ref:`manual pulse-level calibrations ` for individual + instructions on the circuit is in :attr:`calibrations`. This presents as a :class:`dict`, but + should not be mutated directly; use the methods discussed in :ref:`circuit-calibrations`. + + .. autoattribute:: calibrations + + If you have transpiled your circuit, so you have a physical circuit, you can inspect the + :attr:`layout` attribute for information stored by the transpiler about how the virtual qubits + of the source circuit map to the hardware qubits of your physical circuit, both at the start and + end of the circuit. + + .. autoattribute:: layout + + If your circuit was also *scheduled* as part of a transpilation, it will expose the individual + timings of each instruction, along with the total :attr:`duration` of the circuit. + + .. autoattribute:: duration + .. autoattribute:: unit + .. autoattribute:: op_start_times + + Finally, :class:`QuantumCircuit` exposes several simple properties as dynamic read-only numeric + attributes. + + .. autoattribute:: num_ancillas + .. autoattribute:: num_clbits + .. autoattribute:: num_captured_vars + .. autoattribute:: num_declared_vars + .. autoattribute:: num_input_vars + .. autoattribute:: num_parameters + .. autoattribute:: num_qubits + .. autoattribute:: num_vars + + Creating new circuits + ===================== + + ========================= ===================================================================== + Method Summary + ========================= ===================================================================== + :meth:`__init__` Default constructor of no-instruction circuits. + :meth:`copy` Make a complete copy of an existing circuit. + :meth:`copy_empty_like` Copy data objects from one circuit into a new one without any + instructions. + :meth:`from_instructions` Infer data objects needed from a list of instructions. + :meth:`from_qasm_file` Legacy interface to :func:`.qasm2.load`. + :meth:`from_qasm_str` Legacy interface to :func:`.qasm2.loads`. + ========================= ===================================================================== + + The default constructor (``QuantumCircuit(...)``) produces a circuit with no initial + instructions. The arguments to the default constructor can be used to seed the circuit with + quantum and classical data storage, and to provide a name, global phase and arbitrary metadata. + All of these fields can be expanded later. + + .. automethod:: __init__ + + If you have an existing circuit, you can produce a copy of it using :meth:`copy`, including all + its instructions. This is useful if you want to keep partial circuits while extending another, + or to have a version you can mutate in-place while leaving the prior one intact. - A circuit is a list of instructions bound to some registers. + .. automethod:: copy - Args: - regs (list(:class:`~.Register`) or list(``int``) or list(list(:class:`~.Bit`))): The - registers to be included in the circuit. + Similarly, if you want a circuit that contains all the same data objects (bits, registers, + variables, etc) but with none of the instructions, you can use :meth:`copy_empty_like`. This is + quite common when you want to build up a new layer of a circuit to then use apply onto the back + with :meth:`compose`, or to do a full rewrite of a circuit's instructions. + + .. automethod:: copy_empty_like + + In some cases, it is most convenient to generate a list of :class:`.CircuitInstruction`\\ s + separately to an entire circuit context, and then to build a circuit from this. The + :meth:`from_instructions` constructor will automatically capture all :class:`.Qubit` and + :class:`.Clbit` instances used in the instructions, and create a new :class:`QuantumCircuit` + object that has the correct resources and all the instructions. + + .. automethod:: from_instructions + + :class:`QuantumCircuit` also still has two constructor methods that are legacy wrappers around + the importers in :mod:`qiskit.qasm2`. These automatically apply :ref:`the legacy compatibility + settings ` of :func:`~.qasm2.load` and :func:`~.qasm2.loads`. + + .. automethod:: from_qasm_file + .. automethod:: from_qasm_str + + Data objects on circuits + ======================== - * If a list of :class:`~.Register` objects, represents the :class:`.QuantumRegister` - and/or :class:`.ClassicalRegister` objects to include in the circuit. + .. _circuit-adding-data-objects: + + Adding data objects + ------------------- - For example: + ============================= ================================================================= + Method Adds this kind of data + ============================= ================================================================= + :meth:`add_bits` :class:`.Qubit`\\ s and :class:`.Clbit`\\ s. + :meth:`add_register` :class:`.QuantumRegister` and :class:`.ClassicalRegister`. + :meth:`add_var` :class:`~.expr.Var` nodes with local scope and initializers. + :meth:`add_input` :class:`~.expr.Var` nodes that are treated as circuit inputs. + :meth:`add_capture` :class:`~.expr.Var` nodes captured from containing scopes. + :meth:`add_uninitialized_var` :class:`~.expr.Var` nodes with local scope and undefined state. + ============================= ================================================================= - * ``QuantumCircuit(QuantumRegister(4))`` - * ``QuantumCircuit(QuantumRegister(4), ClassicalRegister(3))`` - * ``QuantumCircuit(QuantumRegister(4, 'qr0'), QuantumRegister(2, 'qr1'))`` + Typically you add most of the data objects (:class:`.Qubit`, :class:`.Clbit`, + :class:`.ClassicalRegister`, etc) to the circuit as part of using the :meth:`__init__` default + constructor, or :meth:`copy_empty_like`. However, it is also possible to add these afterwards. + Typed classical data, such as standalone :class:`~.expr.Var` nodes (see + :ref:`circuit-repr-real-time-classical`), can be both constructed and added with separate + methods. - * If a list of ``int``, the amount of qubits and/or classical bits to include in - the circuit. It can either be a single int for just the number of quantum bits, - or 2 ints for the number of quantum bits and classical bits, respectively. + New registerless :class:`.Qubit` and :class:`.Clbit` objects are added using :meth:`add_bits`. + These objects must not already be present in the circuit. You can check if a bit exists in the + circuit already using :meth:`find_bit`. + + .. automethod:: add_bits + + Registers are added to the circuit with :meth:`add_register`. In this method, it is not an + error if some of the bits are already present in the circuit. In this case, the register will + be an "alias" over the bits. This is not generally well-supported by hardware backends; it is + probably best to stay away from relying on it. The registers a given bit is in are part of the + return of :meth:`find_bit`. - For example: + .. automethod:: add_register - * ``QuantumCircuit(4) # A QuantumCircuit with 4 qubits`` - * ``QuantumCircuit(4, 3) # A QuantumCircuit with 4 qubits and 3 classical bits`` + :ref:`Real-time, typed classical data ` is represented on the + circuit by :class:`~.expr.Var` nodes with a well-defined :class:`~.types.Type`. It is possible + to instantiate these separately to a circuit (see :meth:`.Var.new`), but it is often more + convenient to use circuit methods that will automatically manage the types and expression + initialization for you. The two most common methods are :meth:`add_var` (locally scoped + variables) and :meth:`add_input` (inputs to the circuit). - * If a list of python lists containing :class:`.Bit` objects, a collection of - :class:`.Bit` s to be added to the circuit. + .. automethod:: add_var + .. automethod:: add_input + In addition, there are two lower-level methods that can be useful for programmatic generation of + circuits. When working interactively, you will most likely not need these; most uses of + :meth:`add_uninitialized_var` are part of :meth:`copy_empty_like`, and most uses of + :meth:`add_capture` would be better off using :ref:`the control-flow builder interface + `. - name (str): the name of the quantum circuit. If not set, an - automatically generated string will be assigned. - global_phase (float or ParameterExpression): The global phase of the circuit in radians. - metadata (dict): Arbitrary key value metadata to associate with the - circuit. This gets stored as free-form data in a dict in the - :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will - not be directly used in the circuit. - inputs: any variables to declare as ``input`` real-time variables for this circuit. These - should already be existing :class:`.expr.Var` nodes that you build from somewhere else; - if you need to create the inputs as well, use :meth:`QuantumCircuit.add_input`. The - variables given in this argument will be passed directly to :meth:`add_input`. A - circuit cannot have both ``inputs`` and ``captures``. - captures: any variables that that this circuit scope should capture from a containing scope. - The variables given here will be passed directly to :meth:`add_capture`. A circuit - cannot have both ``inputs`` and ``captures``. - declarations: any variables that this circuit should declare and initialize immediately. - You can order this input so that later declarations depend on earlier ones (including - inputs or captures). If you need to depend on values that will be computed later at - runtime, use :meth:`add_var` at an appropriate point in the circuit execution. + .. automethod:: add_uninitialized_var + .. automethod:: add_capture - This argument is intended for convenient circuit initialization when you already have a - set of created variables. The variables used here will be directly passed to - :meth:`add_var`, which you can use directly if this is the first time you are creating - the variable. + Working with bits and registers + ------------------------------- - Raises: - CircuitError: if the circuit name, if given, is not valid. - CircuitError: if both ``inputs`` and ``captures`` are given. + A :class:`.Bit` instance is, on its own, just a unique handle for circuits to use in their own + contexts. If you have got a :class:`.Bit` instance and a circuit, just can find the contexts + that the bit exists in using :meth:`find_bit`, such as its integer index in the circuit and any + registers it is contained in. + + .. automethod:: find_bit + + Similarly, you can query a circuit to see if a register has already been added to it by using + :meth:`has_register`. + + .. automethod:: has_register + + Working with compile-time parameters + ------------------------------------ + + .. seealso:: + :ref:`circuit-compile-time-parameters` + A more complete discussion of what compile-time parametrization is, and how it fits into + Qiskit's data model. + + Unlike bits, registers, and real-time typed classical data, compile-time symbolic parameters are + not manually added to a circuit. Their presence is inferred by being contained in operations + added to circuits and the global phase. An ordered list of all parameters currently in a + circuit is at :attr:`QuantumCircuit.parameters`. + + The most common operation on :class:`.Parameter` instances is to replace them in symbolic + operations with some numeric value, or another symbolic expression. This is done with + :meth:`assign_parameters`. + + .. automethod:: assign_parameters + + The circuit tracks parameters by :class:`.Parameter` instances themselves, and forbids having + multiple parameters of the same name to avoid some problems when interoperating with OpenQASM or + other external formats. You can use :meth:`has_parameter` and :meth:`get_parameter` to query + the circuit for a parameter with the given string name. + + .. automethod:: has_parameter + .. automethod:: get_parameter + + .. _circuit-real-time-methods: + + Working with real-time typed classical data + ------------------------------------------- + + .. seealso:: + :mod:`qiskit.circuit.classical` + Module-level documentation for how the variable-, expression- and type-systems work, the + objects used to represent them, and the classical operations available. + + :ref:`circuit-repr-real-time-classical` + A discussion of how real-time data fits into the entire :mod:`qiskit.circuit` data model + as a whole. + + :ref:`circuit-adding-data-objects` + The methods for adding new :class:`~.expr.Var` variables to a circuit after + initialization. + + You can retrive a :class:`~.expr.Var` instance attached to a circuit by using its variable name + using :meth:`get_var`, or check if a circuit contains a given variable with :meth:`has_var`. + + .. automethod:: get_var + .. automethod:: has_var + + There are also several iterator methods that you can use to get the full set of variables + tracked by a circuit. At least one of :meth:`iter_input_vars` and :meth:`iter_captured_vars` + will be empty, as inputs and captures are mutually exclusive. All of the iterators have + corresponding dynamic properties on :class:`QuantumCircuit` that contain their length: + :attr:`num_vars`, :attr:`num_input_vars`, :attr:`num_captured_vars` and + :attr:`num_declared_vars`. + + .. automethod:: iter_vars + .. automethod:: iter_input_vars + .. automethod:: iter_captured_vars + .. automethod:: iter_declared_vars + + + .. _circuit-adding-operations: + + Adding operations to circuits + ============================= + + You can add anything that implements the :class:`.Operation` interface to a circuit as a single + instruction, though most things you will want to add will be :class:`~.circuit.Instruction` or + :class:`~.circuit.Gate` instances. + + .. seealso:: + :ref:`circuit-operations-instructions` + The :mod:`qiskit.circuit`-level documentation on the different interfaces that Qiskit + uses to define circuit-level instructions. + + .. _circuit-append-compose: + + Methods to add general operations + --------------------------------- + + These are the base methods that handle adding any object, including user-defined ones, onto + circuits. + + =============== =============================================================================== + Method When to use it + =============== =============================================================================== + :meth:`append` Add an instruction as a single object onto a circuit. + :meth:`_append` Same as :meth:`append`, but a low-level interface that elides almost all error + checking. + :meth:`compose` Inline the instructions from one circuit onto another. + :meth:`tensor` Like :meth:`compose`, but strictly for joining circuits that act on disjoint + qubits. + =============== =============================================================================== + + :class:`QuantumCircuit` has two main ways that you will add more operations onto a circuit. + Which to use depends on whether you want to add your object as a single "instruction" + (:meth:`append`), or whether you want to join the instructions from two circuits together + (:meth:`compose`). + + A single instruction or operation appears as a single entry in the :attr:`data` of the circuit, + and as a single box when drawn in the circuit visualizers (see :meth:`draw`). A single + instruction is the "unit" that a hardware backend might be defined in terms of (see + :class:`.Target`). An :class:`~.circuit.Instruction` can come with a + :attr:`~.circuit.Instruction.definition`, which is one rule the transpiler (see + :mod:`qiskit.transpiler`) will be able to fall back on to decompose it for hardware, if needed. + An :class:`.Operation` that is not also an :class:`~.circuit.Instruction` can + only be decomposed if it has some associated high-level synthesis method registered for it (see + :mod:`qiskit.transpiler.passes.synthesis.plugin`). + + A :class:`QuantumCircuit` alone is not a single :class:`~.circuit.Instruction`; it is rather + more complicated, since it can, in general, represent a complete program with typed classical + memory inputs and outputs, and control flow. Qiskit's (and most hardware's) data model does not + yet have the concept of re-usable callable subroutines with virtual quantum operands. You can + convert simple circuits that act only on qubits with unitary operations into a :class:`.Gate` + using :meth:`to_gate`, and simple circuits acting only on qubits and clbits into a + :class:`~.circuit.Instruction` with :meth:`to_instruction`. + + When you have an :class:`.Operation`, :class:`~.circuit.Instruction`, or :class:`.Gate`, add it + to the circuit, specifying the qubit and clbit arguments with :meth:`append`. + + .. automethod:: append + + :meth:`append` does quite substantial error checking to ensure that you cannot accidentally + break the data model of :class:`QuantumCircuit`. If you are programmatically generating a + circuit from known-good data, you can elide much of this error checking by using the fast-path + appender :meth:`_append`, but at the risk that the caller is responsible for ensuring they are + passing only valid data. + + .. automethod:: _append + + In other cases, you may want to join two circuits together, applying the instructions from one + circuit onto specified qubits and clbits on another circuit. This "inlining" operation is + called :meth:`compose` in Qiskit. :meth:`compose` is, in general, more powerful than + a :meth:`to_instruction`-plus-:meth:`append` combination for joining two circuits, because it + can also link typed classical data together, and allows for circuit control-flow operations to + be joined onto another circuit. + + The downsides to :meth:`compose` are that it is a more complex operation that can involve more + rewriting of the operand, and that it necessarily must move data from one circuit object to + another. If you are building up a circuit for yourself and raw performance is a core goal, + consider passing around your base circuit and having different parts of your algorithm write + directly to the base circuit, rather than building a temporary layer circuit. + + .. automethod:: compose + + If you are trying to join two circuits that will apply to completely disjoint qubits and clbits, + :meth:`tensor` is a convenient wrapper around manually adding bit objects and calling + :meth:`compose`. + + .. automethod:: tensor + + As some rules of thumb: + + * If you have a single :class:`.Operation`, :class:`~.circuit.Instruction` or :class:`.Gate`, + you should definitely use :meth:`append` or :meth:`_append`. + * If you have a :class:`QuantumCircuit` that represents a single atomic instruction for a larger + circuit that you want to re-use, you probably want to call :meth:`to_instruction` or + :meth:`to_gate`, and then apply the result of that to the circuit using :meth:`append`. + * If you have a :class:`QuantumCircuit` that represents a larger "layer" of another circuit, or + contains typed classical variables or control flow, you should use :meth:`compose` to merge it + onto another circuit. + * :meth:`tensor` is wanted far more rarely than either :meth:`append` or :meth:`compose`. + Internally, it is mostly a wrapper around :meth:`add_bits` and :meth:`compose`. + + Some potential pitfalls to beware of: + + * Even if you re-use a custom :class:`~.circuit.Instruction` during circuit construction, the + transpiler will generally have to "unroll" each invocation of it to its inner decomposition + before beginning work on it. This should not prevent you from using the + :meth:`to_instruction`-plus-:meth:`append` pattern, as the transpiler will improve in this + regard over time. + * :meth:`compose` will, by default, produce a new circuit for backwards compatibility. This is + more expensive, and not usually what you want, so you should set ``inplace=True``. + * Both :meth:`append` and :meth:`compose` (but not :meth:`_append`) have a ``copy`` keyword + argument that defaults to ``True``. In these cases, the incoming :class:`.Operation` + instances will be copied if Qiskit detects that the objects have mutability about them (such + as taking gate parameters). If you are sure that you will not re-use the objects again in + other places, you should set ``copy=False`` to prevent this copying, which can be a + substantial speed-up for large objects. + + Methods to add standard instructions + ------------------------------------ + + The :class:`QuantumCircuit` class has helper methods to add many of the Qiskit standard-library + instructions and gates onto a circuit. These are generally equivalent to manually constructing + an instance of the relevent :mod:`qiskit.circuit.library` object, then passing that to + :meth:`append` with the remaining arguments placed into the ``qargs`` and ``cargs`` fields as + appropriate. + + The following methods apply special non-unitary :class:`~.circuit.Instruction` operations to the + circuit: + + =============================== ==================================================== + :class:`QuantumCircuit` method :mod:`qiskit.circuit` :class:`~.circuit.Instruction` + =============================== ==================================================== + :meth:`barrier` :class:`Barrier` + :meth:`delay` :class:`Delay` + :meth:`initialize` :class:`~library.Initialize` + :meth:`measure` :class:`Measure` + :meth:`reset` :class:`Reset` + :meth:`store` :class:`Store` + =============================== ==================================================== + + These methods apply uncontrolled unitary :class:`.Gate` instances to the circuit: + + =============================== ============================================ + :class:`QuantumCircuit` method :mod:`qiskit.circuit.library` :class:`.Gate` + =============================== ============================================ + :meth:`dcx` :class:`~library.DCXGate` + :meth:`ecr` :class:`~library.ECRGate` + :meth:`h` :class:`~library.HGate` + :meth:`id` :class:`~library.IGate` + :meth:`iswap` :class:`~library.iSwapGate` + :meth:`ms` :class:`~library.MSGate` + :meth:`p` :class:`~library.PhaseGate` + :meth:`pauli` :class:`~library.PauliGate` + :meth:`prepare_state` :class:`~library.StatePreparation` + :meth:`r` :class:`~library.RGate` + :meth:`rcccx` :class:`~library.RC3XGate` + :meth:`rccx` :class:`~library.RCCXGate` + :meth:`rv` :class:`~library.RVGate` + :meth:`rx` :class:`~library.RXGate` + :meth:`rxx` :class:`~library.RXXGate` + :meth:`ry` :class:`~library.RYGate` + :meth:`ryy` :class:`~library.RYYGate` + :meth:`rz` :class:`~library.RZGate` + :meth:`rzx` :class:`~library.RZXGate` + :meth:`rzz` :class:`~library.RZZGate` + :meth:`s` :class:`~library.SGate` + :meth:`sdg` :class:`~library.SdgGate` + :meth:`swap` :class:`~library.SwapGate` + :meth:`sx` :class:`~library.SXGate` + :meth:`sxdg` :class:`~library.SXdgGate` + :meth:`t` :class:`~library.TGate` + :meth:`tdg` :class:`~library.TdgGate` + :meth:`u` :class:`~library.UGate` + :meth:`unitary` :class:`~library.UnitaryGate` + :meth:`x` :class:`~library.XGate` + :meth:`y` :class:`~library.YGate` + :meth:`z` :class:`~library.ZGate` + =============================== ============================================ + + The following methods apply :class:`Gate` instances that are also controlled gates, so are + direct subclasses of :class:`ControlledGate`: + + =============================== ====================================================== + :class:`QuantumCircuit` method :mod:`qiskit.circuit.library` :class:`.ControlledGate` + =============================== ====================================================== + :meth:`ccx` :class:`~library.CCXGate` + :meth:`ccz` :class:`~library.CCZGate` + :meth:`ch` :class:`~library.CHGate` + :meth:`cp` :class:`~library.CPhaseGate` + :meth:`crx` :class:`~library.CRXGate` + :meth:`cry` :class:`~library.CRYGate` + :meth:`crz` :class:`~library.CRZGate` + :meth:`cs` :class:`~library.CSGate` + :meth:`csdg` :class:`~library.CSdgGate` + :meth:`cswap` :class:`~library.CSwapGate` + :meth:`csx` :class:`~library.CSXGate` + :meth:`cu` :class:`~library.CUGate` + :meth:`cx` :class:`~library.CXGate` + :meth:`cy` :class:`~library.CYGate` + :meth:`cz` :class:`~library.CZGate` + =============================== ====================================================== + + Finally, these methods apply particular generalized multiply controlled gates to the circuit, + often with eager syntheses. They are listed in terms of the *base* gate they are controlling, + since their exact output is often a synthesized version of a gate. + + =============================== ================================================= + :class:`QuantumCircuit` method Base :mod:`qiskit.circuit.library` :class:`.Gate` + =============================== ================================================= + :meth:`mcp` :class:`~library.PhaseGate` + :meth:`mcrx` :class:`~library.RXGate` + :meth:`mcry` :class:`~library.RYGate` + :meth:`mcrz` :class:`~library.RZGate` + :meth:`mcx` :class:`~library.XGate` + =============================== ================================================= + + The rest of this section is the API listing of all the individual methods; the tables above are + summaries whose links will jump you to the correct place. + + .. automethod:: barrier + .. automethod:: ccx + .. automethod:: ccz + .. automethod:: ch + .. automethod:: cp + .. automethod:: crx + .. automethod:: cry + .. automethod:: crz + .. automethod:: cs + .. automethod:: csdg + .. automethod:: cswap + .. automethod:: csx + .. automethod:: cu + .. automethod:: cx + .. automethod:: cy + .. automethod:: cz + .. automethod:: dcx + .. automethod:: delay + .. automethod:: ecr + .. automethod:: h + .. automethod:: id + .. automethod:: initialize + .. automethod:: iswap + .. automethod:: mcp + .. automethod:: mcrx + .. automethod:: mcry + .. automethod:: mcrz + .. automethod:: mcx + .. automethod:: measure + .. automethod:: ms + .. automethod:: p + .. automethod:: pauli + .. automethod:: prepare_state + .. automethod:: r + .. automethod:: rcccx + .. automethod:: rccx + .. automethod:: reset + .. automethod:: rv + .. automethod:: rx + .. automethod:: rxx + .. automethod:: ry + .. automethod:: ryy + .. automethod:: rz + .. automethod:: rzx + .. automethod:: rzz + .. automethod:: s + .. automethod:: sdg + .. automethod:: store + .. automethod:: swap + .. automethod:: sx + .. automethod:: sxdg + .. automethod:: t + .. automethod:: tdg + .. automethod:: u + .. automethod:: unitary + .. automethod:: x + .. automethod:: y + .. automethod:: z + + + .. _circuit-control-flow-methods: + + Adding control flow to circuits + ------------------------------- + + .. seealso:: + :ref:`circuit-control-flow-repr` + + Discussion of how control-flow operations are represented in the whole :mod:`qiskit.circuit` + context. + + ============================== ================================================================ + :class:`QuantumCircuit` method Control-flow instruction + ============================== ================================================================ + :meth:`if_test` :class:`.IfElseOp` with only a ``True`` body. + :meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies. + :meth:`while_loop` :class:`.WhileLoopOp`. + :meth:`switch` :class:`.SwitchCaseOp`. + :meth:`for_loop` :class:`.ForLoopOp`. + :meth:`break_loop` :class:`.BreakLoopOp`. + :meth:`continue_loop` :class:`.ContinueLoopOp`. + ============================== ================================================================ + + :class:`QuantumCircuit` has corresponding methods for all of the control-flow operations that + are supported by Qiskit. These have two forms for calling them. The first is a very + straightfowards convenience wrapper that takes in the block bodies of the instructions as + :class:`QuantumCircuit` arguments, and simply constructs and appends the corresponding + :class:`.ControlFlowOp`. + + The second form, which we strongly recommend you use for constructing control flow, is called + *the builder interface*. Here, the methods take only the real-time discriminant of the + operation, and return `context managers + `__ that you enter using + ``with``. You can then use regular :class:`QuantumCircuit` methods within those blocks to build + up the control-flow bodies, and Qiskit will automatically track which of the data resources are + needed for the inner blocks, building the complete :class:`.ControlFlowOp` as you leave the + ``with`` statement. It is far simpler and less error-prone to build control flow + programmatically this way. + + .. + TODO: expand the examples of the builder interface. + + .. automethod:: break_loop + .. automethod:: continue_loop + .. automethod:: for_loop + .. automethod:: if_else + .. automethod:: if_test + .. automethod:: switch + .. automethod:: while_loop + + + Converting circuits to single objects + ------------------------------------- + + As discussed in :ref:`circuit-append-compose`, you can convert a circuit to either an + :class:`~.circuit.Instruction` or a :class:`.Gate` using two helper methods. + + .. automethod:: to_instruction + .. automethod:: to_gate + + + Helper mutation methods + ----------------------- + + There are two higher-level methods on :class:`QuantumCircuit` for appending measurements to the + end of a circuit. Note that by default, these also add an extra register. + + .. automethod:: measure_active + .. automethod:: measure_all + + There are two "subtractive" methods on :class:`QuantumCircuit` as well. This is not a use-case + that :class:`QuantumCircuit` is designed for; typically you should just look to use + :meth:`copy_empty_like` in place of :meth:`clear`, and run :meth:`remove_final_measurements` as + its transpiler-pass form :class:`.RemoveFinalMeasurements`. + + .. automethod:: clear + .. automethod:: remove_final_measurements + + .. _circuit-calibrations: + + Manual calibration of instructions + ---------------------------------- + + :class:`QuantumCircuit` can store :attr:`calibrations` of instructions that define the pulses + used to run them on one particular hardware backend. You can + + .. automethod:: add_calibration + .. automethod:: has_calibration_for + + + Circuit properties + ================== + + Simple circuit metrics + ---------------------- + + When constructing quantum circuits, there are several properties that help quantify + the "size" of the circuits, and their ability to be run on a noisy quantum device. + Some of these, like number of qubits, are straightforward to understand, while others + like depth and number of tensor components require a bit more explanation. Here we will + explain all of these properties, and, in preparation for understanding how circuits change + when run on actual devices, highlight the conditions under which they change. + + Consider the following circuit: + + .. plot:: + :include-source: + + from qiskit import QuantumCircuit + qc = QuantumCircuit(12) + for idx in range(5): + qc.h(idx) + qc.cx(idx, idx+5) + + qc.cx(1, 7) + qc.x(8) + qc.cx(1, 9) + qc.x(7) + qc.cx(1, 11) + qc.swap(6, 11) + qc.swap(6, 9) + qc.swap(6, 10) + qc.x(6) + qc.draw('mpl') + + From the plot, it is easy to see that this circuit has 12 qubits, and a collection of + Hadamard, CNOT, X, and SWAP gates. But how to quantify this programmatically? Because we + can do single-qubit gates on all the qubits simultaneously, the number of qubits in this + circuit is equal to the :meth:`width` of the circuit:: + + assert qc.width() == 12 + + We can also just get the number of qubits directly using :attr:`num_qubits`:: + + assert qc.num_qubits == 12 + + .. important:: + + For a quantum circuit composed from just qubits, the circuit width is equal + to the number of qubits. This is the definition used in quantum computing. However, + for more complicated circuits with classical registers, and classically controlled gates, + this equivalence breaks down. As such, from now on we will not refer to the number of + qubits in a quantum circuit as the width. + + It is also straightforward to get the number and type of the gates in a circuit using + :meth:`count_ops`:: + + qc.count_ops() + + .. parsed-literal:: + + OrderedDict([('cx', 8), ('h', 5), ('x', 3), ('swap', 3)]) + + We can also get just the raw count of operations by computing the circuits + :meth:`size`:: + + assert qc.size() == 19 - Examples: + A particularly important circuit property is known as the circuit :meth:`depth`. The depth + of a quantum circuit is a measure of how many "layers" of quantum gates, executed in + parallel, it takes to complete the computation defined by the circuit. Because quantum + gates take time to implement, the depth of a circuit roughly corresponds to the amount of + time it takes the quantum computer to execute the circuit. Thus, the depth of a circuit + is one important quantity used to measure if a quantum circuit can be run on a device. - Construct a simple Bell state circuit. + The depth of a quantum circuit has a mathematical definition as the longest path in a + directed acyclic graph (DAG). However, such a definition is a bit hard to grasp, even for + experts. Fortunately, the depth of a circuit can be easily understood by anyone familiar + with playing `Tetris `_. Lets see how to compute this + graphically: - .. plot:: - :include-source: + .. image:: /source_images/depth.gif - from qiskit import QuantumCircuit + We can verify our graphical result using :meth:`QuantumCircuit.depth`:: - qc = QuantumCircuit(2, 2) - qc.h(0) - qc.cx(0, 1) - qc.measure([0, 1], [0, 1]) - qc.draw('mpl') + assert qc.depth() == 9 - Construct a 5-qubit GHZ circuit. + .. automethod:: count_ops + .. automethod:: depth + .. automethod:: get_instructions + .. automethod:: num_connected_components + .. automethod:: num_nonlocal_gates + .. automethod:: num_tensor_factors + .. automethod:: num_unitary_factors + .. automethod:: size + .. automethod:: width - .. code-block:: + Accessing scheduling information + -------------------------------- - from qiskit import QuantumCircuit + If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing + information for individual qubits can be accessed. The whole-circuit timing information is + available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes. + + .. automethod:: qubit_duration + .. automethod:: qubit_start_time + .. automethod:: qubit_stop_time + + Instruction-like methods + ======================== - qc = QuantumCircuit(5) - qc.h(0) - qc.cx(0, range(1, 5)) - qc.measure_all() + .. + These methods really shouldn't be on `QuantumCircuit` at all. They're generally more + appropriate as `Instruction` or `Gate` methods. `reverse_ops` shouldn't be a method _full + stop_---it was copying a `DAGCircuit` method from an implementation detail of the original + `SabreLayout` pass in Qiskit. + + :class:`QuantumCircuit` also contains a small number of methods that are very + :class:`~.circuit.Instruction`-like in detail. You may well find better integration and more + API support if you first convert your circuit to an :class:`~.circuit.Instruction` + (:meth:`to_instruction`) or :class:`.Gate` (:meth:`to_gate`) as appropriate, then call the + corresponding method. - Construct a 4-qubit Bernstein-Vazirani circuit using registers. + .. automethod:: control + .. automethod:: inverse + .. automethod:: power + .. automethod:: repeat + .. automethod:: reverse_ops - .. plot:: - :include-source: + Visualization + ============= - from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit + Qiskit includes some drawing tools to give you a quick feel for what your circuit looks like. + This tooling is primarily targeted at producing either a `Matplotlib + `__- or text-based drawing. There is also a lesser-featured LaTeX + backend for drawing, but this is only for simple circuits, and is not as actively maintained. - qr = QuantumRegister(3, 'q') - anc = QuantumRegister(1, 'ancilla') - cr = ClassicalRegister(3, 'c') - qc = QuantumCircuit(qr, anc, cr) + .. seealso:: + :mod:`qiskit.visualization` + The primary documentation for all of Qiskit's visualization tooling. - qc.x(anc[0]) - qc.h(anc[0]) - qc.h(qr[0:3]) - qc.cx(qr[0:3], anc[0]) - qc.h(qr[0:3]) - qc.barrier(qr) - qc.measure(qr, cr) + .. automethod:: draw - qc.draw('mpl') + In addition to the core :meth:`draw` driver, there are two visualization-related helper methods, + which are mostly useful for quickly unwrapping some inner instructions or reversing the + :ref:`qubit-labelling conventions ` in the drawing. For more general + mutation, including basis-gate rewriting, you should use the transpiler + (:mod:`qiskit.transpiler`). + + .. automethod:: decompose + .. automethod:: reverse_bits + + Internal utilities + ================== + + These functions are not intended for public use, but were accidentally left documented in the + public API during the 1.0 release. They will be removed in Qiskit 2.0, but will be supported + until then. + + .. automethod:: cast + .. automethod:: cbit_argument_conversion + .. automethod:: cls_instances + .. automethod:: cls_prefix + .. automethod:: qbit_argument_conversion """ instances = 0 @@ -228,6 +994,69 @@ def __init__( captures: Iterable[expr.Var] = (), declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): + """ + Default constructor of :class:`QuantumCircuit`. + + .. + `QuantumCirucit` documents its `__init__` method explicitly, unlike most classes where + it's implicitly appended to the class-level documentation, just because the class is so + huge and has a lot of introductory material to its class docstring. + + Args: + regs: The registers to be included in the circuit. + + * If a list of :class:`~.Register` objects, represents the :class:`.QuantumRegister` + and/or :class:`.ClassicalRegister` objects to include in the circuit. + + For example: + + * ``QuantumCircuit(QuantumRegister(4))`` + * ``QuantumCircuit(QuantumRegister(4), ClassicalRegister(3))`` + * ``QuantumCircuit(QuantumRegister(4, 'qr0'), QuantumRegister(2, 'qr1'))`` + + * If a list of ``int``, the amount of qubits and/or classical bits to include in + the circuit. It can either be a single int for just the number of quantum bits, + or 2 ints for the number of quantum bits and classical bits, respectively. + + For example: + + * ``QuantumCircuit(4) # A QuantumCircuit with 4 qubits`` + * ``QuantumCircuit(4, 3) # A QuantumCircuit with 4 qubits and 3 classical bits`` + + * If a list of python lists containing :class:`.Bit` objects, a collection of + :class:`.Bit` s to be added to the circuit. + + name: the name of the quantum circuit. If not set, an automatically generated string + will be assigned. + global_phase: The global phase of the circuit in radians. + metadata: Arbitrary key value metadata to associate with the circuit. This gets + stored as free-form data in a dict in the + :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will not be directly + used in the circuit. + inputs: any variables to declare as ``input`` runtime variables for this circuit. These + should already be existing :class:`.expr.Var` nodes that you build from somewhere + else; if you need to create the inputs as well, use + :meth:`QuantumCircuit.add_input`. The variables given in this argument will be + passed directly to :meth:`add_input`. A circuit cannot have both ``inputs`` and + ``captures``. + captures: any variables that that this circuit scope should capture from a containing + scope. The variables given here will be passed directly to :meth:`add_capture`. A + circuit cannot have both ``inputs`` and ``captures``. + declarations: any variables that this circuit should declare and initialize immediately. + You can order this input so that later declarations depend on earlier ones + (including inputs or captures). If you need to depend on values that will be + computed later at runtime, use :meth:`add_var` at an appropriate point in the + circuit execution. + + This argument is intended for convenient circuit initialization when you already + have a set of created variables. The variables used here will be directly passed to + :meth:`add_var`, which you can use directly if this is the first time you are + creating the variable. + + Raises: + CircuitError: if the circuit name, if given, is not valid. + CircuitError: if both ``inputs`` and ``captures`` are given. + """ if any(not isinstance(reg, (list, QuantumRegister, ClassicalRegister)) for reg in regs): # check if inputs are integers, but also allow e.g. 2.0 @@ -238,12 +1067,15 @@ def __init__( if not valid_reg_size: raise CircuitError( - "Circuit args must be Registers or integers. (%s '%s' was " - "provided)" % ([type(reg).__name__ for reg in regs], regs) + "Circuit args must be Registers or integers. (" + f"{[type(reg).__name__ for reg in regs]} '{regs}' was " + "provided)" ) regs = tuple(int(reg) for reg in regs) # cast to int self._base_name = None + self.name: str + """A human-readable name for the circuit.""" if name is None: self._base_name = self.cls_prefix() self._name_update() @@ -273,7 +1105,11 @@ def __init__( ] = [] self.qregs: list[QuantumRegister] = [] + """A list of the :class:`QuantumRegister`\\ s in this circuit. You should not mutate + this.""" self.cregs: list[ClassicalRegister] = [] + """A list of the :class:`ClassicalRegister`\\ s in this circuit. You should not mutate + this.""" # Dict mapping Qubit or Clbit instances to tuple comprised of 0) the # corresponding index in circuit.{qubits,clbits} and 1) a list of @@ -290,14 +1126,10 @@ def __init__( self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict) self.add_register(*regs) - # Parameter table tracks instructions with variable parameters. - self._parameter_table = ParameterTable() - # Cache to avoid re-sorting parameters self._parameters = None self._layout = None - self._global_phase: ParameterValueType = 0 self.global_phase = global_phase # Add classical variables. Resolve inputs and captures first because they can't depend on @@ -314,9 +1146,25 @@ def __init__( for var, initial in declarations: self.add_var(var, initial) - self.duration = None + self.duration: int | float | None = None + """The total duration of the circuit, set by a scheduling transpiler pass. Its unit is + specified by :attr:`unit`.""" self.unit = "dt" + """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: + """A private constructor from rust space circuit data.""" + out = QuantumCircuit() + 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 def from_instructions( @@ -333,7 +1181,7 @@ def from_instructions( global_phase: ParameterValueType = 0, metadata: dict | None = None, ) -> "QuantumCircuit": - """Construct a circuit from an iterable of CircuitInstructions. + """Construct a circuit from an iterable of :class:`.CircuitInstruction`\\ s. Args: instructions: The instructions to add to the circuit. @@ -390,7 +1238,7 @@ def layout(self) -> Optional[TranspileLayout]: @property def data(self) -> QuantumCircuitData: - """Return the circuit data (instructions and context). + """The circuit data (instructions and context). Returns: QuantumCircuitData: a list-like object containing the :class:`.CircuitInstruction`\\ s @@ -418,7 +1266,6 @@ def data(self, data_input: Iterable): data_input = list(data_input) self._data.clear() self._parameters = None - self._parameter_table = ParameterTable() # Repopulate the parameter table with any global-phase entries. self.global_phase = self.global_phase if not data_input: @@ -541,12 +1388,11 @@ def __deepcopy__(self, memo=None): # Avoids pulling self._data into a Python list # like we would when pickling. - result._data = self._data.copy() + result._data = self._data.copy(deepcopy=True) result._data.replace_bits( qubits=_copy.deepcopy(self._data.qubits, memo), clbits=_copy.deepcopy(self._data.clbits, memo), ) - result._data.map_ops(lambda op: _copy.deepcopy(op, memo)) return result @classmethod @@ -621,9 +1467,7 @@ def reverse_ops(self) -> "QuantumCircuit": q_1: ┤ RX(1.57) ├───── └──────────┘ """ - reverse_circ = QuantumCircuit( - self.qubits, self.clbits, *self.qregs, *self.cregs, name=self.name + "_reverse" - ) + reverse_circ = self.copy_empty_like(self.name + "_reverse") for instruction in reversed(self.data): reverse_circ._append(instruction.replace(operation=instruction.operation.reverse_ops())) @@ -816,7 +1660,7 @@ def power( raise CircuitError( "Cannot raise a parameterized circuit to a non-positive power " "or matrix-power, please bind the free parameters: " - "{}".format(self.parameters) + f"{self.parameters}" ) try: @@ -883,10 +1727,34 @@ def compose( wrap: bool = False, *, copy: bool = True, + var_remap: Mapping[str | expr.Var, str | expr.Var] | None = None, + inline_captures: bool = False, ) -> Optional["QuantumCircuit"]: - """Compose circuit with ``other`` circuit or instruction, optionally permuting wires. + """Apply the instructions from one circuit onto specified qubits and/or clbits on another. - ``other`` can be narrower or of equal width to ``self``. + .. note:: + + By default, this creates a new circuit object, leaving ``self`` untouched. For most + uses of this function, it is far more efficient to set ``inplace=True`` and modify the + base circuit in-place. + + When dealing with realtime variables (:class:`.expr.Var` instances), there are two principal + strategies for using :meth:`compose`: + + 1. The ``other`` circuit is treated as entirely additive, including its variables. The + variables in ``other`` must be entirely distinct from those in ``self`` (use + ``var_remap`` to help with this), and all variables in ``other`` will be declared anew in + the output with matching input/capture/local scoping to how they are in ``other``. This + is generally what you want if you're joining two unrelated circuits. + + 2. The ``other`` circuit was created as an exact extension to ``self`` to be inlined onto + it, including acting on the existing variables in their states at the end of ``self``. + In this case, ``other`` should be created with all these variables to be inlined declared + as "captures", and then you can use ``inline_captures=True`` in this method to link them. + This is generally what you want if you're building up a circuit by defining layers + on-the-fly, or rebuilding a circuit using layers taken from itself. You might find the + ``vars_mode="captures"`` argument to :meth:`copy_empty_like` useful to create each + layer's base, in this case. Args: other (qiskit.circuit.Instruction or QuantumCircuit): @@ -894,17 +1762,38 @@ def compose( this can be anything that :obj:`.append` will accept. qubits (list[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. This is not possible within + front (bool): If ``True``, front composition will be performed. This is not possible within control-flow builder context managers. - inplace (bool): If True, modify the object. Otherwise return composed circuit. - wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on - whether it contains only unitary instructions) before composing it onto self. + inplace (bool): If ``True``, modify the object. Otherwise, return composed circuit. copy (bool): If ``True`` (the default), then the input is treated as shared, and any contained instructions will be copied, if they might need to be mutated in the future. You can set this to ``False`` if the input should be considered owned by the base circuit, in order to avoid unnecessary copies; in this case, it is not - valid to use ``other`` afterwards, and some instructions may have been mutated in + valid to use ``other`` afterward, and some instructions may have been mutated in place. + var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` nodes in ``other`` as + they are inlined into ``self``. This can be used to avoid naming conflicts. + + Both keys and values can be given as strings or direct :class:`.expr.Var` instances. + If a key is a string, it matches any :class:`~.expr.Var` with the same name. If a + value is a string, whenever a new key matches a it, a new :class:`~.expr.Var` is + created with the correct type. If a value is a :class:`~.expr.Var`, its + :class:`~.expr.Expr.type` must exactly match that of the variable it is replacing. + inline_captures (bool): if ``True``, then all "captured" :class:`~.expr.Var` nodes in + the ``other`` :class:`.QuantumCircuit` are assumed to refer to variables already + declared in ``self`` (as any input/capture/local type), and the uses in ``other`` + will apply to the existing variables. If you want to build up a layer for an + existing circuit to use with :meth:`compose`, you might find the + ``vars_mode="captures"`` argument to :meth:`copy_empty_like` useful. Any remapping + in ``vars_remap`` occurs before evaluating this variable inlining. + + If this is ``False`` (the default), then all variables in ``other`` will be required + to be distinct from those in ``self``, and new declarations will be made for them. + wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on + whether it contains only unitary instructions) before composing it onto self. + Rather than using this option, it is almost always better to manually control this + yourself by using :meth:`to_instruction` or :meth:`to_gate`, and then call + :meth:`append`. Returns: QuantumCircuit: the composed circuit (returns None if inplace==True). @@ -961,6 +1850,31 @@ def compose( # error that the user might want to correct in an interactive session. dest = self if inplace else self.copy() + var_remap = {} if var_remap is None else var_remap + + # This doesn't use `functools.cache` so we can access it during the variable remapping of + # instructions. We cache all replacement lookups for a) speed and b) to ensure that + # the same variable _always_ maps to the same replacement even if it's used in different + # places in the recursion tree (such as being a captured variable). + def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: + # This is closing over an argument to `compose`. + nonlocal var_remap + + if out := cache.get(var): + return out + if (replacement := var_remap.get(var)) or (replacement := var_remap.get(var.name)): + if isinstance(replacement, str): + replacement = expr.Var.new(replacement, var.type) + if replacement.type != var.type: + raise CircuitError( + f"mismatched types in replacement for '{var.name}':" + f" '{var.type}' cannot become '{replacement.type}'" + ) + else: + replacement = var + cache[var] = replacement + return replacement + # As a special case, allow composing some clbits onto no clbits - normally the destination # has to be strictly larger. This allows composing final measurements onto unitary circuits. if isinstance(other, QuantumCircuit): @@ -987,7 +1901,7 @@ def compose( clbits = self.clbits[: other.num_clbits] if front: # Need to keep a reference to the data for use after we've emptied it. - old_data = dest._data.copy() + old_data = dest._data.copy(copy_instructions=copy) dest.clear() dest.append(other, qubits, clbits, copy=copy) for instruction in old_data: @@ -1010,10 +1924,10 @@ def compose( edge_map.update(zip(other.qubits, dest.qubits)) else: mapped_qubits = dest.qbit_argument_conversion(qubits) - if len(mapped_qubits) != len(other.qubits): + if len(mapped_qubits) != other.num_qubits: raise CircuitError( f"Number of items in qubits parameter ({len(mapped_qubits)}) does not" - f" match number of qubits in the circuit ({len(other.qubits)})." + f" match number of qubits in the circuit ({other.num_qubits})." ) if len(set(mapped_qubits)) != len(mapped_qubits): raise CircuitError( @@ -1026,10 +1940,10 @@ def compose( edge_map.update(zip(other.clbits, dest.clbits)) else: mapped_clbits = dest.cbit_argument_conversion(clbits) - if len(mapped_clbits) != len(other.clbits): + if len(mapped_clbits) != other.num_clbits: raise CircuitError( f"Number of items in clbits parameter ({len(mapped_clbits)}) does not" - f" match number of clbits in the circuit ({len(other.clbits)})." + f" match number of clbits in the circuit ({other.num_clbits})." ) if len(set(mapped_clbits)) != len(mapped_clbits): raise CircuitError( @@ -1044,38 +1958,100 @@ def compose( dest.unit = "dt" dest.global_phase += other.global_phase - if not other.data: - # Nothing left to do. Plus, accessing 'data' here is necessary - # to trigger any lazy building since we now access '_data' - # directly. - return None if inplace else dest + # This is required to trigger data builds if the `other` is an unbuilt `BlueprintCircuit`, + # so we can the access the complete `CircuitData` object at `_data`. + _ = other.data - variable_mapper = _classical_resource_map.VariableMapper( - dest.cregs, edge_map, dest.add_register - ) + def copy_with_remapping( + source, dest, bit_map, var_map, inline_captures, new_qubits=None, new_clbits=None + ): + # Copy the instructions from `source` into `dest`, remapping variables in instructions + # according to `var_map`. If `new_qubits` or `new_clbits` are given, the qubits and + # clbits of the source instruction are remapped to those as well. + for var in source.iter_input_vars(): + dest.add_input(replace_var(var, var_map)) + if inline_captures: + for var in source.iter_captured_vars(): + replacement = replace_var(var, var_map) + if not dest.has_var(replace_var(var, var_map)): + if var is replacement: + raise CircuitError( + f"Variable '{var}' to be inlined is not in the base circuit." + " If you wanted it to be automatically added, use" + " `inline_captures=False`." + ) + raise CircuitError( + f"Replacement '{replacement}' for variable '{var}' is not in the" + " base circuit. Is the replacement correct?" + ) + else: + for var in source.iter_captured_vars(): + dest.add_capture(replace_var(var, var_map)) + for var in source.iter_declared_vars(): + dest.add_uninitialized_var(replace_var(var, var_map)) + + def recurse_block(block): + # Recurse the remapping into a control-flow block. Note that this doesn't remap the + # clbits within; the story around nested classical-register-based control-flow + # doesn't really work in the current data model, and we hope to replace it with + # `Expr`-based control-flow everywhere. + new_block = block.copy_empty_like() + new_block._vars_input = {} + new_block._vars_capture = {} + new_block._vars_local = {} + # For the recursion, we never want to inline captured variables because we're not + # copying onto a base that has variables. + copy_with_remapping(block, new_block, bit_map, var_map, inline_captures=False) + return new_block + + variable_mapper = _classical_resource_map.VariableMapper( + dest.cregs, bit_map, var_map, add_register=dest.add_register + ) - def map_vars(op): - n_op = op.copy() if copy else op - if (condition := getattr(n_op, "condition", None)) is not None: - n_op.condition = variable_mapper.map_condition(condition) - if isinstance(n_op, SwitchCaseOp): - n_op = n_op.copy() if n_op is op else n_op - n_op.target = variable_mapper.map_target(n_op.target) - return n_op + def map_vars(op): + n_op = op + is_control_flow = isinstance(n_op, ControlFlowOp) + if ( + not is_control_flow + and (condition := getattr(n_op, "condition", None)) is not None + ): + n_op = n_op.copy() if n_op is op and copy else n_op + n_op.condition = variable_mapper.map_condition(condition) + elif is_control_flow: + n_op = n_op.replace_blocks(recurse_block(block) for block in n_op.blocks) + if isinstance(n_op, (IfElseOp, WhileLoopOp)): + n_op.condition = variable_mapper.map_condition(n_op.condition) + elif isinstance(n_op, SwitchCaseOp): + n_op.target = variable_mapper.map_target(n_op.target) + elif isinstance(n_op, Store): + n_op = Store( + variable_mapper.map_expr(n_op.lvalue), variable_mapper.map_expr(n_op.rvalue) + ) + return n_op.copy() if n_op is op and copy else n_op - mapped_instrs: CircuitData = other._data.copy() - mapped_instrs.replace_bits(qubits=mapped_qubits, clbits=mapped_clbits) - mapped_instrs.map_ops(map_vars) + instructions = source._data.copy(copy_instructions=copy) + instructions.replace_bits(qubits=new_qubits, clbits=new_clbits) + instructions.map_ops(map_vars) + dest._current_scope().extend(instructions) append_existing = None if front: - append_existing = dest._data.copy() + append_existing = dest._data.copy(copy_instructions=copy) dest.clear() - - circuit_scope = dest._current_scope() - circuit_scope.extend(mapped_instrs) + copy_with_remapping( + other, + dest, + bit_map=edge_map, + # The actual `Var: Var` map gets built up from the more freeform user input as we + # encounter the variables, since the user might be using string keys to refer to more + # than one variable in separated scopes of control-flow operations. + var_map={}, + inline_captures=inline_captures, + new_qubits=mapped_qubits, + new_clbits=mapped_clbits, + ) if append_existing: - circuit_scope.extend(append_existing) + dest._current_scope().extend(append_existing) return None if inplace else dest @@ -1097,7 +2073,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu Args: other (QuantumCircuit): The other circuit to tensor this circuit with. - inplace (bool): If True, modify the object. Otherwise return composed circuit. + inplace (bool): If ``True``, modify the object. Otherwise return composed circuit. Examples: @@ -1113,7 +2089,7 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu tensored.draw('mpl') Returns: - QuantumCircuit: The tensored circuit (returns None if inplace==True). + QuantumCircuit: The tensored circuit (returns ``None`` if ``inplace=True``). """ num_qubits = self.num_qubits + other.num_qubits num_clbits = self.num_clbits + other.num_clbits @@ -1171,23 +2147,20 @@ def tensor(self, other: "QuantumCircuit", inplace: bool = False) -> Optional["Qu @property def qubits(self) -> list[Qubit]: - """ - Returns a list of quantum bits in the order that the registers were added. - """ + """A list of :class:`Qubit`\\ s in the order that they were added. You should not mutate + this.""" return self._data.qubits @property def clbits(self) -> list[Clbit]: - """ - Returns a list of classical bits in the order that the registers were added. - """ + """A list of :class:`Clbit`\\ s in the order that they were added. You should not mutate + this.""" return self._data.clbits @property def ancillas(self) -> list[AncillaQubit]: - """ - Returns a list of ancilla bits in the order that the registers were added. - """ + """A list of :class:`AncillaQubit`\\ s in the order that they were added. You should not + mutate this.""" return self._ancillas @property @@ -1328,6 +2301,35 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list clbit_representation, self.clbits, self._clbit_indices, Clbit ) + def _append_standard_gate( + self, + op: StandardGate, + params: Sequence[ParameterValueType] | None = None, + qargs: Sequence[QubitSpecifier] | None = None, + cargs: Sequence[ClbitSpecifier] | None = None, + label: str | None = None, + ) -> InstructionSet: + """An internal method to bypass some checking when directly appending a standard gate.""" + circuit_scope = self._current_scope() + + if params is None: + params = [] + + expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] + expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] + if params is not None: + for param in params: + Gate.validate_parameter(op, param) + + instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) + broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs) + for qarg, carg in broadcast_iter: + self._check_dups(qarg) + instruction = CircuitInstruction(op, qarg, carg, params=params, label=label) + circuit_scope.append(instruction, _standard_gate=True) + instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) + return instructions + def append( self, instruction: Operation | CircuitInstruction, @@ -1425,9 +2427,38 @@ def append( if isinstance(operation, Instruction) else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) ) + params = None + if isinstance(operation, Gate): + params = operation.params + operation = PyGate( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Instruction): + params = operation.params + operation = PyInstruction( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Operation): + params = getattr(operation, "params", ()) + operation = PyOperation( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + for qarg, carg in broadcast_iter: self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) + instruction = CircuitInstruction(operation, qarg, carg, params=params) circuit_scope.append(instruction) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions @@ -1435,32 +2466,32 @@ def append( # Preferred new style. @typing.overload def _append( - self, instruction: CircuitInstruction, _qargs: None = None, _cargs: None = None + self, instruction: CircuitInstruction, *, _standard_gate: bool ) -> CircuitInstruction: ... # To-be-deprecated old style. @typing.overload def _append( self, - operation: Operation, + instruction: Operation, qargs: Sequence[Qubit], cargs: Sequence[Clbit], ) -> Operation: ... - def _append( - self, - instruction: CircuitInstruction | Instruction, - qargs: Sequence[Qubit] | None = None, - cargs: Sequence[Clbit] | None = None, - ): + def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = False): """Append an instruction to the end of the circuit, modifying the circuit in place. .. warning:: This is an internal fast-path function, and it is the responsibility of the caller to ensure that all the arguments are valid; there is no error checking here. In - particular, all the qubits and clbits must already exist in the circuit and there can be - no duplicates in the list. + particular: + + * all the qubits and clbits must already exist in the circuit and there can be no + duplicates in the list. + * any control-flow operations or classically conditioned instructions must act only on + variables present in the circuit. + * the circuit must not be within a control-flow builder context. .. note:: @@ -1469,53 +2500,58 @@ def _append( and the only reference to the circuit the instructions are being appended to is within that same function. In particular, it is not safe to call :meth:`QuantumCircuit._append` on a circuit that is received by a function argument. - This is because :meth:`.QuantumCircuit._append` will not recognise the scoping + This is because :meth:`.QuantumCircuit._append` will not recognize the scoping constructs of the control-flow builder interface. Args: - instruction: Operation instance to append - qargs: Qubits to attach the instruction to. - cargs: Clbits to attach the instruction to. + instruction: A complete well-formed :class:`.CircuitInstruction` of the operation and + its context to be added. + + In the legacy compatibility form, this can be a bare :class:`.Operation`, in which + case ``qargs`` and ``cargs`` must be explicitly given. + qargs: Legacy argument for qubits to attach the bare :class:`.Operation` to. Ignored if + the first argument is in the preferential :class:`.CircuitInstruction` form. + cargs: Legacy argument for clbits to attach the bare :class:`.Operation` to. Ignored if + the first argument is in the preferential :class:`.CircuitInstruction` form. Returns: - Operation: a handle to the instruction that was just added + CircuitInstruction: a handle to the instruction that was just added. :meta public: """ + if _standard_gate: + new_param = self._data.append(instruction) + if new_param: + self._parameters = None + self.duration = None + self.unit = "dt" + return instruction + old_style = not isinstance(instruction, CircuitInstruction) if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) - self._data.append(instruction) - self._track_operation(instruction.operation) - return instruction.operation if old_style else instruction + # If there is a reference to the outer circuit in an + # instruction param the inner rust append method will raise a runtime error. + # When this happens we need to handle the parameters separately. + # This shouldn't happen in practice but 2 tests were doing this and it's not + # explicitly prohibted by the API so this and the `params` optional argument + # path guard against it. + try: + new_param = self._data.append(instruction) + except RuntimeError: + params = [] + for idx, param in enumerate(instruction.operation.params): + if isinstance(param, (ParameterExpression, QuantumCircuit)): + params.append((idx, list(set(param.parameters)))) + new_param = self._data.append(instruction, params) + if new_param: + # clear cache if new parameter is added + self._parameters = None - def _track_operation(self, operation: Operation): - """Sync all non-data-list internal data structures for a newly tracked operation.""" - if isinstance(operation, Instruction): - self._update_parameter_table(operation) + # Invalidate whole circuit duration if an instruction is added self.duration = None self.unit = "dt" - - def _update_parameter_table(self, instruction: Instruction): - for param_index, param in enumerate(instruction.params): - if isinstance(param, (ParameterExpression, QuantumCircuit)): - # Scoped constructs like the control-flow ops use QuantumCircuit as a parameter. - atomic_parameters = set(param.parameters) - else: - atomic_parameters = set() - - for parameter in atomic_parameters: - if parameter in self._parameter_table: - self._parameter_table[parameter].add((instruction, param_index)) - else: - if parameter.name in self._parameter_table.get_names(): - raise CircuitError(f"Name conflict on adding parameter: {parameter.name}") - self._parameter_table[parameter] = ParameterReferences( - ((instruction, param_index),) - ) - - # clear cache if new parameter is added - self._parameters = None + return instruction.operation if old_style else instruction @typing.overload def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ... @@ -1546,7 +2582,7 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: my_param = Parameter("my_param") - # Create a parametrised circuit. + # Create a parametrized circuit. qc = QuantumCircuit(1) qc.rx(my_param, 0) @@ -1566,7 +2602,7 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: A similar method, but for :class:`.expr.Var` run-time variables instead of :class:`.Parameter` compile-time parameters. """ - if (parameter := self._parameter_table.parameter_from_name(name, None)) is None: + if (parameter := self._data.get_param_from_name(name)) is None: if default is Ellipsis: raise KeyError(f"no parameter named '{name}' is present") return default @@ -1762,10 +2798,21 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # two classical registers we measured into above. qc.add_var(my_var, expr.bit_and(cr1, cr2)) """ - # Validate the initialiser first to catch cases where the variable to be declared is being - # used in the initialiser. + # Validate the initializer first to catch cases where the variable to be declared is being + # used in the initializer. circuit_scope = self._current_scope() - initial = _validate_expr(circuit_scope, expr.lift(initial)) + # Convenience method to widen Python integer literals to the right width during the initial + # lift, if the type is already known via the variable. + if ( + isinstance(name_or_var, expr.Var) + and name_or_var.type.kind is types.Uint + and isinstance(initial, int) + and not isinstance(initial, bool) + ): + coerce_type = name_or_var.type + else: + coerce_type = None + initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) elif not name_or_var.standalone: @@ -1776,7 +2823,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V var = name_or_var circuit_scope.add_uninitialized_var(var) try: - # Store is responsible for ensuring the type safety of the initialisation. + # Store is responsible for ensuring the type safety of the initialization. store = Store(var, initial) except CircuitError: circuit_scope.remove_var(var) @@ -1806,7 +2853,7 @@ def add_uninitialized_var(self, var: expr.Var, /): # name, and to be a bit less ergonomic than `add_var` (i.e. not allowing the (name, type) # overload) to discourage people from using it when they should use `add_var`. # - # This function exists so that there is a method to emulate `copy_empty_like`'s behaviour of + # This function exists so that there is a method to emulate `copy_empty_like`'s behavior of # adding uninitialised variables, which there's no obvious way around. We need to be sure # that _some_ sort of handling of uninitialised variables is taken into account in our # structures, so that doesn't become a huge edge case, even though we make no assertions @@ -1840,7 +2887,7 @@ def add_capture(self, var: expr.Var): """ if self._control_flow_scopes: # Allow manual capturing. Not sure why it'd be useful, but there's a clear expected - # behaviour here. + # behavior here. self._control_flow_scopes[-1].use_var(var) return if self._vars_input: @@ -1911,14 +2958,14 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: raise CircuitError( "QuantumCircuit parameters can be Registers or Integers." " If Integers, up to 2 arguments. QuantumCircuit was called" - " with %s." % (regs,) + f" with {(regs,)}." ) for register in regs: if isinstance(register, Register) and any( register.name == reg.name for reg in self.qregs + self.cregs ): - raise CircuitError('register name "%s" already exists' % register.name) + raise CircuitError(f'register name "{register.name}" already exists') if isinstance(register, AncillaRegister): for bit in register: @@ -1934,7 +2981,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: self._data.add_qubit(bit) self._qubit_indices[bit] = BitLocations( - len(self._data.qubits) - 1, [(register, idx)] + self._data.num_qubits - 1, [(register, idx)] ) elif isinstance(register, ClassicalRegister): @@ -1946,7 +2993,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: self._data.add_clbit(bit) self._clbit_indices[bit] = BitLocations( - len(self._data.clbits) - 1, [(register, idx)] + self._data.num_clbits - 1, [(register, idx)] ) elif isinstance(register, list): @@ -1967,37 +3014,65 @@ def add_bits(self, bits: Iterable[Bit]) -> None: self._ancillas.append(bit) if isinstance(bit, Qubit): self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations(len(self._data.qubits) - 1, []) + self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, []) elif isinstance(bit, Clbit): self._data.add_clbit(bit) - self._clbit_indices[bit] = BitLocations(len(self._data.clbits) - 1, []) + self._clbit_indices[bit] = BitLocations(self._data.num_clbits - 1, []) else: raise CircuitError( "Expected an instance of Qubit, Clbit, or " - "AncillaQubit, but was passed {}".format(bit) + f"AncillaQubit, but was passed {bit}" ) def find_bit(self, bit: Bit) -> BitLocations: """Find locations in the circuit which can be used to reference a given :obj:`~Bit`. + In particular, this function can find the integer index of a qubit, which corresponds to its + hardware index for a transpiled circuit. + + .. note:: + The circuit index of a :class:`.AncillaQubit` will be its index in :attr:`qubits`, not + :attr:`ancillas`. + 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:`~QuantumCircuit.qubits`, :obj:`~QuantumCircuit.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). - - Notes: - The circuit index of an :obj:`~AncillaQubit` will be its index in - :obj:`~QuantumCircuit.qubits`, not :obj:`~QuantumCircuit.ancillas`. + contains the index at which the ``Bit`` can be found (in either + :obj:`~QuantumCircuit.qubits`, :obj:`~QuantumCircuit.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: CircuitError: If the supplied :obj:`~Bit` was of an unknown type. CircuitError: If the supplied :obj:`~Bit` could not be found on the circuit. + + Examples: + Loop through a circuit, getting the qubit and clbit indices of each operation:: + + from qiskit.circuit import QuantumCircuit, Qubit + + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.measure([0, 1, 2], [0, 1, 2]) + + # The `.qubits` and `.clbits` fields are not integers. + assert isinstance(qc.data[0].qubits[0], Qubit) + # ... but we can use `find_bit` to retrieve them. + assert qc.find_bit(qc.data[0].qubits[0]).index == 0 + + simple = [ + ( + instruction.operation.name, + [qc.find_bit(bit).index for bit in instruction.qubits], + [qc.find_bit(bit).index for bit in instruction.clbits], + ) + for instruction in qc.data + ] """ try: @@ -2023,18 +3098,22 @@ def to_instruction( parameter_map: dict[Parameter, ParameterValueType] | None = None, label: str | None = None, ) -> Instruction: - """Create an Instruction out of this circuit. + """Create an :class:`~.circuit.Instruction` out of this circuit. + + .. seealso:: + :func:`circuit_to_instruction` + The underlying driver of this method. Args: - parameter_map(dict): For parameterized circuits, a mapping from + parameter_map: For parameterized circuits, a mapping from parameters in the circuit to parameters to be used in the instruction. If None, existing circuit parameters will also parameterize the instruction. - label (str): Optional gate label. + label: Optional gate label. Returns: - qiskit.circuit.Instruction: a composite instruction encapsulating this circuit - (can be decomposed back) + qiskit.circuit.Instruction: a composite instruction encapsulating this circuit (can be + decomposed back). """ from qiskit.converters.circuit_to_instruction import circuit_to_instruction @@ -2045,18 +3124,21 @@ def to_gate( parameter_map: dict[Parameter, ParameterValueType] | None = None, label: str | None = None, ) -> Gate: - """Create a Gate out of this circuit. + """Create a :class:`.Gate` out of this circuit. The circuit must act only qubits and + contain only unitary operations. + + .. seealso:: + :func:`circuit_to_gate` + The underlying driver of this method. Args: - parameter_map(dict): For parameterized circuits, a mapping from - parameters in the circuit to parameters to be used in the - gate. If None, existing circuit parameters will also - parameterize the gate. - label (str): Optional gate label. + parameter_map: For parameterized circuits, a mapping from parameters in the circuit to + parameters to be used in the gate. If ``None``, existing circuit parameters will + also parameterize the gate. + label : Optional gate label. Returns: - Gate: a composite gate encapsulating this circuit - (can be decomposed back) + Gate: a composite gate encapsulating this circuit (can be decomposed back). """ from qiskit.converters.circuit_to_gate import circuit_to_gate @@ -2108,7 +3190,7 @@ def draw( reverse_bits: bool | None = None, justify: str | None = None, vertical_compression: str | None = "medium", - idle_wires: bool = True, + idle_wires: bool | None = None, with_layout: bool = True, fold: int | None = None, # The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be @@ -2139,7 +3221,7 @@ def draw( Args: output: Select the output method to use for drawing the circuit. Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``. - By default the `text` drawer is used unless the user config file + By default, the ``text`` drawer is used unless the user config file (usually ``~/.qiskit/settings.conf``) has an alternative backend set as the default. For example, ``circuit_drawer = latex``. If the output kwarg is set, that backend will always be used over the default in @@ -2185,7 +3267,9 @@ def draw( will take less vertical room. Default is ``medium``. Only used by the ``text`` output, will be silently ignored otherwise. idle_wires: Include idle wires (wires with no circuit elements) - in output visualization. Default is ``True``. + in output visualization. Default is ``True`` unless the + user config file (usually ``~/.qiskit/settings.conf``) has an + alternative value set. For example, ``circuit_idle_wires = False``. with_layout: Include layout information, with labels on the physical layout. Default is ``True``. fold: Sets pagination. It can be disabled using -1. In ``text``, @@ -2274,7 +3358,7 @@ def size( Args: filter_function (callable): a function to filter out some instructions. Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)). - By default filters out "directives", such as barrier or snapshot. + By default, filters out "directives", such as barrier or snapshot. Returns: int: Total number of gate operations. @@ -2283,79 +3367,74 @@ def size( def depth( self, - filter_function: Callable[..., int] = lambda x: not getattr( + filter_function: Callable[[CircuitInstruction], bool] = lambda x: not getattr( x.operation, "_directive", False ), ) -> int: """Return circuit depth (i.e., length of critical path). + .. warning:: + This operation is not well defined if the circuit contains control-flow operations. + Args: - filter_function (callable): A function to filter instructions. - Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)). - Instructions for which the function returns False are ignored in the - computation of the circuit depth. - By default filters out "directives", such as barrier or snapshot. + filter_function: A function to decide which instructions count to increase depth. + Should take as a single positional input a :class:`CircuitInstruction`. + Instructions for which the function returns ``False`` are ignored in the + computation of the circuit depth. By default, filters out "directives", such as + :class:`.Barrier`. Returns: int: Depth of circuit. - Notes: - The circuit depth and the DAG depth need not be the - same. + Examples: + Simple calculation of total circuit depth:: + + from qiskit.circuit import QuantumCircuit + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.h(2) + qc.cx(2, 3) + assert qc.depth() == 2 + + Modifying the previous example to only calculate the depth of multi-qubit gates:: + + assert qc.depth(lambda instr: len(instr.qubits) > 1) == 1 """ - # Assign each bit in the circuit a unique integer - # to index into op_stack. - bit_indices: dict[Qubit | Clbit, int] = { - bit: idx for idx, bit in enumerate(self.qubits + self.clbits) + obj_depths = { + obj: 0 for objects in (self.qubits, self.clbits, self.iter_vars()) for obj in objects } - # If no bits, return 0 - if not bit_indices: - return 0 + def update_from_expr(objects, node): + for var in expr.iter_vars(node): + if var.standalone: + objects.add(var) + else: + objects.update(_builder_utils.node_resources(var).clbits) - # A list that holds the height of each qubit - # and classical bit. - op_stack = [0] * len(bit_indices) - - # Here we are playing a modified version of - # Tetris where we stack gates, but multi-qubit - # gates, or measurements have a block for each - # qubit or cbit that are connected by a virtual - # line so that they all stacked at the same depth. - # Conditional gates act on all cbits in the register - # they are conditioned on. - # The max stack height is the circuit depth. for instruction in self._data: - levels = [] - reg_ints = [] - for ind, reg in enumerate(instruction.qubits + instruction.clbits): - # Add to the stacks of the qubits and - # cbits used in the gate. - reg_ints.append(bit_indices[reg]) - if filter_function(instruction): - levels.append(op_stack[reg_ints[ind]] + 1) + objects = set(itertools.chain(instruction.qubits, instruction.clbits)) + if (condition := getattr(instruction.operation, "condition", None)) is not None: + objects.update(_builder_utils.condition_resources(condition).clbits) + if isinstance(condition, expr.Expr): + update_from_expr(objects, condition) else: - levels.append(op_stack[reg_ints[ind]]) - # Assuming here that there is no conditional - # snapshots or barriers ever. - if getattr(instruction.operation, "condition", None): - # Controls operate over all bits of a classical register - # or over a single bit - if isinstance(instruction.operation.condition[0], Clbit): - condition_bits = [instruction.operation.condition[0]] - else: - condition_bits = instruction.operation.condition[0] - for cbit in condition_bits: - idx = bit_indices[cbit] - if idx not in reg_ints: - reg_ints.append(idx) - levels.append(op_stack[idx] + 1) - - max_level = max(levels) - for ind in reg_ints: - op_stack[ind] = max_level - - return max(op_stack) + objects.update(_builder_utils.condition_resources(condition).clbits) + elif isinstance(instruction.operation, SwitchCaseOp): + update_from_expr(objects, expr.lift(instruction.operation.target)) + elif isinstance(instruction.operation, Store): + update_from_expr(objects, instruction.operation.lvalue) + update_from_expr(objects, instruction.operation.rvalue) + + # If we're counting this as adding to depth, do so. If not, it still functions as a + # data synchronisation point between the objects (think "barrier"), so the depths still + # get updated to match the current max over the affected objects. + new_depth = max((obj_depths[obj] for obj in objects), default=0) + if filter_function(instruction): + new_depth += 1 + for obj in objects: + obj_depths[obj] = new_depth + return max(obj_depths.values(), default=0) def width(self) -> int: """Return number of qubits plus clbits in circuit. @@ -2364,12 +3443,12 @@ def width(self) -> int: int: Width of circuit. """ - return len(self.qubits) + len(self.clbits) + return self._data.width() @property def num_qubits(self) -> int: """Return number of qubits.""" - return len(self.qubits) + return self._data.num_qubits @property def num_ancillas(self) -> int: @@ -2379,7 +3458,7 @@ def num_ancillas(self) -> int: @property def num_clbits(self) -> int: """Return number of classical bits.""" - return len(self.clbits) + return self._data.num_clbits # The stringified return type is because OrderedDict can't be subscripted before Python 3.9, and # typing.OrderedDict wasn't added until 3.7.2. It can be turned into a proper type once 3.6 @@ -2400,13 +3479,7 @@ def num_nonlocal_gates(self) -> int: Conditional nonlocal gates are also included. """ - multi_qubit_gates = 0 - for instruction in self._data: - if instruction.operation.num_qubits > 1 and not getattr( - instruction.operation, "_directive", False - ): - multi_qubit_gates += 1 - return multi_qubit_gates + return self._data.num_nonlocal_gates() def get_instructions(self, name: str) -> list[CircuitInstruction]: """Get instructions matching name. @@ -2432,7 +3505,7 @@ def num_connected_components(self, unitary_only: bool = False) -> int: bits = self.qubits if unitary_only else (self.qubits + self.clbits) bit_indices: dict[Qubit | Clbit, int] = {bit: idx for idx, bit in enumerate(bits)} - # Start with each qubit or cbit being its own subgraph. + # Start with each qubit or clbit being its own subgraph. sub_graphs = [[bit] for bit in range(len(bit_indices))] num_sub_graphs = len(sub_graphs) @@ -2509,7 +3582,7 @@ def num_tensor_factors(self) -> int: """ return self.num_unitary_factors() - def copy(self, name: str | None = None) -> "QuantumCircuit": + def copy(self, name: str | None = None) -> typing.Self: """Copy the circuit. Args: @@ -2520,49 +3593,49 @@ def copy(self, name: str | None = None) -> "QuantumCircuit": """ cpy = self.copy_empty_like(name) cpy._data = self._data.copy() - - # The special global-phase sentinel doesn't need copying, but it's - # added here to ensure it's recognised. The global phase itself was - # already copied over in `copy_empty_like`. - operation_copies = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} - - def memo_copy(op): - if (out := operation_copies.get(id(op))) is not None: - return out - copied = op.copy() - operation_copies[id(op)] = copied - return copied - - cpy._data.map_ops(memo_copy) - cpy._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_copies[id(operation)], param_index) - for operation, param_index in self._parameter_table[param] - ) - for param in self._parameter_table - } - ) return cpy - def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": + def copy_empty_like( + self, + name: str | None = None, + *, + vars_mode: Literal["alike", "captures", "drop"] = "alike", + ) -> typing.Self: """Return a copy of self with the same structure but empty. That structure includes: - * name, calibrations and other metadata - * global phase - * all the qubits and clbits, including the registers + + * name, calibrations and other metadata + * global phase + * all the qubits and clbits, including the registers + * the realtime variables defined in the circuit, handled according to the ``vars`` keyword + argument. .. warning:: If the circuit contains any local variable declarations (those added by the ``declarations`` argument to the circuit constructor, or using :meth:`add_var`), they - will be **uninitialized** in the output circuit. You will need to manually add store + may be **uninitialized** in the output circuit. You will need to manually add store instructions for them (see :class:`.Store` and :meth:`.QuantumCircuit.store`) to initialize them. Args: - name (str): Name for the copied circuit. If None, then the name stays the same. + name: Name for the copied circuit. If None, then the name stays the same. + vars_mode: The mode to handle realtime variables in. + + alike + The variables in the output circuit 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 circuit. + + captures + All variables will be converted to captured variables. This is useful when you + are building a new layer for an existing circuit 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 circuit will have no variables defined. Returns: QuantumCircuit: An empty copy of self. @@ -2580,19 +3653,29 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() - # Note that this causes the local variables to be uninitialised, because the stores are not - # copied. This can leave the circuit in a potentially dangerous state for users if they - # don't re-add initialiser stores. - cpy._vars_local = self._vars_local.copy() - cpy._vars_input = self._vars_input.copy() - cpy._vars_capture = self._vars_capture.copy() - - cpy._parameter_table = ParameterTable() - for parameter in getattr(cpy.global_phase, "parameters", ()): - cpy._parameter_table[parameter] = ParameterReferences( - [(ParameterTable.GLOBAL_PHASE, None)] - ) - cpy._data = CircuitData(self._data.qubits, self._data.clbits) + if vars_mode == "alike": + # Note that this causes the local variables to be uninitialised, because the stores are + # not copied. This can leave the circuit in a potentially dangerous state for users if + # they don't re-add initializer stores. + cpy._vars_local = self._vars_local.copy() + cpy._vars_input = self._vars_input.copy() + cpy._vars_capture = self._vars_capture.copy() + elif vars_mode == "captures": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {var.name: var for var in self.iter_vars()} + elif vars_mode == "drop": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {} + else: # pragma: no cover + raise ValueError(f"unknown vars_mode: '{vars_mode}'") + + cpy._data = CircuitData( + self._data.qubits, self._data.clbits, global_phase=self._data.global_phase + ) + # Invalidate parameters caching. + cpy._parameters = None cpy._calibrations = _copy.deepcopy(self._calibrations) cpy._metadata = _copy.deepcopy(self._metadata) @@ -2605,9 +3688,13 @@ def clear(self) -> None: """Clear all instructions in self. Clearing the circuits will keep the metadata and calibrations. + + .. seealso:: + :meth:`copy_empty_like` + A method to produce a new circuit with no instructions and all the same tracking of + quantum and classical typed data, but without mutating the original circuit. """ self._data.clear() - self._parameter_table.clear() # Repopulate the parameter table with any phase symbols. self.global_phase = self.global_phase @@ -2669,7 +3756,13 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: :meth:`add_var` Create a new variable in the circuit that can be written to with this method. """ - return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), (), copy=False) + # As a convenience, lift integer-literal rvalues to the matching width. + lvalue = expr.lift(lvalue) + rvalue_type = ( + lvalue.type if isinstance(rvalue, int) and not isinstance(rvalue, bool) else None + ) + rvalue = expr.lift(rvalue, rvalue_type) + return self.append(Store(lvalue, rvalue), (), (), copy=False) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). @@ -2758,7 +3851,7 @@ def measure_active(self, inplace: bool = True) -> Optional["QuantumCircuit"]: inplace (bool): All measurements inplace or return new circuit. Returns: - QuantumCircuit: Returns circuit with measurements when `inplace = False`. + QuantumCircuit: Returns circuit with measurements when ``inplace = False``. """ from qiskit.converters.circuit_to_dag import circuit_to_dag @@ -2805,18 +3898,18 @@ def measure_all( else: circ = self.copy() if add_bits: - new_creg = circ._create_creg(len(circ.qubits), "meas") + new_creg = circ._create_creg(circ.num_qubits, "meas") circ.add_register(new_creg) circ.barrier() circ.measure(circ.qubits, new_creg) else: - if len(circ.clbits) < len(circ.qubits): + if circ.num_clbits < circ.num_qubits: raise CircuitError( "The number of classical bits must be equal or greater than " "the number of qubits." ) circ.barrier() - circ.measure(circ.qubits, circ.clbits[0 : len(circ.qubits)]) + circ.measure(circ.qubits, circ.clbits[0 : circ.num_qubits]) if not inplace: return circ @@ -2833,6 +3926,28 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi Measurements and barriers are considered final if they are followed by no other operations (aside from other measurements or barriers.) + .. note:: + This method has rather complex behavior, particularly around the removal of newly idle + classical bits and registers. It is much more efficient to avoid adding unnecessary + classical data in the first place, rather than trying to remove it later. + + .. seealso:: + :class:`.RemoveFinalMeasurements` + A transpiler pass that removes final measurements and barriers. This does not + remove the classical data. If this is your goal, you can call that with:: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import RemoveFinalMeasurements + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.barrier() + qc.measure([0, 1], [0, 1]) + + pass_ = RemoveFinalMeasurements() + just_bell = pass_(qc) + Args: inplace (bool): All measurements removed inplace or return new circuit. @@ -2863,10 +3978,9 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi circ._clbit_indices = {} # Clear instruction info - circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data)) - circ._parameter_table.clear() - # Repopulate the parameter table with any global-phase entries. - circ.global_phase = circ.global_phase + circ._data = CircuitData( + qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase + ) # We must add the clbits first to preserve the original circuit # order. This way, add_register never adds clbits and just @@ -2936,10 +4050,10 @@ def from_qasm_str(qasm_str: str) -> "QuantumCircuit": @property def global_phase(self) -> ParameterValueType: - """Return the global phase of the current circuit scope in radians.""" + """The global phase of the current circuit scope in radians.""" if self._control_flow_scopes: return self._control_flow_scopes[-1].global_phase - return self._global_phase + return self._data.global_phase @global_phase.setter def global_phase(self, angle: ParameterValueType): @@ -2949,24 +4063,19 @@ def global_phase(self, angle: ParameterValueType): angle (float, ParameterExpression): radians """ # If we're currently parametric, we need to throw away the references. This setter is - # called by some subclasses before the inner `_global_phase` is initialised. - global_phase_reference = (ParameterTable.GLOBAL_PHASE, None) - if isinstance(previous := getattr(self, "_global_phase", None), ParameterExpression): + # called by some subclasses before the inner `_global_phase` is initialized. + if isinstance(getattr(self._data, "global_phase", None), ParameterExpression): self._parameters = None - self._parameter_table.discard_references(previous, global_phase_reference) - - if isinstance(angle, ParameterExpression) and angle.parameters: - for parameter in angle.parameters: - if parameter not in self._parameter_table: - self._parameters = None - self._parameter_table[parameter] = ParameterReferences(()) - self._parameter_table[parameter].add(global_phase_reference) + if isinstance(angle, ParameterExpression): + if angle.parameters: + self._parameters = None else: angle = _normalize_global_phase(angle) + if self._control_flow_scopes: self._control_flow_scopes[-1].global_phase = angle else: - self._global_phase = angle + self._data.global_phase = angle @property def parameters(self) -> ParameterView: @@ -3036,7 +4145,7 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: """The number of parameter objects in the circuit.""" - return len(self._parameter_table) + return self._data.num_params() def _unsorted_parameters(self) -> set[Parameter]: """Efficiently get all parameters in the circuit, without any sorting overhead. @@ -3049,7 +4158,7 @@ def _unsorted_parameters(self) -> set[Parameter]: """ # This should be free, by accessing the actual backing data structure of the table, but that # means that we need to copy it if adding keys from the global phase. - return self._parameter_table.get_keys() + return self._data.get_params_unsorted() @overload def assign_parameters( @@ -3166,7 +4275,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc target._increment_instances() target._name_update() - # Normalise the inputs into simple abstract interfaces, so we've dispatched the "iteration" + # Normalize the inputs into simple abstract interfaces, so we've dispatched the "iteration" # logic in one place at the start of the function. This lets us do things like calculate # and cache expensive properties for (e.g.) the sequence format only if they're used; for # many large, close-to-hardware circuits, we won't need the extra handling for @@ -3198,7 +4307,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc target._parameters = None # This is deliberately eager, because we want the side effect of clearing the table. all_references = [ - (parameter, value, target._parameter_table.pop(parameter, ())) + (parameter, value, target._data.pop_param(parameter.uuid.int, parameter.name, ())) for parameter, value in parameter_binds.items() ] seen_operations = {} @@ -3209,20 +4318,28 @@ def assign_parameters( # pylint: disable=missing-raises-doc if isinstance(bound_value, ParameterExpression) else () ) - for operation, index in references: - seen_operations[id(operation)] = operation - if operation is ParameterTable.GLOBAL_PHASE: + for inst_index, index in references: + if inst_index == self._data.global_phase_param_index: + operation = None + seen_operations[inst_index] = None assignee = target.global_phase validate = _normalize_global_phase else: + operation = target._data[inst_index].operation + seen_operations[inst_index] = operation assignee = operation.params[index] validate = operation.validate_parameter if isinstance(assignee, ParameterExpression): new_parameter = assignee.assign(to_bind, bound_value) for parameter in update_parameters: - if parameter not in target._parameter_table: - target._parameter_table[parameter] = ParameterReferences(()) - target._parameter_table[parameter].add((operation, index)) + if not target._data.contains_param(parameter.uuid.int): + target._data.add_new_parameter(parameter, inst_index, index) + else: + target._data.update_parameter_entry( + parameter.uuid.int, + inst_index, + index, + ) if not new_parameter.parameters: new_parameter = validate(new_parameter.numeric()) elif isinstance(assignee, QuantumCircuit): @@ -3234,12 +4351,18 @@ def assign_parameters( # pylint: disable=missing-raises-doc f"Saw an unknown type during symbolic binding: {assignee}." " This may indicate an internal logic error in symbol tracking." ) - if operation is ParameterTable.GLOBAL_PHASE: + if inst_index == self._data.global_phase_param_index: # We've already handled parameter table updates in bulk, so we need to skip the # public setter trying to do it again. - target._global_phase = new_parameter + target._data.global_phase = new_parameter else: - operation.params[index] = new_parameter + temp_params = operation.params + temp_params[index] = new_parameter + operation.params = temp_params + target._data.setitem_no_param_table_update( + inst_index, + target._data[inst_index].replace(operation=operation, params=temp_params), + ) # After we've been through everything at the top level, make a single visit to each # operation we've seen, rebinding its definition if necessary. @@ -3286,6 +4409,7 @@ def map_calibration(qubits, parameters, schedule): for gate, calibrations in target._calibrations.items() ), ) + target._parameters = None return None if inplace else target def _unroll_param_dict( @@ -3368,9 +4492,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.h import HGate - - return self.append(HGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.HGate, [], qargs=[qubit]) def ch( self, @@ -3394,6 +4516,12 @@ def ch( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CHGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.h import CHGate return self.append( @@ -3414,9 +4542,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali Returns: A handle to the instructions created. """ - from .library.standard_gates.i import IGate - - return self.append(IGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.IGate, None, qargs=[qubit]) def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.MSGate`. @@ -3447,9 +4573,7 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.p import PhaseGate - - return self.append(PhaseGate(theta), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.PhaseGate, [theta], qargs=[qubit]) def cp( self, @@ -3475,6 +4599,12 @@ def cp( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CPhaseGate, [theta], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.p import CPhaseGate return self.append( @@ -3531,9 +4661,7 @@ def r( Returns: A handle to the instructions created. """ - from .library.standard_gates.r import RGate - - return self.append(RGate(theta, phi), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RGate, [theta, phi], qargs=[qubit]) def rv( self, @@ -3630,9 +4758,7 @@ def rx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rx import RXGate - - return self.append(RXGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], None, label=label) def crx( self, @@ -3658,6 +4784,12 @@ def crx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRXGate, [theta], [control_qubit, target_qubit], None, label=label + ) + from .library.standard_gates.rx import CRXGate return self.append( @@ -3682,9 +4814,7 @@ def rxx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rxx import RXXGate - - return self.append(RXXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RXXGate, [theta], [qubit1, qubit2]) def ry( self, theta: ParameterValueType, qubit: QubitSpecifier, label: str | None = None @@ -3701,9 +4831,7 @@ def ry( Returns: A handle to the instructions created. """ - from .library.standard_gates.ry import RYGate - - return self.append(RYGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], None, label=label) def cry( self, @@ -3729,6 +4857,12 @@ def cry( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRYGate, [theta], [control_qubit, target_qubit], None, label=label + ) + from .library.standard_gates.ry import CRYGate return self.append( @@ -3753,9 +4887,7 @@ def ryy( Returns: A handle to the instructions created. """ - from .library.standard_gates.ryy import RYYGate - - return self.append(RYYGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RYYGate, [theta], [qubit1, qubit2]) def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.RZGate`. @@ -3769,9 +4901,7 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.rz import RZGate - - return self.append(RZGate(phi), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RZGate, [phi], [qubit], None) def crz( self, @@ -3797,6 +4927,12 @@ def crz( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CRZGate, [theta], [control_qubit, target_qubit], None, label=label + ) + from .library.standard_gates.rz import CRZGate return self.append( @@ -3821,9 +4957,7 @@ def rzx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzx import RZXGate - - return self.append(RZXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZXGate, [theta], [qubit1, qubit2]) def rzz( self, theta: ParameterValueType, qubit1: QubitSpecifier, qubit2: QubitSpecifier @@ -3840,9 +4974,7 @@ def rzz( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzz import RZZGate - - return self.append(RZZGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZZGate, [theta], [qubit1, qubit2]) def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.ECRGate`. @@ -3855,9 +4987,7 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.ecr import ECRGate - - return self.append(ECRGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.ECRGate, [], qargs=[qubit1, qubit2]) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -3870,9 +5000,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.s import SGate - - return self.append(SGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SGate, [], qargs=[qubit]) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -3885,9 +5013,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.s import SdgGate - - return self.append(SdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SdgGate, [], qargs=[qubit]) def cs( self, @@ -3911,6 +5037,12 @@ def cs( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSGate return self.append( @@ -3942,6 +5074,12 @@ def csdg( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSdgGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSdgGate return self.append( @@ -3962,9 +5100,11 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet Returns: A handle to the instructions created. """ - from .library.standard_gates.swap import SwapGate - - return self.append(SwapGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate( + StandardGate.SwapGate, + [], + qargs=[qubit1, qubit2], + ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.iSwapGate`. @@ -3977,9 +5117,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe Returns: A handle to the instructions created. """ - from .library.standard_gates.iswap import iSwapGate - - return self.append(iSwapGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.ISwapGate, [], qargs=[qubit1, qubit2]) def cswap( self, @@ -4005,6 +5143,15 @@ def cswap( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSwapGate, + [], + qargs=[control_qubit, target_qubit1, target_qubit2], + label=label, + ) + from .library.standard_gates.swap import CSwapGate return self.append( @@ -4025,9 +5172,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.sx import SXGate - - return self.append(SXGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SXGate, [], qargs=[qubit]) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -4040,9 +5185,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.sx import SXdgGate - - return self.append(SXdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SXdgGate, [], qargs=[qubit]) def csx( self, @@ -4066,6 +5209,12 @@ def csx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSXGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.sx import CSXGate return self.append( @@ -4086,9 +5235,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.t import TGate - - return self.append(TGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.TGate, [], qargs=[qubit]) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -4101,9 +5248,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.t import TdgGate - - return self.append(TdgGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.TdgGate, [], qargs=[qubit]) def u( self, @@ -4125,9 +5270,7 @@ def u( Returns: A handle to the instructions created. """ - from .library.standard_gates.u import UGate - - return self.append(UGate(theta, phi, lam), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.UGate, [theta, phi, lam], qargs=[qubit]) def cu( self, @@ -4180,9 +5323,7 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.x import XGate - - return self.append(XGate(label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.XGate, None, qargs=[qubit], label=label) def cx( self, @@ -4206,6 +5347,15 @@ def cx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CXGate, + [], + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, + ) from .library.standard_gates.x import CXGate @@ -4228,9 +5378,7 @@ def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.dcx import DCXGate - - return self.append(DCXGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(op=StandardGate.DCXGate, qargs=[qubit1, qubit2]) def ccx( self, @@ -4254,6 +5402,15 @@ def ccx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CCXGate, + [], + qargs=[control_qubit1, control_qubit2, target_qubit], + cargs=None, + ) + from .library.standard_gates.x import CCXGate return self.append( @@ -4299,12 +5456,12 @@ def mcx( ValueError: if the given mode is not known, or if too few ancilla qubits are passed. AttributeError: if no ancilla qubits are passed, but some are needed. """ - from .library.standard_gates.x import MCXGrayCode, MCXRecursive, MCXVChain + from .library.standard_gates.x import MCXGate, MCXRecursive, MCXVChain num_ctrl_qubits = len(control_qubits) available_implementations = { - "noancilla": MCXGrayCode(num_ctrl_qubits, ctrl_state=ctrl_state), + "noancilla": MCXGate(num_ctrl_qubits, ctrl_state=ctrl_state), "recursion": MCXRecursive(num_ctrl_qubits, ctrl_state=ctrl_state), "v-chain": MCXVChain(num_ctrl_qubits, False, ctrl_state=ctrl_state), "v-chain-dirty": MCXVChain(num_ctrl_qubits, dirty_ancillas=True, ctrl_state=ctrl_state), @@ -4358,9 +5515,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.y import YGate - - return self.append(YGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.YGate, None, qargs=[qubit]) def cy( self, @@ -4384,6 +5539,16 @@ def cy( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CYGate, + [], + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, + ) + from .library.standard_gates.y import CYGate return self.append( @@ -4404,9 +5569,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.z import ZGate - - return self.append(ZGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.ZGate, None, qargs=[qubit]) def cz( self, @@ -4430,6 +5593,12 @@ def cz( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.z import CZGate return self.append( @@ -4624,7 +5793,7 @@ class to prepare the qubits in a specified state. * Statevector or vector of complex amplitudes to initialize to. * Labels of basis states of the Pauli eigenstates Z, X, Y. See :meth:`.Statevector.from_label`. Notice the order of the labels is reversed with - respect to the qubit index to be applied to. Example label '01' initializes the + respect to the qubit index to be applied to. Example label ``'01'`` initializes the qubit zero to :math:`|1\rangle` and the qubit one to :math:`|0\rangle`. * An integer that is used as a bitmap indicating which qubits to initialize to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit @@ -4798,7 +5967,7 @@ def _pop_scope(self) -> ControlFlowBuilderBlock: """Finish a scope used in the control-flow builder interface, and return it to the caller. This should only be done by the control-flow context managers, since they naturally - synchronise the creation and deletion of stack elements.""" + synchronize the creation and deletion of stack elements.""" return self._control_flow_scopes.pop() def _peek_previous_instruction_in_scope(self) -> CircuitInstruction: @@ -4825,36 +5994,9 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: if not self._data: raise CircuitError("This circuit contains no instructions.") instruction = self._data.pop() - if isinstance(instruction.operation, Instruction): - self._update_parameter_table_on_instruction_removal(instruction) + self._parameters = None return instruction - def _update_parameter_table_on_instruction_removal(self, instruction: CircuitInstruction): - """Update the :obj:`.ParameterTable` of this circuit given that an instance of the given - ``instruction`` has just been removed from the circuit. - - .. note:: - - This does not account for the possibility for the same instruction instance being added - more than once to the circuit. At the time of writing (2021-11-17, main commit 271a82f) - there is a defensive ``deepcopy`` of parameterised instructions inside - :meth:`.QuantumCircuit.append`, so this should be safe. Trying to account for it would - involve adding a potentially quadratic-scaling loop to check each entry in ``data``. - """ - atomic_parameters: list[tuple[Parameter, int]] = [] - for index, parameter in enumerate(instruction.operation.params): - if isinstance(parameter, (ParameterExpression, QuantumCircuit)): - atomic_parameters.extend((p, index) for p in parameter.parameters) - for atomic_parameter, index in atomic_parameters: - new_entries = self._parameter_table[atomic_parameter].copy() - new_entries.discard((instruction.operation, index)) - if not new_entries: - del self._parameter_table[atomic_parameter] - # Invalidate cache. - self._parameters = None - else: - self._parameter_table[atomic_parameter] = new_entries - @typing.overload def while_loop( self, @@ -5032,15 +6174,7 @@ def for_loop( ) @typing.overload - def if_test( - self, - condition: tuple[ClassicalRegister | Clbit, int], - true_body: None, - qubits: None, - clbits: None, - *, - label: str | None, - ) -> IfContext: ... + def if_test(self, condition: tuple[ClassicalRegister | Clbit, int]) -> IfContext: ... @typing.overload def if_test( @@ -5508,13 +6642,15 @@ def __init__(self, circuit: QuantumCircuit): def instructions(self): return self.circuit._data - def append(self, instruction): + def append(self, instruction, *, _standard_gate: bool = False): # QuantumCircuit._append is semi-public, so we just call back to it. - return self.circuit._append(instruction) + return self.circuit._append(instruction, _standard_gate=_standard_gate) def extend(self, data: CircuitData): self.circuit._data.extend(data) - data.foreach_op(self.circuit._track_operation) + self.circuit._parameters = None + self.circuit.duration = None + self.circuit.unit = "dt" def resolve_classical_resource(self, specifier): # This is slightly different to cbit_argument_conversion, because it should not diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 3e29f36c6be..9ecc8e6a6ca 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -45,8 +45,6 @@ def __setitem__(self, key, value): operation, qargs, cargs = value value = self._resolve_legacy_value(operation, qargs, cargs) self._circuit._data[key] = value - if isinstance(value.operation, Instruction): - self._circuit._update_parameter_table(value.operation) def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: """Resolve the old-style 3-tuple into the new :class:`CircuitInstruction` type.""" @@ -76,7 +74,7 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: return CircuitInstruction(operation, tuple(qargs), tuple(cargs)) def insert(self, index, value): - self._circuit._data.insert(index, CircuitInstruction(None, (), ())) + self._circuit._data.insert(index, value.replace(qubits=(), clbits=())) try: self[index] = value except CircuitError: diff --git a/qiskit/circuit/quantumregister.py b/qiskit/circuit/quantumregister.py index 2ae815b1d17..97d1392698e 100644 --- a/qiskit/circuit/quantumregister.py +++ b/qiskit/circuit/quantumregister.py @@ -43,7 +43,7 @@ def __init__(self, register=None, index=None): super().__init__(register, index) else: raise CircuitError( - "Qubit needs a QuantumRegister and %s was provided" % type(register).__name__ + f"Qubit needs a QuantumRegister and {type(register).__name__} was provided" ) diff --git a/qiskit/circuit/random/__init__.py b/qiskit/circuit/random/__init__.py index 3e3dc752d5a..06e817bb4de 100644 --- a/qiskit/circuit/random/__init__.py +++ b/qiskit/circuit/random/__init__.py @@ -12,4 +12,4 @@ """Method for generating random circuits.""" -from .utils import random_circuit +from .utils import random_circuit, random_clifford_circuit diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 71809735aa8..3bcdbeaef4a 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017. +# (C) Copyright IBM 2017, 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 @@ -21,7 +21,14 @@ def random_circuit( - num_qubits, depth, max_operands=4, measure=False, conditional=False, reset=False, seed=None + num_qubits, + depth, + max_operands=4, + measure=False, + conditional=False, + reset=False, + seed=None, + num_operand_distribution: dict = None, ): """Generate random circuit of arbitrary size and form. @@ -44,6 +51,10 @@ def random_circuit( conditional (bool): if True, insert middle measurements and conditionals reset (bool): if True, insert middle resets seed (int): sets random seed (optional) + num_operand_distribution (dict): a distribution of gates that specifies the ratio + of 1-qubit, 2-qubit, 3-qubit, ..., n-qubit gates in the random circuit. Expect a + deviation from the specified ratios that depends on the size of the requested + random circuit. (optional) Returns: QuantumCircuit: constructed circuit @@ -51,11 +62,38 @@ def random_circuit( Raises: CircuitError: when invalid options given """ + if seed is None: + seed = np.random.randint(0, np.iinfo(np.int32).max) + rng = np.random.default_rng(seed) + + if num_operand_distribution: + if min(num_operand_distribution.keys()) < 1 or max(num_operand_distribution.keys()) > 4: + raise CircuitError("'num_operand_distribution' must have keys between 1 and 4") + for key, prob in num_operand_distribution.items(): + if key > num_qubits and prob != 0.0: + raise CircuitError( + f"'num_operand_distribution' cannot have {key}-qubit gates" + f" for circuit with {num_qubits} qubits" + ) + num_operand_distribution = dict(sorted(num_operand_distribution.items())) + + if not num_operand_distribution and max_operands: + if max_operands < 1 or max_operands > 4: + raise CircuitError("max_operands must be between 1 and 4") + max_operands = max_operands if num_qubits > max_operands else num_qubits + rand_dist = rng.dirichlet( + np.ones(max_operands) + ) # This will create a random distribution that sums to 1 + num_operand_distribution = {i + 1: rand_dist[i] for i in range(max_operands)} + num_operand_distribution = dict(sorted(num_operand_distribution.items())) + + # Here we will use np.isclose() because very rarely there might be floating + # point precision errors + if not np.isclose(sum(num_operand_distribution.values()), 1): + raise CircuitError("The sum of all the values in 'num_operand_distribution' is not 1.") + if num_qubits == 0: return QuantumCircuit() - if max_operands < 1 or max_operands > 4: - raise CircuitError("max_operands must be between 1 and 4") - max_operands = max_operands if num_qubits > max_operands else num_qubits gates_1q = [ # (Gate class, number of qubits, number of parameters) @@ -119,17 +157,26 @@ def random_circuit( (standard_gates.RC3XGate, 4, 0), ] - gates = gates_1q.copy() - if max_operands >= 2: - gates.extend(gates_2q) - if max_operands >= 3: - gates.extend(gates_3q) - if max_operands >= 4: - gates.extend(gates_4q) - gates = np.array( - gates, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] + gates_1q = np.array( + gates_1q, dtype=[("class", object), ("num_qubits", np.int64), ("num_params", np.int64)] ) - gates_1q = np.array(gates_1q, dtype=gates.dtype) + gates_2q = np.array(gates_2q, dtype=gates_1q.dtype) + gates_3q = np.array(gates_3q, dtype=gates_1q.dtype) + gates_4q = np.array(gates_4q, dtype=gates_1q.dtype) + + all_gate_lists = [gates_1q, gates_2q, gates_3q, gates_4q] + + # Here we will create a list 'gates_to_consider' that will have a + # subset of different n-qubit gates and will also create a list for + # ratio (or probability) for each gates + gates_to_consider = [] + distribution = [] + for n_qubits, ratio in num_operand_distribution.items(): + gate_list = all_gate_lists[n_qubits - 1] + gates_to_consider.extend(gate_list) + distribution.extend([ratio / len(gate_list)] * len(gate_list)) + + gates = np.array(gates_to_consider, dtype=gates_1q.dtype) qc = QuantumCircuit(num_qubits) @@ -137,33 +184,65 @@ def random_circuit( cr = ClassicalRegister(num_qubits, "c") qc.add_register(cr) - if seed is None: - seed = np.random.randint(0, np.iinfo(np.int32).max) - rng = np.random.default_rng(seed) - qubits = np.array(qc.qubits, dtype=object, copy=True) + # Counter to keep track of number of different gate types + counter = np.zeros(len(all_gate_lists) + 1, dtype=np.int64) + total_gates = 0 + # Apply arbitrary random operations in layers across all qubits. for layer_number in range(depth): # We generate all the randomness for the layer in one go, to avoid many separate calls to - # the randomisation routines, which can be fairly slow. + # the randomization routines, which can be fairly slow. # This reliably draws too much randomness, but it's less expensive than looping over more # calls to the rng. After, trim it down by finding the point when we've used all the qubits. - gate_specs = rng.choice(gates, size=len(qubits)) + + # Due to the stochastic nature of generating a random circuit, the resulting ratios + # may not precisely match the specified values from `num_operand_distribution`. Expect + # greater deviations from the target ratios in quantum circuits with fewer qubits and + # shallower depths, and smaller deviations in larger and deeper quantum circuits. + # For more information on how the distribution changes with number of qubits and depth + # refer to the pull request #12483 on Qiskit GitHub. + + gate_specs = rng.choice(gates, size=len(qubits), p=distribution) cumulative_qubits = np.cumsum(gate_specs["num_qubits"], dtype=np.int64) + # Efficiently find the point in the list where the total gates would use as many as # possible of, but not more than, the number of qubits in the layer. If there's slack, fill # it with 1q gates. max_index = np.searchsorted(cumulative_qubits, num_qubits, side="right") gate_specs = gate_specs[:max_index] + slack = num_qubits - cumulative_qubits[max_index - 1] - if slack: - gate_specs = np.hstack((gate_specs, rng.choice(gates_1q, size=slack))) - # For efficiency in the Python loop, this uses Numpy vectorisation to pre-calculate the + # Updating the counter for 1-qubit, 2-qubit, 3-qubit and 4-qubit gates + gate_qubits = gate_specs["num_qubits"] + counter += np.bincount(gate_qubits, minlength=len(all_gate_lists) + 1) + + total_gates += len(gate_specs) + + # Slack handling loop, this loop will add gates to fill + # the slack while respecting the 'num_operand_distribution' + while slack > 0: + gate_added_flag = False + + for key, dist in sorted(num_operand_distribution.items(), reverse=True): + if slack >= key and counter[key] / total_gates < dist: + gate_to_add = np.array( + all_gate_lists[key - 1][rng.integers(0, len(all_gate_lists[key - 1]))] + ) + gate_specs = np.hstack((gate_specs, gate_to_add)) + counter[key] += 1 + total_gates += 1 + slack -= key + gate_added_flag = True + if not gate_added_flag: + break + + # For efficiency in the Python loop, this uses Numpy vectorization to pre-calculate the # indices into the lists of qubits and parameters for every gate, and then suitably - # randomises those lists. + # randomizes those lists. q_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) p_indices = np.empty(len(gate_specs) + 1, dtype=np.int64) q_indices[0] = p_indices[0] = 0 @@ -202,8 +281,76 @@ def random_circuit( ): operation = gate(*parameters[p_start:p_end]) qc._append(CircuitInstruction(operation=operation, qubits=qubits[q_start:q_end])) - if measure: qc.measure(qc.qubits, cr) return qc + + +def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): + """Generate a pseudo-random Clifford circuit. + + This function will generate a Clifford circuit by randomly selecting the chosen amount of Clifford + gates from the set of standard gates in :mod:`qiskit.circuit.library.standard_gates`. For example: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') + + Args: + num_qubits (int): number of quantum wires. + num_gates (int): number of gates in the circuit. + gates (list[str]): optional list of Clifford gate names to randomly sample from. + If ``"all"`` (default), use all Clifford gates in the standard library. + seed (int | np.random.Generator): sets random seed/generator (optional). + + Returns: + QuantumCircuit: constructed circuit + """ + + gates_1q = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg"] + gates_2q = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] + if gates == "all": + if num_qubits == 1: + gates = gates_1q + else: + gates = gates_1q + gates_2q + + instructions = { + "i": (standard_gates.IGate(), 1), + "x": (standard_gates.XGate(), 1), + "y": (standard_gates.YGate(), 1), + "z": (standard_gates.ZGate(), 1), + "h": (standard_gates.HGate(), 1), + "s": (standard_gates.SGate(), 1), + "sdg": (standard_gates.SdgGate(), 1), + "sx": (standard_gates.SXGate(), 1), + "sxdg": (standard_gates.SXdgGate(), 1), + "cx": (standard_gates.CXGate(), 2), + "cy": (standard_gates.CYGate(), 2), + "cz": (standard_gates.CZGate(), 2), + "swap": (standard_gates.SwapGate(), 2), + "iswap": (standard_gates.iSwapGate(), 2), + "ecr": (standard_gates.ECRGate(), 2), + "dcx": (standard_gates.DCXGate(), 2), + } + + if isinstance(seed, np.random.Generator): + rng = seed + else: + rng = np.random.default_rng(seed) + + samples = rng.choice(gates, num_gates) + + circ = QuantumCircuit(num_qubits) + + for name in samples: + gate, nqargs = instructions[name] + qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() + circ.append(gate, qargs, copy=False) + + return circ diff --git a/qiskit/circuit/register.py b/qiskit/circuit/register.py index e927d10e736..39345705aae 100644 --- a/qiskit/circuit/register.py +++ b/qiskit/circuit/register.py @@ -67,7 +67,7 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): if (size, bits) == (None, None) or (size is not None and bits is not None): raise CircuitError( "Exactly one of the size or bits arguments can be " - "provided. Provided size=%s bits=%s." % (size, bits) + f"provided. Provided size={size} bits={bits}." ) # validate (or cast) size @@ -81,20 +81,18 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): if not valid_size: raise CircuitError( - "Register size must be an integer. (%s '%s' was provided)" - % (type(size).__name__, size) + f"Register size must be an integer. ({type(size).__name__} '{size}' was provided)" ) size = int(size) # cast to int if size < 0: raise CircuitError( - "Register size must be non-negative (%s '%s' was provided)" - % (type(size).__name__, size) + f"Register size must be non-negative ({type(size).__name__} '{size}' was provided)" ) # validate (or cast) name if name is None: - name = "%s%i" % (self.prefix, next(self.instances_counter)) + name = f"{self.prefix}{next(self.instances_counter)}" else: try: name = str(name) @@ -108,7 +106,7 @@ def __init__(self, size: int | None = None, name: str | None = None, bits=None): self._size = size self._hash = hash((type(self), self._name, self._size)) - self._repr = "%s(%d, '%s')" % (self.__class__.__qualname__, self.size, self.name) + self._repr = f"{self.__class__.__qualname__}({self.size}, '{self.name}')" if bits is not None: # check duplicated bits if self._size != len(set(bits)): diff --git a/qiskit/circuit/singleton.py b/qiskit/circuit/singleton.py index bd689b6be10..874b979ff58 100644 --- a/qiskit/circuit/singleton.py +++ b/qiskit/circuit/singleton.py @@ -42,7 +42,7 @@ heart of Qiskit's data model for circuits. From a library-author perspective, the minimum that is needed to enhance a :class:`.Gate` or -:class:`~.circuit.Instruction` with this behaviour is to inherit from :class:`SingletonGate` +:class:`~.circuit.Instruction` with this behavior is to inherit from :class:`SingletonGate` (:class:`SingletonInstruction`) instead of :class:`.Gate` (:class:`~.circuit.Instruction`), and for the ``__init__`` method to have defaults for all of its arguments (these will be the state of the singleton instance). For example:: @@ -175,7 +175,7 @@ def _singleton_lookup_key(n=1, label=None): This section is primarily developer documentation for the code; none of the machinery described here is public, and it is not safe to inherit from any of it directly. -There are several moving parts to tackle here. The behaviour of having ``XGate()`` return some +There are several moving parts to tackle here. The behavior of having ``XGate()`` return some singleton object that is an (inexact) instance of :class:`.XGate` but *without* calling ``__init__`` requires us to override :class:`type.__call__ `. This means that :class:`.XGate` must have a metaclass that defines ``__call__`` to return the singleton instance. @@ -484,7 +484,7 @@ class they are providing overrides for has more lazy attributes or user-exposed instruction._define() # We use this `list` subclass that rejects all mutation rather than a simple `tuple` because # the `params` typing is specified as `list`. Various places in the library and beyond do - # `x.params.copy()` when they want to produce a version they own, which is good behaviour, + # `x.params.copy()` when they want to produce a version they own, which is good behavior, # and would fail if we switched to a `tuple`, which has no `copy` method. instruction._params = _frozenlist(instruction._params) return instruction diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py index 857cb4f6c2d..6bbc5439332 100644 --- a/qiskit/circuit/store.py +++ b/qiskit/circuit/store.py @@ -59,6 +59,9 @@ class Store(Instruction): :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for subclassing.""" + # This is a compiler/backend intrinsic operation, separate to any quantum processing. + _directive = True + def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): """ Args: diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py index d3614b74782..334b9683ae9 100644 --- a/qiskit/circuit/tools/pi_check.py +++ b/qiskit/circuit/tools/pi_check.py @@ -104,9 +104,9 @@ def normalize(single_inpt): if power[0].shape[0]: if output == "qasm": if ndigits is None: - str_out = "{}".format(single_inpt) + str_out = str(single_inpt) else: - str_out = "{:.{}g}".format(single_inpt, ndigits) + str_out = f"{single_inpt:.{ndigits}g}" elif output == "latex": str_out = f"{neg_str}{pi}^{power[0][0] + 2}" elif output == "mpl": @@ -119,9 +119,9 @@ def normalize(single_inpt): # multiple or power of pi, since no fractions will exceed MAX_FRAC * pi if abs(single_inpt) >= (MAX_FRAC * np.pi): if ndigits is None: - str_out = "{}".format(single_inpt) + str_out = str(single_inpt) else: - str_out = "{:.{}g}".format(single_inpt, ndigits) + str_out = f"{single_inpt:.{ndigits}g}" return str_out # Fourth check is for fractions for 1*pi in the numer and any diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index a6c5212e233..522e1c503dd 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -34,7 +34,7 @@ def _log_assembly_time(start_time, end_time): - log_msg = "Total Assembly Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Assembly Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) @@ -311,8 +311,8 @@ def _parse_common_args( raise QiskitError("Argument 'shots' should be of type 'int'") elif max_shots and max_shots < shots: raise QiskitError( - "Number of shots specified: %s exceeds max_shots property of the " - "backend: %s." % (shots, max_shots) + f"Number of shots specified: {max_shots} exceeds max_shots property of the " + f"backend: {max_shots}." ) dynamic_reprate_enabled = getattr(backend_config, "dynamic_reprate_enabled", False) @@ -397,9 +397,8 @@ def _check_lo_freqs( raise QiskitError(f"Each element of {lo_type} LO range must be a 2d list.") if freq < freq_range[0] or freq > freq_range[1]: raise QiskitError( - "Qubit {} {} LO frequency is {}. The range is [{}, {}].".format( - i, lo_type, freq, freq_range[0], freq_range[1] - ) + f"Qubit {i} {lo_type} LO frequency is {freq}. " + f"The range is [{freq_range[0]}, {freq_range[1]}]." ) @@ -429,9 +428,8 @@ def _parse_pulse_args( if meas_level not in getattr(backend_config, "meas_levels", [MeasLevel.CLASSIFIED]): raise QiskitError( - ("meas_level = {} not supported for backend {}, only {} is supported").format( - meas_level, backend_config.backend_name, backend_config.meas_levels - ) + f"meas_level = {meas_level} not supported for backend " + f"{backend_config.backend_name}, only {backend_config.meas_levels} is supported" ) meas_map = meas_map or getattr(backend_config, "meas_map", None) @@ -522,14 +520,12 @@ def _parse_rep_delay( if rep_delay_range is not None and isinstance(rep_delay_range, list): if len(rep_delay_range) != 2: raise QiskitError( - "Backend rep_delay_range {} must be a list with two entries.".format( - rep_delay_range - ) + f"Backend rep_delay_range {rep_delay_range} must be a list with two entries." ) if not rep_delay_range[0] <= rep_delay <= rep_delay_range[1]: raise QiskitError( - "Supplied rep delay {} not in the supported " - "backend range {}".format(rep_delay, rep_delay_range) + f"Supplied rep delay {rep_delay} not in the supported " + f"backend range {rep_delay_range}" ) rep_delay = rep_delay * 1e6 # convert sec to μs diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py index 0a30b07a49b..f141902b706 100644 --- a/qiskit/compiler/scheduler.py +++ b/qiskit/compiler/scheduler.py @@ -31,7 +31,7 @@ def _log_schedule_time(start_time, end_time): - log_msg = "Total Scheduling Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Scheduling Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 5514bb168fa..183e260739b 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -13,7 +13,6 @@ # pylint: disable=invalid-sequence-index """Circuit transpile function""" -import copy import logging from time import time from typing import List, Union, Dict, Callable, Any, Optional, TypeVar @@ -30,11 +29,10 @@ from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget -from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType +from qiskit.transpiler.instruction_durations import InstructionDurationsType from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.transpiler.timing_constraints import TimingConstraints -from qiskit.transpiler.target import Target, target_to_backend_properties +from qiskit.transpiler.target import Target logger = logging.getLogger(__name__) @@ -335,117 +333,32 @@ def callback_func(**kwargs): UserWarning, ) - _skip_target = False - _given_inst_map = bool(inst_map) # check before inst_map is overwritten - # If a target is specified have it override any implicit selections from a backend - if target is not None: - if coupling_map is None: - coupling_map = target.build_coupling_map() - if basis_gates is None: - basis_gates = list(target.operation_names) - if instruction_durations is None: - instruction_durations = target.durations() - if inst_map is None: - inst_map = target.instruction_schedule_map() - if dt is None: - dt = target.dt - if timing_constraints is None: - timing_constraints = target.timing_constraints() - if backend_properties is None: - backend_properties = target_to_backend_properties(target) - # If target is not specified and any hardware constraint object is - # manually specified then do not use the target from the backend as - # it is invalidated by a custom basis gate list, custom coupling map, - # custom dt or custom instruction_durations - elif ( - basis_gates is not None # pylint: disable=too-many-boolean-expressions - or coupling_map is not None - or dt is not None - or instruction_durations is not None - or backend_properties is not None - or timing_constraints is not None - ): - _skip_target = True - else: - target = getattr(backend, "target", None) - - initial_layout = _parse_initial_layout(initial_layout) - coupling_map = _parse_coupling_map(coupling_map, backend) - approximation_degree = _parse_approximation_degree(approximation_degree) - - output_name = _parse_output_name(output_name, circuits) - inst_map = _parse_inst_map(inst_map, backend) - - _check_circuits_coupling_map(circuits, coupling_map, backend) - - timing_constraints = _parse_timing_constraints(backend, timing_constraints) - - if _given_inst_map and inst_map.has_custom_gate() and target is not None: - # Do not mutate backend target - target = copy.deepcopy(target) - target.update_from_instruction_schedule_map(inst_map) - if not ignore_backend_supplied_default_methods: if scheduling_method is None and hasattr(backend, "get_scheduling_stage_plugin"): scheduling_method = backend.get_scheduling_stage_plugin() if translation_method is None and hasattr(backend, "get_translation_stage_plugin"): translation_method = backend.get_translation_stage_plugin() - if instruction_durations or dt: - # If durations are provided and there is more than one circuit - # we need to serialize the execution because the full durations - # is dependent on the circuit calibrations which are per circuit - if len(circuits) > 1: - out_circuits = [] - for circuit in circuits: - instruction_durations = _parse_instruction_durations( - backend, instruction_durations, dt, circuit - ) - pm = generate_preset_pass_manager( - optimization_level, - backend=backend, - target=target, - basis_gates=basis_gates, - inst_map=inst_map, - coupling_map=coupling_map, - instruction_durations=instruction_durations, - backend_properties=backend_properties, - timing_constraints=timing_constraints, - initial_layout=initial_layout, - layout_method=layout_method, - routing_method=routing_method, - translation_method=translation_method, - scheduling_method=scheduling_method, - approximation_degree=approximation_degree, - seed_transpiler=seed_transpiler, - unitary_synthesis_method=unitary_synthesis_method, - unitary_synthesis_plugin_config=unitary_synthesis_plugin_config, - hls_config=hls_config, - init_method=init_method, - optimization_method=optimization_method, - _skip_target=_skip_target, - ) - out_circuits.append(pm.run(circuit, callback=callback, num_processes=num_processes)) - for name, circ in zip(output_name, out_circuits): - circ.name = name - end_time = time() - _log_transpile_time(start_time, end_time) - return out_circuits - else: - instruction_durations = _parse_instruction_durations( - backend, instruction_durations, dt, circuits[0] - ) + initial_layout = _parse_initial_layout(initial_layout) + approximation_degree = _parse_approximation_degree(approximation_degree) + output_name = _parse_output_name(output_name, circuits) + + coupling_map = _parse_coupling_map(coupling_map) + _check_circuits_coupling_map(circuits, coupling_map, backend) + # Edge cases require using the old model (loose constraints) instead of building a target, + # but we don't populate the passmanager config with loose constraints unless it's one of + # the known edge cases to control the execution path. pm = generate_preset_pass_manager( optimization_level, - backend=backend, target=target, + backend=backend, basis_gates=basis_gates, - inst_map=inst_map, coupling_map=coupling_map, instruction_durations=instruction_durations, backend_properties=backend_properties, timing_constraints=timing_constraints, + inst_map=inst_map, initial_layout=initial_layout, layout_method=layout_method, routing_method=routing_method, @@ -458,14 +371,15 @@ def callback_func(**kwargs): hls_config=hls_config, init_method=init_method, optimization_method=optimization_method, - _skip_target=_skip_target, + dt=dt, ) - out_circuits = pm.run(circuits, callback=callback) + + out_circuits = pm.run(circuits, callback=callback, num_processes=num_processes) + for name, circ in zip(output_name, out_circuits): circ.name = name end_time = time() _log_transpile_time(start_time, end_time) - if arg_circuits_list: return out_circuits else: @@ -491,35 +405,24 @@ def _check_circuits_coupling_map(circuits, cmap, backend): def _log_transpile_time(start_time, end_time): - log_msg = "Total Transpile Time - %.5f (ms)" % ((end_time - start_time) * 1000) + log_msg = f"Total Transpile Time - {((end_time - start_time) * 1000):.5f} (ms)" logger.info(log_msg) -def _parse_inst_map(inst_map, backend): - # try getting inst_map from user, else backend - if inst_map is None and backend is not None: - inst_map = backend.target.instruction_schedule_map() - return inst_map - - -def _parse_coupling_map(coupling_map, backend): - # try getting coupling_map from user, else backend - if coupling_map is None and backend is not None: - coupling_map = backend.coupling_map - +def _parse_coupling_map(coupling_map): # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] - if coupling_map is None or isinstance(coupling_map, CouplingMap): - return coupling_map if isinstance(coupling_map, list) and all( isinstance(i, list) and len(i) == 2 for i in coupling_map ): return CouplingMap(coupling_map) - else: + elif isinstance(coupling_map, list): raise TranspilerError( "Only a single input coupling map can be used with transpile() if you need to " "target different coupling maps for different circuits you must call transpile() " "multiple times" ) + else: + return coupling_map def _parse_initial_layout(initial_layout): @@ -535,34 +438,6 @@ def _parse_initial_layout(initial_layout): return initial_layout -def _parse_instruction_durations(backend, inst_durations, dt, circuit): - """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided, - the backend will be ignored, otherwise, the durations will be populated from the - backend. If any circuits have gate calibrations, those calibration durations would - take precedence over backend durations, but be superceded by ``inst_duration``s. - """ - if not inst_durations: - backend_durations = InstructionDurations() - if backend is not None: - backend_durations = backend.instruction_durations - - circ_durations = InstructionDurations() - if not inst_durations: - circ_durations.update(backend_durations, dt or backend_durations.dt) - - if circuit.calibrations: - cal_durations = [] - for gate, gate_cals in circuit.calibrations.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) - circ_durations.update(cal_durations, circ_durations.dt) - - if inst_durations: - circ_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) - - return circ_durations - - def _parse_approximation_degree(approximation_degree): if approximation_degree is None: return None @@ -601,17 +476,7 @@ def _parse_output_name(output_name, circuits): else: raise TranspilerError( "The parameter output_name should be a string or a" - "list of strings: %s was used." % type(output_name) + f"list of strings: {type(output_name)} was used." ) else: return [circuit.name for circuit in circuits] - - -def _parse_timing_constraints(backend, timing_constraints): - if isinstance(timing_constraints, TimingConstraints): - return timing_constraints - if backend is None and timing_constraints is None: - timing_constraints = TimingConstraints() - elif backend is not None: - timing_constraints = backend.target.timing_constraints() - return timing_constraints diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index 459b739ee01..f3d3edb5b77 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -17,12 +17,27 @@ .. currentmodule:: qiskit.converters -.. autofunction:: circuit_to_dag -.. autofunction:: dag_to_circuit +QuantumCircuit -> circuit components +==================================== + .. autofunction:: circuit_to_instruction .. autofunction:: circuit_to_gate + +QuantumCircuit <-> DagCircuit +============================= + +.. autofunction:: circuit_to_dag +.. autofunction:: dag_to_circuit + +QuantumCircuit <-> DagDependency +================================ + .. autofunction:: dagdependency_to_circuit .. autofunction:: circuit_to_dagdependency + +DagCircuit <-> DagDependency +============================ + .. autofunction:: dag_to_dagdependency .. autofunction:: dagdependency_to_dag """ diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index e2612b43d3e..b2c1df2a037 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -79,6 +79,13 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord 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) diff --git a/qiskit/converters/circuit_to_gate.py b/qiskit/converters/circuit_to_gate.py index 283dd87dbd7..c9f9ac6e1af 100644 --- a/qiskit/converters/circuit_to_gate.py +++ b/qiskit/converters/circuit_to_gate.py @@ -58,14 +58,14 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label if circuit.clbits: raise QiskitError("Circuit with classical bits cannot be converted to gate.") + if circuit.num_vars: + raise QiskitError("circuits with realtime classical variables cannot be converted to gates") for instruction in circuit.data: if not _check_is_gate(instruction.operation): raise QiskitError( - ( - "One or more instructions cannot be converted to" - ' a gate. "{}" is not a gate instruction' - ).format(instruction.operation.name) + "One or more instructions cannot be converted to" + f' a gate. "{instruction.operation.name}" is not a gate instruction' ) if parameter_map is None: @@ -75,10 +75,8 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label if parameter_dict.keys() != circuit.parameters: raise QiskitError( - ( - "parameter_map should map all circuit parameters. " - "Circuit parameters: {}, parameter_map: {}" - ).format(circuit.parameters, parameter_dict) + "parameter_map should map all circuit parameters. " + f"Circuit parameters: {circuit.parameters}, parameter_map: {parameter_dict}" ) gate = Gate( diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index e4bba13b033..4d0570542b0 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Helper function for converting a circuit to an instruction.""" -from qiskit.circuit.parametertable import ParameterTable, ParameterReferences from qiskit.exceptions import QiskitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumregister import QuantumRegister @@ -61,6 +60,28 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None # pylint: disable=cyclic-import from qiskit.circuit.quantumcircuit import QuantumCircuit + if circuit.num_input_vars: + # This could be supported by moving the `input` variables to be parameters of the + # instruction, but we don't really have a good representation of that yet, so safer to + # forbid it. + raise QiskitError("Circuits with 'input' variables cannot yet be converted to instructions") + if circuit.num_captured_vars: + raise QiskitError("Circuits that capture variables cannot be converted to instructions") + if circuit.num_declared_vars: + # This could very easily be supported in representations, since the variables are allocated + # and freed within the instruction itself. The reason to initially forbid it is to avoid + # needing to support unrolling such instructions within the transpiler; we would potentially + # need to remap variables to unique names in the larger context, and we don't yet have a way + # to return that information from the transpiler. We have to catch that in the transpiler + # as well since a user could manually make an instruction with such a definition, but + # forbidding it here means users get a more meaningful error at the point that the + # instruction actually gets created (since users often aren't aware that + # `QuantumCircuit.append(QuantumCircuit)` implicitly converts to an instruction). + raise QiskitError( + "Circuits with internal variables cannot yet be converted to instructions." + " You may be able to use `QuantumCircuit.compose` to inline this circuit into another." + ) + if parameter_map is None: parameter_dict = {p: p for p in circuit.parameters} else: @@ -68,10 +89,8 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None if parameter_dict.keys() != circuit.parameters: raise QiskitError( - ( - "parameter_map should map all circuit parameters. " - "Circuit parameters: {}, parameter_map: {}" - ).format(circuit.parameters, parameter_dict) + "parameter_map should map all circuit parameters. " + f"Circuit parameters: {circuit.parameters}, parameter_map: {parameter_dict}" ) out_instruction = Instruction( @@ -99,7 +118,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None regs.append(creg) clbit_map = {bit: creg[idx] for idx, bit in enumerate(circuit.clbits)} - operation_map = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} + operation_map = {} def fix_condition(op): original_id = id(op) @@ -127,15 +146,6 @@ def fix_condition(op): qc = QuantumCircuit(*regs, name=out_instruction.name) qc._data = data - qc._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_map[id(operation)], param_index) - for operation, param_index in target._parameter_table[param] - ) - for param in target._parameter_table - } - ) if circuit.global_phase: qc.global_phase = circuit.global_phase diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 5a32f0bba1e..ede026c247c 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -62,7 +62,11 @@ def dag_to_circuit(dag, copy_operations=True): *dag.cregs.values(), name=name, global_phase=dag.global_phase, + inputs=dag.iter_input_vars(), + captures=dag.iter_captured_vars(), ) + for var in dag.iter_declared_vars(): + circuit.add_uninitialized_var(var) circuit.metadata = dag.metadata circuit.calibrations = dag.calibrations diff --git a/qiskit/dagcircuit/collect_blocks.py b/qiskit/dagcircuit/collect_blocks.py index ea574536f45..c5c7b49144f 100644 --- a/qiskit/dagcircuit/collect_blocks.py +++ b/qiskit/dagcircuit/collect_blocks.py @@ -288,8 +288,8 @@ def run(self, block): self.group[self.find_leader(first)].append(node) blocks = [] - for index in self.leader: - if self.leader[index] == index: + for index, item in self.leader.items(): + if index == item: blocks.append(self.group[index]) return blocks diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 8c1332a8e60..626b7ef053e 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -22,11 +22,13 @@ """ from __future__ import annotations -from collections import OrderedDict, defaultdict, deque, namedtuple -from collections.abc import Callable, Sequence, Generator, Iterable import copy +import enum +import itertools import math -from typing import Any +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 @@ -39,7 +41,9 @@ 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 @@ -52,6 +56,8 @@ from qiskit.pulse import Schedule BitLocations = namedtuple("BitLocations", ("index", "registers")) +# The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. +_VarsMode = Literal["alike", "captures", "drop"] class DAGCircuit: @@ -78,13 +84,24 @@ def __init__(self): # Cache of dag op node sort keys self._key_cache = {} - # Set of wires (Register,idx) in the dag + # 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 (Register,idx) to input nodes of the graph + # Map from wire to input nodes of the graph self.input_map = OrderedDict() - # Map from wire (Register,idx) to output nodes of the graph + # Map from wire to output nodes of the graph self.output_map = OrderedDict() # Directed multigraph whose nodes are inputs, outputs, or operations. @@ -92,7 +109,7 @@ def __init__(self): # 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 (reg,idx) and each operation has + # Edges carry wire labels and each operation has # corresponding in- and out-edges with the same wire labels. self._multi_graph = rx.PyDAG() @@ -110,6 +127,16 @@ def __init__(self): # 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) @@ -122,7 +149,11 @@ def __init__(self): @property def wires(self): """Return a list of the wires in order.""" - return self.qubits + self.clbits + return ( + self.qubits + + self.clbits + + [var for vars in self._vars_by_type.values() for var in vars] + ) @property def node_counter(self): @@ -240,7 +271,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGCircuitError("duplicate qubits %s" % duplicate_qubits) + raise DAGCircuitError(f"duplicate qubits {duplicate_qubits}") for qubit in qubits: self.qubits.append(qubit) @@ -254,7 +285,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGCircuitError("duplicate clbits %s" % duplicate_clbits) + raise DAGCircuitError(f"duplicate clbits {duplicate_clbits}") for clbit in clbits: self.clbits.append(clbit) @@ -266,7 +297,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGCircuitError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGCircuitError("duplicate register %s" % qreg.name) + raise DAGCircuitError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -284,7 +315,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGCircuitError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGCircuitError("duplicate register %s" % creg.name) + raise DAGCircuitError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): @@ -297,6 +328,57 @@ def add_creg(self, creg): ) 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. @@ -369,17 +451,17 @@ def remove_clbits(self, *clbits): """ if any(not isinstance(clbit, Clbit) for clbit in clbits): raise DAGCircuitError( - "clbits not of type Clbit: %s" % [b for b in clbits if not isinstance(b, Clbit)] + 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("clbits not in circuit: %s" % 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("clbits not idle: %s" % 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)} @@ -405,13 +487,13 @@ def remove_cregs(self, *cregs): """ if any(not isinstance(creg, ClassicalRegister) for creg in cregs): raise DAGCircuitError( - "cregs not of type ClassicalRegister: %s" - % [r for r in cregs if not isinstance(r, ClassicalRegister)] + "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("cregs not in circuit: %s" % unknown_cregs) + raise DAGCircuitError(f"cregs not in circuit: {unknown_cregs}") for creg in cregs: del self.cregs[creg.name] @@ -435,17 +517,17 @@ def remove_qubits(self, *qubits): """ if any(not isinstance(qubit, Qubit) for qubit in qubits): raise DAGCircuitError( - "qubits not of type Qubit: %s" % [b for b in qubits if not isinstance(b, Qubit)] + 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("qubits not in circuit: %s" % 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("qubits not idle: %s" % 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)} @@ -462,7 +544,7 @@ def remove_qubits(self, *qubits): def remove_qregs(self, *qregs): """ - Remove classical registers from the circuit, leaving underlying bits + Remove quantum registers from the circuit, leaving underlying bits in place. Raises: @@ -471,13 +553,13 @@ def remove_qregs(self, *qregs): """ if any(not isinstance(qreg, QuantumRegister) for qreg in qregs): raise DAGCircuitError( - "qregs not of type QuantumRegister: %s" - % [r for r in qregs if not isinstance(r, QuantumRegister)] + 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("qregs not in circuit: %s" % unknown_qregs) + raise DAGCircuitError(f"qregs not in circuit: {unknown_qregs}") for qreg in qregs: del self.qregs[qreg.name] @@ -499,13 +581,13 @@ def _is_wire_idle(self, wire): DAGCircuitError: the wire is not in the circuit. """ if wire not in self._wires: - raise DAGCircuitError("wire %s not in circuit" % wire) + raise DAGCircuitError(f"wire {wire} not in circuit") try: child = next(self.successors(self.input_map[wire])) except StopIteration as e: raise DAGCircuitError( - "Invalid dagcircuit input node %s has no output" % self.input_map[wire] + f"Invalid dagcircuit input node {self.input_map[wire]} has no output" ) from e return child is self.output_map[wire] @@ -543,14 +625,14 @@ def _check_condition(self, name, condition): if not set(resources.clbits).issubset(self.clbits): raise DAGCircuitError(f"invalid clbits in condition for {name}") - def _check_bits(self, args, amap): - """Check the values of a list of (qu)bit arguments. + 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 (list[Bit]): the elements to be checked - amap (dict): a dictionary keyed on Qubits/Clbits + args: the elements to be checked + amap: a dictionary keyed on Qubits/Clbits Raises: DAGCircuitError: if a qubit is not contained in amap @@ -558,46 +640,7 @@ def _check_bits(self, args, amap): # Check for each wire for wire in args: if wire not in amap: - raise DAGCircuitError(f"(qu)bit {wire} not found in {amap}") - - @staticmethod - def _bits_in_operation(operation): - """Return an iterable over the classical bits that are inherent to an instruction. This - includes a `condition`, or the `target` of a :class:`.ControlFlowOp`. - - Args: - instruction: the :class:`~.circuit.Instruction` instance for a node. - - Returns: - Iterable[Clbit]: the :class:`.Clbit`\\ s involved. - """ - # If updating this, also update the fast-path checker `DAGCirucit._operation_may_have_bits`. - if (condition := getattr(operation, "condition", None)) is not None: - yield from condition_resources(condition).clbits - if isinstance(operation, SwitchCaseOp): - target = operation.target - if isinstance(target, Clbit): - yield target - elif isinstance(target, ClassicalRegister): - yield from target - else: - yield from node_resources(target).clbits - - @staticmethod - def _operation_may_have_bits(operation) -> bool: - """Return whether a given :class:`.Operation` may contain any :class:`.Clbit` instances - in itself (e.g. a control-flow operation). - - Args: - operation (qiskit.circuit.Operation): the operation to check. - """ - # This is separate to `_bits_in_operation` because most of the time there won't be any bits, - # so we want a fast path to be able to skip creating and testing a generator for emptiness. - # - # If updating this, also update `DAGCirucit._bits_in_operation`. - return getattr(operation, "condition", None) is not None or isinstance( - operation, SwitchCaseOp - ) + raise DAGCircuitError(f"wire {wire} not found in {amap}") def _increment_op(self, op): if op.name in self._op_names: @@ -611,14 +654,32 @@ def _decrement_op(self, op): else: self._op_names[op.name] -= 1 - def copy_empty_like(self): + 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 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. @@ -639,6 +700,21 @@ def copy_empty_like(self): 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_operation_back( @@ -669,17 +745,17 @@ def apply_operation_back( """ qargs = tuple(qargs) cargs = tuple(cargs) + additional = () - if self._operation_may_have_bits(op): + if _may_have_additional_wires(op): # This is the slow path; most of the time, this won't happen. - all_cbits = set(self._bits_in_operation(op)).union(cargs) - else: - all_cbits = cargs + additional = set(_additional_wires(op)).difference(cargs) if check: self._check_condition(op.name, getattr(op, "condition", None)) - self._check_bits(qargs, self.output_map) - self._check_bits(all_cbits, self.output_map) + self._check_wires(qargs, self.output_map) + self._check_wires(cargs, self.output_map) + self._check_wires(additional, self.output_map) node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) @@ -690,7 +766,7 @@ def apply_operation_back( # 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 (qargs, all_cbits) for bit in bits], + [self.output_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], ) return node @@ -721,17 +797,17 @@ def apply_operation_front( """ qargs = tuple(qargs) cargs = tuple(cargs) + additional = () - if self._operation_may_have_bits(op): + if _may_have_additional_wires(op): # This is the slow path; most of the time, this won't happen. - all_cbits = set(self._bits_in_operation(op)).union(cargs) - else: - all_cbits = cargs + additional = set(_additional_wires(op)).difference(cargs) if check: self._check_condition(op.name, getattr(op, "condition", None)) - self._check_bits(qargs, self.input_map) - self._check_bits(all_cbits, self.input_map) + self._check_wires(qargs, self.output_map) + self._check_wires(cargs, self.output_map) + self._check_wires(additional, self.output_map) node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) @@ -742,11 +818,13 @@ def apply_operation_front( # 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 (qargs, all_cbits) for bit in bits], + [self.input_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], ) return node - def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): + 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 @@ -760,6 +838,18 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): 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). @@ -822,27 +912,51 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): 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, _reject_new_register + dag.cregs.values(), edge_map, add_register=_reject_new_register ) for nd in other.topological_nodes(): if isinstance(nd, DAGInNode): - # 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( - "wire %s[%d] not in self" % (m_wire.register.name, m_wire.index) - ) - if nd.wire not in other._wires: - raise DAGCircuitError( - "inconsistent wire type for %s[%d] in other" - % (nd.register.name, nd.wire.index) - ) + 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 @@ -859,7 +973,7 @@ def _reject_new_register(reg): op.target = variable_mapper.map_target(op.target) dag.apply_operation_back(op, m_qargs, m_cargs, check=False) else: - raise DAGCircuitError("bad node type %s" % type(nd)) + raise DAGCircuitError(f"bad node type {type(nd)}") if not inplace: return dag @@ -1030,6 +1144,52 @@ 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 @@ -1047,6 +1207,11 @@ def __eq__(self, other): 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)} @@ -1130,7 +1295,8 @@ def replace_block_with_op( 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. + 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 @@ -1197,12 +1363,22 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit Args: node (DAGOpNode): node to substitute - input_dag (DAGCircuit): circuit that will substitute the node + 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 @@ -1227,9 +1403,9 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit 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 self._operation_may_have_bits(node.op): + if not propagate_condition and _may_have_additional_wires(node.op): node_wire_order += [ - bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs + wire for wire in _additional_wires(node.op) if wire not in node_cargs ] if len(wires) != len(node_wire_order): raise DAGCircuitError( @@ -1241,12 +1417,27 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit 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.op): + 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 @@ -1325,14 +1516,22 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit node._node_id, lambda edge, wire=self_wire: edge == wire )[0] self._multi_graph.add_edge(pred._node_id, succ._node_id, self_wire) - - # Exlude any nodes from in_dag that are not a DAGOpNode or are on - # bits outside the set specified by the wires kwarg + 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 qarg in node.qargs: - if qarg not in wire_map: + for _, _, wire in in_dag.edges(node): + if wire not in wire_map: return False return True @@ -1369,7 +1568,7 @@ def edge_weight_map(wire): self._decrement_op(node.op) variable_mapper = _classical_resource_map.VariableMapper( - self.cregs.values(), wire_map, self.add_creg + 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 @@ -1416,7 +1615,7 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ 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 + 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: @@ -1432,30 +1631,21 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ if node.op.num_qubits != op.num_qubits or node.op.num_clbits != op.num_clbits: raise DAGCircuitError( - "Cannot replace node of width ({} qubits, {} clbits) with " - "operation of mismatched width ({} qubits, {} clbits).".format( - node.op.num_qubits, node.op.num_clbits, op.num_qubits, op.num_clbits - ) + 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) - if (new_condition := getattr(op, "condition", None)) is not None: - new_wires.update(condition_resources(new_condition).clbits) - elif isinstance(op, SwitchCaseOp): - if isinstance(op.target, Clbit): - new_wires.add(op.target) - elif isinstance(op.target, ClassicalRegister): - new_wires.update(op.target) - else: - new_wires.update(node_resources(op.target).clbits) + 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 new_condition is not None: + if getattr(op, "condition", None) is not None: raise DAGCircuitError( "Cannot propagate a condition to an operation that already has one." ) @@ -1491,13 +1681,17 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ self._decrement_op(node.op) return new_node - def separable_circuits(self, remove_idle_qubits: bool = False) -> list["DAGCircuit"]: + 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 @@ -1522,7 +1716,7 @@ def _key(x): # Create new DAGCircuit objects from each of the rustworkx subgraph objects decomposed_dags = [] for subgraph in disconnected_subgraphs: - new_dag = self.copy_empty_like() + 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): @@ -1684,6 +1878,14 @@ 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) @@ -1706,7 +1908,7 @@ def classical_predecessors(self, node): connected by a classical edge as DAGOpNodes and DAGInNodes.""" return iter( self._multi_graph.find_predecessors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) ) ) @@ -1739,7 +1941,7 @@ def classical_successors(self, node): connected by a classical edge as DAGOpNodes and DAGInNodes.""" return iter( self._multi_graph.find_successors_by_edge( - node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + node._node_id, lambda edge_data: not isinstance(edge_data, Qubit) ) ) @@ -1750,8 +1952,8 @@ def remove_op_node(self, node): """ if not isinstance(node, DAGOpNode): raise DAGCircuitError( - 'The method remove_op_node only works on DAGOpNodes. A "%s" ' - "node type was wrongly provided." % type(node) + 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( @@ -1804,7 +2006,7 @@ def front_layer(self): return op_nodes - def layers(self): + 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., @@ -1821,6 +2023,10 @@ def layers(self): 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: @@ -1845,7 +2051,7 @@ def layers(self): return # Construct a shallow copy of self - new_layer = self.copy_empty_like() + new_layer = self.copy_empty_like(vars_mode=vars_mode) for node in op_nodes: # this creates new DAGOpNodes in the new_layer @@ -1860,14 +2066,18 @@ def layers(self): yield {"graph": new_layer, "partition": support_list} - def serial_layers(self): + 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() + new_layer = self.copy_empty_like(vars_mode=vars_mode) # Save the support of the operation we add to the layer support_list = [] @@ -1971,7 +2181,7 @@ def nodes_on_wire(self, wire, only_ops=False): current_node = self.input_map.get(wire, None) if not current_node: - raise DAGCircuitError("The given wire %s is not present in the circuit" % str(wire)) + raise DAGCircuitError(f"The given wire {str(wire)} is not present in the circuit") more_nodes = True while more_nodes: @@ -2054,36 +2264,44 @@ def quantum_causal_cone(self, qubit): output_node = self.output_map.get(qubit, None) if not output_node: raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - # Add the qubit to the causal cone. - qubits_to_check = {qubit} - # Add predecessors of output node to the queue. - queue = deque(self.predecessors(output_node)) - # While queue isn't empty + 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: - # Pop first element. node_to_check = queue.popleft() - # Check whether element is input or output node. + if isinstance(node_to_check, DAGOpNode): - # Keep all the qubits in the operation inside a set. - qubit_set = set(node_to_check.qargs) - # Check if there are any qubits in common and that the operation is not a barrier. - if ( - len(qubit_set.intersection(qubits_to_check)) > 0 - and node_to_check.op.name != "barrier" - and not getattr(node_to_check.op, "_directive") - ): - # If so, add all the qubits to the causal cone. - qubits_to_check = qubits_to_check.union(qubit_set) - # For each predecessor of the current node, filter input/output nodes, - # also make sure it has at least one qubit in common. Then append. - for node in self.quantum_predecessors(node_to_check): - if ( - isinstance(node, DAGOpNode) - and len(qubits_to_check.intersection(set(node.qargs))) > 0 - ): - queue.append(node) - return qubits_to_check + # 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.""" @@ -2123,3 +2341,82 @@ def draw(self, scale=0.7, filename=None, style="color"): 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(operation) -> bool: + """Return whether a given :class:`.Operation` may contain references to additional wires + locations within itself. If this is ``False``, it doesn't necessarily mean that the operation + _will_ access memory inherently, but a ``True`` 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.circuit.Operation): 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 getattr(operation, "condition", None) is not None or isinstance( + operation, (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 diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index 4316c947140..63b91f92063 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -187,7 +187,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGDependencyError("duplicate qubits %s" % duplicate_qubits) + raise DAGDependencyError(f"duplicate qubits {duplicate_qubits}") self.qubits.extend(qubits) @@ -198,7 +198,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGDependencyError("duplicate clbits %s" % duplicate_clbits) + raise DAGDependencyError(f"duplicate clbits {duplicate_clbits}") self.clbits.extend(clbits) @@ -207,7 +207,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGDependencyError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGDependencyError("duplicate register %s" % qreg.name) + raise DAGDependencyError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -219,7 +219,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGDependencyError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGDependencyError("duplicate register %s" % creg.name) + raise DAGDependencyError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): diff --git a/qiskit/dagcircuit/dagdependency_v2.py b/qiskit/dagcircuit/dagdependency_v2.py index cb5d447162c..e50c47b24f9 100644 --- a/qiskit/dagcircuit/dagdependency_v2.py +++ b/qiskit/dagcircuit/dagdependency_v2.py @@ -247,7 +247,7 @@ def add_qubits(self, qubits): duplicate_qubits = set(self.qubits).intersection(qubits) if duplicate_qubits: - raise DAGDependencyError("duplicate qubits %s" % duplicate_qubits) + raise DAGDependencyError(f"duplicate qubits {duplicate_qubits}") for qubit in qubits: self.qubits.append(qubit) @@ -260,7 +260,7 @@ def add_clbits(self, clbits): duplicate_clbits = set(self.clbits).intersection(clbits) if duplicate_clbits: - raise DAGDependencyError("duplicate clbits %s" % duplicate_clbits) + raise DAGDependencyError(f"duplicate clbits {duplicate_clbits}") for clbit in clbits: self.clbits.append(clbit) @@ -271,7 +271,7 @@ def add_qreg(self, qreg): if not isinstance(qreg, QuantumRegister): raise DAGDependencyError("not a QuantumRegister instance.") if qreg.name in self.qregs: - raise DAGDependencyError("duplicate register %s" % qreg.name) + raise DAGDependencyError(f"duplicate register {qreg.name}") self.qregs[qreg.name] = qreg existing_qubits = set(self.qubits) for j in range(qreg.size): @@ -288,7 +288,7 @@ def add_creg(self, creg): if not isinstance(creg, ClassicalRegister): raise DAGDependencyError("not a ClassicalRegister instance.") if creg.name in self.cregs: - raise DAGDependencyError("duplicate register %s" % creg.name) + raise DAGDependencyError(f"duplicate register {creg.name}") self.cregs[creg.name] = creg existing_clbits = set(self.clbits) for j in range(creg.size): diff --git a/qiskit/dagcircuit/dagdepnode.py b/qiskit/dagcircuit/dagdepnode.py index fe63f57d3d4..cc00db9725c 100644 --- a/qiskit/dagcircuit/dagdepnode.py +++ b/qiskit/dagcircuit/dagdepnode.py @@ -83,7 +83,7 @@ def __init__( def op(self): """Returns the Instruction object corresponding to the op for the node, else None""" if not self.type or self.type != "op": - raise QiskitError("The node %s is not an op node" % (str(self))) + raise QiskitError(f"The node {str(self)} is not an op node") return self._op @op.setter diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 97283c75b8f..9f35f6eda89 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -14,14 +14,11 @@ """Objects to represent the information at a node in the DAGCircuit.""" from __future__ import annotations -import itertools import typing import uuid -from collections.abc import Iterable - +import qiskit._accelerate.circuit from qiskit.circuit import ( - Qubit, Clbit, ClassicalRegister, ControlFlowOp, @@ -30,7 +27,6 @@ SwitchCaseOp, ForLoopOp, Parameter, - Operation, QuantumCircuit, ) from qiskit.circuit.classical import expr @@ -39,6 +35,12 @@ from qiskit.dagcircuit import DAGCircuit +DAGNode = qiskit._accelerate.circuit.DAGNode +DAGOpNode = qiskit._accelerate.circuit.DAGOpNode +DAGInNode = qiskit._accelerate.circuit.DAGInNode +DAGOutNode = qiskit._accelerate.circuit.DAGOutNode + + def _legacy_condition_eq(cond1, cond2, bit_indices1, bit_indices2) -> bool: if cond1 is cond2 is None: return True @@ -175,158 +177,65 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2): _SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"}) -class DAGNode: - """Parent class for DAGOpNode, DAGInNode, and DAGOutNode.""" - - __slots__ = ["_node_id"] - - def __init__(self, nid=-1): - """Create a node""" - self._node_id = nid - - def __lt__(self, other): - return self._node_id < other._node_id - - def __gt__(self, other): - return self._node_id > other._node_id - - def __str__(self): - # TODO is this used anywhere other than in DAG drawing? - # needs to be unique as it is what pydot uses to distinguish nodes - return str(id(self)) - - @staticmethod - 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 +# 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 + ) -class DAGOpNode(DAGNode): - """Object to represent an Instruction at a node in the DAGCircuit.""" - - __slots__ = ["op", "qargs", "cargs", "sort_key"] - - def __init__( - self, op: Operation, qargs: Iterable[Qubit] = (), cargs: Iterable[Clbit] = (), dag=None - ): - """Create an Instruction node""" - super().__init__() - self.op = op - self.qargs = tuple(qargs) - self.cargs = tuple(cargs) - if dag is not None: - cache_key = (self.qargs, self.cargs) - key = dag._key_cache.get(cache_key, None) - if key is not None: - self.sort_key = key - else: - self.sort_key = ",".join( - f"{dag.find_bit(q).index:04d}" for q in itertools.chain(*cache_key) - ) - dag._key_cache[cache_key] = self.sort_key - else: - self.sort_key = str(self.qargs) - - @property - def name(self): - """Returns the Instruction name corresponding to the op for this node""" - return self.op.name - - @name.setter - def name(self, new_name): - """Sets the Instruction name corresponding to the op for this node""" - self.op.name = new_name - - def __repr__(self): - """Returns a representation of the DAGOpNode""" - return f"DAGOpNode(op={self.op}, qargs={self.qargs}, cargs={self.cargs})" - - -class DAGInNode(DAGNode): - """Object to represent an incoming wire node in the DAGCircuit.""" - - __slots__ = ["wire", "sort_key"] - - def __init__(self, wire): - """Create an incoming node""" - super().__init__() - self.wire = wire - # TODO sort_key which is used in dagcircuit.topological_nodes - # only works as str([]) for DAGInNodes. Need to figure out why. - self.sort_key = str([]) - - def __repr__(self): - """Returns a representation of the DAGInNode""" - return f"DAGInNode(wire={self.wire})" - - -class DAGOutNode(DAGNode): - """Object to represent an outgoing wire node in the DAGCircuit.""" - - __slots__ = ["wire", "sort_key"] - - def __init__(self, wire): - """Create an outgoing node""" - super().__init__() - self.wire = wire - # TODO sort_key which is used in dagcircuit.topological_nodes - # only works as str([]) for DAGOutNodes. Need to figure out why. - self.sort_key = str([]) - - def __repr__(self): - """Returns a representation of the DAGOutNode""" - return f"DAGOutNode(wire={self.wire})" +# Bind semantic_eq from Python to Rust implementation +DAGNode.semantic_eq = staticmethod(_semantic_eq) diff --git a/qiskit/passmanager/flow_controllers.py b/qiskit/passmanager/flow_controllers.py index c7d952d048d..dcfcba70458 100644 --- a/qiskit/passmanager/flow_controllers.py +++ b/qiskit/passmanager/flow_controllers.py @@ -84,7 +84,7 @@ def iter_tasks(self, state: PassManagerState) -> Generator[Task, PassManagerStat return # Remove stored tasks from the completed task collection for next loop state.workflow_status.completed_passes.difference_update(self.tasks) - raise PassManagerError("Maximum iteration reached. max_iteration=%i" % max_iteration) + raise PassManagerError(f"Maximum iteration reached. max_iteration={max_iteration}") class ConditionalController(BaseController): diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py index ba416dfb063..85f422f181b 100644 --- a/qiskit/passmanager/passmanager.py +++ b/qiskit/passmanager/passmanager.py @@ -21,7 +21,7 @@ import dill -from qiskit.utils.parallel import parallel_map +from qiskit.utils.parallel import parallel_map, should_run_in_parallel from .base_tasks import Task, PassManagerIR from .exceptions import PassManagerError from .flow_controllers import FlowControllerLinear @@ -130,7 +130,7 @@ def __add__(self, other): return new_passmanager except PassManagerError as ex: raise TypeError( - "unsupported operand type + for %s and %s" % (self.__class__, other.__class__) + f"unsupported operand type + for {self.__class__} and {other.__class__}" ) from ex @abstractmethod @@ -225,16 +225,16 @@ def callback_func(**kwargs): in_programs = [in_programs] is_list = False - if len(in_programs) == 1: - out_program = _run_workflow( - program=in_programs[0], - pass_manager=self, - callback=callback, - **kwargs, - ) - if is_list: - return [out_program] - return out_program + # If we're not going to run in parallel, we want to avoid spending time `dill` serializing + # ourselves, since that can be quite expensive. + if len(in_programs) == 1 or not should_run_in_parallel(num_processes): + out = [ + _run_workflow(program=program, pass_manager=self, callback=callback, **kwargs) + for program in in_programs + ] + if len(in_programs) == 1 and not is_list: + return out[0] + return out del callback del kwargs @@ -242,7 +242,7 @@ def callback_func(**kwargs): # Pass manager may contain callable and we need to serialize through dill rather than pickle. # See https://github.com/Qiskit/qiskit-terra/pull/3290 # Note that serialized object is deserialized as a different object. - # Thus, we can resue the same manager without state collision, without building it per thread. + # Thus, we can reuse the same manager without state collision, without building it per thread. return parallel_map( _run_workflow_in_new_process, values=in_programs, diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 0a36dbbb0bd..7cf8355354e 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -51,7 +51,7 @@ * a collection parameter value sets to bind the circuit against, :math:`\theta_k`. Running an estimator returns a :class:`~qiskit.primitives.BasePrimitiveJob` object, where calling -the method :meth:`~qiskit.primitives.BasePrimitiveJob.result` results in expectation value estimates +the method :meth:`~qiskit.primitives.BasePrimitiveJob.result` results in expectation value estimates and metadata for each pub: .. math:: @@ -86,24 +86,24 @@ estimator = Estimator() # calculate [ ] - job = estimator.run([(psi1, hamiltonian1, [theta1])]) + job = estimator.run([(psi1, H1, [theta1])]) job_result = job.result() # It will block until the job finishes. - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # calculate [ [, # ], # [] ] job2 = estimator.run( [ - (psi1, [hamiltonian1, hamiltonian3], [theta1, theta3]), - (psi2, hamiltonian2, theta2) + (psi1, [H1, H3], [theta1, theta3]), + (psi2, H2, theta2) ], precision=0.01 ) job_result = job2.result() print(f"The primitive-job finished with result {job_result}") - + Overview of SamplerV2 ===================== @@ -153,12 +153,12 @@ # collect 128 shots from the Bell circuit job = sampler.run([bell], shots=128) job_result = job.result() - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # run a sampler job on the parameterized circuits - job2 = sampler.run([(pqc, theta1), (pqc2, theta2)] + job2 = sampler.run([(pqc, theta1), (pqc2, theta2)]) job_result = job2.result() - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") Overview of EstimatorV1 @@ -214,14 +214,14 @@ # calculate [ ] job = estimator.run([psi1], [H1], [theta1]) job_result = job.result() # It will block until the job finishes. - print(f"The primitive-job finished with result {job_result}")) + print(f"The primitive-job finished with result {job_result}") # calculate [ , # , # ] job2 = estimator.run( - [psi1, psi2, psi1], - [H1, H2, H3], + [psi1, psi2, psi1], + [H1, H2, H3], [theta1, theta2, theta3] ) job_result = job2.result() @@ -417,6 +417,7 @@ DataBin PrimitiveResult PubResult + SamplerPubResult BasePrimitiveJob PrimitiveJob @@ -466,6 +467,7 @@ PubResult, EstimatorPubLike, SamplerPubLike, + SamplerPubResult, BindingsArrayLike, ObservableLike, ObservablesArrayLike, diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 23556e2efe2..8446c870b1f 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -65,6 +65,8 @@ def _run_circuits( max_circuits = getattr(backend.configuration(), "max_experiments", None) elif isinstance(backend, BackendV2): max_circuits = backend.max_circuits + else: + raise RuntimeError("Backend version not supported") if max_circuits: jobs = [ backend.run(circuits[pos : pos + max_circuits], **run_options) @@ -198,7 +200,7 @@ def _transpile(self): transpiled_circuit = common_circuit.copy() final_index_layout = list(range(common_circuit.num_qubits)) else: - transpiled_circuit = transpile( + transpiled_circuit = transpile( # pylint:disable=unexpected-keyword-arg common_circuit, self.backend, **self.transpile_options.__dict__ ) if transpiled_circuit.layout is not None: diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 9afc6d892f3..d94d3674e9a 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -31,7 +31,7 @@ from .backend_estimator import _pauli_expval_with_variance, _prepare_counts, _run_circuits from .base import BaseEstimatorV2 -from .containers import EstimatorPubLike, PrimitiveResult, PubResult +from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.bindings_array import BindingsArray from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob @@ -256,8 +256,7 @@ def _postprocess_pub( evs[index] += expval * coeff variances[index] += variance * coeff**2 stds = np.sqrt(variances / shots) - data_bin_cls = self._make_data_bin(pub) - data_bin = data_bin_cls(evs=evs, stds=stds) + data_bin = DataBin(evs=evs, stds=stds, shape=evs.shape) return PubResult(data_bin, metadata={"target_precision": pub.precision}) def _bind_and_add_measurements( diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index 94c1c3c88b5..f1399a54893 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -176,7 +176,7 @@ def _transpile(self): start = len(self._transpiled_circuits) self._transpiled_circuits.extend( - transpile( + transpile( # pylint:disable=unexpected-keyword-arg self.preprocessed_circuits[start:], self.backend, **self.transpile_options.__dict__, diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index 51d1ded1500..ff7a32580fe 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -27,10 +27,10 @@ from qiskit.primitives.base import BaseSamplerV2 from qiskit.primitives.containers import ( BitArray, + DataBin, PrimitiveResult, - PubResult, SamplerPubLike, - make_data_bin, + SamplerPubResult, ) from qiskit.primitives.containers.bit_array import _min_num_bytes from qiskit.primitives.containers.sampler_pub import SamplerPub @@ -124,7 +124,7 @@ def options(self) -> Options: def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> PrimitiveJob[PrimitiveResult[PubResult]]: + ) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]: if shots is None: shots = self._options.default_shots coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] @@ -142,7 +142,7 @@ def _validate_pubs(self, pubs: list[SamplerPub]): UserWarning, ) - def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: + def _run(self, pubs: list[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: pub_dict = defaultdict(list) # consolidate pubs with the same number of shots for i, pub in enumerate(pubs): @@ -157,7 +157,7 @@ def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: results[i] = pub_result return PrimitiveResult(results) - def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[PubResult]: + def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]: """Compute results for pubs that all require the same value of ``shots``.""" # prepare circuits bound_circuits = [pub.parameter_values.bind_all(pub.circuit) for pub in pubs] @@ -197,7 +197,7 @@ def _postprocess_pub( shape: tuple[int, ...], meas_info: list[_MeasureInfo], max_num_bytes: int, - ) -> PubResult: + ) -> SamplerPubResult: """Converts the memory data into an array of bit arrays with the shape of the pub.""" arrays = { item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8) @@ -210,15 +210,10 @@ def _postprocess_pub( ary = _samples_to_packed_array(samples, item.num_bits, item.start) arrays[item.creg_name][index] = ary - data_bin_cls = make_data_bin( - [(item.creg_name, BitArray) for item in meas_info], - shape=shape, - ) meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - data_bin = data_bin_cls(**meas) - return PubResult(data_bin, metadata={}) + return SamplerPubResult(DataBin(**meas, shape=shape), metadata={}) def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 47c0ba10bf0..0a7c0ec8628 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -18,8 +18,6 @@ from collections.abc import Iterable, Sequence from copy import copy from typing import Generic, TypeVar -import numpy as np -from numpy.typing import NDArray from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job @@ -27,7 +25,6 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from ..containers import ( - make_data_bin, DataBin, EstimatorPubLike, PrimitiveResult, @@ -205,12 +202,10 @@ class BaseEstimatorV2(ABC): """ @staticmethod - def _make_data_bin(pub: EstimatorPub) -> DataBin: - # provide a standard way to construct estimator databins to ensure that names match - # across implementations - return make_data_bin( - (("evs", NDArray[np.float64]), ("stds", NDArray[np.float64])), pub.shape - ) + def _make_data_bin(_: EstimatorPub) -> type[DataBin]: + # this method is present for backwards compat. new primitive implementations + # should avoid it. + return DataBin @abstractmethod def run( diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 1d071a15728..94c9a7681d0 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -23,8 +23,8 @@ from qiskit.providers import JobV1 as Job from ..containers.primitive_result import PrimitiveResult -from ..containers.pub_result import PubResult from ..containers.sampler_pub import SamplerPubLike +from ..containers.sampler_pub_result import SamplerPubResult from . import validation from .base_primitive import BasePrimitive from .base_primitive_job import BasePrimitiveJob @@ -165,7 +165,7 @@ class BaseSamplerV2(ABC): @abstractmethod def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: + ) -> BasePrimitiveJob[PrimitiveResult[SamplerPubResult]]: """Run and collect samples from each pub. Args: diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 63c9c600de1..62fb49a3fb9 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -15,11 +15,12 @@ """ +from .bindings_array import BindingsArrayLike from .bit_array import BitArray -from .data_bin import make_data_bin, DataBin +from .data_bin import DataBin, make_data_bin +from .estimator_pub import EstimatorPubLike +from .observables_array import ObservableLike, ObservablesArrayLike from .primitive_result import PrimitiveResult from .pub_result import PubResult -from .estimator_pub import EstimatorPubLike from .sampler_pub import SamplerPubLike -from .bindings_array import BindingsArrayLike -from .observables_array import ObservableLike, ObservablesArrayLike +from .sampler_pub_result import SamplerPubResult diff --git a/qiskit/primitives/containers/bindings_array.py b/qiskit/primitives/containers/bindings_array.py index 6ab60f4771d..89730e5ce94 100644 --- a/qiskit/primitives/containers/bindings_array.py +++ b/qiskit/primitives/containers/bindings_array.py @@ -95,7 +95,7 @@ def __init__( be inferred from the provided arrays. Ambiguity arises whenever the key of an entry of ``data`` contains only one parameter and the corresponding array's shape ends in a one. In this case, it can't be decided whether that one is an index over parameters, or whether - it should be encorporated in :attr:`~shape`. + it should be incorporated in :attr:`~shape`. Since :class:`~.Parameter` objects are only allowed to represent float values, this class casts all given values to float. If an incompatible dtype is given, such as complex @@ -131,7 +131,7 @@ class casts all given values to float. If an incompatible dtype is given, such a def __getitem__(self, args) -> BindingsArray: # because the parameters live on the last axis, we don't need to do anything special to - # accomodate them because there will always be an implicit slice(None, None, None) + # accommodate them because there will always be an implicit slice(None, None, None) # on all unspecified trailing dimensions # separately, we choose to not disallow args which touch the last dimension, even though it # would not be a particularly friendly way to chop parameters diff --git a/qiskit/primitives/containers/bit_array.py b/qiskit/primitives/containers/bit_array.py index 308e51f782b..24d52ca4e85 100644 --- a/qiskit/primitives/containers/bit_array.py +++ b/qiskit/primitives/containers/bit_array.py @@ -19,13 +19,15 @@ from collections import defaultdict from functools import partial from itertools import chain, repeat -from typing import Callable, Iterable, Literal, Mapping +from typing import Callable, Iterable, Literal, Mapping, Sequence import numpy as np from numpy.typing import NDArray -from qiskit.result import Counts +from qiskit.exceptions import QiskitError +from qiskit.result import Counts, sampled_expectation_value +from .observables_array import ObservablesArray, ObservablesArrayLike from .shape import ShapedMixin, ShapeInput, shape_tuple # this lookup table tells you how many bits are 1 in each uint8 value @@ -37,6 +39,23 @@ def _min_num_bytes(num_bits: int) -> int: return num_bits // 8 + (num_bits % 8 > 0) +def _unpack(bit_array: BitArray) -> NDArray[np.uint8]: + arr = np.unpackbits(bit_array.array, axis=-1, bitorder="big") + arr = arr[..., -1 : -bit_array.num_bits - 1 : -1] + return arr + + +def _pack(arr: NDArray[np.uint8]) -> tuple[NDArray[np.uint8], int]: + arr = arr[..., ::-1] + num_bits = arr.shape[-1] + pad_size = -num_bits % 8 + if pad_size > 0: + pad_width = [(0, 0)] * (arr.ndim - 1) + [(pad_size, 0)] + arr = np.pad(arr, pad_width, constant_values=0) + arr = np.packbits(arr, axis=-1, bitorder="big") + return arr, num_bits + + class BitArray(ShapedMixin): """Stores an array of bit values. @@ -110,6 +129,13 @@ def __repr__(self): desc = f"" return f"BitArray({desc})" + def __getitem__(self, indices): + if isinstance(indices, tuple) and len(indices) >= self.ndim + 2: + raise ValueError( + "BitArrays cannot be sliced along the bits axis, see slice_bits() instead." + ) + return BitArray(self._array[indices], self.num_bits) + @property def array(self) -> NDArray[np.uint8]: """The raw NumPy array of data.""" @@ -347,3 +373,267 @@ def reshape(self, *shape: ShapeInput) -> "BitArray": else: raise ValueError("Cannot change the size of the array.") return BitArray(self._array.reshape(shape), self.num_bits) + + def transpose(self, *axes) -> "BitArray": + """Return a bit array with axes transposed. + + Args: + axes: None, tuple of ints or n ints. See `ndarray.transpose + `_ + for the details. + + Returns: + BitArray: A bit array with axes permuted. + + Raises: + ValueError: If ``axes`` don't match this bit array. + ValueError: If ``axes`` includes any indices that are out of bounds. + """ + if len(axes) == 0: + axes = tuple(reversed(range(self.ndim))) + if len(axes) == 1 and isinstance(axes[0], Sequence): + axes = axes[0] + if len(axes) != self.ndim: + raise ValueError("axes don't match bit array") + for i in axes: + if i >= self.ndim or self.ndim + i < 0: + raise ValueError( + f"axis {i} is out of bounds for bit array of dimension {self.ndim}." + ) + axes = tuple(i if i >= 0 else self.ndim + i for i in axes) + (-2, -1) + return BitArray(self._array.transpose(axes), self.num_bits) + + def slice_bits(self, indices: int | Sequence[int]) -> "BitArray": + """Return a bit array sliced along the bit axis of some indices of interest. + + .. note:: + + The convention used by this method is that the index ``0`` corresponds to + the least-significant bit in the :attr:`~array`, or equivalently + the right-most bitstring entry as returned by + :meth:`~get_counts` or :meth:`~get_bitstrings`, etc. + + If this bit array was produced by a sampler, then an index ``i`` corresponds to the + :class:`~.ClassicalRegister` location ``creg[i]``. + + Args: + indices: The bit positions of interest to slice along. + + Returns: + A bit array sliced along the bit axis. + + Raises: + ValueError: If there are any invalid indices of the bit axis. + """ + if isinstance(indices, int): + indices = (indices,) + for index in indices: + if index < 0 or index >= self.num_bits: + raise ValueError( + f"index {index} is out of bounds for the number of bits {self.num_bits}." + ) + # This implementation introduces a temporary 8x memory overhead due to bit + # unpacking. This could be fixed using bitwise functions, at the expense of a + # more complicated implementation. + arr = _unpack(self) + arr = arr[..., indices] + arr, num_bits = _pack(arr) + return BitArray(arr, num_bits) + + def slice_shots(self, indices: int | Sequence[int]) -> "BitArray": + """Return a bit array sliced along the shots axis of some indices of interest. + + Args: + indices: The shots positions of interest to slice along. + + Returns: + A bit array sliced along the shots axis. + + Raises: + ValueError: If there are any invalid indices of the shots axis. + """ + if isinstance(indices, int): + indices = (indices,) + for index in indices: + if index < 0 or index >= self.num_shots: + raise ValueError( + f"index {index} is out of bounds for the number of shots {self.num_shots}." + ) + arr = self._array + arr = arr[..., indices, :] + return BitArray(arr, self.num_bits) + + def expectation_values(self, observables: ObservablesArrayLike) -> NDArray[np.float64]: + """Compute the expectation values of the provided observables, broadcasted against + this bit array. + + .. note:: + + This method returns the real part of the expectation value even if + the operator has complex coefficients due to the specification of + :func:`~.sampled_expectation_value`. + + Args: + observables: The observable(s) to take the expectation value of. + Must have a shape broadcastable with with this bit array and + the same number of qubits as the number of bits of this bit array. + The observables must be diagonal (I, Z, 0 or 1) too. + + Returns: + An array of expectation values whose shape is the broadcast shape of ``observables`` + and this bit array. + + Raises: + ValueError: If the provided observables does not have a shape broadcastable with + this bit array. + ValueError: If the provided observables does not have the same number of qubits as + the number of bits of this bit array. + ValueError: If the provided observables are not diagonal. + """ + observables = ObservablesArray.coerce(observables) + arr_indices = np.fromiter(np.ndindex(self.shape), dtype=object).reshape(self.shape) + bc_indices, bc_obs = np.broadcast_arrays(arr_indices, observables) + counts = {} + arr = np.zeros_like(bc_indices, dtype=float) + for index in np.ndindex(bc_indices.shape): + loc = bc_indices[index] + for pauli, coeff in bc_obs[index].items(): + if loc not in counts: + counts[loc] = self.get_counts(loc) + try: + expval = sampled_expectation_value(counts[loc], pauli) + except QiskitError as ex: + raise ValueError(ex.message) from ex + arr[index] += expval * coeff + return arr + + @staticmethod + def concatenate(bit_arrays: Sequence[BitArray], axis: int = 0) -> BitArray: + """Join a sequence of bit arrays along an existing axis. + + Args: + bit_arrays: The bit arrays must have (1) the same number of bits, + (2) the same number of shots, and + (3) the same shape, except in the dimension corresponding to axis + (the first, by default). + axis: The axis along which the arrays will be joined. Default is 0. + + Returns: + The concatenated bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of bits. + ValueError: If any bit arrays has a different number of shots. + ValueError: If any bit arrays has a different number of dimensions. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to concatenate") + num_bits = bit_arrays[0].num_bits + num_shots = bit_arrays[0].num_shots + ndim = bit_arrays[0].ndim + if ndim == 0: + raise ValueError("Zero-dimensional bit arrays cannot be concatenated") + for i, ba in enumerate(bit_arrays): + if ba.num_bits != num_bits: + raise ValueError( + "All bit arrays must have same number of bits, " + f"but the bit array at index 0 has {num_bits} bits " + f"and the bit array at index {i} has {ba.num_bits} bits." + ) + if ba.num_shots != num_shots: + raise ValueError( + "All bit arrays must have same number of shots, " + f"but the bit array at index 0 has {num_shots} shots " + f"and the bit array at index {i} has {ba.num_shots} shots." + ) + if ba.ndim != ndim: + raise ValueError( + "All bit arrays must have same number of dimensions, " + f"but the bit array at index 0 has {ndim} dimension(s) " + f"and the bit array at index {i} has {ba.ndim} dimension(s)." + ) + if axis < 0 or axis >= ndim: + raise ValueError(f"axis {axis} is out of bounds for bit array of dimension {ndim}.") + data = np.concatenate([ba.array for ba in bit_arrays], axis=axis) + return BitArray(data, num_bits) + + @staticmethod + def concatenate_shots(bit_arrays: Sequence[BitArray]) -> BitArray: + """Join a sequence of bit arrays along the shots axis. + + Args: + bit_arrays: The bit arrays must have (1) the same number of bits, + and (2) the same shape. + + Returns: + The stacked bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of bits. + ValueError: If any bit arrays has a different shape. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to stack") + num_bits = bit_arrays[0].num_bits + shape = bit_arrays[0].shape + for i, ba in enumerate(bit_arrays): + if ba.num_bits != num_bits: + raise ValueError( + "All bit arrays must have same number of bits, " + f"but the bit array at index 0 has {num_bits} bits " + f"and the bit array at index {i} has {ba.num_bits} bits." + ) + if ba.shape != shape: + raise ValueError( + "All bit arrays must have same shape, " + f"but the bit array at index 0 has shape {shape} " + f"and the bit array at index {i} has shape {ba.shape}." + ) + data = np.concatenate([ba.array for ba in bit_arrays], axis=-2) + return BitArray(data, num_bits) + + @staticmethod + def concatenate_bits(bit_arrays: Sequence[BitArray]) -> BitArray: + """Join a sequence of bit arrays along the bits axis. + + .. note:: + This method is equivalent to per-shot bitstring concatenation. + + Args: + bit_arrays: Bit arrays that have (1) the same number of shots, + and (2) the same shape. + + Returns: + The stacked bit array. + + Raises: + ValueError: If the sequence of bit arrays is empty. + ValueError: If any bit arrays has a different number of shots. + ValueError: If any bit arrays has a different shape. + """ + if len(bit_arrays) == 0: + raise ValueError("Need at least one bit array to stack") + num_shots = bit_arrays[0].num_shots + shape = bit_arrays[0].shape + for i, ba in enumerate(bit_arrays): + if ba.num_shots != num_shots: + raise ValueError( + "All bit arrays must have same number of shots, " + f"but the bit array at index 0 has {num_shots} shots " + f"and the bit array at index {i} has {ba.num_shots} shots." + ) + if ba.shape != shape: + raise ValueError( + "All bit arrays must have same shape, " + f"but the bit array at index 0 has shape {shape} " + f"and the bit array at index {i} has shape {ba.shape}." + ) + # This implementation introduces a temporary 8x memory overhead due to bit + # unpacking. This could be fixed using bitwise functions, at the expense of a + # more complicated implementation. + data = np.concatenate([_unpack(ba) for ba in bit_arrays], axis=-1) + data, num_bits = _pack(data) + return BitArray(data, num_bits) diff --git a/qiskit/primitives/containers/data_bin.py b/qiskit/primitives/containers/data_bin.py index 50934b6cdfd..5ea31f7510e 100644 --- a/qiskit/primitives/containers/data_bin.py +++ b/qiskit/primitives/containers/data_bin.py @@ -15,110 +15,151 @@ """ from __future__ import annotations -from collections.abc import Iterable, Sequence -from dataclasses import make_dataclass -from typing import Any +from typing import Any, ItemsView, Iterable, KeysView, ValuesView +import numpy as np -class DataBinMeta(type): - """Metaclass for :class:`DataBin` that adds the shape to the type name. +from .shape import ShapedMixin, ShapeInput, shape_tuple - This is so that the class has a custom repr with DataBin<*shape> notation. - """ - def __repr__(cls): - name = cls.__name__ - if cls._SHAPE is None: - return name - shape = ",".join(map(str, cls._SHAPE)) - return f"{name}<{shape}>" +def _value_repr(value: Any) -> str: + """Helper function for :meth:`DataBin.__repr__`.""" + if isinstance(value, np.ndarray): + return f"np.ndarray()" + return repr(value) + + +class DataBin(ShapedMixin): + """Namespace for storing data. + + .. code-block:: python + data = DataBin( + alpha=BitArray.from_bitstrings(["0010"]), + beta=np.array([1.2]) + ) -class DataBin(metaclass=DataBinMeta): - """Base class for data bin containers. + print("alpha data:", data.alpha) + print("beta data:", data.beta) - Subclasses are typically made via :class:`~make_data_bin`, which is a specialization of - :class:`make_dataclass`. """ - _RESTRICTED_NAMES = { - "_RESTRICTED_NAMES", - "_SHAPE", - "_FIELDS", - "_FIELD_TYPES", - "keys", - "values", - "items", - } - _SHAPE: tuple[int, ...] | None = None - _FIELDS: tuple[str, ...] = () - """The fields allowed in this data bin.""" - _FIELD_TYPES: tuple[type, ...] = () - """The types of each field.""" + __slots__ = ("_data", "_shape") + + _RESTRICTED_NAMES = frozenset( + { + "_RESTRICTED_NAMES", + "_SHAPE", + "_FIELDS", + "_FIELD_TYPES", + "_data", + "_shape", + "keys", + "values", + "items", + "shape", + "ndim", + "size", + } + ) + + def __init__(self, *, shape: ShapeInput = (), **data): + """ + Args: + data: Name/value data to place in the data bin. + shape: The leading shape common to all entries in the data bin. This defaults to + the trivial leading shape of ``()`` that is compatible with all objects. + + Raises: + ValueError: If a name overlaps with a method name on this class. + ValueError: If some value is inconsistent with the provided shape. + """ + if not self._RESTRICTED_NAMES.isdisjoint(data): + bad_names = sorted(self._RESTRICTED_NAMES.intersection(data)) + raise ValueError(f"Cannot assign with these field names: {bad_names}") + + _setattr = super().__setattr__ + _setattr("_shape", shape_tuple(shape)) + _setattr("_data", data) + + ndim = len(self._shape) + for name, value in data.items(): + if getattr(value, "shape", shape)[:ndim] != shape: + raise ValueError(f"The value of '{name}' does not lead with the shape {shape}.") + _setattr(name, value) + + super().__init__() def __len__(self): - return len(self._FIELDS) + return len(self._data) + + def __setattr__(self, *_): + raise NotImplementedError def __repr__(self): - vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) - return f"{type(self)}({', '.join(vals)})" + vals = [f"{name}={_value_repr(val)}" for name, val in self.items()] + if self.ndim: + vals.append(f"shape={self.shape}") + return f"{type(self).__name__}({', '.join(vals)})" def __getitem__(self, key: str) -> Any: - if key not in self._FIELDS: - raise KeyError(f"Key ({key}) does not exist in this data bin.") - return getattr(self, key) + try: + return self._data[key] + except KeyError as ex: + raise KeyError(f"Key ({key}) does not exist in this data bin.") from ex def __contains__(self, key: str) -> bool: - return key in self._FIELDS + return key in self._data def __iter__(self) -> Iterable[str]: - return iter(self._FIELDS) + return iter(self._data) - def keys(self) -> Sequence[str]: - """Return a list of field names.""" - return tuple(self._FIELDS) + def keys(self) -> KeysView[str]: + """Return a view of field names.""" + return self._data.keys() - def values(self) -> Sequence[Any]: - """Return a list of values.""" - return tuple(getattr(self, key) for key in self._FIELDS) + def values(self) -> ValuesView[Any]: + """Return a view of values.""" + return self._data.values() - def items(self) -> Sequence[tuple[str, Any]]: - """Return a list of field names and values""" - return tuple((key, getattr(self, key)) for key in self._FIELDS) + def items(self) -> ItemsView[str, Any]: + """Return a view of field names and values""" + return self._data.items() + # The following properties exist to provide support to legacy private class attributes which + # gained widespread prior to qiskit 1.1. These properties will be removed once the internal + # projects have made the appropriate changes. + @property + def _FIELDS(self) -> tuple[str, ...]: # pylint: disable=invalid-name + return tuple(self._data) + + @property + def _FIELD_TYPES(self) -> tuple[Any, ...]: # pylint: disable=invalid-name + return tuple(map(type, self.values())) + + @property + def _SHAPE(self) -> tuple[int, ...]: # pylint: disable=invalid-name + return self.shape + + +# pylint: disable=unused-argument def make_data_bin( fields: Iterable[tuple[str, type]], shape: tuple[int, ...] | None = None ) -> type[DataBin]: - """Return a new subclass of :class:`~DataBin` with the provided fields and shape. + """Return the :class:`~DataBin` type. - .. code-block:: python - - my_bin = make_data_bin([("alpha", np.NDArray[np.float64])], shape=(20, 30)) - - # behaves like a dataclass - my_bin(alpha=np.empty((20, 30))) + .. note:: + This class used to return a subclass of :class:`~DataBin`. However, that caused confusion + and didn't have a useful purpose. Several internal projects made use of this internal + function prior to qiskit 1.1. This function will be removed once these internal projects + have made the appropriate changes. Args: fields: Tuples ``(name, type)`` specifying the attributes of the returned class. shape: The intended shape of every attribute of this class. Returns: - A new class. + The :class:`DataBin` type. """ - field_names, field_types = zip(*fields) if fields else ((), ()) - for name in field_names: - if name in DataBin._RESTRICTED_NAMES: - raise ValueError(f"'{name}' is a restricted name for a DataBin.") - cls = make_dataclass( - "DataBin", - dict(zip(field_names, field_types)), - bases=(DataBin,), - frozen=True, - unsafe_hash=True, - repr=False, - ) - cls._SHAPE = shape - cls._FIELDS = field_names - cls._FIELD_TYPES = field_types - return cls + return DataBin diff --git a/qiskit/primitives/containers/pub_result.py b/qiskit/primitives/containers/pub_result.py index 369179e4629..1facb850ade 100644 --- a/qiskit/primitives/containers/pub_result.py +++ b/qiskit/primitives/containers/pub_result.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ -Base Pub class +Base Pub result class """ from __future__ import annotations diff --git a/qiskit/primitives/containers/sampler_pub_result.py b/qiskit/primitives/containers/sampler_pub_result.py new file mode 100644 index 00000000000..7a2d2a9e3fc --- /dev/null +++ b/qiskit/primitives/containers/sampler_pub_result.py @@ -0,0 +1,74 @@ +# 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. + +""" +Sampler Pub result class +""" + +from __future__ import annotations + +from typing import Iterable + +import numpy as np + +from .bit_array import BitArray +from .pub_result import PubResult + + +class SamplerPubResult(PubResult): + """Result of Sampler Pub.""" + + def join_data(self, names: Iterable[str] | None = None) -> BitArray | np.ndarray: + """Join data from many registers into one data container. + + Data is joined along the bits axis. For example, for :class:`~.BitArray` data, this corresponds + to bitstring concatenation. + + Args: + names: Which registers to join. Their order is maintained, for example, given + ``["alpha", "beta"]``, the data from register ``alpha`` is placed to the left of the + data from register ``beta``. When ``None`` is given, this value is set to the + ordered list of register names, which will have been preserved from the input circuit + order. + + Returns: + Joint data. + + Raises: + ValueError: If specified names are empty. + ValueError: If specified name does not exist. + TypeError: If specified data comes from incompatible types. + """ + if names is None: + names = list(self.data) + if not names: + raise ValueError("No entry exists in the data bin.") + else: + names = list(names) + if not names: + raise ValueError("An empty name list is given.") + for name in names: + if name not in self.data: + raise ValueError(f"Name '{name}' does not exist.") + + data = [self.data[name] for name in names] + if isinstance(data[0], BitArray): + if not all(isinstance(datum, BitArray) for datum in data): + raise TypeError("Data comes from incompatible types.") + joint_data = BitArray.concatenate_bits(data) + elif isinstance(data[0], np.ndarray): + if not all(isinstance(datum, np.ndarray) for datum in data): + raise TypeError("Data comes from incompatible types.") + joint_data = np.concatenate(data, axis=-1) + else: + raise TypeError("Data comes from incompatible types.") + return joint_data diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index a3b2cddefdd..a5dc029edf7 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -22,7 +22,7 @@ from qiskit.quantum_info import SparsePauliOp, Statevector from .base import BaseEstimatorV2 -from .containers import EstimatorPubLike, PrimitiveResult, PubResult +from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob from .utils import bound_circuit_to_instruction @@ -160,6 +160,6 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: raise ValueError("Given operator is not Hermitian and noise cannot be added.") expectation_value = rng.normal(expectation_value, precision) evs[index] = expectation_value - data_bin_cls = self._make_data_bin(pub) - data_bin = data_bin_cls(evs=evs, stds=stds) - return PubResult(data_bin, metadata={"precision": precision}) + + data = DataBin(evs=evs, stds=stds, shape=evs.shape) + return PubResult(data, metadata={"precision": precision}) diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index d04eb3894ff..90fe452ad12 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -15,9 +15,9 @@ from __future__ import annotations +import warnings from dataclasses import dataclass from typing import Iterable -import warnings import numpy as np from numpy.typing import NDArray @@ -30,10 +30,10 @@ from .base.validation import _has_measure from .containers import ( BitArray, + DataBin, PrimitiveResult, - PubResult, + SamplerPubResult, SamplerPubLike, - make_data_bin, ) from .containers.sampler_pub import SamplerPub from .containers.bit_array import _min_num_bytes @@ -154,7 +154,7 @@ def seed(self) -> np.random.Generator | int | None: def run( self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> PrimitiveJob[PrimitiveResult[PubResult]]: + ) -> PrimitiveJob[PrimitiveResult[SamplerPubResult]]: if shots is None: shots = self._default_shots coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] @@ -169,11 +169,11 @@ def run( job._submit() return job - def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[PubResult]: + def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]: results = [self._run_pub(pub) for pub in pubs] return PrimitiveResult(results) - def _run_pub(self, pub: SamplerPub) -> PubResult: + def _run_pub(self, pub: SamplerPub) -> SamplerPubResult: circuit, qargs, meas_info = _preprocess_circuit(pub.circuit) bound_circuits = pub.parameter_values.bind_all(circuit) arrays = { @@ -194,15 +194,10 @@ def _run_pub(self, pub: SamplerPub) -> PubResult: ary = _samples_to_packed_array(samples_array, item.num_bits, item.qreg_indices) arrays[item.creg_name][index] = ary - data_bin_cls = make_data_bin( - [(item.creg_name, BitArray) for item in meas_info], - shape=bound_circuits.shape, - ) meas = { item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info } - data_bin = data_bin_cls(**meas) - return PubResult(data_bin, metadata={"shots": pub.shots}) + return SamplerPubResult(DataBin(**meas, shape=pub.shape), metadata={"shots": pub.shots}) def _preprocess_circuit(circuit: QuantumCircuit): diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index 19d300c9bbf..6736d67a214 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -131,7 +131,6 @@ .. autoexception:: JobTimeoutError .. autoexception:: BackendConfigurationError -===================== Writing a New Backend ===================== @@ -164,7 +163,7 @@ `qiskit-aqt-provider `__ Provider -======== +-------- A provider class serves a single purpose: to get backend objects that enable executing circuits on a device or simulator. The expectation is that any @@ -195,7 +194,7 @@ def backends(self, name=None, **kwargs): method matches the required interface. The rest is up to the specific provider on how to implement. Backend -======= +------- The backend classes are the core to the provider. These classes are what provide the interface between Qiskit and the hardware or simulator that will @@ -276,8 +275,8 @@ def run(circuits, **kwargs): return MyJob(self. job_handle, job_json, circuit) -Transpiler Interface --------------------- +Backend's Transpiler Interface +------------------------------ The key piece of the :class:`~qiskit.providers.Backend` object is how it describes itself to the compiler. This is handled with the :class:`~qiskit.transpiler.Target` class which defines @@ -453,8 +452,45 @@ def get_translation_stage_plugin(self): efficient output on ``Mybackend`` the transpiler will be able to perform these custom steps without any manual user input. -Run Method ----------- +.. _providers-guide-real-time-variables: + +Real-time variables +^^^^^^^^^^^^^^^^^^^ + +The transpiler will automatically handle real-time typed classical variables (see +:mod:`qiskit.circuit.classical`) and treat the :class:`.Store` instruction as a built-in +"directive", similar to :class:`.Barrier`. No special handling from backends is necessary to permit +this. + +If your backend is *unable* to handle classical variables and storage, we recommend that you comment +on this in your documentation, and insert a check into your :meth:`~.BackendV2.run` method (see +:ref:`providers-guide-backend-run`) to eagerly reject circuits containing them. You can examine +:attr:`.QuantumCircuit.num_vars` for the presence of variables at the top level. If you accept +:ref:`control-flow operations `, you might need to recursively search the +internal :attr:`~.ControlFlowOp.blocks` of each for scope-local variables with +:attr:`.QuantumCircuit.num_declared_vars`. + +For example, a function to check for the presence of any manual storage locations, or manual stores +to memory:: + + from qiskit.circuit import Store, ControlFlowOp, QuantumCircuit + + def has_realtime_logic(circuit: QuantumCircuit) -> bool: + if circuit.num_vars: + return True + for instruction in circuit.data: + if isinstance(instruction.operation, Store): + return True + elif isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + if has_realtime_logic(block): + return True + return False + +.. _providers-guide-backend-run: + +Backend.run Method +------------------ Of key importance is the :meth:`~qiskit.providers.BackendV2.run` method, which is used to actually submit circuits to a device or simulator. The run method @@ -484,8 +520,8 @@ def run(self, circuits. **kwargs): job_handle = submit_to_backend(job_jsonb) return MyJob(self. job_handle, job_json, circuit) -Options -------- +Backend Options +--------------- There are often several options for a backend that control how a circuit is run. The typical example of this is something like the number of ``shots`` which is @@ -515,7 +551,7 @@ def _default_options(cls): Job -=== +--- The output from the :obj:`~qiskit.providers.BackendV2.run` method is a :class:`~qiskit.providers.JobV1` object. Each provider is expected to implement a custom job subclass that @@ -612,7 +648,7 @@ def status(self): return JobStatus.DONE Primitives -========== +---------- While not directly part of the provider interface, the :mod:`qiskit.primitives` module is tightly coupled with providers. Specifically the primitive @@ -640,12 +676,8 @@ def status(self): :class:`~.Estimator`, :class:`~.BackendSampler`, and :class:`~.BackendEstimator` can serve as references/models on how to implement these as well. -====================================== -Migrating between Backend API Versions -====================================== - -BackendV1 -> BackendV2 -====================== +Migrating from BackendV1 to BackendV2 +===================================== The :obj:`~BackendV2` class re-defined user access for most properties of a backend to make them work with native Qiskit data structures and have flatter diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 8ffc7765109..931dbed479e 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -86,14 +86,8 @@ def __init__(self, configuration, provider=None, **fields): .. This next bit is necessary just because autosummary generally won't summarise private - methods; changing that behaviour would have annoying knock-on effects through all the + methods; changing that behavior would have annoying knock-on effects through all the rest of the documentation, so instead we just hard-code the automethod directive. - - In addition to the public abstract methods, subclasses should also implement the following - private methods: - - .. automethod:: _default_options - :noindex: """ self._configuration = configuration self._options = self._default_options() @@ -101,7 +95,7 @@ def __init__(self, configuration, provider=None, **fields): if fields: for field in fields: if field not in self._options.data: - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_config(**fields) @classmethod @@ -135,7 +129,7 @@ def set_options(self, **fields): """ for field in fields: if not hasattr(self._options, field): - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_options(**fields) def configuration(self): @@ -358,7 +352,7 @@ def __init__( if fields: for field in fields: if field not in self._options.data: - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_config(**fields) self.name = name """Name of the backend.""" @@ -604,7 +598,7 @@ def set_options(self, **fields): """ for field in fields: if not hasattr(self._options, field): - raise AttributeError("Options field %s is not valid for this backend" % field) + raise AttributeError(f"Options field {field} is not valid for this backend") self._options.update_options(**fields) @property diff --git a/qiskit/providers/basic_provider/__init__.py b/qiskit/providers/basic_provider/__init__.py index 48427c73fca..4fc0f06d76a 100644 --- a/qiskit/providers/basic_provider/__init__.py +++ b/qiskit/providers/basic_provider/__init__.py @@ -27,36 +27,15 @@ backend = BasicProvider().get_backend('basic_simulator') -Simulators -========== +Classes +======= .. autosummary:: :toctree: ../stubs/ BasicSimulator - -Provider -======== - -.. autosummary:: - :toctree: ../stubs/ - BasicProvider - -Job Class -========= - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderJob - -Exceptions -========== - -.. autosummary:: - :toctree: ../stubs/ - BasicProviderError """ diff --git a/qiskit/providers/basic_provider/basic_provider_tools.py b/qiskit/providers/basic_provider/basic_provider_tools.py index 030c629275e..786815dda53 100644 --- a/qiskit/providers/basic_provider/basic_provider_tools.py +++ b/qiskit/providers/basic_provider/basic_provider_tools.py @@ -23,7 +23,30 @@ from qiskit.exceptions import QiskitError # Single qubit gates supported by ``single_gate_params``. -SINGLE_QUBIT_GATES = ("U", "u", "h", "p", "u1", "u2", "u3", "rz", "sx", "x") +SINGLE_QUBIT_GATES = { + "U": gates.UGate, + "u": gates.UGate, + "u1": gates.U1Gate, + "u2": gates.U2Gate, + "u3": gates.U3Gate, + "h": gates.HGate, + "p": gates.PhaseGate, + "s": gates.SGate, + "sdg": gates.SdgGate, + "sx": gates.SXGate, + "sxdg": gates.SXdgGate, + "t": gates.TGate, + "tdg": gates.TdgGate, + "x": gates.XGate, + "y": gates.YGate, + "z": gates.ZGate, + "id": gates.IGate, + "i": gates.IGate, + "r": gates.RGate, + "rx": gates.RXGate, + "ry": gates.RYGate, + "rz": gates.RZGate, +} def single_gate_matrix(gate: str, params: list[float] | None = None) -> np.ndarray: @@ -40,42 +63,55 @@ def single_gate_matrix(gate: str, params: list[float] | None = None) -> np.ndarr """ if params is None: params = [] - - if gate == "U": - gc = gates.UGate - elif gate == "u3": - gc = gates.U3Gate - elif gate == "h": - gc = gates.HGate - elif gate == "u": - gc = gates.UGate - elif gate == "p": - gc = gates.PhaseGate - elif gate == "u2": - gc = gates.U2Gate - elif gate == "u1": - gc = gates.U1Gate - elif gate == "rz": - gc = gates.RZGate - elif gate == "id": - gc = gates.IGate - elif gate == "sx": - gc = gates.SXGate - elif gate == "x": - gc = gates.XGate + if gate in SINGLE_QUBIT_GATES: + gc = SINGLE_QUBIT_GATES[gate] else: - raise QiskitError("Gate is not a valid basis gate for this simulator: %s" % gate) + raise QiskitError(f"Gate is not a valid basis gate for this simulator: {gate}") return gc(*params).to_matrix() -# Cache CX matrix as no parameters. -_CX_MATRIX = gates.CXGate().to_matrix() - - -def cx_gate_matrix() -> np.ndarray: - """Get the matrix for a controlled-NOT gate.""" - return _CX_MATRIX +# Two qubit gates WITHOUT parameters: name -> matrix +TWO_QUBIT_GATES = { + "CX": gates.CXGate().to_matrix(), + "cx": gates.CXGate().to_matrix(), + "ecr": gates.ECRGate().to_matrix(), + "cy": gates.CYGate().to_matrix(), + "cz": gates.CZGate().to_matrix(), + "swap": gates.SwapGate().to_matrix(), + "iswap": gates.iSwapGate().to_matrix(), + "ch": gates.CHGate().to_matrix(), + "cs": gates.CSGate().to_matrix(), + "csdg": gates.CSdgGate().to_matrix(), + "csx": gates.CSXGate().to_matrix(), + "dcx": gates.DCXGate().to_matrix(), +} + +# Two qubit gates WITH parameters: name -> class +TWO_QUBIT_GATES_WITH_PARAMETERS = { + "cp": gates.CPhaseGate, + "crx": gates.CRXGate, + "cry": gates.CRYGate, + "crz": gates.CRZGate, + "cu": gates.CUGate, + "cu1": gates.CU1Gate, + "cu3": gates.CU3Gate, + "rxx": gates.RXXGate, + "ryy": gates.RYYGate, + "rzz": gates.RZZGate, + "rzx": gates.RZXGate, + "xx_minus_yy": gates.XXMinusYYGate, + "xx_plus_yy": gates.XXPlusYYGate, +} + + +# Three qubit gates: name -> matrix +THREE_QUBIT_GATES = { + "ccx": gates.CCXGate().to_matrix(), + "ccz": gates.CCZGate().to_matrix(), + "rccx": gates.RCCXGate().to_matrix(), + "cswap": gates.CSwapGate().to_matrix(), +} def einsum_matmul_index(gate_indices: list[int], number_of_qubits: int) -> str: diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index e1902151919..9971bf36725 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -40,7 +40,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import UnitaryGate -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping +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 @@ -51,8 +51,12 @@ from .basic_provider_job import BasicProviderJob from .basic_provider_tools import single_gate_matrix -from .basic_provider_tools import SINGLE_QUBIT_GATES -from .basic_provider_tools import cx_gate_matrix +from .basic_provider_tools import ( + SINGLE_QUBIT_GATES, + TWO_QUBIT_GATES, + TWO_QUBIT_GATES_WITH_PARAMETERS, + THREE_QUBIT_GATES, +) from .basic_provider_tools import einsum_vecmul_index from .exceptions import BasicProviderError @@ -138,21 +142,59 @@ def _build_basic_target(self) -> Target: num_qubits=None, ) basis_gates = [ + "ccx", + "ccz", + "ch", + "cp", + "crx", + "cry", + "crz", + "cs", + "csdg", + "cswap", + "csx", + "cu", + "cu1", + "cu3", + "cx", + "cy", + "cz", + "dcx", + "delay", + "ecr", + "global_phase", "h", - "u", + "id", + "iswap", + "measure", "p", + "r", + "rccx", + "reset", + "rx", + "rxx", + "ry", + "ryy", + "rz", + "rzx", + "rzz", + "s", + "sdg", + "swap", + "sx", + "sxdg", + "t", + "tdg", + "u", "u1", "u2", "u3", - "rz", - "sx", - "x", - "cx", - "id", "unitary", - "measure", - "delay", - "reset", + "x", + "xx_minus_yy", + "xx_plus_yy", + "y", + "z", ] inst_mapping = get_standard_gate_name_mapping() for name in basis_gates: @@ -166,7 +208,7 @@ def _build_basic_target(self) -> Target: target.add_instruction(UnitaryGate, name="unitary") else: raise BasicProviderError( - "Gate is not a valid basis gate for this simulator: %s" % name + f"Gate is not a valid basis gate for this simulator: {name}" ) return target @@ -486,13 +528,13 @@ def run( from qiskit.compiler import assemble out_options = {} - for key in backend_options: + for key, value in backend_options.items(): if not hasattr(self.options, key): warnings.warn( - "Option %s is not used by this backend" % key, UserWarning, stacklevel=2 + f"Option {key} is not used by this backend", UserWarning, stacklevel=2 ) else: - out_options[key] = backend_options[key] + out_options[key] = value qobj = assemble(run_input, self, **out_options) qobj_options = qobj.config self._set_options(qobj_config=qobj_options, backend_options=backend_options) @@ -617,24 +659,41 @@ def run_experiment(self, experiment: QasmQobjExperiment) -> dict[str, ...]: value >>= 1 if value != int(operation.conditional.val, 16): continue - # Check if single gate if operation.name == "unitary": qubits = operation.qubits gate = operation.params[0] self._add_unitary(gate, qubits) + elif operation.name in ("id", "u0", "delay"): + pass + elif operation.name == "global_phase": + params = getattr(operation, "params", None) + gate = GlobalPhaseGate(*params).to_matrix() + self._add_unitary(gate, []) + # Check if single qubit gate elif operation.name in SINGLE_QUBIT_GATES: params = getattr(operation, "params", None) qubit = operation.qubits[0] gate = single_gate_matrix(operation.name, params) self._add_unitary(gate, [qubit]) - # Check if CX gate + elif operation.name in TWO_QUBIT_GATES_WITH_PARAMETERS: + params = getattr(operation, "params", None) + qubit0 = operation.qubits[0] + qubit1 = operation.qubits[1] + gate = TWO_QUBIT_GATES_WITH_PARAMETERS[operation.name](*params).to_matrix() + self._add_unitary(gate, [qubit0, qubit1]) elif operation.name in ("id", "u0"): pass - elif operation.name in ("CX", "cx"): + elif operation.name in TWO_QUBIT_GATES: qubit0 = operation.qubits[0] qubit1 = operation.qubits[1] - gate = cx_gate_matrix() + gate = TWO_QUBIT_GATES[operation.name] self._add_unitary(gate, [qubit0, qubit1]) + elif operation.name in THREE_QUBIT_GATES: + qubit0 = operation.qubits[0] + qubit1 = operation.qubits[1] + qubit2 = operation.qubits[2] + gate = THREE_QUBIT_GATES[operation.name] + self._add_unitary(gate, [qubit0, qubit1, qubit2]) # Check if reset elif operation.name == "reset": qubit = operation.qubits[0] diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 00dadd2ad25..9526793f0e1 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -24,7 +24,7 @@ useful for testing the transpiler and other backend-facing functionality. Example Usage -============= +------------- Here is an example of using a simulated backend for transpilation and running. diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index d84aba46371..4a638f31557 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -143,8 +143,8 @@ def run(self, run_input, **kwargs): pulse_job = False if pulse_job is None: raise QiskitError( - "Invalid input object %s, must be either a " - "QuantumCircuit, Schedule, or a list of either" % circuits + f"Invalid input object {circuits}, must be either a " + "QuantumCircuit, Schedule, or a list of either" ) if pulse_job: raise QiskitError("Pulse simulation is currently not supported for fake backends.") diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index e806c75ea3a..214754080e5 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -375,6 +375,11 @@ def _build_generic_target(self): f"in the standard qiskit circuit library." ) gate = self._supported_gates[name] + if self.num_qubits < gate.num_qubits: + raise QiskitError( + f"Provided basis gate {name} needs more qubits than {self.num_qubits}, " + f"which is the size of the backend." + ) noise_params = self._get_noise_defaults(name, gate.num_qubits) self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) @@ -496,8 +501,8 @@ def run(self, run_input, **options): pulse_job = False if pulse_job is None: # submitted job is invalid raise QiskitError( - "Invalid input object %s, must be either a " - "QuantumCircuit, Schedule, or a list of either" % circuits + f"Invalid input object {circuits}, must be either a " + "QuantumCircuit, Schedule, or a list of either" ) if pulse_job: # pulse job raise QiskitError("Pulse simulation is currently not supported for V2 backends.") diff --git a/qiskit/providers/models/__init__.py b/qiskit/providers/models/__init__.py index a69038eb78c..bf90a9d16c0 100644 --- a/qiskit/providers/models/__init__.py +++ b/qiskit/providers/models/__init__.py @@ -19,8 +19,8 @@ Qiskit schema-conformant objects used by the backends and providers. -Backend Objects -=============== +Classes +======= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/providers/models/backendconfiguration.py b/qiskit/providers/models/backendconfiguration.py index e346e293a38..ebd0a6d9bbb 100644 --- a/qiskit/providers/models/backendconfiguration.py +++ b/qiskit/providers/models/backendconfiguration.py @@ -892,9 +892,9 @@ def get_qubit_channels(self, qubit: Union[int, Iterable[int]]) -> List[Channel]: channels = set() try: if isinstance(qubit, int): - for key in self._qubit_channel_map.keys(): + for key, value in self._qubit_channel_map.items(): if qubit in key: - channels.update(self._qubit_channel_map[key]) + channels.update(value) if len(channels) == 0: raise KeyError elif isinstance(qubit, list): diff --git a/qiskit/providers/models/backendproperties.py b/qiskit/providers/models/backendproperties.py index 3b5b9c5e010..332aac7c5ed 100644 --- a/qiskit/providers/models/backendproperties.py +++ b/qiskit/providers/models/backendproperties.py @@ -404,9 +404,9 @@ def qubit_property( if name is not None: result = result[name] except KeyError as ex: + formatted_name = "y '" + name + "'" if name else "ies" raise BackendPropertyError( - "Couldn't find the propert{name} for qubit " - "{qubit}.".format(name="y '" + name + "'" if name else "ies", qubit=qubit) + f"Couldn't find the propert{formatted_name} for qubit {qubit}." ) from ex return result diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index 13becb1c956..7c1864bad9e 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -296,9 +296,4 @@ def __str__(self): 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 "<{name}({insts}{qfreq}\n{mfreq})>".format( - name=self.__class__.__name__, - insts=str(self.instruction_schedule_map), - qfreq=qfreq, - mfreq=mfreq, - ) + return f"<{self.__class__.__name__}({str(self.instruction_schedule_map)}{qfreq}\n{mfreq})>" diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index 7a5b7a26035..4d716fb372b 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -116,7 +116,7 @@ def __len__(self): def __setitem__(self, key, value): self.update_options(**{key: value}) - # backwards-compatibilty with Qiskit Experiments: + # backwards-compatibility with Qiskit Experiments: @property def __dict__(self): @@ -154,7 +154,7 @@ def __copy__(self): The returned option and validator values are shallow copies of the originals. """ - out = self.__new__(type(self)) + out = self.__new__(type(self)) # pylint:disable=no-value-for-parameter out.__setstate__((self._fields.copy(), self.validator.copy())) return out @@ -170,7 +170,7 @@ def __init__(self, **kwargs): def __repr__(self): items = (f"{k}={v!r}" for k, v in self._fields.items()) - return "{}({})".format(type(self).__name__, ", ".join(items)) + return f"{type(self).__name__}({', '.join(items)})" def __eq__(self, other): if isinstance(self, Options) and isinstance(other, Options): @@ -211,7 +211,7 @@ def set_validator(self, field, validator_value): """ if field not in self._fields: - raise KeyError("Field '%s' is not present in this options object" % field) + raise KeyError(f"Field '{field}' is not present in this options object") if isinstance(validator_value, tuple): if len(validator_value) != 2: raise ValueError( @@ -229,28 +229,28 @@ def set_validator(self, field, validator_value): f"{type(validator_value)} is not a valid validator type, it " "must be a tuple, list, or class/type" ) - self.validator[field] = validator_value + self.validator[field] = validator_value # pylint: disable=unsupported-assignment-operation def update_options(self, **fields): """Update options with kwargs""" - for field in fields: - field_validator = self.validator.get(field, None) + for field_name, field in fields.items(): + field_validator = self.validator.get(field_name, None) if isinstance(field_validator, tuple): - if fields[field] > field_validator[1] or fields[field] < field_validator[0]: + if field > field_validator[1] or field < field_validator[0]: raise ValueError( - f"Specified value for '{field}' is not a valid value, " + f"Specified value for '{field_name}' is not a valid value, " f"must be >={field_validator[0]} or <={field_validator[1]}" ) elif isinstance(field_validator, list): - if fields[field] not in field_validator: + if field not in field_validator: raise ValueError( - f"Specified value for {field} is not a valid choice, " + f"Specified value for {field_name} is not a valid choice, " f"must be one of {field_validator}" ) elif isinstance(field_validator, type): - if not isinstance(fields[field], field_validator): + if not isinstance(field, field_validator): raise TypeError( - f"Specified value for {field} is not of required type {field_validator}" + f"Specified value for {field_name} is not of required type {field_validator}" ) self._fields.update(fields) diff --git a/qiskit/providers/providerutils.py b/qiskit/providers/providerutils.py index 1e65499d756..36592dc1bc6 100644 --- a/qiskit/providers/providerutils.py +++ b/qiskit/providers/providerutils.py @@ -21,7 +21,9 @@ logger = logging.getLogger(__name__) -def filter_backends(backends: list[Backend], filters: Callable = None, **kwargs) -> list[Backend]: +def filter_backends( + backends: list[Backend], filters: Callable[[Backend], bool] | None = None, **kwargs +) -> list[Backend]: """Return the backends matching the specified filtering. Filter the `backends` list by their `configuration` or `status` diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index b7bdbe85c19..8767e8c4e93 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -74,8 +74,8 @@ The builder initializes a :class:`.pulse.Schedule`, ``pulse_prog`` and then begins to construct the program within the context. The output pulse -schedule will survive after the context is exited and can be transpiled and executed like a -normal Qiskit schedule using ``backend.run(transpile(pulse_prog, backend))``. +schedule will survive after the context is exited and can be used like a +normal Qiskit schedule. Pulse programming has a simple imperative style. This leaves the programmer to worry about the raw experimental physics of pulse programming and not diff --git a/qiskit/pulse/configuration.py b/qiskit/pulse/configuration.py index 4668152973f..1bfd1f13e2e 100644 --- a/qiskit/pulse/configuration.py +++ b/qiskit/pulse/configuration.py @@ -55,11 +55,9 @@ def __init__(self, name: str | None = None, **params): self.params = params def __repr__(self): - return "{}({}{})".format( - self.__class__.__name__, - "'" + self.name + "', " or "", - ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()), - ) + name_repr = "'" + self.name + "', " + params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) + return f"{self.__class__.__name__}({name_repr}{params_repr})" def __eq__(self, other): if isinstance(other, Kernel): @@ -83,11 +81,9 @@ def __init__(self, name: str | None = None, **params): self.params = params def __repr__(self): - return "{}({}{})".format( - self.__class__.__name__, - "'" + self.name + "', " or "", - ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()), - ) + name_repr = "'" + self.name + "', " or "" + params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) + return f"{self.__class__.__name__}({name_repr}{params_repr})" def __eq__(self, other): if isinstance(other, Discriminator): @@ -184,7 +180,7 @@ def add_lo(self, channel: DriveChannel | MeasureChannel, freq: float): self.check_lo(channel, freq) self._m_lo_freq[channel] = freq else: - raise PulseError("Specified channel %s cannot be configured." % channel.name) + raise PulseError(f"Specified channel {channel.name} cannot be configured.") def add_lo_range( self, channel: DriveChannel | MeasureChannel, lo_range: LoRange | tuple[int, int] @@ -236,7 +232,7 @@ def channel_lo(self, channel: DriveChannel | MeasureChannel) -> float: if channel in self.meas_los: return self.meas_los[channel] - raise PulseError("Channel %s is not configured" % channel) + raise PulseError(f"Channel {channel} is not configured") @property def qubit_los(self) -> dict[DriveChannel, float]: diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index 3d3767509f2..afa71b6825a 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -169,10 +169,8 @@ def assert_has( if not self.has(instruction, _to_tuple(qubits)): if instruction in self._map: raise PulseError( - "Operation '{inst}' exists, but is only defined for qubits " - "{qubits}.".format( - inst=instruction, qubits=self.qubits_with_instruction(instruction) - ) + f"Operation '{instruction}' exists, but is only defined for qubits " + f"{self.qubits_with_instruction(instruction)}." ) raise PulseError(f"Operation '{instruction}' is not defined for this system.") @@ -250,7 +248,7 @@ def add( # validation of target qubit qubits = _to_tuple(qubits) - if qubits == (): + if not qubits: raise PulseError(f"Cannot add definition {instruction} with no target qubits.") # generate signature diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py index 066163a79b0..98fbf460c1b 100644 --- a/qiskit/pulse/instructions/acquire.py +++ b/qiskit/pulse/instructions/acquire.py @@ -138,12 +138,11 @@ def is_parameterized(self) -> bool: return isinstance(self.duration, ParameterExpression) or super().is_parameterized() def __repr__(self) -> str: - return "{}({}{}{}{}{}{})".format( - self.__class__.__name__, - self.duration, - ", " + str(self.channel), - ", " + str(self.mem_slot) if self.mem_slot else "", - ", " + str(self.reg_slot) if self.reg_slot else "", - ", " + str(self.kernel) if self.kernel else "", - ", " + str(self.discriminator) if self.discriminator else "", + mem_slot_repr = str(self.mem_slot) if self.mem_slot else "" + reg_slot_repr = str(self.reg_slot) if self.reg_slot else "" + kernel_repr = str(self.kernel) if self.kernel else "" + discriminator_repr = str(self.discriminator) if self.discriminator else "" + return ( + f"{self.__class__.__name__}({self.duration}, {str(self.channel)}, " + f"{mem_slot_repr}, {reg_slot_repr}, {kernel_repr}, {discriminator_repr})" ) diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py index ece20545b50..61ebe67777f 100644 --- a/qiskit/pulse/instructions/instruction.py +++ b/qiskit/pulse/instructions/instruction.py @@ -264,6 +264,5 @@ def __lshift__(self, time: int): def __repr__(self) -> str: operands = ", ".join(str(op) for op in self.operands) - return "{}({}{})".format( - self.__class__.__name__, operands, f", name='{self.name}'" if self.name else "" - ) + name_repr = f", name='{self.name}'" if self.name else "" + return f"{self.__class__.__name__}({operands}{name_repr})" diff --git a/qiskit/pulse/library/samplers/decorators.py b/qiskit/pulse/library/samplers/decorators.py index ac78fba8595..db6aabd7b1d 100644 --- a/qiskit/pulse/library/samplers/decorators.py +++ b/qiskit/pulse/library/samplers/decorators.py @@ -182,9 +182,9 @@ def _update_docstring(discretized_pulse: Callable, sampler_inst: Callable) -> Ca header, body = wrapped_docstring.split("\n", 1) body = textwrap.indent(body, " ") wrapped_docstring = header + body - updated_ds = """ - Discretized continuous pulse function: `{continuous_name}` using - sampler: `{sampler_name}`. + updated_ds = f""" + Discretized continuous pulse function: `{discretized_pulse.__name__}` using + sampler: `{sampler_inst.__name__}`. The first argument (time) of the continuous pulse function has been replaced with a discretized `duration` of type (int). @@ -198,12 +198,8 @@ def _update_docstring(discretized_pulse: Callable, sampler_inst: Callable) -> Ca Sampled continuous function: - {continuous_doc} - """.format( - continuous_name=discretized_pulse.__name__, - sampler_name=sampler_inst.__name__, - continuous_doc=wrapped_docstring, - ) + {wrapped_docstring} + """ discretized_pulse.__doc__ = updated_ds return discretized_pulse diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9041dbc1951..33d428771b2 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -570,11 +570,8 @@ def __eq__(self, other: object) -> bool: def __repr__(self) -> str: param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) - return "{}({}{})".format( - self._pulse_type, - param_repr, - f", name='{self.name}'" if self.name is not None else "", - ) + name_repr = f", name='{self.name}'" if self.name is not None else "" + return f"{self._pulse_type}({param_repr}{name_repr})" __hash__ = None @@ -677,8 +674,8 @@ def __eq__(self, other: object) -> bool: if not np.isclose(complex_amp1, complex_amp2): return False - for key in self.parameters: - if key not in ["amp", "angle"] and self.parameters[key] != other.parameters[key]: + for key, value in self.parameters.items(): + if key not in ["amp", "angle"] and value != other.parameters[key]: return False return True diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py index e9ad9bcbc71..ad852f226ac 100644 --- a/qiskit/pulse/library/waveform.py +++ b/qiskit/pulse/library/waveform.py @@ -130,8 +130,5 @@ def __repr__(self) -> str: opt = np.get_printoptions() np.set_printoptions(threshold=50) np.set_printoptions(**opt) - return "{}({}{})".format( - self.__class__.__name__, - repr(self.samples), - f", name='{self.name}'" if self.name is not None else "", - ) + name_repr = f", name='{self.name}'" if self.name is not None else "" + return f"{self.__class__.__name__}({repr(self.samples)}{name_repr})" diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 1995d6d20c4..3a39932e5b1 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -124,16 +124,21 @@ def _measure_v1( for qubit in qubits: measure_groups.add(tuple(meas_map[qubit])) for measure_group_qubits in measure_groups: - if qubit_mem_slots is not None: - unused_mem_slots = set(measure_group_qubits) - set(qubit_mem_slots.values()) + + unused_mem_slots = ( + set() + if qubit_mem_slots is None + else set(measure_group_qubits) - set(qubit_mem_slots.values()) + ) + try: default_sched = inst_map.get(measure_name, measure_group_qubits) except exceptions.PulseError as ex: raise exceptions.PulseError( - "We could not find a default measurement schedule called '{}'. " + f"We could not find a default measurement schedule called '{measure_name}'. " "Please provide another name using the 'measure_name' keyword " "argument. For assistance, the instructions which are defined are: " - "{}".format(measure_name, inst_map.instructions) + f"{inst_map.instructions}" ) from ex for time, inst in default_sched.instructions: if inst.channel.index not in qubits: @@ -198,10 +203,10 @@ def _measure_v2( schedule += _schedule_remapping_memory_slot(default_sched, qubit_mem_slots) except KeyError as ex: raise exceptions.PulseError( - "We could not find a default measurement schedule called '{}'. " + f"We could not find a default measurement schedule called '{measure_name}'. " "Please provide another name using the 'measure_name' keyword " "argument. For assistance, the instructions which are defined are: " - "{}".format(measure_name, target.instructions) + f"{target.instructions}" ) from ex return schedule diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 561eac01f55..e5a4a1a1d2b 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -54,7 +54,7 @@ from copy import copy from typing import Any, Mapping, Sequence -from qiskit.circuit import ParameterVector +from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse import instructions, channels @@ -62,7 +62,11 @@ from qiskit.pulse.library import SymbolicPulse, Waveform from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind -from qiskit.pulse.utils import format_parameter_value +from qiskit.pulse.utils import ( + format_parameter_value, + _validate_parameter_vector, + _validate_parameter_value, +) class NodeVisitor: @@ -362,7 +366,8 @@ def assign_parameters( self, pulse_program: Any, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], ) -> Any: """Modify and return program data with parameters assigned according to the input. @@ -397,7 +402,7 @@ def update_parameter_table(self, new_node: Any): def _unroll_param_dict( self, parameter_binds: Mapping[ - Parameter | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + Parameter | ParameterVector | str, ParameterValueType | Sequence[ParameterValueType] ], ) -> Mapping[Parameter, ParameterValueType]: """ @@ -410,21 +415,31 @@ def _unroll_param_dict( A dictionary from parameter to value. """ out = {} + param_name_dict = {param.name: [] for param in self.parameters} + for param in self.parameters: + param_name_dict[param.name].append(param) + param_vec_dict = { + param.vector.name: param.vector + for param in self.parameters + if isinstance(param, ParameterVectorElement) + } + for name in param_vec_dict.keys(): + if name in param_name_dict: + param_name_dict[name].append(param_vec_dict[name]) + else: + param_name_dict[name] = [param_vec_dict[name]] + for parameter, value in parameter_binds.items(): if isinstance(parameter, ParameterVector): - if not isinstance(value, Sequence): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to a single value." - ) - if len(parameter) != len(value): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to {len(value)} values." - ) + _validate_parameter_vector(parameter, value) out.update(zip(parameter, value)) elif isinstance(parameter, str): - out[self.get_parameters(parameter)] = value + for param in param_name_dict[parameter]: + is_vec = _validate_parameter_value(param, value) + if is_vec: + out.update(zip(param, value)) + else: + out[param] = value else: out[parameter] = value return out diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py index a9e752f562e..e9cd4917a7c 100644 --- a/qiskit/pulse/parser.py +++ b/qiskit/pulse/parser.py @@ -120,17 +120,15 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio if kwargs: for key, val in kwargs.items(): if key in self.params: - if key not in self._locals_dict.keys(): + if key not in self._locals_dict: self._locals_dict[key] = val else: raise PulseError( - "%s got multiple values for argument '%s'" - % (self.__class__.__name__, key) + f"{self.__class__.__name__} got multiple values for argument '{key}'" ) else: raise PulseError( - "%s got an unexpected keyword argument '%s'" - % (self.__class__.__name__, key) + f"{self.__class__.__name__} got an unexpected keyword argument '{key}'" ) expr = self.visit(self._tree) @@ -139,7 +137,7 @@ def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpressio if self._partial_binding: return PulseExpression(expr, self._partial_binding) else: - raise PulseError("Parameters %s are not all bound." % self.params) + raise PulseError(f"Parameters {self.params} are not all bound.") return expr.body.value @staticmethod @@ -160,7 +158,7 @@ def _match_ops(opr: ast.AST, opr_dict: dict, *args) -> complex: for op_type, op_func in opr_dict.items(): if isinstance(opr, op_type): return op_func(*args) - raise PulseError("Operator %s is not supported." % opr.__class__.__name__) + raise PulseError(f"Operator {opr.__class__.__name__} is not supported.") def visit_Expression(self, node: ast.Expression) -> ast.Expression: """Evaluate children nodes of expression. @@ -272,8 +270,8 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: node = copy.copy(node) node.args = [self.visit(arg) for arg in node.args] if all(isinstance(arg, ast.Constant) for arg in node.args): - if node.func.id not in self._math_ops.keys(): - raise PulseError("Function %s is not supported." % node.func.id) + if node.func.id not in self._math_ops: + raise PulseError(f"Function {node.func.id} is not supported.") _args = [arg.value for arg in node.args] _val = self._math_ops[node.func.id](*_args) if not _val.imag: @@ -283,7 +281,7 @@ def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: return node def generic_visit(self, node): - raise PulseError("Unsupported node: %s" % node.__class__.__name__) + raise PulseError(f"Unsupported node: {node.__class__.__name__}") def parse_string_expr(source: str, partial_binding: bool = False) -> PulseExpression: diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index a4d1ad844e5..7ccd5053e6e 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -553,17 +553,10 @@ def _add_timeslots(self, time: int, schedule: "ScheduleComponent") -> None: self._timeslots[channel].insert(index, interval) except PulseError as ex: raise PulseError( - "Schedule(name='{new}') cannot be inserted into Schedule(name='{old}') at " - "time {time} because its instruction on channel {ch} scheduled from time " - "{t0} to {tf} overlaps with an existing instruction." - "".format( - new=schedule.name or "", - old=self.name or "", - time=time, - ch=channel, - t0=interval[0], - tf=interval[1], - ) + f"Schedule(name='{schedule.name or ''}') cannot be inserted into " + f"Schedule(name='{self.name or ''}') at " + f"time {time} because its instruction on channel {channel} scheduled from time " + f"{interval[0]} to {interval[1]} overlaps with an existing instruction." ) from ex _check_nonnegative_timeslot(self._timeslots) @@ -598,10 +591,8 @@ def _remove_timeslots(self, time: int, schedule: "ScheduleComponent"): continue raise PulseError( - "Cannot find interval ({t0}, {tf}) to remove from " - "channel {ch} in Schedule(name='{name}').".format( - ch=channel, t0=interval[0], tf=interval[1], name=schedule.name - ) + f"Cannot find interval ({interval[0]}, {interval[1]}) to remove from " + f"channel {channel} in Schedule(name='{schedule.name}')." ) if not channel_timeslots: @@ -715,16 +706,17 @@ def is_parameterized(self) -> bool: def assign_parameters( self, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], inplace: bool = True, ) -> "Schedule": """Assign the parameters in this schedule according to the input. Args: - value_dict: A mapping from parameters (parameter vectors) to either - numeric values (list of numeric values) - or another Parameter expression (list of Parameter expressions). + value_dict: A mapping from parameters or parameter names (parameter vector + or parameter vector name) to either numeric values (list of numeric values) + or another parameter expression (list of parameter expressions). inplace: Set ``True`` to override this instance with new parameter. Returns: @@ -1416,15 +1408,16 @@ def is_referenced(self) -> bool: def assign_parameters( self, value_dict: dict[ - ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType] + ParameterExpression | ParameterVector | str, + ParameterValueType | Sequence[ParameterValueType], ], inplace: bool = True, ) -> "ScheduleBlock": """Assign the parameters in this schedule according to the input. Args: - value_dict: A mapping from parameters (parameter vectors) to either numeric values - (list of numeric values) + value_dict: A mapping from parameters or parameter names (parameter vector + or parameter vector name) to either numeric values (list of numeric values) or another parameter expression (list of parameter expressions). inplace: Set ``True`` to override this instance with new parameter. @@ -1613,8 +1606,9 @@ def __repr__(self) -> str: blocks = ", ".join([repr(instr) for instr in self.blocks[:50]]) if len(self.blocks) > 25: blocks += ", ..." - return '{}({}, name="{}", transform={})'.format( - self.__class__.__name__, blocks, name, repr(self.alignment_context) + return ( + f'{self.__class__.__name__}({blocks}, name="{name}",' + f" transform={repr(self.alignment_context)})" ) def __add__(self, other: "BlockComponent") -> "ScheduleBlock": diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py index 569219777f2..5e383972c25 100644 --- a/qiskit/pulse/transforms/alignments.py +++ b/qiskit/pulse/transforms/alignments.py @@ -398,9 +398,7 @@ def align(self, schedule: Schedule) -> Schedule: _t_center = self.duration * self.func(ind + 1) _t0 = int(_t_center - 0.5 * child.duration) if _t0 < 0 or _t0 > self.duration: - raise PulseError( - "Invalid schedule position t=%d is specified at index=%d" % (_t0, ind) - ) + raise PulseError(f"Invalid schedule position t={_t0} is specified at index={ind}") aligned.insert(_t0, child, inplace=True) return aligned diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py index fddc9469add..5f345917761 100644 --- a/qiskit/pulse/utils.py +++ b/qiskit/pulse/utils.py @@ -11,13 +11,14 @@ # that they have been altered from the originals. """Module for common pulse programming utilities.""" -from typing import List, Dict, Union +from typing import List, Dict, Union, Sequence import warnings import numpy as np +from qiskit.circuit import ParameterVector, Parameter from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError +from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError, PulseError def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]: @@ -107,13 +108,42 @@ def instruction_duration_validation(duration: int): """ if isinstance(duration, ParameterExpression): raise UnassignedDurationError( - "Instruction duration {} is not assigned. " + f"Instruction duration {repr(duration)} is not assigned. " "Please bind all durations to an integer value before playing in the Schedule, " "or use ScheduleBlock to align instructions with unassigned duration." - "".format(repr(duration)) ) if not isinstance(duration, (int, np.integer)) or duration < 0: raise QiskitError( f"Instruction duration must be a non-negative integer, got {duration} instead." ) + + +def _validate_parameter_vector(parameter: ParameterVector, value): + """Validate parameter vector and its value.""" + if not isinstance(value, Sequence): + raise PulseError( + f"Parameter vector '{parameter.name}' has length {len(parameter)}," + f" but was assigned to {value}." + ) + if len(parameter) != len(value): + raise PulseError( + f"Parameter vector '{parameter.name}' has length {len(parameter)}," + f" but was assigned to {len(value)} values." + ) + + +def _validate_single_parameter(parameter: Parameter, value): + """Validate single parameter and its value.""" + if not isinstance(value, (int, float, complex, ParameterExpression)): + raise PulseError(f"Parameter '{parameter.name}' is not assignable to {value}.") + + +def _validate_parameter_value(parameter, value): + """Validate parameter and its value.""" + if isinstance(parameter, ParameterVector): + _validate_parameter_vector(parameter, value) + return True + else: + _validate_single_parameter(parameter, value) + return False diff --git a/qiskit/qasm2/__init__.py b/qiskit/qasm2/__init__.py index 5a2f189c410..f17fe29113e 100644 --- a/qiskit/qasm2/__init__.py +++ b/qiskit/qasm2/__init__.py @@ -20,7 +20,7 @@ .. note:: - OpenQASM 2 is a simple language, and not suitable for general serialisation of Qiskit objects. + OpenQASM 2 is a simple language, and not suitable for general serialization of Qiskit objects. See :ref:`some discussion of alternatives below `, if that is what you are looking for. @@ -95,7 +95,7 @@ Exporting API ============= -Similar to other serialisation modules in Python, this module offers two public functions: +Similar to other serialization modules in Python, this module offers two public functions: :func:`dump` and :func:`dumps`, which take a :class:`.QuantumCircuit` and write out a representative OpenQASM 2 program to a file-like object or return a string, respectively. @@ -394,7 +394,7 @@ def add_one(x): :meth:`.QuantumCircuit.from_qasm_str` and :meth:`~.QuantumCircuit.from_qasm_file` used to make a few additions on top of the raw specification. Qiskit originally tried to use OpenQASM 2 as a sort of -serialisation format, and expanded its behaviour as Qiskit expanded. The new parser under all its +serialization format, and expanded its behavior as Qiskit expanded. The new parser under all its defaults implements the specification more strictly. In particular, in the legacy importers: @@ -445,11 +445,11 @@ def add_one(x): * the parsed grammar is effectively the same as :ref:`the strict mode of the new importers `. -You can emulate this behaviour in :func:`load` and :func:`loads` by setting `include_path` +You can emulate this behavior in :func:`load` and :func:`loads` by setting `include_path` appropriately (try inspecting the variable ``qiskit.__file__`` to find the installed location), and by passing a list of :class:`CustomInstruction` instances for each of the custom gates you care about. To make things easier we make three tuples available, which each contain one component of -a configuration that is equivalent to Qiskit's legacy converter behaviour. +a configuration that is equivalent to Qiskit's legacy converter behavior. .. py:data:: LEGACY_CUSTOM_INSTRUCTIONS @@ -473,7 +473,7 @@ def add_one(x): instruction, it does not matter how the gates are actually defined and used, the legacy importer will always attempt to output its custom objects for them. This can result in errors during the circuit construction, even after a successful parse. There is no way to emulate this buggy -behaviour with :mod:`qiskit.qasm2`; only an ``include "qelib1.inc";`` statement or the +behavior with :mod:`qiskit.qasm2`; only an ``include "qelib1.inc";`` statement or the `custom_instructions` argument can cause built-in Qiskit instructions to be used, and the signatures of these match each other. @@ -549,7 +549,7 @@ def add_one(x): def _normalize_path(path: Union[str, os.PathLike]) -> str: - """Normalise a given path into a path-like object that can be passed to Rust. + """Normalize a given path into a path-like object that can be passed to Rust. Ideally this would be something that we can convert to Rust's `OSString`, but in practice, Python uses `os.fsencode` to produce a `bytes` object, but this doesn't map especially well. diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 9247c9233e0..3cf0d894255 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -157,7 +157,7 @@ def dumps(circuit: QuantumCircuit, /) -> str: _make_unique(_escape_name(reg.name, "reg_"), register_escaped_names) ] = reg bit_labels: dict[Qubit | Clbit, str] = { - bit: "%s[%d]" % (name, idx) + bit: f"{name}[{idx}]" for name, register in register_escaped_names.items() for (idx, bit) in enumerate(register) } @@ -244,18 +244,14 @@ def _instruction_call_site(operation): else: qasm2_call = operation.name if operation.params: - qasm2_call = "{}({})".format( - qasm2_call, - ",".join([pi_check(i, output="qasm", eps=1e-12) for i in operation.params]), - ) + params = ",".join([pi_check(i, output="qasm", eps=1e-12) for i in operation.params]) + qasm2_call = f"{qasm2_call}({params})" if operation.condition is not None: if not isinstance(operation.condition[0], ClassicalRegister): raise QASM2ExportError( "OpenQASM 2 can only condition on registers, but got '{operation.condition[0]}'" ) - qasm2_call = ( - "if(%s==%d) " % (operation.condition[0].name, operation.condition[1]) + qasm2_call - ) + qasm2_call = f"if({operation.condition[0].name}=={operation.condition[1]:d}) " + qasm2_call return qasm2_call @@ -312,7 +308,7 @@ def _define_custom_operation(operation, gates_to_define): lib.U3Gate, } - # In known-good situations we want to use a manually parametrised object as the source of the + # In known-good situations we want to use a manually parametrized object as the source of the # definition, but still continue to return the given object as the call-site object. if operation.base_class in known_good_parameterized: parameterized_operation = type(operation)(*_FIXED_PARAMETERS[: len(operation.params)]) diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 5cb8137b5f0..a40270a99b8 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -16,6 +16,8 @@ import math from typing import Iterable, Callable +import numpy as np + from qiskit.circuit import ( Barrier, CircuitInstruction, @@ -30,6 +32,7 @@ Reset, library as lib, ) +from qiskit.quantum_info import Operator from qiskit._accelerate.qasm2 import ( OpCode, UnaryOpCode, @@ -284,7 +287,7 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): class _DefinedGate(Gate): """A gate object defined by a `gate` statement in an OpenQASM 2 program. This object lazily - binds its parameters to its definition, so it is only synthesised when required.""" + binds its parameters to its definition, so it is only synthesized when required.""" def __init__(self, name, num_qubits, params, gates, bytecode): self._gates = gates @@ -315,6 +318,11 @@ def _define(self): raise ValueError(f"received invalid bytecode to build gate: {op}") self._definition = qc + def __array__(self, dtype=None, copy=None): + if copy is False: + raise ValueError("unable to avoid copy while creating an array as requested") + return np.asarray(Operator(self.definition), dtype=dtype) + # It's fiddly to implement pickling for PyO3 types (the bytecode stream), so instead if we need # to pickle ourselves, we just eagerly create the definition and pickle that. diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index fd7aa11d481..0bae60144af 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -123,6 +123,10 @@ class FloatType(ClassicalType, enum.Enum): OCT = 256 +class BoolType(ClassicalType): + """Type information for a Boolean.""" + + class IntType(ClassicalType): """Type information for a signed integer.""" @@ -130,6 +134,13 @@ def __init__(self, size: Optional[int] = None): self.size = size +class UintType(ClassicalType): + """Type information for an unsigned integer.""" + + def __init__(self, size: Optional[int] = None): + self.size = size + + class BitType(ClassicalType): """Type information for a single bit.""" @@ -241,6 +252,8 @@ class Op(enum.Enum): GREATER_EQUAL = ">=" EQUAL = "==" NOT_EQUAL = "!=" + SHIFT_LEFT = "<<" + SHIFT_RIGHT = ">>" def __init__(self, op: Op, left: Expression, right: Expression): self.op = op @@ -254,6 +267,12 @@ def __init__(self, type: ClassicalType, operand: Expression): self.operand = operand +class Index(Expression): + def __init__(self, target: Expression, index: Expression): + self.target = target + self.index = index + + class IndexSet(ASTNode): """ A literal index set of values:: @@ -298,7 +317,7 @@ def __init__(self, expression: Expression): class ClassicalDeclaration(Statement): - """Declaration of a classical type, optionally initialising it to a value.""" + """Declaration of a classical type, optionally initializing it to a value.""" def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=None): self.type = type_ diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index d8d3a42087a..6d5344bcc25 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -33,6 +33,7 @@ Qubit, Reset, Delay, + Store, ) from qiskit.circuit.bit import Bit from qiskit.circuit.classical import expr, types @@ -62,7 +63,6 @@ _RESERVED_KEYWORDS = frozenset( { "OPENQASM", - "U", "angle", "array", "barrier", @@ -239,6 +239,7 @@ class GlobalNamespace: def __init__(self, includelist, basis_gates=()): self._data = {gate: self.BASIS_GATE for gate in basis_gates} + self._data["U"] = self.BASIS_GATE for includefile in includelist: if includefile == "stdgates.inc": @@ -251,6 +252,9 @@ def __init__(self, includelist, basis_gates=()): def __setitem__(self, name_str, instruction): self._data[name_str] = instruction.base_class self._data[id(instruction)] = name_str + ctrl_state = str(getattr(instruction, "ctrl_state", "")) + + self._data[f"{instruction.name}_{ctrl_state}_{instruction.params}"] = name_str def __getitem__(self, key): if isinstance(key, Instruction): @@ -261,7 +265,9 @@ def __getitem__(self, key): pass # Built-in gates. if key.name not in self._data: - raise KeyError(key) + # Registerd qiskit standard gate without stgates.inc + ctrl_state = str(getattr(key, "ctrl_state", "")) + return self._data[f"{key.name}_{ctrl_state}_{key.params}"] return key.name return self._data[key] @@ -282,6 +288,10 @@ def __contains__(self, instruction): return True return False + def has_symbol(self, name: str) -> bool: + """Whether a symbol's name is present in the table.""" + return name in self._data + def register(self, instruction): """Register an instruction in the namespace""" # The second part of the condition is a nasty hack to ensure that gates that come with at @@ -324,7 +334,7 @@ def register(self, instruction): class QASM3Builder: """QASM3 builder constructs an AST from a QuantumCircuit.""" - builtins = (Barrier, Measure, Reset, Delay, BreakLoopOp, ContinueLoopOp) + builtins = (Barrier, Measure, Reset, Delay, BreakLoopOp, ContinueLoopOp, Store) loose_bit_prefix = "_bit" loose_qubit_prefix = "_qubit" gate_parameter_prefix = "_gate_p" @@ -348,14 +358,12 @@ def __init__( self.includeslist = includeslist # `_global_io_declarations` and `_global_classical_declarations` are stateful, and any # operation that needs a parameter can append to them during the build. We make all - # classical declarations global because the IBM QSS stack (our initial consumer of OQ3 - # strings) prefers declarations to all be global, and it's valid OQ3, so it's not vendor + # classical declarations global because the IBM qe-compiler stack (our initial consumer of + # OQ3 strings) prefers declarations to all be global, and it's valid OQ3, so it's not vendor # lock-in. It's possibly slightly memory inefficient, but that's not likely to be a problem # in the near term. self._global_io_declarations = [] - self._global_classical_declarations = [] - self._gate_to_declare = {} - self._opaque_to_declare = {} + self._global_classical_forward_declarations = [] # An arbitrary counter to help with generation of unique ids for symbol names when there are # clashes (though we generally prefer to keep user names if possible). self._counter = itertools.count() @@ -367,18 +375,15 @@ def __init__( def _unique_name(self, prefix: str, scope: _Scope) -> str: table = scope.symbol_map name = basename = _escape_invalid_identifier(prefix) - while name in table or name in _RESERVED_KEYWORDS: + while name in table or name in _RESERVED_KEYWORDS or self.global_namespace.has_symbol(name): name = f"{basename}__generated{next(self._counter)}" return name def _register_gate(self, gate): self.global_namespace.register(gate) - self._gate_to_declare[id(gate)] = gate def _register_opaque(self, instruction): - if instruction not in self.global_namespace: - self.global_namespace.register(instruction) - self._opaque_to_declare[id(instruction)] = instruction + self.global_namespace.register(instruction) def _register_variable(self, variable, scope: _Scope, name=None) -> ast.Identifier: """Register a variable in the symbol table for the given scope, returning the name that @@ -399,6 +404,10 @@ def _register_variable(self, variable, scope: _Scope, name=None) -> ast.Identifi raise QASM3ExporterError( f"tried to reserve '{name}', but it is already used by '{table[name]}'" ) + if self.global_namespace.has_symbol(name): + raise QASM3ExporterError( + f"tried to reserve '{name}', but it is already used by a gate" + ) else: name = self._unique_name(variable.name, scope) identifier = ast.Identifier(name) @@ -441,15 +450,66 @@ def build_header(self): def build_program(self): """Builds a Program""" - self.hoist_declarations(self.global_scope(assert_=True).circuit.data) - return ast.Program(self.build_header(), self.build_global_statements()) + circuit = self.global_scope(assert_=True).circuit + if circuit.num_captured_vars: + raise QASM3ExporterError( + "cannot export an inner scope with captured variables as a top-level program" + ) + header = self.build_header() + + opaques_to_declare, gates_to_declare = self.hoist_declarations( + circuit.data, opaques=[], gates=[] + ) + opaque_definitions = [ + self.build_opaque_definition(instruction) for instruction in opaques_to_declare + ] + gate_definitions = [ + self.build_gate_definition(instruction) for instruction in gates_to_declare + ] + + # Early IBM runtime parametrization uses unbound `Parameter` instances as `input` variables, + # not the explicit realtime `Var` variables, so we need this explicit scan. + self.hoist_global_parameter_declarations() + # Qiskit's clbits and classical registers need to get mapped to implicit OQ3 variables, but + # only if they're in the top-level circuit. The QuantumCircuit data model is that inner + # clbits are bound to outer bits, and inner registers must be closing over outer ones. + self.hoist_classical_register_declarations() + # We hoist registers before new-style vars because registers are an older part of the data + # model (and used implicitly in PrimitivesV2 outputs) so they get the first go at reserving + # names in the symbol table. + self.hoist_classical_io_var_declarations() + + # Similarly, QuantumCircuit qubits/registers are only new variables in the global scope. + quantum_declarations = self.build_quantum_declarations() + # This call has side-effects - it can populate `self._global_io_declarations` and + # `self._global_classical_declarations` as a courtesy to the qe-compiler that prefers our + # hacky temporary `switch` target variables to be globally defined. + main_statements = self.build_current_scope() + + statements = [ + statement + for source in ( + # In older versions of the reference OQ3 grammar, IO declarations had to come before + # anything else, so we keep doing that as a courtesy. + self._global_io_declarations, + opaque_definitions, + gate_definitions, + self._global_classical_forward_declarations, + quantum_declarations, + main_statements, + ) + for statement in source + ] + return ast.Program(header, statements) - def hoist_declarations(self, instructions): - """Walks the definitions in gates/instructions to make a list of gates to declare.""" + def hoist_declarations(self, instructions, *, opaques, gates): + """Walks the definitions in gates/instructions to make a list of gates to declare. + + Mutates ``opaques`` and ``gates`` in-place if given, and returns them.""" for instruction in instructions: if isinstance(instruction.operation, ControlFlowOp): for block in instruction.operation.blocks: - self.hoist_declarations(block.data) + self.hoist_declarations(block.data, opaques=opaques, gates=gates) continue if instruction.operation in self.global_namespace or isinstance( instruction.operation, self.builtins @@ -461,15 +521,20 @@ def hoist_declarations(self, instructions): # tree, but isn't an OQ3 built-in. We use `isinstance` because we haven't fully # fixed what the name/class distinction is (there's a test from the original OQ3 # exporter that tries a naming collision with 'cx'). - if instruction.operation not in self.global_namespace: - self._register_gate(instruction.operation) - if instruction.operation.definition is None: + self._register_gate(instruction.operation) + gates.append(instruction.operation) + elif instruction.operation.definition is None: self._register_opaque(instruction.operation) + opaques.append(instruction.operation) elif not isinstance(instruction.operation, Gate): raise QASM3ExporterError("Exporting non-unitary instructions is not yet supported.") else: - self.hoist_declarations(instruction.operation.definition.data) + self.hoist_declarations( + instruction.operation.definition.data, opaques=opaques, gates=gates + ) self._register_gate(instruction.operation) + gates.append(instruction.operation) + return opaques, gates def global_scope(self, assert_=False): """Return the global circuit scope that is used as the basis of the full program. If @@ -540,40 +605,6 @@ def build_includes(self): """Builds a list of included files.""" return [ast.Include(filename) for filename in self.includeslist] - def build_global_statements(self) -> List[ast.Statement]: - """Get a list of the statements that form the global scope of the program.""" - definitions = self.build_definitions() - # These two "declarations" functions populate stateful variables, since the calls to - # `build_quantum_instructions` might also append to those declarations. - self.build_parameter_declarations() - self.build_classical_declarations() - context = self.global_scope(assert_=True).circuit - quantum_declarations = self.build_quantum_declarations() - quantum_instructions = self.build_quantum_instructions(context.data) - - return [ - statement - for source in ( - # In older versions of the reference OQ3 grammar, IO declarations had to come before - # anything else, so we keep doing that as a courtesy. - self._global_io_declarations, - definitions, - self._global_classical_declarations, - quantum_declarations, - quantum_instructions, - ) - for statement in source - ] - - def build_definitions(self): - """Builds all the definition.""" - ret = [] - for instruction in self._opaque_to_declare.values(): - ret.append(self.build_opaque_definition(instruction)) - for instruction in self._gate_to_declare.values(): - ret.append(self.build_gate_definition(instruction)) - return ret - def build_opaque_definition(self, instruction): """Builds an Opaque gate definition as a CalibrationDefinition""" # We can't do anything sensible with this yet, so it's better to loudly say that. @@ -604,7 +635,7 @@ def build_gate_definition(self, gate): self.push_context(gate.definition) signature = self.build_gate_signature(gate) - body = ast.QuantumBlock(self.build_quantum_instructions(gate.definition.data)) + body = ast.QuantumBlock(self.build_current_scope()) self.pop_context() return ast.QuantumGateDefinition(signature, body) @@ -627,8 +658,10 @@ def build_gate_signature(self, gate): ] return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None) - def build_parameter_declarations(self): - """Builds lists of the input, output and standard variables used in this program.""" + def hoist_global_parameter_declarations(self): + """Extend ``self._global_io_declarations`` and ``self._global_classical_declarations`` with + any implicit declarations used to support the early IBM efforts to use :class:`.Parameter` + as an input variable.""" global_scope = self.global_scope(assert_=True) for parameter in global_scope.circuit.parameters: parameter_name = self._register_variable(parameter, global_scope) @@ -640,13 +673,15 @@ def build_parameter_declarations(self): if isinstance(declaration, ast.IODeclaration): self._global_io_declarations.append(declaration) else: - self._global_classical_declarations.append(declaration) + self._global_classical_forward_declarations.append(declaration) - def build_classical_declarations(self): - """Extend the global classical declarations with AST nodes declaring all the classical bits - and registers. + def hoist_classical_register_declarations(self): + """Extend the global classical declarations with AST nodes declaring all the global-scope + circuit :class:`.Clbit` and :class:`.ClassicalRegister` instances. Qiskit's data model + doesn't involve the declaration of *new* bits or registers in inner scopes; only the + :class:`.expr.Var` mechanism allows that. - The behaviour of this function depends on the setting ``allow_aliasing``. If this + The behavior of this function depends on the setting ``allow_aliasing``. If this is ``True``, then the output will be in the same form as the output of :meth:`.build_classical_declarations`, with the registers being aliases. If ``False``, it will instead return a :obj:`.ast.ClassicalDeclaration` for each classical register, and one @@ -670,12 +705,14 @@ def build_classical_declarations(self): ) for i, clbit in enumerate(scope.circuit.clbits) ) - self._global_classical_declarations.extend(clbits) - self._global_classical_declarations.extend(self.build_aliases(scope.circuit.cregs)) + self._global_classical_forward_declarations.extend(clbits) + self._global_classical_forward_declarations.extend( + self.build_aliases(scope.circuit.cregs) + ) return # If we're here, we're in the clbit happy path where there are no clbits that are in more # than one register. We can output things very naturally. - self._global_classical_declarations.extend( + self._global_classical_forward_declarations.extend( ast.ClassicalDeclaration( ast.BitType(), self._register_variable( @@ -691,10 +728,26 @@ def build_classical_declarations(self): scope.symbol_map[bit] = ast.SubscriptedIdentifier( name.string, ast.IntegerLiteral(i) ) - self._global_classical_declarations.append( + self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.BitArrayType(len(register)), name) ) + def hoist_classical_io_var_declarations(self): + """Hoist the declarations of classical IO :class:`.expr.Var` nodes into the global state. + + Local :class:`.expr.Var` declarations are handled by the regular local-block scope builder, + and the :class:`.QuantumCircuit` data model ensures that the only time an IO variable can + occur is in an outermost block.""" + scope = self.global_scope(assert_=True) + for var in scope.circuit.iter_input_vars(): + self._global_io_declarations.append( + ast.IODeclaration( + ast.IOModifier.INPUT, + _build_ast_type(var.type), + self._register_variable(var, scope), + ) + ) + def build_quantum_declarations(self): """Return a list of AST nodes declaring all the qubits in the current scope, and all the alias declarations for these qubits.""" @@ -760,21 +813,37 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen out.append(ast.AliasStatement(name, ast.IndexSet(elements))) return out - def build_quantum_instructions(self, instructions): - """Builds a list of call statements""" - ret = [] - for instruction in instructions: - if isinstance(instruction.operation, ForLoopOp): - ret.append(self.build_for_loop(instruction)) - continue - if isinstance(instruction.operation, WhileLoopOp): - ret.append(self.build_while_loop(instruction)) - continue - if isinstance(instruction.operation, IfElseOp): - ret.append(self.build_if_statement(instruction)) - continue - if isinstance(instruction.operation, SwitchCaseOp): - ret.extend(self.build_switch_statement(instruction)) + def build_current_scope(self) -> List[ast.Statement]: + """Build the instructions that occur in the current scope. + + In addition to everything literally in the circuit's ``data`` field, this also includes + declarations for any local :class:`.expr.Var` nodes. + """ + scope = self.current_scope() + + # We forward-declare all local variables uninitialised at the top of their scope. It would + # be nice to declare the variable at the point of first store (so we can write things like + # `uint[8] a = 12;`), but there's lots of edge-case logic to catch with that around + # use-before-definition errors in the OQ3 output, for example if the user has side-stepped + # the `QuantumCircuit` API protection to produce a circuit that uses an uninitialised + # variable, or the initial write to a variable is within a control-flow scope. (It would be + # easier to see the def/use chain needed to do this cleanly if we were using `DAGCircuit`.) + statements = [ + ast.ClassicalDeclaration(_build_ast_type(var.type), self._register_variable(var, scope)) + for var in scope.circuit.iter_declared_vars() + ] + for instruction in scope.circuit.data: + if isinstance(instruction.operation, ControlFlowOp): + if isinstance(instruction.operation, ForLoopOp): + statements.append(self.build_for_loop(instruction)) + elif isinstance(instruction.operation, WhileLoopOp): + statements.append(self.build_while_loop(instruction)) + elif isinstance(instruction.operation, IfElseOp): + statements.append(self.build_if_statement(instruction)) + elif isinstance(instruction.operation, SwitchCaseOp): + statements.extend(self.build_switch_statement(instruction)) + else: + raise RuntimeError(f"unhandled control-flow construct: {instruction.operation}") continue # Build the node, ignoring any condition. if isinstance(instruction.operation, Gate): @@ -795,6 +864,13 @@ def build_quantum_instructions(self, instructions): ] elif isinstance(instruction.operation, Delay): nodes = [self.build_delay(instruction)] + elif isinstance(instruction.operation, Store): + nodes = [ + ast.AssignmentStatement( + self.build_expression(instruction.operation.lvalue), + self.build_expression(instruction.operation.rvalue), + ) + ] elif isinstance(instruction.operation, BreakLoopOp): nodes = [ast.BreakStatement()] elif isinstance(instruction.operation, ContinueLoopOp): @@ -803,16 +879,16 @@ def build_quantum_instructions(self, instructions): nodes = [self.build_subroutine_call(instruction)] if instruction.operation.condition is None: - ret.extend(nodes) + statements.extend(nodes) else: body = ast.ProgramBlock(nodes) - ret.append( + statements.append( ast.BranchingStatement( self.build_expression(_lift_condition(instruction.operation.condition)), body, ) ) - return ret + return statements def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingStatement: """Build an :obj:`.IfElseOp` into a :obj:`.ast.BranchingStatement`.""" @@ -820,14 +896,14 @@ def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingSt true_circuit = instruction.operation.blocks[0] self.push_scope(true_circuit, instruction.qubits, instruction.clbits) - true_body = self.build_program_block(true_circuit.data) + true_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() if len(instruction.operation.blocks) == 1: return ast.BranchingStatement(condition, true_body, None) false_circuit = instruction.operation.blocks[1] self.push_scope(false_circuit, instruction.qubits, instruction.clbits) - false_body = self.build_program_block(false_circuit.data) + false_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.BranchingStatement(condition, true_body, false_body) @@ -838,7 +914,7 @@ def build_switch_statement(self, instruction: CircuitInstruction) -> Iterable[as target = self._reserve_variable_name( ast.Identifier(self._unique_name("switch_dummy", global_scope)), global_scope ) - self._global_classical_declarations.append( + self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.IntType(), target, None) ) @@ -851,7 +927,7 @@ def case(values, case_block): for v in values ] self.push_scope(case_block, instruction.qubits, instruction.clbits) - case_body = self.build_program_block(case_block.data) + case_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return values, case_body @@ -866,12 +942,12 @@ def case(values, case_block): ), ] - # Handle the stabilised syntax. + # Handle the stabilized syntax. cases = [] default = None for values, block in instruction.operation.cases_specifier(): self.push_scope(block, instruction.qubits, instruction.clbits) - case_body = self.build_program_block(block.data) + case_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() if CASE_DEFAULT in values: # Even if it's mixed in with other cases, we can skip them and only output the @@ -891,7 +967,7 @@ def build_while_loop(self, instruction: CircuitInstruction) -> ast.WhileLoopStat condition = self.build_expression(_lift_condition(instruction.operation.condition)) loop_circuit = instruction.operation.blocks[0] self.push_scope(loop_circuit, instruction.qubits, instruction.clbits) - loop_body = self.build_program_block(loop_circuit.data) + loop_body = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.WhileLoopStatement(condition, loop_body) @@ -921,7 +997,7 @@ def build_for_loop(self, instruction: CircuitInstruction) -> ast.ForLoopStatemen "The values in OpenQASM 3 'for' loops must all be integers, but received" f" '{indexset}'." ) from None - body_ast = self.build_program_block(loop_circuit) + body_ast = ast.ProgramBlock(self.build_current_scope()) self.pop_scope() return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast) @@ -961,10 +1037,6 @@ def build_integer(self, value) -> ast.IntegerLiteral: raise QASM3ExporterError(f"'{value}' is not an integer") # pragma: no cover return ast.IntegerLiteral(int(value)) - def build_program_block(self, instructions): - """Builds a ProgramBlock""" - return ast.ProgramBlock(self.build_quantum_instructions(instructions)) - def _rebind_scoped_parameters(self, expression): """If the input is a :class:`.ParameterExpression`, rebind any internal :class:`.Parameter`\\ s so that their names match their names in the scope. Other inputs @@ -1008,8 +1080,8 @@ def _infer_variable_declaration( This is very simplistic; it assumes all parameters are real numbers that need to be input to the program, unless one is used as a loop variable, in which case it shouldn't be declared at all, - because the ``for`` loop declares it implicitly (per the Qiskit/QSS reading of the OpenQASM - spec at Qiskit/openqasm@8ee55ec). + because the ``for`` loop declares it implicitly (per the Qiskit/qe-compiler reading of the + OpenQASM spec at openqasm/openqasm@8ee55ec). .. note:: @@ -1035,7 +1107,8 @@ def is_loop_variable(circuit, parameter): # _should_ be an intrinsic part of the parameter, or somewhere publicly accessible, but # Terra doesn't have those concepts yet. We can only try and guess at the type by looking # at all the places it's used in the circuit. - for instruction, index in circuit._parameter_table[parameter]: + for instr_index, index in circuit._data._get_param(parameter.uuid.int): + instruction = circuit.data[instr_index].operation if isinstance(instruction, ForLoopOp): # The parameters of ForLoopOp are (indexset, loop_parameter, body). if index == 1: @@ -1058,6 +1131,14 @@ def _lift_condition(condition): return expr.lift_legacy_condition(condition) +def _build_ast_type(type_: types.Type) -> ast.ClassicalType: + if type_.kind is types.Bool: + return ast.BoolType() + if type_.kind is types.Uint: + return ast.UintType(type_.width) + raise RuntimeError(f"unhandled expr type '{type_}'") + + class _ExprBuilder(expr.ExprVisitor[ast.Expression]): __slots__ = ("lookup",) @@ -1069,7 +1150,7 @@ def __init__(self, lookup): self.lookup = lookup def visit_var(self, node, /): - return self.lookup(node.var) + return self.lookup(node) if node.standalone else self.lookup(node.var) def visit_value(self, node, /): if node.type.kind is types.Bool: @@ -1080,14 +1161,8 @@ def visit_value(self, node, /): def visit_cast(self, node, /): if node.implicit: - return node.accept(self) - if node.type.kind is types.Bool: - oq3_type = ast.BoolType() - elif node.type.kind is types.Uint: - oq3_type = ast.BitArrayType(node.type.width) - else: - raise RuntimeError(f"unhandled cast type '{node.type}'") - return ast.Cast(oq3_type, node.operand.accept(self)) + return node.operand.accept(self) + return ast.Cast(_build_ast_type(node.type), node.operand.accept(self)) def visit_unary(self, node, /): return ast.Unary(ast.Unary.Op[node.op.name], node.operand.accept(self)) @@ -1096,3 +1171,6 @@ def visit_binary(self, node, /): return ast.Binary( ast.Binary.Op[node.op.name], node.left.accept(self), node.right.accept(self) ) + + def visit_index(self, node, /): + return ast.Index(node.target.accept(self), node.index.accept(self)) diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 94d12a7ecff..58f689c2c2e 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -34,13 +34,16 @@ # indexing and casting are all higher priority than these, so we just ignore them. _BindingPower = collections.namedtuple("_BindingPower", ("left", "right"), defaults=(255, 255)) _BINDING_POWER = { - # Power: (21, 22) + # Power: (24, 23) # - ast.Unary.Op.LOGIC_NOT: _BindingPower(right=20), - ast.Unary.Op.BIT_NOT: _BindingPower(right=20), + ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22), + ast.Unary.Op.BIT_NOT: _BindingPower(right=22), # - # Multiplication/division/modulo: (17, 18) - # Addition/subtraction: (15, 16) + # Multiplication/division/modulo: (19, 20) + # Addition/subtraction: (17, 18) + # + ast.Binary.Op.SHIFT_LEFT: _BindingPower(15, 16), + ast.Binary.Op.SHIFT_RIGHT: _BindingPower(15, 16), # ast.Binary.Op.LESS: _BindingPower(13, 14), ast.Binary.Op.LESS_EQUAL: _BindingPower(13, 14), @@ -204,11 +207,19 @@ def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDecla def _visit_FloatType(self, node: ast.FloatType) -> None: self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]") + def _visit_BoolType(self, _node: ast.BoolType) -> None: + self.stream.write("bool") + def _visit_IntType(self, node: ast.IntType) -> None: self.stream.write("int") if node.size is not None: self.stream.write(f"[{node.size}]") + def _visit_UintType(self, node: ast.UintType) -> None: + self.stream.write("uint") + if node.size is not None: + self.stream.write(f"[{node.size}]") + def _visit_BitType(self, _node: ast.BitType) -> None: self.stream.write("bit") @@ -324,6 +335,17 @@ def _visit_Cast(self, node: ast.Cast): self.visit(node.operand) self.stream.write(")") + def _visit_Index(self, node: ast.Index): + if isinstance(node.target, (ast.Unary, ast.Binary)): + self.stream.write("(") + self.visit(node.target) + self.stream.write(")") + else: + self.visit(node.target) + self.stream.write("[") + self.visit(node.index) + self.stream.write("]") + def _visit_ClassicalDeclaration(self, node: ast.ClassicalDeclaration) -> None: self._start_line() self.visit(node.type) diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 77e811100f3..8f34ee0855a 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -89,7 +89,7 @@ class InstructionToQobjConverter: The transfer layer format must be the text representation that coforms to the `OpenPulse specification`__. Extention to the OpenPulse can be achieved by subclassing this this with - extra methods corresponding to each augumented instruction. For example, + extra methods corresponding to each augmented instruction. For example, .. code-block:: python @@ -234,7 +234,7 @@ def _convert_set_frequency( "name": "setf", "t0": time_offset + instruction.start_time, "ch": instruction.channel.name, - "frequency": instruction.frequency / 1e9, + "frequency": instruction.frequency / 10**9, } return self._qobj_model(**command_dict) @@ -257,7 +257,7 @@ def _convert_shift_frequency( "name": "shiftf", "t0": time_offset + instruction.start_time, "ch": instruction.channel.name, - "frequency": instruction.frequency / 1e9, + "frequency": instruction.frequency / 10**9, } return self._qobj_model(**command_dict) @@ -503,7 +503,7 @@ class QobjToInstructionConverter: The transfer layer format must be the text representation that coforms to the `OpenPulse specification`__. Extention to the OpenPulse can be achieved by subclassing this this with - extra methods corresponding to each augumented instruction. For example, + extra methods corresponding to each augmented instruction. For example, .. code-block:: python @@ -621,7 +621,7 @@ def get_channel(self, channel: str) -> channels.PulseChannel: elif prefix == channels.ControlChannel.prefix: return channels.ControlChannel(index) - raise QiskitError("Channel %s is not valid" % channel) + raise QiskitError(f"Channel {channel} is not valid") @staticmethod def disassemble_value(value_expr: Union[float, str]) -> Union[float, ParameterExpression]: @@ -746,7 +746,7 @@ def _convert_setf( .. note:: We assume frequency value is expressed in string with "GHz". - Operand value is thus scaled by a factor of 1e9. + Operand value is thus scaled by a factor of 10^9. Args: instruction: SetFrequency qobj instruction @@ -755,7 +755,7 @@ def _convert_setf( Qiskit Pulse set frequency instructions """ channel = self.get_channel(instruction.ch) - frequency = self.disassemble_value(instruction.frequency) * 1e9 + frequency = self.disassemble_value(instruction.frequency) * 10**9 yield instructions.SetFrequency(frequency, channel) @@ -768,7 +768,7 @@ def _convert_shiftf( .. note:: We assume frequency value is expressed in string with "GHz". - Operand value is thus scaled by a factor of 1e9. + Operand value is thus scaled by a factor of 10^9. Args: instruction: ShiftFrequency qobj instruction @@ -777,7 +777,7 @@ def _convert_shiftf( Qiskit Pulse shift frequency schedule instructions """ channel = self.get_channel(instruction.ch) - frequency = self.disassemble_value(instruction.frequency) * 1e9 + frequency = self.disassemble_value(instruction.frequency) * 10**9 yield instructions.ShiftFrequency(frequency, channel) @@ -827,9 +827,7 @@ def _convert_parametric_pulse( pulse_name = instruction.label except AttributeError: sorted_params = sorted(instruction.parameters.items(), key=lambda x: x[0]) - base_str = "{pulse}_{params}".format( - pulse=instruction.pulse_shape, params=str(sorted_params) - ) + base_str = f"{instruction.pulse_shape}_{str(sorted_params)}" short_pulse_id = hashlib.md5(base_str.encode("utf-8")).hexdigest()[:4] pulse_name = f"{instruction.pulse_shape}_{short_pulse_id}" params = dict(instruction.parameters) diff --git a/qiskit/qobj/pulse_qobj.py b/qiskit/qobj/pulse_qobj.py index e5f45b4d2ac..3552d83ada8 100644 --- a/qiskit/qobj/pulse_qobj.py +++ b/qiskit/qobj/pulse_qobj.py @@ -209,8 +209,8 @@ def __repr__(self): return out def __str__(self): - out = "Instruction: %s\n" % self.name - out += "\t\tt0: %s\n" % self.t0 + 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" @@ -434,10 +434,10 @@ def __str__(self): header = pprint.pformat(self.header.to_dict() or {}) else: header = "{}" - out += "Header:\n%s\n" % header - out += "Config:\n%s\n\n" % config + out += f"Header:\n{header}\n" + out += f"Config:\n{config}\n\n" for instruction in self.instructions: - out += "\t%s\n" % instruction + out += f"\t{instruction}\n" return out @classmethod @@ -567,23 +567,20 @@ def __init__(self, qobj_id, config, experiments, header=None): def __repr__(self): experiments_str = [repr(x) for x in self.experiments] experiments_repr = "[" + ", ".join(experiments_str) + "]" - out = "PulseQobj(qobj_id='{}', config={}, experiments={}, header={})".format( - self.qobj_id, - repr(self.config), - experiments_repr, - repr(self.header), + return ( + f"PulseQobj(qobj_id='{self.qobj_id}', config={repr(self.config)}, " + f"experiments={experiments_repr}, header={repr(self.header)})" ) - return out def __str__(self): - out = "Pulse Qobj: %s:\n" % self.qobj_id + out = f"Pulse Qobj: {self.qobj_id}:\n" config = pprint.pformat(self.config.to_dict()) - out += "Config: %s\n" % str(config) + out += f"Config: {str(config)}\n" header = pprint.pformat(self.header.to_dict()) - out += "Header: %s\n" % str(header) + out += f"Header: {str(header)}\n" out += "Experiments:\n" for experiment in self.experiments: - out += "%s" % str(experiment) + out += str(experiment) return out def to_dict(self): diff --git a/qiskit/qobj/qasm_qobj.py b/qiskit/qobj/qasm_qobj.py index 983da1dcfd3..88d775b3b77 100644 --- a/qiskit/qobj/qasm_qobj.py +++ b/qiskit/qobj/qasm_qobj.py @@ -131,7 +131,7 @@ def to_dict(self): return out_dict def __repr__(self): - out = "QasmQobjInstruction(name='%s'" % self.name + out = f"QasmQobjInstruction(name='{self.name}'" for attr in [ "params", "qubits", @@ -155,7 +155,7 @@ def __repr__(self): return out def __str__(self): - out = "Instruction: %s\n" % self.name + out = f"Instruction: {self.name}\n" for attr in [ "params", "qubits", @@ -215,21 +215,19 @@ def __init__(self, config=None, header=None, instructions=None): def __repr__(self): instructions_str = [repr(x) for x in self.instructions] instructions_repr = "[" + ", ".join(instructions_str) + "]" - out = "QasmQobjExperiment(config={}, header={}, instructions={})".format( - repr(self.config), - repr(self.header), - instructions_repr, + return ( + f"QasmQobjExperiment(config={repr(self.config)}, header={repr(self.header)}," + f" instructions={instructions_repr})" ) - return out def __str__(self): out = "\nOpenQASM2 Experiment:\n" config = pprint.pformat(self.config.to_dict()) header = pprint.pformat(self.header.to_dict()) - out += "Header:\n%s\n" % header - out += "Config:\n%s\n\n" % config + out += f"Header:\n{header}\n" + out += f"Config:\n{config}\n\n" for instruction in self.instructions: - out += "\t%s\n" % instruction + out += f"\t{instruction}\n" return out def to_dict(self): @@ -568,23 +566,20 @@ def __init__(self, qobj_id=None, config=None, experiments=None, header=None): def __repr__(self): experiments_str = [repr(x) for x in self.experiments] experiments_repr = "[" + ", ".join(experiments_str) + "]" - out = "QasmQobj(qobj_id='{}', config={}, experiments={}, header={})".format( - self.qobj_id, - repr(self.config), - experiments_repr, - repr(self.header), + return ( + f"QasmQobj(qobj_id='{self.qobj_id}', config={repr(self.config)}," + f" experiments={experiments_repr}, header={repr(self.header)})" ) - return out def __str__(self): - out = "QASM Qobj: %s:\n" % self.qobj_id + out = f"QASM Qobj: {self.qobj_id}:\n" config = pprint.pformat(self.config.to_dict()) - out += "Config: %s\n" % str(config) + out += f"Config: {str(config)}\n" header = pprint.pformat(self.header.to_dict()) - out += "Header: %s\n" % str(header) + out += f"Header: {str(header)}\n" out += "Experiments:\n" for experiment in self.experiments: - out += "%s" % str(experiment) + out += str(experiment) return out def to_dict(self): diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index fed09b8717a..e072536fef4 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -########################################################### +===================================== QPY serialization (:mod:`qiskit.qpy`) -########################################################### +===================================== .. currentmodule:: qiskit.qpy @@ -32,9 +32,8 @@ version (it is also `potentially insecure `__). -********* -Using QPY -********* +Basic Usage +=========== Using QPY is defined to be straightforward and mirror the user API of the serializers in Python's standard library, ``pickle`` and ``json``. There are @@ -79,6 +78,12 @@ .. autoexception:: QpyError +When a lower-than-maximum target QPY version is set for serialization, but the object to be +serialized contains features that cannot be represented in that format, a subclass of +:exc:`QpyError` is raised: + +.. autoexception:: UnsupportedFeatureForVersion + Attributes: QPY_VERSION (int): The current QPY format version as of this release. This is the default value of the ``version`` keyword argument on @@ -122,7 +127,7 @@ Qiskit (and qiskit-terra prior to Qiskit 1.0.0) release going back to the introduction of QPY in qiskit-terra 0.18.0. -.. list-table: QPY Format Version History +.. list-table:: QPY Format Version History :header-rows: 1 * - Qiskit (qiskit-terra for < 1.0.0) version @@ -133,7 +138,7 @@ - 12 * - 1.0.2 - 10, 11 - - 12 + - 11 * - 1.0.1 - 10, 11 - 11 @@ -242,9 +247,8 @@ .. _qpy_format: -********** QPY Format -********** +========== The QPY serialization format is a portable cross-platform binary serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic @@ -285,16 +289,116 @@ The file header is immediately followed by the circuit payloads. Each individual circuit is composed of the following parts: -``HEADER | METADATA | REGISTERS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` +``HEADER | METADATA | REGISTERS | STANDALONE_VARS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` + +The ``STANDALONE_VARS`` are new in QPY version 12; before that, there was no data between +``REGISTERS`` and ``CUSTOM_DEFINITIONS``. There is a circuit payload for each circuit (where the total number is dictated by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_12: + +Version 12 +---------- + +Version 12 adds support for: + +* circuits containing memory-owning :class:`.expr.Var` variables. + +Changes to HEADER +~~~~~~~~~~~~~~~~~ + +The HEADER struct for an individual circuit has added three ``uint32_t`` counts of the input, +captured and locally declared variables in the circuit. The new form looks like: + +.. code-block:: c + + struct { + uint16_t name_size; + char global_phase_type; + uint16_t global_phase_size; + uint32_t num_qubits; + uint32_t num_clbits; + uint64_t metadata_size; + uint32_t num_registers; + uint64_t num_instructions; + uint32_t num_vars; + } HEADER_V12; + +The ``HEADER_V12`` struct is followed immediately by the same name, global-phase, metadata +and register information as the V2 version of the header. Immediately following the registers is +``num_vars`` instances of ``EXPR_VAR_STANDALONE`` that define the variables in this circuit. After +that, the data continues with custom definitions and instructions as in prior versions of QPY. + + +EXPR_VAR_DECLARATION +~~~~~~~~~~~~~~~~~~~~ + +An ``EXPR_VAR_DECLARATION`` defines an :class:`.expr.Var` instance that is standalone; that is, it +represents a self-owned memory location rather than wrapping a :class:`.Clbit` or +:class:`.ClassicalRegister`. The payload is a C struct: + +.. code-block:: c + + struct { + char uuid_bytes[16]; + char usage; + uint16_t name_size; + } + +which is immediately followed by an ``EXPR_TYPE`` payload and then ``name_size`` bytes of UTF-8 +encoding string data containing the name of the variable. + +The ``char`` usage type code takes the following values: + +========= ========================================================================================= +Type code Meaning +========= ========================================================================================= +``I`` An ``input`` variable to the circuit. + +``C`` A ``capture`` variable to the circuit. + +``L`` A locally declared variable to the circuit. +========= ========================================================================================= + + +Changes to EXPR_VAR +~~~~~~~~~~~~~~~~~~~ + +The EXPR_VAR variable has gained a new type code and payload, in addition to the pre-existing ones: + +=========================== ========= ============================================================ +Python class Type code Payload +=========================== ========= ============================================================ +:class:`.UUID` ``U`` One ``uint32_t`` index of the variable into the series of + ``EXPR_VAR_STANDALONE`` variables that were written + immediately after the circuit header. +=========================== ========= ============================================================ + +Notably, this new type-code indexes into pre-defined variables from the circuit header, rather than +redefining the variable again in each location it is used. + + +Changes to EXPRESSION +--------------------- + +The EXPRESSION type code has a new possible entry, ``i``, corresponding to :class:`.expr.Index` +nodes. + +====================== ========= ======================================================= ======== +Qiskit class Type code Payload Children +====================== ========= ======================================================= ======== +:class:`~.expr.Index` ``i`` No additional payload. The children are the target 2 + and the index, in that order. +====================== ========= ======================================================= ======== + + .. _qpy_version_11: Version 11 -========== +---------- Version 11 is identical to Version 10 except for the following. First, the names in the CUSTOM_INSTRUCTION blocks @@ -312,7 +416,7 @@ .. _modifier_qpy: MODIFIER --------- +~~~~~~~~ This represents :class:`~qiskit.circuit.annotated_operation.Modifier` @@ -335,19 +439,20 @@ .. _qpy_version_10: Version 10 -========== +---------- + +Version 10 adds support for: -Version 10 adds support for symengine-native serialization for objects of type -:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks. Version -10 also adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit -0.45.0 release. +* symengine-native serialization for objects of type :class:`~.ParameterExpression` as well as + symbolic expressions in Pulse schedule blocks. +* new fields in the :class:`~.TranspileLayout` class added in the Qiskit 0.45.0 release. The symbolic_encoding field is added to the file header, and a new encoding type char is introduced, mapped to each symbolic library as follows: ``p`` refers to sympy encoding and ``e`` refers to symengine encoding. -FILE_HEADER ------------ +Changes to FILE_HEADER +~~~~~~~~~~~~~~~~~~~~~~ The contents of FILE_HEADER after V10 are defined as a C struct as: @@ -360,10 +465,10 @@ uint8_t qiskit_patch_version; uint64_t num_circuits; char symbolic_encoding; - } + } FILE_HEADER_V10; -LAYOUT ------- +Changes to LAYOUT +~~~~~~~~~~~~~~~~~ The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field. With version 10 the ``LAYOUT`` struct is now: @@ -386,14 +491,14 @@ .. _qpy_version_9: Version 9 -========= +--------- Version 9 adds support for classical :class:`~.expr.Expr` nodes and their associated :class:`~.types.Type`\\ s. EXPRESSION ----------- +~~~~~~~~~~ An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is represented by (in order in the byte stream): @@ -425,7 +530,7 @@ EXPR_TYPE ---------- +~~~~~~~~~ A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, followed by a payload that varies depending on the type. The defined codes are: @@ -440,7 +545,7 @@ EXPR_VAR --------- +~~~~~~~~ This represents a runtime variable of a :class:`~.expr.Var` node. These are a type code, followed by a type-code-specific payload: @@ -457,7 +562,7 @@ EXPR_VALUE ----------- +~~~~~~~~~~ This represents a literal object in the classical type system, such as an integer. Currently there are very few such literals. These are encoded as a type code, followed by a type-code-specific @@ -475,7 +580,7 @@ Changes to INSTRUCTION ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ To support the use of :class:`~.expr.Expr` nodes in the fields :attr:`.IfElseOp.condition`, :attr:`.WhileLoopOp.condition` and :attr:`.SwitchCaseOp.target`, the INSTRUCTION struct is changed @@ -522,7 +627,7 @@ Changes to INSTRUCTION_PARAM ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A new type code ``x`` is added that defines an EXPRESSION parameter. @@ -530,7 +635,7 @@ .. _qpy_version_8: Version 8 -========= +--------- Version 8 adds support for handling a :class:`~.TranspileLayout` stored in the :attr:`.QuantumCircuit.layout` attribute. In version 8 immediately following the @@ -539,7 +644,7 @@ :class:`~.TranspileLayout` class. LAYOUT ------- +~~~~~~ .. code-block:: c @@ -561,7 +666,7 @@ :attr:`.TranspileLayout.initial_layout` attribute. INITIAL_LAYOUT_BIT ------------------- +~~~~~~~~~~~~~~~~~~ .. code-block:: c @@ -587,7 +692,7 @@ .. _qpy_version_7: Version 7 -========= +--------- Version 7 adds support for :class:`.~Reference` instruction and serialization of a :class:`.~ScheduleBlock` program while keeping its reference to subroutines:: @@ -633,7 +738,7 @@ .. _qpy_version_6: Version 6 -========= +--------- Version 6 adds support for :class:`.~ScalableSymbolicPulse`. These objects are saved and read like `SymbolicPulse` objects, and the class name is added to the data to correctly handle @@ -660,7 +765,7 @@ .. _qpy_version_5: Version 5 -========= +--------- Version 5 changes from :ref:`qpy_version_4` by adding support for :class:`.~ScheduleBlock` and changing two payloads the INSTRUCTION metadata payload and the CUSTOM_INSTRUCTION block. @@ -695,7 +800,7 @@ .. _qpy_schedule_block: SCHEDULE_BLOCK --------------- +~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` is first supported in QPY Version 5. This allows users to save pulse programs in the QPY binary format as follows: @@ -720,7 +825,7 @@ .. _qpy_schedule_block_header: SCHEDULE_BLOCK_HEADER ---------------------- +~~~~~~~~~~~~~~~~~~~~~ :class:`~.ScheduleBlock` block starts with the following header: @@ -739,7 +844,7 @@ .. _qpy_schedule_alignments: SCHEDULE_BLOCK_ALIGNMENTS -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ Then, alignment context of the schedule block starts with ``char`` representing the supported context type followed by the :ref:`qpy_sequence` block representing @@ -757,7 +862,7 @@ .. _qpy_schedule_instructions: SCHEDULE_BLOCK_INSTRUCTIONS ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ This alignment block is further followed by ``num_element`` length of block elements which may consist of nested schedule blocks and schedule instructions. @@ -782,7 +887,7 @@ .. _qpy_schedule_operands: SCHEDULE_BLOCK_OPERANDS ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The operands of these instances can be serialized through the standard QPY value serialization mechanism, however there are special object types that only appear in the schedule operands. @@ -799,7 +904,7 @@ .. _qpy_schedule_channel: CHANNEL -------- +~~~~~~~ Channel block starts with channel subtype ``char`` that maps an object data to :class:`~qiskit.pulse.channels.Channel` subclass. Mapping is defined as follows: @@ -816,7 +921,7 @@ .. _qpy_schedule_waveform: Waveform --------- +~~~~~~~~ Waveform block starts with WAVEFORM header: @@ -838,7 +943,7 @@ .. _qpy_schedule_symbolic_pulse: SymbolicPulse -------------- +~~~~~~~~~~~~~ SymbolicPulse block starts with SYMBOLIC_PULSE header: @@ -872,7 +977,7 @@ .. _qpy_mapping: MAPPING -------- +~~~~~~~ The MAPPING is a representation for arbitrary mapping object. This is a fixed length :ref:`qpy_sequence` of key-value pair represented by the MAP_ITEM payload. @@ -894,7 +999,7 @@ .. _qpy_circuit_calibrations: CIRCUIT_CALIBRATIONS --------------------- +~~~~~~~~~~~~~~~~~~~~ The CIRCUIT_CALIBRATIONS block is a dictionary to define pulse calibrations of the custom instruction set. This block starts with the following CALIBRATION header: @@ -929,7 +1034,7 @@ .. _qpy_instruction_v5: INSTRUCTION ------------ +~~~~~~~~~~~ The INSTRUCTION block was modified to add two new fields ``num_ctrl_qubits`` and ``ctrl_state`` which are used to model the :attr:`.ControlledGate.num_ctrl_qubits` and @@ -955,7 +1060,7 @@ :ref:`qpy_instructions` for the details of the full payload. CUSTOM_INSTRUCTION ------------------- +~~~~~~~~~~~~~~~~~~ The CUSTOM_INSTRUCTION block in QPY version 5 adds a new field ``base_gate_size`` which is used to define the size of the @@ -998,7 +1103,7 @@ .. _qpy_version_4: Version 4 -========= +--------- Version 4 is identical to :ref:`qpy_version_3` except that it adds 2 new type strings to the INSTRUCTION_PARAM struct, ``z`` to represent ``None`` (which is encoded as @@ -1028,7 +1133,7 @@ .. _qpy_range_pack: RANGE ------ +~~~~~ A RANGE is a representation of a ``range`` object. It is defined as: @@ -1043,7 +1148,7 @@ .. _qpy_sequence: SEQUENCE --------- +~~~~~~~~ A SEQUENCE is a representation of an arbitrary sequence object. As sequence are just fixed length containers of arbitrary python objects their QPY can't fully represent any sequence, @@ -1065,7 +1170,7 @@ .. _qpy_version_3: Version 3 -========= +--------- Version 3 of the QPY format is identical to :ref:`qpy_version_2` except that it defines a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1080,7 +1185,7 @@ .. _pauli_evo_qpy: PAULI_EVOLUTION ---------------- +~~~~~~~~~~~~~~~ This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` @@ -1108,7 +1213,7 @@ .. _qpy_pauli_sum_op: SPARSE_PAULI_OP_LIST_ELEM -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ This represents an instance of :class:`.SparsePauliOp`. @@ -1132,7 +1237,7 @@ .. _qpy_param_vector: PARAMETER_VECTOR_ELEMENT ------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~ A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are @@ -1154,7 +1259,7 @@ PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ Additionally, since QPY format version v3 distinguishes between a :class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement` @@ -1208,14 +1313,14 @@ .. _qpy_version_2: Version 2 -========= +--------- Version 2 of the QPY format is identical to version 1 except for the HEADER section is slightly different. You can refer to the :ref:`qpy_version_1` section for the details on the rest of the payload format. HEADER ------- +~~~~~~ The contents of HEADER are defined as a C struct are: @@ -1245,10 +1350,10 @@ .. _qpy_version_1: Version 1 -========= +--------- HEADER ------- +~~~~~~ The contents of HEADER as defined as a C struct are: @@ -1268,7 +1373,7 @@ of the circuit. METADATA --------- +~~~~~~~~ The METADATA field is a UTF8 encoded JSON string. After reading the HEADER (which is a fixed size at the start of the QPY file) and the ``name`` string @@ -1278,7 +1383,7 @@ .. _qpy_registers: REGISTERS ---------- +~~~~~~~~~ The contents of REGISTERS is a number of REGISTER object. If num_registers is > 0 then after reading METADATA you read that number of REGISTER structs defined @@ -1328,7 +1433,7 @@ .. _qpy_custom_definition: CUSTOM_DEFINITIONS ------------------- +~~~~~~~~~~~~~~~~~~ This section specifies custom definitions for any of the instructions in the circuit. @@ -1368,7 +1473,7 @@ .. _qpy_instructions: INSTRUCTIONS ------------- +~~~~~~~~~~~~ The contents of INSTRUCTIONS is a list of INSTRUCTION metadata objects @@ -1444,7 +1549,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_struct: PARAMETER ---------- +~~~~~~~~~ A PARAMETER represents a :class:`~qiskit.circuit.Parameter` object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER are defined as: @@ -1462,7 +1567,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_param_expr: PARAMETER_EXPR --------------- +~~~~~~~~~~~~~~ A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR @@ -1501,7 +1606,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. _qpy_complex: COMPLEX -------- +~~~~~~~ When representing a double precision complex value in QPY the following struct is used: @@ -1522,7 +1627,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom .. [#f3] https://docs.python.org/3/c-api/complex.html#c.Py_complex """ -from .exceptions import QpyError, QPYLoadingDeprecatedFeatureWarning +from .exceptions import QpyError, UnsupportedFeatureForVersion, QPYLoadingDeprecatedFeatureWarning from .interface import dump, load # For backward compatibility. Provide, Runtime, Experiment call these private functions. diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 40bb5850043..0e2045d5be5 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -40,13 +40,39 @@ from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.qpy import common, formats, type_keys +from qiskit.qpy import common, formats, type_keys, exceptions from qiskit.qpy.binary_io import value, schedules from qiskit.quantum_info.operators import SparsePauliOp, Clifford from qiskit.synthesis import evolution as evo_synth from qiskit.transpiler.layout import Layout, TranspileLayout +def _read_header_v12(file_obj, version, vectors, metadata_deserializer=None): + data = formats.CIRCUIT_HEADER_V12._make( + struct.unpack( + formats.CIRCUIT_HEADER_V12_PACK, file_obj.read(formats.CIRCUIT_HEADER_V12_SIZE) + ) + ) + name = file_obj.read(data.name_size).decode(common.ENCODE) + global_phase = value.loads_value( + data.global_phase_type, + file_obj.read(data.global_phase_size), + version=version, + vectors=vectors, + ) + header = { + "global_phase": global_phase, + "num_qubits": data.num_qubits, + "num_clbits": data.num_clbits, + "num_registers": data.num_registers, + "num_instructions": data.num_instructions, + "num_vars": data.num_vars, + } + metadata_raw = file_obj.read(data.metadata_size) + metadata = json.loads(metadata_raw, cls=metadata_deserializer) + return header, name, metadata + + def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None): data = formats.CIRCUIT_HEADER_V2._make( struct.unpack( @@ -102,7 +128,7 @@ def _read_registers_v4(file_obj, num_registers): ) ) name = file_obj.read(data.name_size).decode("utf8") - REGISTER_ARRAY_PACK = "!%sq" % data.size + REGISTER_ARRAY_PACK = f"!{data.size}q" bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) if data.type.decode("utf8") == "q": @@ -122,7 +148,7 @@ def _read_registers(file_obj, num_registers): ) ) name = file_obj.read(data.name_size).decode("utf8") - REGISTER_ARRAY_PACK = "!%sI" % data.size + REGISTER_ARRAY_PACK = f"!{data.size}I" bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) if data.type.decode("utf8") == "q": @@ -133,7 +159,14 @@ def _read_registers(file_obj, num_registers): def _loads_instruction_parameter( - type_key, data_bytes, version, vectors, registers, circuit, use_symengine + type_key, + data_bytes, + version, + vectors, + registers, + circuit, + use_symengine, + standalone_vars, ): if type_key == type_keys.Program.CIRCUIT: param = common.data_from_binary(data_bytes, read_circuit, version=version) @@ -152,6 +185,7 @@ def _loads_instruction_parameter( registers=registers, circuit=circuit, use_symengine=use_symengine, + standalone_vars=standalone_vars, ) ) elif type_key == type_keys.Value.INTEGER: @@ -172,6 +206,7 @@ def _loads_instruction_parameter( clbits=clbits, cregs=registers["c"], use_symengine=use_symengine, + standalone_vars=standalone_vars, ) return param @@ -186,7 +221,14 @@ def _loads_register_param(data_bytes, circuit, registers): def _read_instruction( - file_obj, circuit, registers, custom_operations, version, vectors, use_symengine + file_obj, + circuit, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ): if version < 5: instruction = formats.CIRCUIT_INSTRUCTION._make( @@ -224,6 +266,7 @@ def _read_instruction( clbits=circuit.clbits, cregs=registers["c"], use_symengine=use_symengine, + standalone_vars=standalone_vars, ) # Load Arguments if circuit is not None: @@ -252,14 +295,28 @@ def _read_instruction( for _param in range(instruction.num_parameters): type_key, data_bytes = common.read_generic_typed_data(file_obj) param = _loads_instruction_parameter( - type_key, data_bytes, version, vectors, registers, circuit, use_symengine + type_key, + data_bytes, + version, + vectors, + registers, + circuit, + use_symengine, + standalone_vars, ) params.append(param) # Load Gate object if gate_name in {"Gate", "Instruction", "ControlledGate"}: inst_obj = _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ) inst_obj.condition = condition if instruction.label_size > 0: @@ -270,7 +327,14 @@ def _read_instruction( return None elif gate_name in custom_operations: inst_obj = _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ) inst_obj.condition = condition if instruction.label_size > 0: @@ -288,7 +352,7 @@ def _read_instruction( elif gate_name == "Clifford": gate_class = Clifford else: - raise AttributeError("Invalid instruction type: %s" % gate_name) + raise AttributeError(f"Invalid instruction type: {gate_name}") if instruction.label_size <= 0: label = None @@ -361,7 +425,14 @@ def _read_instruction( def _parse_custom_operation( - custom_operations, gate_name, params, version, vectors, registers, use_symengine + custom_operations, + gate_name, + params, + version, + vectors, + registers, + use_symengine, + standalone_vars, ): if version >= 5: ( @@ -375,6 +446,7 @@ def _parse_custom_operation( ) = custom_operations[gate_name] else: type_str, num_qubits, num_clbits, definition = custom_operations[gate_name] + base_gate_raw = ctrl_state = num_ctrl_qubits = None # Strip the trailing "_{uuid}" from the gate name if the version >=11 if version >= 11: gate_name = "_".join(gate_name.split("_")[:-1]) @@ -394,7 +466,14 @@ def _parse_custom_operation( if version >= 5 and type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: with io.BytesIO(base_gate_raw) as base_gate_obj: base_gate = _read_instruction( - base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine + base_gate_obj, + None, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ) if ctrl_state < 2**num_ctrl_qubits - 1: # If open controls, we need to discard the control suffix when setting the name. @@ -413,7 +492,14 @@ def _parse_custom_operation( if version >= 11 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION: with io.BytesIO(base_gate_raw) as base_gate_obj: base_gate = _read_instruction( - base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine + base_gate_obj, + None, + registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_vars, ) inst_obj = AnnotatedOperation(base_op=base_gate, modifiers=params) return inst_obj @@ -421,7 +507,7 @@ def _parse_custom_operation( if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: return definition - raise ValueError("Invalid custom instruction type '%s'" % type_str) + raise ValueError(f"Invalid custom instruction type '{type_str}'") def _read_pauli_evolution_gate(file_obj, version, vectors): @@ -572,10 +658,12 @@ def _dumps_register(register, index_map): return b"\x00" + str(index_map["c"][register]).encode(common.ENCODE) -def _dumps_instruction_parameter(param, index_map, use_symengine): +def _dumps_instruction_parameter( + param, index_map, use_symengine, *, version, standalone_var_indices +): if isinstance(param, QuantumCircuit): type_key = type_keys.Program.CIRCUIT - data_bytes = common.data_to_binary(param, write_circuit) + data_bytes = common.data_to_binary(param, write_circuit, version=version) elif isinstance(param, Modifier): type_key = type_keys.Value.MODIFIER data_bytes = common.data_to_binary(param, _write_modifier) @@ -585,7 +673,12 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): elif isinstance(param, tuple): type_key = type_keys.Container.TUPLE data_bytes = common.sequence_to_binary( - param, _dumps_instruction_parameter, index_map=index_map, use_symengine=use_symengine + param, + _dumps_instruction_parameter, + index_map=index_map, + use_symengine=use_symengine, + version=version, + standalone_var_indices=standalone_var_indices, ) elif isinstance(param, int): # TODO This uses little endian. This should be fixed in next QPY version. @@ -600,14 +693,26 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): data_bytes = _dumps_register(param, index_map) else: type_key, data_bytes = value.dumps_value( - param, index_map=index_map, use_symengine=use_symengine + param, + index_map=index_map, + use_symengine=use_symengine, + standalone_var_indices=standalone_var_indices, + version=version, ) return type_key, data_bytes # pylint: disable=too-many-boolean-expressions -def _write_instruction(file_obj, instruction, custom_operations, index_map, use_symengine, version): +def _write_instruction( + file_obj, + instruction, + custom_operations, + index_map, + use_symengine, + version, + standalone_var_indices=None, +): if isinstance(instruction.operation, Instruction): gate_class_name = instruction.operation.base_class.__name__ else: @@ -702,7 +807,13 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_ file_obj.write(gate_class_name) file_obj.write(label_raw) if condition_type is type_keys.Condition.EXPRESSION: - value.write_value(file_obj, op_condition, index_map=index_map) + value.write_value( + file_obj, + op_condition, + version=version, + index_map=index_map, + standalone_var_indices=standalone_var_indices, + ) else: file_obj.write(condition_register) # Encode instruction args @@ -718,12 +829,18 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_ file_obj.write(instruction_arg_raw) # Encode instruction params for param in instruction_params: - type_key, data_bytes = _dumps_instruction_parameter(param, index_map, use_symengine) + type_key, data_bytes = _dumps_instruction_parameter( + param, + index_map, + use_symengine, + version=version, + standalone_var_indices=standalone_var_indices, + ) common.write_generic_typed_data(file_obj, type_key, data_bytes) return custom_operations_list -def _write_pauli_evolution_gate(file_obj, evolution_gate): +def _write_pauli_evolution_gate(file_obj, evolution_gate, version): operator_list = evolution_gate.operator standalone = False if not isinstance(operator_list, list): @@ -742,7 +859,7 @@ def _write_elem(buffer, op): data = common.data_to_binary(operator, _write_elem) pauli_data_buf.write(data) - time_type, time_data = value.dumps_value(evolution_gate.time) + time_type, time_data = value.dumps_value(evolution_gate.time, version=version) time_size = len(time_data) synth_class = str(type(evolution_gate.synthesis).__name__) settings_dict = evolution_gate.synthesis.settings @@ -788,7 +905,9 @@ def _write_modifier(file_obj, modifier): file_obj.write(modifier_data) -def _write_custom_operation(file_obj, name, operation, custom_operations, use_symengine, version): +def _write_custom_operation( + file_obj, name, operation, custom_operations, use_symengine, version, *, standalone_var_indices +): type_key = type_keys.CircuitInstruction.assign(operation) has_definition = False size = 0 @@ -802,7 +921,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: has_definition = True - data = common.data_to_binary(operation, _write_pauli_evolution_gate) + data = common.data_to_binary(operation, _write_pauli_evolution_gate, version=version) size = len(data) elif type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: # For ControlledGate, we have to access and store the private `_definition` rather than the @@ -813,7 +932,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy # Build internal definition to support overloaded subclasses by # calling definition getter on object operation.definition # pylint: disable=pointless-statement - data = common.data_to_binary(operation._definition, write_circuit) + data = common.data_to_binary(operation._definition, write_circuit, version=version) size = len(data) num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state @@ -823,7 +942,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy base_gate = operation.base_op elif operation.definition is not None: has_definition = True - data = common.data_to_binary(operation.definition, write_circuit) + data = common.data_to_binary(operation.definition, write_circuit, version=version) size = len(data) if base_gate is None: base_gate_raw = b"" @@ -836,6 +955,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy {}, use_symengine, version, + standalone_var_indices=standalone_var_indices, ) base_gate_raw = base_gate_buffer.getvalue() name_raw = name.encode(common.ENCODE) @@ -859,7 +979,7 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy return new_custom_instruction -def _write_calibrations(file_obj, calibrations, metadata_serializer): +def _write_calibrations(file_obj, calibrations, metadata_serializer, version): flatten_dict = {} for gate, caldef in calibrations.items(): for (qubits, params), schedule in caldef.items(): @@ -883,8 +1003,8 @@ def _write_calibrations(file_obj, calibrations, metadata_serializer): for qubit in qubits: file_obj.write(struct.pack("!q", qubit)) for param in params: - value.write_value(file_obj, param) - schedules.write_schedule_block(file_obj, schedule, metadata_serializer) + value.write_value(file_obj, param, version=version) + schedules.write_schedule_block(file_obj, schedule, metadata_serializer, version=version) def _write_registers(file_obj, in_circ_regs, full_bits): @@ -911,7 +1031,7 @@ def _write_registers(file_obj, in_circ_regs, full_bits): ) ) file_obj.write(reg_name) - REGISTER_ARRAY_PACK = "!%sq" % reg.size + REGISTER_ARRAY_PACK = f"!{reg.size}q" bit_indices = [] for bit in reg: bit_indices.append(bitmap.get(bit, -1)) @@ -1094,7 +1214,7 @@ def write_circuit( metadata_size = len(metadata_raw) num_instructions = len(circuit) circuit_name = circuit.name.encode(common.ENCODE) - global_phase_type, global_phase_data = value.dumps_value(circuit.global_phase) + global_phase_type, global_phase_data = value.dumps_value(circuit.global_phase, version=version) with io.BytesIO() as reg_buf: num_qregs = _write_registers(reg_buf, circuit.qregs, circuit.qubits) @@ -1103,23 +1223,49 @@ def write_circuit( num_registers = num_qregs + num_cregs # Write circuit header - header_raw = formats.CIRCUIT_HEADER_V2( - name_size=len(circuit_name), - global_phase_type=global_phase_type, - global_phase_size=len(global_phase_data), - num_qubits=circuit.num_qubits, - num_clbits=circuit.num_clbits, - metadata_size=metadata_size, - num_registers=num_registers, - num_instructions=num_instructions, - ) - header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) - file_obj.write(header) - file_obj.write(circuit_name) - file_obj.write(global_phase_data) - file_obj.write(metadata_raw) - # Write header payload - file_obj.write(registers_raw) + if version >= 12: + header_raw = formats.CIRCUIT_HEADER_V12( + name_size=len(circuit_name), + global_phase_type=global_phase_type, + global_phase_size=len(global_phase_data), + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + metadata_size=metadata_size, + num_registers=num_registers, + num_instructions=num_instructions, + num_vars=circuit.num_vars, + ) + header = struct.pack(formats.CIRCUIT_HEADER_V12_PACK, *header_raw) + file_obj.write(header) + file_obj.write(circuit_name) + file_obj.write(global_phase_data) + file_obj.write(metadata_raw) + # Write header payload + file_obj.write(registers_raw) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + else: + if circuit.num_vars: + raise exceptions.UnsupportedFeatureForVersion( + "circuits containing realtime variables", required=12, target=version + ) + header_raw = formats.CIRCUIT_HEADER_V2( + name_size=len(circuit_name), + global_phase_type=global_phase_type, + global_phase_size=len(global_phase_data), + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + metadata_size=metadata_size, + num_registers=num_registers, + num_instructions=num_instructions, + ) + header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) + file_obj.write(header) + file_obj.write(circuit_name) + file_obj.write(global_phase_data) + file_obj.write(metadata_raw) + file_obj.write(registers_raw) + standalone_var_indices = {} + instruction_buffer = io.BytesIO() custom_operations = {} index_map = {} @@ -1127,7 +1273,13 @@ def write_circuit( index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)} for instruction in circuit.data: _write_instruction( - instruction_buffer, instruction, custom_operations, index_map, use_symengine, version + instruction_buffer, + instruction, + custom_operations, + index_map, + use_symengine, + version, + standalone_var_indices=standalone_var_indices, ) with io.BytesIO() as custom_operations_buffer: @@ -1145,6 +1297,7 @@ def write_circuit( custom_operations, use_symengine, version, + standalone_var_indices=standalone_var_indices, ) ) @@ -1155,7 +1308,7 @@ def write_circuit( instruction_buffer.close() # Write calibrations - _write_calibrations(file_obj, circuit.calibrations, metadata_serializer) + _write_calibrations(file_obj, circuit.calibrations, metadata_serializer, version=version) _write_layout(file_obj, circuit) @@ -1186,16 +1339,21 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa vectors = {} if version < 2: header, name, metadata = _read_header(file_obj, metadata_deserializer=metadata_deserializer) - else: + elif version < 12: header, name, metadata = _read_header_v2( file_obj, version, vectors, metadata_deserializer=metadata_deserializer ) + else: + header, name, metadata = _read_header_v12( + file_obj, version, vectors, metadata_deserializer=metadata_deserializer + ) global_phase = header["global_phase"] num_qubits = header["num_qubits"] num_clbits = header["num_clbits"] num_registers = header["num_registers"] num_instructions = header["num_instructions"] + num_vars = header.get("num_vars", 0) # `out_registers` is two "name: register" maps segregated by type for the rest of QPY, and # `all_registers` is the complete ordered list used to construct the `QuantumCircuit`. out_registers = {"q": {}, "c": {}} @@ -1252,6 +1410,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) circ = QuantumCircuit( out_bits["q"], out_bits["c"], @@ -1259,11 +1418,22 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa name=name, global_phase=global_phase, metadata=metadata, + inputs=var_segments[type_keys.ExprVarDeclaration.INPUT], + captures=var_segments[type_keys.ExprVarDeclaration.CAPTURE], ) + for declaration in var_segments[type_keys.ExprVarDeclaration.LOCAL]: + circ.add_uninitialized_var(declaration) custom_operations = _read_custom_operations(file_obj, version, vectors) for _instruction in range(num_instructions): _read_instruction( - file_obj, circ, out_registers, custom_operations, version, vectors, use_symengine + file_obj, + circ, + out_registers, + custom_operations, + version, + vectors, + use_symengine, + standalone_var_indices, ) # Read calibrations diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index a94372f3bfc..eae5e6f57ad 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -328,7 +328,7 @@ def _read_element(file_obj, version, metadata_deserializer, use_symengine): return instance -def _loads_reference_item(type_key, data_bytes, version, metadata_deserializer): +def _loads_reference_item(type_key, data_bytes, metadata_deserializer, version): if type_key == type_keys.Value.NULL: return None if type_key == type_keys.Program.SCHEDULE_BLOCK: @@ -346,13 +346,13 @@ def _loads_reference_item(type_key, data_bytes, version, metadata_deserializer): ) -def _write_channel(file_obj, data): +def _write_channel(file_obj, data, version): type_key = type_keys.ScheduleChannel.assign(data) common.write_type_key(file_obj, type_key) - value.write_value(file_obj, data.index) + value.write_value(file_obj, data.index, version=version) -def _write_waveform(file_obj, data): +def _write_waveform(file_obj, data, version): samples_bytes = common.data_to_binary(data.samples, np.save) header = struct.pack( @@ -363,39 +363,43 @@ def _write_waveform(file_obj, data): ) file_obj.write(header) file_obj.write(samples_bytes) - value.write_value(file_obj, data.name) + value.write_value(file_obj, data.name, version=version) -def _dumps_obj(obj): +def _dumps_obj(obj, version): """Wraps `value.dumps_value` to serialize dictionary and list objects which are not supported by `value.dumps_value`. """ if isinstance(obj, dict): with BytesIO() as container: - common.write_mapping(file_obj=container, mapping=obj, serializer=_dumps_obj) + common.write_mapping( + file_obj=container, mapping=obj, serializer=_dumps_obj, version=version + ) binary_data = container.getvalue() return b"D", binary_data elif isinstance(obj, list): with BytesIO() as container: - common.write_sequence(file_obj=container, sequence=obj, serializer=_dumps_obj) + common.write_sequence( + file_obj=container, sequence=obj, serializer=_dumps_obj, version=version + ) binary_data = container.getvalue() return b"l", binary_data else: - return value.dumps_value(obj) + return value.dumps_value(obj, version=version) -def _write_kernel(file_obj, data): +def _write_kernel(file_obj, data, version): name = data.name params = data.params - common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj) - value.write_value(file_obj, name) + common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj, version=version) + value.write_value(file_obj, name, version=version) -def _write_discriminator(file_obj, data): +def _write_discriminator(file_obj, data, version): name = data.name params = data.params - common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj) - value.write_value(file_obj, name) + common.write_mapping(file_obj=file_obj, mapping=params, serializer=_dumps_obj, version=version) + value.write_value(file_obj, name, version=version) def _dumps_symbolic_expr(expr, use_symengine): @@ -410,7 +414,7 @@ def _dumps_symbolic_expr(expr, use_symengine): return zlib.compress(expr_bytes) -def _write_symbolic_pulse(file_obj, data, use_symengine): +def _write_symbolic_pulse(file_obj, data, use_symengine, version): class_name_bytes = data.__class__.__name__.encode(common.ENCODE) pulse_type_bytes = data.pulse_type.encode(common.ENCODE) envelope_bytes = _dumps_symbolic_expr(data.envelope, use_symengine) @@ -436,52 +440,51 @@ def _write_symbolic_pulse(file_obj, data, use_symengine): file_obj, mapping=data._params, serializer=value.dumps_value, + version=version, ) - value.write_value(file_obj, data.duration) - value.write_value(file_obj, data.name) + value.write_value(file_obj, data.duration, version=version) + value.write_value(file_obj, data.name, version=version) -def _write_alignment_context(file_obj, context): +def _write_alignment_context(file_obj, context, version): type_key = type_keys.ScheduleAlignment.assign(context) common.write_type_key(file_obj, type_key) common.write_sequence( - file_obj, - sequence=context._context_params, - serializer=value.dumps_value, + file_obj, sequence=context._context_params, serializer=value.dumps_value, version=version ) -def _dumps_operand(operand, use_symengine): +def _dumps_operand(operand, use_symengine, version): if isinstance(operand, library.Waveform): type_key = type_keys.ScheduleOperand.WAVEFORM - data_bytes = common.data_to_binary(operand, _write_waveform) + data_bytes = common.data_to_binary(operand, _write_waveform, version=version) elif isinstance(operand, library.SymbolicPulse): type_key = type_keys.ScheduleOperand.SYMBOLIC_PULSE data_bytes = common.data_to_binary( - operand, _write_symbolic_pulse, use_symengine=use_symengine + operand, _write_symbolic_pulse, use_symengine=use_symengine, version=version ) elif isinstance(operand, channels.Channel): type_key = type_keys.ScheduleOperand.CHANNEL - data_bytes = common.data_to_binary(operand, _write_channel) + data_bytes = common.data_to_binary(operand, _write_channel, version=version) elif isinstance(operand, str): type_key = type_keys.ScheduleOperand.OPERAND_STR data_bytes = operand.encode(common.ENCODE) elif isinstance(operand, Kernel): type_key = type_keys.ScheduleOperand.KERNEL - data_bytes = common.data_to_binary(operand, _write_kernel) + data_bytes = common.data_to_binary(operand, _write_kernel, version=version) elif isinstance(operand, Discriminator): type_key = type_keys.ScheduleOperand.DISCRIMINATOR - data_bytes = common.data_to_binary(operand, _write_discriminator) + data_bytes = common.data_to_binary(operand, _write_discriminator, version=version) else: - type_key, data_bytes = value.dumps_value(operand) + type_key, data_bytes = value.dumps_value(operand, version=version) return type_key, data_bytes -def _write_element(file_obj, element, metadata_serializer, use_symengine): +def _write_element(file_obj, element, metadata_serializer, use_symengine, version): if isinstance(element, ScheduleBlock): common.write_type_key(file_obj, type_keys.Program.SCHEDULE_BLOCK) - write_schedule_block(file_obj, element, metadata_serializer, use_symengine) + write_schedule_block(file_obj, element, metadata_serializer, use_symengine, version=version) else: type_key = type_keys.ScheduleInstruction.assign(element) common.write_type_key(file_obj, type_key) @@ -490,11 +493,12 @@ def _write_element(file_obj, element, metadata_serializer, use_symengine): sequence=element.operands, serializer=_dumps_operand, use_symengine=use_symengine, + version=version, ) - value.write_value(file_obj, element.name) + value.write_value(file_obj, element.name, version=version) -def _dumps_reference_item(schedule, metadata_serializer): +def _dumps_reference_item(schedule, metadata_serializer, version): if schedule is None: type_key = type_keys.Value.NULL data_bytes = b"" @@ -504,6 +508,7 @@ def _dumps_reference_item(schedule, metadata_serializer): obj=schedule, serializer=write_schedule_block, metadata_serializer=metadata_serializer, + version=version, ) return type_key, data_bytes @@ -517,7 +522,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symen metadata_deserializer (JSONDecoder): An optional JSONDecoder class that will be used for the ``cls`` kwarg on the internal ``json.load`` call used to deserialize the JSON payload used for - the :attr:`.ScheduleBlock.metadata` attribute for a schdule block + the :attr:`.ScheduleBlock.metadata` attribute for a schedule block in the file-like object. If this is not specified the circuit metadata will be parsed as JSON with the stdlib ``json.load()`` function using the default ``JSONDecoder`` class. @@ -576,7 +581,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symen def write_schedule_block( file_obj, block, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION -): # pylint: disable=unused-argument +): """Write a single ScheduleBlock object in the file like object. Args: @@ -610,11 +615,11 @@ def write_schedule_block( file_obj.write(block_name) file_obj.write(metadata) - _write_alignment_context(file_obj, block.alignment_context) + _write_alignment_context(file_obj, block.alignment_context, version=version) for block_elm in block._blocks: # Do not call block.blocks. This implicitly assigns references to instruction. # This breaks original reference structure. - _write_element(file_obj, block_elm, metadata_serializer, use_symengine) + _write_element(file_obj, block_elm, metadata_serializer, use_symengine, version=version) # Write references flat_key_refdict = {} @@ -627,4 +632,5 @@ def write_schedule_block( mapping=flat_key_refdict, serializer=_dumps_reference_item, metadata_serializer=metadata_serializer, + version=version, ) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 1c11d4ad27c..105d4364c07 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -45,7 +45,7 @@ def _write_parameter_vec(file_obj, obj): struct.pack( formats.PARAMETER_VECTOR_ELEMENT_PACK, len(name_bytes), - obj._vector._size, + len(obj._vector), obj.uuid.bytes, obj._index, ) @@ -53,7 +53,7 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _write_parameter_expression(file_obj, obj, use_symengine): +def _write_parameter_expression(file_obj, obj, use_symengine, *, version): if use_symengine: expr_bytes = obj._symbol_expr.__reduce__()[1][0] else: @@ -81,7 +81,7 @@ def _write_parameter_expression(file_obj, obj, use_symengine): value_key = symbol_key value_data = bytes() else: - value_key, value_data = dumps_value(value, use_symengine=use_symengine) + value_key, value_data = dumps_value(value, version=version, use_symengine=use_symengine) elem_header = struct.pack( formats.PARAM_EXPR_MAP_ELEM_V3_PACK, @@ -95,11 +95,13 @@ def _write_parameter_expression(file_obj, obj, use_symengine): class _ExprWriter(expr.ExprVisitor[None]): - __slots__ = ("file_obj", "clbit_indices") + __slots__ = ("file_obj", "clbit_indices", "standalone_var_indices", "version") - def __init__(self, file_obj, clbit_indices): + def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.file_obj = file_obj self.clbit_indices = clbit_indices + self.standalone_var_indices = standalone_var_indices + self.version = version def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") @@ -107,7 +109,15 @@ def visit_generic(self, node, /): def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) _write_expr_type(self.file_obj, node.type) - if isinstance(node.var, Clbit): + if node.standalone: + self.file_obj.write(type_keys.ExprVar.UUID) + self.file_obj.write( + struct.pack( + formats.EXPR_VAR_UUID_PACK, + *formats.EXPR_VAR_UUID(self.standalone_var_indices[node]), + ) + ) + elif isinstance(node.var, Clbit): self.file_obj.write(type_keys.ExprVar.CLBIT) self.file_obj.write( struct.pack( @@ -172,14 +182,30 @@ def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) _write_expr_type(self.file_obj, node.type) self.file_obj.write( - struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) + struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) node.left.accept(self) node.right.accept(self) + def visit_index(self, node, /): + if self.version < 12: + raise exceptions.UnsupportedFeatureForVersion( + "the 'Index' expression", required=12, target=self.version + ) + self.file_obj.write(type_keys.Expression.INDEX) + _write_expr_type(self.file_obj, node.type) + node.target.accept(self) + node.index.accept(self) -def _write_expr(file_obj, node: expr.Expr, clbit_indices: collections.abc.Mapping[Clbit, int]): - node.accept(_ExprWriter(file_obj, clbit_indices)) + +def _write_expr( + file_obj, + node: expr.Expr, + clbit_indices: collections.abc.Mapping[Clbit, int], + standalone_var_indices: collections.abc.Mapping[expr.Var, int], + version: int, +): + node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) def _write_expr_type(file_obj, type_: types.Type): @@ -251,7 +277,7 @@ def _read_parameter_expression(file_obj): elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary(binary_data, _read_parameter_expression) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") symbol_map[symbol] = value return ParameterExpression(symbol_map, expr_) @@ -285,7 +311,7 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): elif symbol_key == type_keys.Value.PARAMETER_VECTOR: symbol = _read_parameter_vec(file_obj, vectors) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % symbol_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {symbol_key}") elem_key = type_keys.Value(elem_data.type) binary_data = file_obj.read(elem_data.size) @@ -305,7 +331,7 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): use_symengine=use_symengine, ) else: - raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") symbol_map[symbol] = value return ParameterExpression(symbol_map, expr_) @@ -315,12 +341,18 @@ def _read_expr( file_obj, clbits: collections.abc.Sequence[Clbit], cregs: collections.abc.Mapping[str, ClassicalRegister], + standalone_vars: collections.abc.Sequence[expr.Var], ) -> expr.Expr: # pylint: disable=too-many-return-statements type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) type_ = _read_expr_type(file_obj) if type_key == type_keys.Expression.VAR: var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) + if var_type_key == type_keys.ExprVar.UUID: + payload = formats.EXPR_VAR_UUID._make( + struct.unpack(formats.EXPR_VAR_UUID_PACK, file_obj.read(formats.EXPR_VAR_UUID_SIZE)) + ) + return standalone_vars[payload.var_index] if var_type_key == type_keys.ExprVar.CLBIT: payload = formats.EXPR_VAR_CLBIT._make( struct.unpack( @@ -360,14 +392,20 @@ def _read_expr( payload = formats.EXPRESSION_CAST._make( struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) ) - return expr.Cast(_read_expr(file_obj, clbits, cregs), type_, implicit=payload.implicit) + return expr.Cast( + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, implicit=payload.implicit + ) if type_key == type_keys.Expression.UNARY: payload = formats.EXPRESSION_UNARY._make( struct.unpack( formats.EXPRESSION_UNARY_PACK, file_obj.read(formats.EXPRESSION_UNARY_SIZE) ) ) - return expr.Unary(expr.Unary.Op(payload.opcode), _read_expr(file_obj, clbits, cregs), type_) + return expr.Unary( + expr.Unary.Op(payload.opcode), + _read_expr(file_obj, clbits, cregs, standalone_vars), + type_, + ) if type_key == type_keys.Expression.BINARY: payload = formats.EXPRESSION_BINARY._make( struct.unpack( @@ -376,11 +414,17 @@ def _read_expr( ) return expr.Binary( expr.Binary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs), - _read_expr(file_obj, clbits, cregs), + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), + type_, + ) + if type_key == type_keys.Expression.INDEX: + return expr.Index( + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) - raise exceptions.QpyError("Invalid classical-expression Expr key '{type_key}'") + raise exceptions.QpyError(f"Invalid classical-expression Expr key '{type_key}'") def _read_expr_type(file_obj) -> types.Type: @@ -395,11 +439,92 @@ def _read_expr_type(file_obj) -> types.Type: raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def dumps_value(obj, *, index_map=None, use_symengine=False): +def read_standalone_vars(file_obj, num_vars): + """Read the ``num_vars`` standalone variable declarations from the file. + + Args: + file_obj (File): a file-like object to read from. + num_vars (int): the number of variables to read. + + Returns: + tuple[dict, list]: the first item is a mapping of the ``ExprVarDeclaration`` type keys to + the variables defined by that type key, and the second is the total order of variable + declarations. + """ + read_vars = { + type_keys.ExprVarDeclaration.INPUT: [], + type_keys.ExprVarDeclaration.CAPTURE: [], + type_keys.ExprVarDeclaration.LOCAL: [], + } + var_order = [] + for _ in range(num_vars): + data = formats.EXPR_VAR_DECLARATION._make( + struct.unpack( + formats.EXPR_VAR_DECLARATION_PACK, + file_obj.read(formats.EXPR_VAR_DECLARATION_SIZE), + ) + ) + type_ = _read_expr_type(file_obj) + name = file_obj.read(data.name_size).decode(common.ENCODE) + var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) + read_vars[data.usage].append(var) + var_order.append(var) + return read_vars, var_order + + +def _write_standalone_var(file_obj, var, type_key): + name = var.name.encode(common.ENCODE) + file_obj.write( + struct.pack( + formats.EXPR_VAR_DECLARATION_PACK, + *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), + ) + ) + _write_expr_type(file_obj, var.type) + file_obj.write(name) + + +def write_standalone_vars(file_obj, circuit): + """Write the standalone variables out from a circuit. + + Args: + file_obj (File): the file-like object to write to. + circuit (QuantumCircuit): the circuit to take the variables from. + + Returns: + dict[expr.Var, int]: a mapping of the variables written to the index that they were written + at. + """ + index = 0 + out = {} + for var in circuit.iter_input_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + out[var] = index + index += 1 + for var in circuit.iter_captured_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + out[var] = index + index += 1 + for var in circuit.iter_declared_vars(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + out[var] = index + index += 1 + return out + + +def dumps_value( + obj, + *, + version, + index_map=None, + use_symengine=False, + standalone_var_indices=None, +): """Serialize input value object. Args: obj (any): Arbitrary value object to serialize. + version (int): the target QPY version for the dump. index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their integer indices. @@ -407,6 +532,8 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_var_indices (dict): Dictionary that maps standalone :class:`.expr.Var` entries to + the index that should be used to refer to them. Returns: tuple: TypeKey and binary data. @@ -434,23 +561,33 @@ def dumps_value(obj, *, index_map=None, use_symengine=False): binary_data = common.data_to_binary(obj, _write_parameter) elif type_key == type_keys.Value.PARAMETER_EXPRESSION: binary_data = common.data_to_binary( - obj, _write_parameter_expression, use_symengine=use_symengine + obj, _write_parameter_expression, use_symengine=use_symengine, version=version ) elif type_key == type_keys.Value.EXPRESSION: clbit_indices = {} if index_map is None else index_map["c"] - binary_data = common.data_to_binary(obj, _write_expr, clbit_indices=clbit_indices) + standalone_var_indices = {} if standalone_var_indices is None else standalone_var_indices + binary_data = common.data_to_binary( + obj, + _write_expr, + clbit_indices=clbit_indices, + standalone_var_indices=standalone_var_indices, + version=version, + ) else: raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") return type_key, binary_data -def write_value(file_obj, obj, *, index_map=None, use_symengine=False): +def write_value( + file_obj, obj, *, version, index_map=None, use_symengine=False, standalone_var_indices=None +): """Write a value to the file like object. Args: file_obj (File): A file like object to write data. obj (any): Value to write. + version (int): the target QPY version for the dump. index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their integer indices. @@ -458,13 +595,29 @@ def write_value(file_obj, obj, *, index_map=None, use_symengine=False): native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_var_indices (dict): Dictionary that maps standalone :class:`.expr.Var` entries to + the index that should be used to refer to them. """ - type_key, data = dumps_value(obj, index_map=index_map, use_symengine=use_symengine) + type_key, data = dumps_value( + obj, + version=version, + index_map=index_map, + use_symengine=use_symengine, + standalone_var_indices=standalone_var_indices, + ) common.write_generic_typed_data(file_obj, type_key, data) def loads_value( - type_key, binary_data, version, vectors, *, clbits=(), cregs=None, use_symengine=False + type_key, + binary_data, + version, + vectors, + *, + clbits=(), + cregs=None, + use_symengine=False, + standalone_vars=(), ): """Deserialize input binary data to value object. @@ -479,6 +632,8 @@ def loads_value( native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_vars (Sequence[Var]): standalone :class:`.expr.Var` nodes in the order that they + were declared by the circuit header. Returns: any: Deserialized value object. @@ -520,12 +675,27 @@ def loads_value( use_symengine=use_symengine, ) if type_key == type_keys.Value.EXPRESSION: - return common.data_from_binary(binary_data, _read_expr, clbits=clbits, cregs=cregs or {}) + return common.data_from_binary( + binary_data, + _read_expr, + clbits=clbits, + cregs=cregs or {}, + standalone_vars=standalone_vars, + ) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") -def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengine=False): +def read_value( + file_obj, + version, + vectors, + *, + clbits=(), + cregs=None, + use_symengine=False, + standalone_vars=(), +): """Read a value from the file like object. Args: @@ -538,6 +708,8 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengi native mechanism. This is a faster serialization alternative, but not supported in all platforms. Please check that your target platform is supported by the symengine library before setting this option, as it will be required by qpy to deserialize the payload. + standalone_vars (Sequence[expr.Var]): standalone variables in the order they were defined in + the QPY payload. Returns: any: Deserialized value object. @@ -545,5 +717,12 @@ def read_value(file_obj, version, vectors, *, clbits=(), cregs=None, use_symengi type_key, data = common.read_generic_typed_data(file_obj) return loads_value( - type_key, data, version, vectors, clbits=clbits, cregs=cregs, use_symengine=use_symengine + type_key, + data, + version, + vectors, + clbits=clbits, + cregs=cregs, + use_symengine=use_symengine, + standalone_vars=standalone_vars, ) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 7cc11fb7ca0..048320d5cad 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 11 +QPY_VERSION = 12 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/exceptions.py b/qiskit/qpy/exceptions.py index c6cdb4303a6..5662e602937 100644 --- a/qiskit/qpy/exceptions.py +++ b/qiskit/qpy/exceptions.py @@ -28,6 +28,26 @@ def __str__(self): return repr(self.message) +class UnsupportedFeatureForVersion(QpyError): + """QPY error raised when the target dump version is too low for a feature that is present in the + object to be serialized.""" + + def __init__(self, feature: str, required: int, target: int): + """ + Args: + feature: a description of the problematic feature. + required: the minimum version of QPY that would be required to represent this + feature. + target: the version of QPY that is being used in the serialization. + """ + self.feature = feature + self.required = required + self.target = target + super().__init__( + f"Dumping QPY version {target}, but version {required} is required for: {feature}." + ) + + class QPYLoadingDeprecatedFeatureWarning(QiskitWarning): """Visible deprecation warning for QPY loading functions without a stable point in the call stack.""" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 958bebd8dad..a48a9ea777f 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -42,6 +42,24 @@ FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) + +CIRCUIT_HEADER_V12 = namedtuple( + "HEADER", + [ + "name_size", + "global_phase_type", + "global_phase_size", + "num_qubits", + "num_clbits", + "metadata_size", + "num_registers", + "num_instructions", + "num_vars", + ], +) +CIRCUIT_HEADER_V12_PACK = "!H1cHIIQIQI" +CIRCUIT_HEADER_V12_SIZE = struct.calcsize(CIRCUIT_HEADER_V12_PACK) + # CIRCUIT_HEADER_V2 CIRCUIT_HEADER_V2 = namedtuple( "HEADER", @@ -309,6 +327,13 @@ INITIAL_LAYOUT_BIT_PACK = "!ii" INITIAL_LAYOUT_BIT_SIZE = struct.calcsize(INITIAL_LAYOUT_BIT_PACK) +# EXPR_VAR_DECLARATION + +EXPR_VAR_DECLARATION = namedtuple("EXPR_VAR_DECLARATION", ["uuid_bytes", "usage", "name_size"]) +EXPR_VAR_DECLARATION_PACK = "!16scH" +EXPR_VAR_DECLARATION_SIZE = struct.calcsize(EXPR_VAR_DECLARATION_PACK) + + # EXPRESSION EXPRESSION_DISCRIMINATOR_SIZE = 1 @@ -351,6 +376,10 @@ EXPR_VAR_REGISTER_PACK = "!H" EXPR_VAR_REGISTER_SIZE = struct.calcsize(EXPR_VAR_REGISTER_PACK) +EXPR_VAR_UUID = namedtuple("EXPR_VAR_UUID", ["var_index"]) +EXPR_VAR_UUID_PACK = "!H" +EXPR_VAR_UUID_SIZE = struct.calcsize(EXPR_VAR_UUID_PACK) + # EXPR_VALUE diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 34503dbab13..d89117bc6a1 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -304,10 +304,11 @@ def load( ): warnings.warn( "The qiskit version used to generate the provided QPY " - "file, %s, is newer than the current qiskit version %s. " + f"file, {'.'.join([str(x) for x in qiskit_version])}, " + f"is newer than the current qiskit version {__version__}. " "This may result in an error if the QPY file uses " "instructions not present in this current qiskit " - "version" % (".".join([str(x) for x in qiskit_version]), __version__) + "version" ) if data.qpy_version < 5: diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index dd0e7fe2269..60262440d03 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -16,6 +16,7 @@ QPY Type keys for several namespace. """ +import uuid from abc import abstractmethod from enum import Enum, IntEnum @@ -158,7 +159,7 @@ class Condition(IntEnum): """Type keys for the ``conditional_key`` field of the INSTRUCTION struct.""" # This class is deliberately raw integers and not in terms of ASCII characters for backwards - # compatiblity in the form as an old Boolean value was expanded; `NONE` and `TWO_TUPLE` must + # compatibility in the form as an old Boolean value was expanded; `NONE` and `TWO_TUPLE` must # have the enumeration values 0 and 1. NONE = 0 @@ -275,7 +276,7 @@ class ScheduleInstruction(TypeKeyBase): REFERENCE = b"y" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. - # Call instructon is not supported by QPY. + # Call instruction is not supported by QPY. # This instruction has been excluded from ScheduleBlock instructions with # qiskit-terra/#8005 and new instruction Reference will be added instead. # Call is only applied to Schedule which is not supported by QPY. @@ -456,6 +457,7 @@ class Expression(TypeKeyBase): CAST = b"c" UNARY = b"u" BINARY = b"b" + INDEX = b"i" @classmethod def assign(cls, obj): @@ -471,6 +473,22 @@ def retrieve(cls, type_key): raise NotImplementedError +class ExprVarDeclaration(TypeKeyBase): + """Type keys for the ``EXPR_VAR_DECLARATION`` QPY item.""" + + INPUT = b"I" + CAPTURE = b"C" + LOCAL = b"L" + + @classmethod + def assign(cls, obj): + raise NotImplementedError + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + class ExprType(TypeKeyBase): """Type keys for the ``EXPR_TYPE`` QPY item.""" @@ -496,9 +514,12 @@ class ExprVar(TypeKeyBase): CLBIT = b"C" REGISTER = b"R" + UUID = b"U" @classmethod def assign(cls, obj): + if isinstance(obj, uuid.UUID): + return cls.UUID if isinstance(obj, Clbit): return cls.CLBIT if isinstance(obj, ClassicalRegister): diff --git a/qiskit/quantum_info/operators/channel/quantum_channel.py b/qiskit/quantum_info/operators/channel/quantum_channel.py index 16df920e2e0..ff20feb5bf4 100644 --- a/qiskit/quantum_info/operators/channel/quantum_channel.py +++ b/qiskit/quantum_info/operators/channel/quantum_channel.py @@ -66,12 +66,9 @@ def __init__( def __repr__(self): prefix = f"{self._channel_rep}(" pad = len(prefix) * " " - return "{}{},\n{}input_dims={}, output_dims={})".format( - prefix, - np.array2string(np.asarray(self.data), separator=", ", prefix=prefix), - pad, - self.input_dims(), - self.output_dims(), + return ( + f"{prefix}{np.array2string(np.asarray(self.data), separator=', ', prefix=prefix)}" + f",\n{pad}input_dims={self.input_dims()}, output_dims={self.output_dims()})" ) def __eq__(self, other: Self): diff --git a/qiskit/quantum_info/operators/channel/superop.py b/qiskit/quantum_info/operators/channel/superop.py index 19867696ec6..f07652e22d7 100644 --- a/qiskit/quantum_info/operators/channel/superop.py +++ b/qiskit/quantum_info/operators/channel/superop.py @@ -355,8 +355,8 @@ def _append_instruction(self, obj, qargs=None): raise QiskitError(f"Cannot apply Instruction: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; " - "expected QuantumCircuit".format(obj.name, type(obj.definition)) + f"{obj.name} instruction definition is {type(obj.definition)}; " + "expected QuantumCircuit" ) qubit_indices = {bit: idx for idx, bit in enumerate(obj.definition.qubits)} for instruction in obj.definition.data: diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 18987e5e943..8f429cad8ce 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -228,7 +228,7 @@ def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): # This should be a call to la.eigh, but there is an OpenBlas # threading issue that is causing segfaults. # Need schur here since la.eig does not - # guarentee orthogonality in degenerate subspaces + # guarantee orthogonality in degenerate subspaces w, v = la.schur(data, output="complex") w = w.diagonal().real # Check eigenvalues are non-negative diff --git a/qiskit/quantum_info/operators/dihedral/dihedral.py b/qiskit/quantum_info/operators/dihedral/dihedral.py index 4f49879063e..bcd9f6b094a 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral.py @@ -97,7 +97,7 @@ class CNOTDihedral(BaseOperator, AdjointMixin): with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ @@ -325,7 +325,7 @@ def to_circuit(self): with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ # pylint: disable=cyclic-import @@ -452,8 +452,7 @@ def conjugate(self): new_qubits = [bit_indices[tup] for tup in instruction.qubits] if instruction.operation.name == "p": params = 2 * np.pi - instruction.operation.params[0] - instruction.operation.params[0] = params - new_circ.append(instruction.operation, new_qubits) + new_circ.p(params, new_qubits) elif instruction.operation.name == "t": instruction.operation.name = "tdg" new_circ.append(instruction.operation, new_qubits) diff --git a/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py b/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py index 7104dced9df..bfe76a2f3ca 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral_circuits.py @@ -92,9 +92,7 @@ def _append_circuit(elem, circuit, qargs=None): raise QiskitError(f"Cannot apply Instruction: {gate.name}") if not isinstance(gate.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - gate.name, type(gate.definition) - ) + f"{gate.name} instruction definition is {type(gate.definition)}; expected QuantumCircuit" ) flat_instr = gate.definition diff --git a/qiskit/quantum_info/operators/dihedral/random.py b/qiskit/quantum_info/operators/dihedral/random.py index f339cf98377..8223f87e9a1 100644 --- a/qiskit/quantum_info/operators/dihedral/random.py +++ b/qiskit/quantum_info/operators/dihedral/random.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 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 @@ -49,9 +49,12 @@ def random_cnotdihedral(num_qubits, seed=None): # Random affine function # Random invertible binary matrix - from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.synthesis.linear import ( # pylint: disable=cyclic-import + random_invertible_binary_matrix, + ) - linear = random_invertible_binary_matrix(num_qubits, seed=rng) + seed = rng.integers(100000, size=1, dtype=np.uint64)[0] + linear = random_invertible_binary_matrix(num_qubits, seed=seed).astype(int, copy=False) elem.linear = linear # Random shift diff --git a/qiskit/quantum_info/operators/measures.py b/qiskit/quantum_info/operators/measures.py index 617e9f64b68..8b6350ab6fd 100644 --- a/qiskit/quantum_info/operators/measures.py +++ b/qiskit/quantum_info/operators/measures.py @@ -93,7 +93,7 @@ def process_fidelity( if channel.dim != target.dim: raise QiskitError( "Input quantum channel and target unitary must have the same " - "dimensions ({} != {}).".format(channel.dim, target.dim) + f"dimensions ({channel.dim} != {target.dim})." ) # Validate complete-positivity and trace-preserving @@ -316,7 +316,7 @@ def cvx_bmat(mat_r, mat_i): iden = sparse.eye(dim_out) # Watrous uses row-vec convention for his Choi matrix while we use - # col-vec. It turns out row-vec convention is requried for CVXPY too + # col-vec. It turns out row-vec convention is required for CVXPY too # since the cvxpy.kron function must have a constant as its first argument. c_r = cvxpy.bmat([[cvxpy.kron(iden, r0_r), x_r], [x_r.T, cvxpy.kron(iden, r1_r)]]) c_i = cvxpy.bmat([[cvxpy.kron(iden, r0_i), x_i], [-x_i.T, cvxpy.kron(iden, r1_i)]]) diff --git a/qiskit/quantum_info/operators/op_shape.py b/qiskit/quantum_info/operators/op_shape.py index 4f95126ea14..42f05a8c53a 100644 --- a/qiskit/quantum_info/operators/op_shape.py +++ b/qiskit/quantum_info/operators/op_shape.py @@ -193,7 +193,7 @@ def _validate(self, shape, raise_exception=False): if raise_exception: raise QiskitError( "Output dimensions do not match matrix shape " - "({} != {})".format(reduce(mul, self._dims_l), shape[0]) + f"({reduce(mul, self._dims_l)} != {shape[0]})" ) return False elif shape[0] != 2**self._num_qargs_l: @@ -207,7 +207,7 @@ def _validate(self, shape, raise_exception=False): if raise_exception: raise QiskitError( "Input dimensions do not match matrix shape " - "({} != {})".format(reduce(mul, self._dims_r), shape[1]) + f"({reduce(mul, self._dims_r)} != {shape[1]})" ) return False elif shape[1] != 2**self._num_qargs_r: @@ -430,7 +430,7 @@ def compose(self, other, qargs=None, front=False): if self._num_qargs_r != other._num_qargs_l or self._dims_r != other._dims_l: raise QiskitError( "Left and right compose dimensions don't match " - "({} != {})".format(self.dims_r(), other.dims_l()) + f"({self.dims_r()} != {other.dims_l()})" ) ret._dims_l = self._dims_l ret._dims_r = other._dims_r @@ -440,7 +440,7 @@ def compose(self, other, qargs=None, front=False): if self._num_qargs_l != other._num_qargs_r or self._dims_l != other._dims_r: raise QiskitError( "Left and right compose dimensions don't match " - "({} != {})".format(self.dims_l(), other.dims_r()) + f"({self.dims_l()} != {other.dims_r()})" ) ret._dims_l = other._dims_l ret._dims_r = self._dims_r @@ -453,15 +453,13 @@ def compose(self, other, qargs=None, front=False): ret._num_qargs_l = self._num_qargs_l if len(qargs) != other._num_qargs_l: raise QiskitError( - "Number of qargs does not match ({} != {})".format( - len(qargs), other._num_qargs_l - ) + f"Number of qargs does not match ({len(qargs)} != {other._num_qargs_l})" ) if self._dims_r or other._dims_r: if self.dims_r(qargs) != other.dims_l(): raise QiskitError( "Subsystem dimension do not match on specified qargs " - "{} != {}".format(self.dims_r(qargs), other.dims_l()) + f"{self.dims_r(qargs)} != {other.dims_l()}" ) dims_r = list(self.dims_r()) for i, dim in zip(qargs, other.dims_r()): @@ -475,15 +473,13 @@ def compose(self, other, qargs=None, front=False): ret._num_qargs_r = self._num_qargs_r if len(qargs) != other._num_qargs_r: raise QiskitError( - "Number of qargs does not match ({} != {})".format( - len(qargs), other._num_qargs_r - ) + f"Number of qargs does not match ({len(qargs)} != {other._num_qargs_r})" ) if self._dims_l or other._dims_l: if self.dims_l(qargs) != other.dims_r(): raise QiskitError( "Subsystem dimension do not match on specified qargs " - "{} != {}".format(self.dims_l(qargs), other.dims_r()) + f"{self.dims_l(qargs)} != {other.dims_r()}" ) dims_l = list(self.dims_l()) for i, dim in zip(qargs, other.dims_l()): @@ -508,26 +504,22 @@ def _validate_add(self, other, qargs=None): if self.dims_l(qargs) != other.dims_l(): raise QiskitError( "Cannot add shapes width different left " - "dimension on specified qargs {} != {}".format( - self.dims_l(qargs), other.dims_l() - ) + f"dimension on specified qargs {self.dims_l(qargs)} != {other.dims_l()}" ) if self.dims_r(qargs) != other.dims_r(): raise QiskitError( "Cannot add shapes width different total right " - "dimension on specified qargs{} != {}".format( - self.dims_r(qargs), other.dims_r() - ) + f"dimension on specified qargs{self.dims_r(qargs)} != {other.dims_r()}" ) elif self != other: if self._dim_l != other._dim_l: raise QiskitError( "Cannot add shapes width different total left " - "dimension {} != {}".format(self._dim_l, other._dim_l) + f"dimension {self._dim_l} != {other._dim_l}" ) if self._dim_r != other._dim_r: raise QiskitError( "Cannot add shapes width different total right " - "dimension {} != {}".format(self._dim_r, other._dim_r) + f"dimension {self._dim_r} != {other._dim_r}" ) return self diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index d119a381249..a4e93f36480 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -82,6 +82,9 @@ def __init__( a Numpy array of shape (2**N, 2**N) qubit systems will be used. If the input operator is not an N-qubit operator, it will assign a single subsystem with dimension specified by the shape of the input. + Note that two operators initialized via this method are only considered equivalent if they + match up to their canonical qubit order (or: permutation). See :meth:`.Operator.from_circuit` + to specify a different qubit permutation. """ op_shape = None if isinstance(data, (list, np.ndarray)): @@ -125,12 +128,9 @@ def __array__(self, dtype=None, copy=_numpy_compat.COPY_ONLY_IF_NEEDED): def __repr__(self): prefix = "Operator(" pad = len(prefix) * " " - return "{}{},\n{}input_dims={}, output_dims={})".format( - prefix, - np.array2string(self.data, separator=", ", prefix=prefix), - pad, - self.input_dims(), - self.output_dims(), + return ( + f"{prefix}{np.array2string(self.data, separator=', ', prefix=prefix)},\n" + f"{pad}input_dims={self.input_dims()}, output_dims={self.output_dims()})" ) def __eq__(self, other): @@ -391,8 +391,7 @@ def from_circuit( Returns: Operator: An operator representing the input circuit """ - dimension = 2**circuit.num_qubits - op = cls(np.eye(dimension)) + if layout is None: if not ignore_set_layout: layout = getattr(circuit, "_layout", None) @@ -403,27 +402,36 @@ def from_circuit( initial_layout=layout, input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)}, ) + + initial_layout = layout.initial_layout if layout is not None else None + if final_layout is None: if not ignore_set_layout and layout is not None: final_layout = getattr(layout, "final_layout", None) - qargs = None - # If there was a layout specified (either from the circuit - # or via user input) use that to set qargs to permute qubits - # based on that layout - if layout is not None: - physical_to_virtual = layout.initial_layout.get_physical_bits() - qargs = [ - layout.input_qubit_mapping[physical_to_virtual[physical_bit]] - for physical_bit in range(len(physical_to_virtual)) - ] - # Convert circuit to an instruction - instruction = circuit.to_instruction() - op._append_instruction(instruction, qargs=qargs) - # If final layout is set permute output indices based on layout - if final_layout is not None: - perm_pattern = [final_layout._v2p[v] for v in circuit.qubits] - op = op.apply_permutation(perm_pattern, front=False) + from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern + + op = Operator(circuit) + + if initial_layout is not None: + input_qubits = [None] * len(layout.input_qubit_mapping) + for q, p in layout.input_qubit_mapping.items(): + input_qubits[p] = q + + initial_permutation = initial_layout.to_permutation(input_qubits) + initial_permutation_inverse = _inverse_pattern(initial_permutation) + op = op.apply_permutation(initial_permutation, True) + + if final_layout is not None: + final_permutation = final_layout.to_permutation(circuit.qubits) + final_permutation_inverse = _inverse_pattern(final_permutation) + op = op.apply_permutation(final_permutation_inverse, False) + op = op.apply_permutation(initial_permutation_inverse, False) + elif final_layout is not None: + final_permutation = final_layout.to_permutation(circuit.qubits) + final_permutation_inverse = _inverse_pattern(final_permutation) + op = op.apply_permutation(final_permutation_inverse, False) + return op def is_unitary(self, atol=None, rtol=None): @@ -752,10 +760,8 @@ def _append_instruction(self, obj, qargs=None): raise QiskitError(f"Cannot apply Operation: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - 'Operation "{}" ' - "definition is {} but expected QuantumCircuit.".format( - obj.name, type(obj.definition) - ) + f'Operation "{obj.name}" ' + f"definition is {type(obj.definition)} but expected QuantumCircuit." ) if obj.definition.global_phase: dimension = 2**obj.num_qubits diff --git a/qiskit/quantum_info/operators/predicates.py b/qiskit/quantum_info/operators/predicates.py index 57b7df64f26..f432195cd57 100644 --- a/qiskit/quantum_info/operators/predicates.py +++ b/qiskit/quantum_info/operators/predicates.py @@ -22,6 +22,7 @@ def matrix_equal(mat1, mat2, ignore_phase=False, rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT, props=None): + # pylint: disable-next=consider-using-f-string """Test if two arrays are equal. The final comparison is implemented using Numpy.allclose. See its diff --git a/qiskit/quantum_info/operators/symplectic/base_pauli.py b/qiskit/quantum_info/operators/symplectic/base_pauli.py index e43eca4aff2..38e471f0b0a 100644 --- a/qiskit/quantum_info/operators/symplectic/base_pauli.py +++ b/qiskit/quantum_info/operators/symplectic/base_pauli.py @@ -215,12 +215,12 @@ def commutes(self, other: BasePauli, qargs: list | None = None) -> np.ndarray: if qargs is not None and len(qargs) != other.num_qubits: raise QiskitError( "Number of qubits of other Pauli does not match number of " - "qargs ({} != {}).".format(other.num_qubits, len(qargs)) + f"qargs ({other.num_qubits} != {len(qargs)})." ) if qargs is None and self.num_qubits != other.num_qubits: raise QiskitError( "Number of qubits of other Pauli does not match the current " - "Pauli ({} != {}).".format(other.num_qubits, self.num_qubits) + f"Pauli ({other.num_qubits} != {self.num_qubits})." ) if qargs is not None: inds = list(qargs) @@ -262,15 +262,12 @@ def evolve( # Check dimension if qargs is not None and len(qargs) != other.num_qubits: raise QiskitError( - "Incorrect number of qubits for Clifford circuit ({} != {}).".format( - other.num_qubits, len(qargs) - ) + f"Incorrect number of qubits for Clifford circuit ({other.num_qubits} != {len(qargs)})." ) if qargs is None and self.num_qubits != other.num_qubits: raise QiskitError( - "Incorrect number of qubits for Clifford circuit ({} != {}).".format( - other.num_qubits, self.num_qubits - ) + f"Incorrect number of qubits for Clifford circuit " + f"({other.num_qubits} != {self.num_qubits})." ) # Evolve via Pauli @@ -571,9 +568,8 @@ def _append_circuit(self, circuit, qargs=None): raise QiskitError(f"Cannot apply Instruction: {gate.name}") if not isinstance(gate.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - gate.name, type(gate.definition) - ) + f"{gate.name} instruction definition is {type(gate.definition)};" + f" expected QuantumCircuit" ) circuit = gate.definition diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index 9a5e8732ae6..435120dd531 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -185,7 +185,7 @@ def __init__(self, data, validate=True, copy=True): isinstance(data, (list, np.ndarray)) and (data_asarray := np.asarray(data, dtype=bool)).ndim == 2 ): - # This little dance is to avoid Numpy 1/2 incompatiblities between the availability + # This little dance is to avoid Numpy 1/2 incompatibilities between the availability # and meaning of the 'copy' argument in 'array' and 'asarray', when the input needs # its dtype converting. 'asarray' prefers to return 'self' if possible in both. if copy and np.may_share_memory(data, data_asarray): diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index e1bcfa29ebc..867867eeb98 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -144,13 +144,13 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be .. code-block:: python - p = Pauli('-iXYZ') + P = Pauli('-iXYZ') print('P[0] =', repr(P[0])) print('P[1] =', repr(P[1])) print('P[2] =', repr(P[2])) print('P[:] =', repr(P[:])) - print('P[::-1] =, repr(P[::-1])) + print('P[::-1] =', repr(P[::-1])) """ # Set the max Pauli string size before truncation @@ -344,7 +344,7 @@ def delete(self, qubits: int | list) -> Pauli: if max(qubits) > self.num_qubits - 1: raise QiskitError( "Qubit index is larger than the number of qubits " - "({}>{}).".format(max(qubits), self.num_qubits - 1) + f"({max(qubits)}>{self.num_qubits - 1})." ) if len(qubits) == self.num_qubits: raise QiskitError("Cannot delete all qubits of Pauli") @@ -379,12 +379,12 @@ def insert(self, qubits: int | list, value: Pauli) -> Pauli: if len(qubits) != value.num_qubits: raise QiskitError( "Number of indices does not match number of qubits for " - "the inserted Pauli ({}!={})".format(len(qubits), value.num_qubits) + f"the inserted Pauli ({len(qubits)}!={value.num_qubits})" ) if max(qubits) > ret.num_qubits - 1: raise QiskitError( "Index is too larger for combined Pauli number of qubits " - "({}>{}).".format(max(qubits), ret.num_qubits - 1) + f"({max(qubits)}>{ret.num_qubits - 1})." ) # Qubit positions for original op self_qubits = [i for i in range(ret.num_qubits) if i not in qubits] @@ -736,8 +736,11 @@ def apply_layout( n_qubits = num_qubits if layout is None: layout = list(range(self.num_qubits)) - elif any(x >= n_qubits for x in layout): - raise QiskitError("Provided layout contains indices outside the number of qubits.") + else: + if any(x < 0 or x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indices outside the number of qubits.") + if len(set(layout)) != len(layout): + raise QiskitError("Provided layout contains duplicate indices.") new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index 3d348d23638..2b6e5a8774c 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -382,8 +382,8 @@ def delete(self, ind: int | list, qubit: bool = False) -> PauliList: if not qubit: if max(ind) >= len(self): raise QiskitError( - "Indices {} are not all less than the size" - " of the PauliList ({})".format(ind, len(self)) + f"Indices {ind} are not all less than the size" + f" of the PauliList ({len(self)})" ) z = np.delete(self._z, ind, axis=0) x = np.delete(self._x, ind, axis=0) @@ -394,8 +394,8 @@ def delete(self, ind: int | list, qubit: bool = False) -> PauliList: # Column (qubit) deletion if max(ind) >= self.num_qubits: raise QiskitError( - "Indices {} are not all less than the number of" - " qubits in the PauliList ({})".format(ind, self.num_qubits) + f"Indices {ind} are not all less than the number of" + f" qubits in the PauliList ({self.num_qubits})" ) z = np.delete(self._z, ind, axis=1) x = np.delete(self._x, ind, axis=1) @@ -432,8 +432,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: if not qubit: if ind > size: raise QiskitError( - "Index {} is larger than the number of rows in the" - " PauliList ({}).".format(ind, size) + f"Index {ind} is larger than the number of rows in the" f" PauliList ({size})." ) base_z = np.insert(self._z, ind, value._z, axis=0) base_x = np.insert(self._x, ind, value._x, axis=0) @@ -443,8 +442,8 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: # Column insertion if ind > self.num_qubits: raise QiskitError( - "Index {} is greater than number of qubits" - " in the PauliList ({})".format(ind, self.num_qubits) + f"Index {ind} is greater than number of qubits" + f" in the PauliList ({self.num_qubits})" ) if len(value) == 1: # Pad blocks to correct size @@ -461,7 +460,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: raise QiskitError( "Input PauliList must have a single row, or" " the same number of rows as the Pauli Table" - " ({}).".format(size) + f" ({size})." ) # Build new array by blocks z = np.hstack([self.z[:, :ind], value_z, self.z[:, ind:]]) @@ -647,7 +646,7 @@ def unique(self, return_index: bool = False, return_counts: bool = False) -> Pau index = index[sort_inds] unique = PauliList(BasePauli(self._z[index], self._x[index], self._phase[index])) - # Concatinate return tuples + # Concatenate return tuples ret = (unique,) if return_index: ret += (index,) diff --git a/qiskit/quantum_info/operators/symplectic/random.py b/qiskit/quantum_info/operators/symplectic/random.py index 1a845100b91..06b23ca2980 100644 --- a/qiskit/quantum_info/operators/symplectic/random.py +++ b/qiskit/quantum_info/operators/symplectic/random.py @@ -81,7 +81,7 @@ def random_pauli_list( z = rng.integers(2, size=(size, num_qubits)).astype(bool) x = rng.integers(2, size=(size, num_qubits)).astype(bool) if phase: - _phase = rng.integers(4, size=(size)) + _phase = rng.integers(4, size=size) return PauliList.from_symplectic(z, x, _phase) return PauliList.from_symplectic(z, x) @@ -163,7 +163,7 @@ def _sample_qmallows(n, rng=None): if rng is None: rng = np.random.default_rng() - # Hadmard layer + # Hadamard layer had = np.zeros(n, dtype=bool) # Permutation layer diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index dffe5b2396b..91d8d82deca 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -54,7 +54,7 @@ class SparsePauliOp(LinearOp): :class:`~qiskit.quantum_info.Operator` in terms of N-qubit :class:`~qiskit.quantum_info.PauliList` and complex coefficients. - It can be used for performing operator arithmetic for hundred of qubits + It can be used for performing operator arithmetic for hundreds of qubits if the number of non-zero Pauli basis terms is sufficiently small. The Pauli basis components are stored as a @@ -135,19 +135,19 @@ def __init__( pauli_list = PauliList(data.copy() if copy and hasattr(data, "copy") else data) - if isinstance(coeffs, np.ndarray): - dtype = object if coeffs.dtype == object else complex - elif coeffs is not None: - if not isinstance(coeffs, (np.ndarray, Sequence)): - coeffs = [coeffs] - if any(isinstance(coeff, ParameterExpression) for coeff in coeffs): - dtype = object - else: - dtype = complex - if coeffs is None: coeffs = np.ones(pauli_list.size, dtype=complex) else: + if isinstance(coeffs, np.ndarray): + dtype = object if coeffs.dtype == object else complex + else: + if not isinstance(coeffs, Sequence): + coeffs = [coeffs] + if any(isinstance(coeff, ParameterExpression) for coeff in coeffs): + dtype = object + else: + dtype = complex + coeffs_asarray = np.asarray(coeffs, dtype=dtype) coeffs = ( coeffs_asarray.copy() @@ -172,7 +172,7 @@ def __init__( if self._coeffs.shape != (self._pauli_list.size,): raise QiskitError( "coeff vector is incorrect shape for number" - " of Paulis {} != {}".format(self._coeffs.shape, self._pauli_list.size) + f" of Paulis {self._coeffs.shape} != {self._pauli_list.size}" ) # Initialize LinearOp super().__init__(num_qubits=self._pauli_list.num_qubits) @@ -186,11 +186,9 @@ def __array__(self, dtype=None, copy=None): def __repr__(self): prefix = "SparsePauliOp(" pad = len(prefix) * " " - return "{}{},\n{}coeffs={})".format( - prefix, - self.paulis.to_labels(), - pad, - np.array2string(self.coeffs, separator=", "), + return ( + f"{prefix}{self.paulis.to_labels()},\n{pad}" + f"coeffs={np.array2string(self.coeffs, separator=', ')})" ) def __eq__(self, other): @@ -1139,7 +1137,6 @@ def apply_layout( specified will be applied without any expansion. If layout is None, the operator will be expanded to the given number of qubits. - Returns: A new :class:`.SparsePauliOp` with the provided layout applied """ @@ -1159,10 +1156,15 @@ def apply_layout( f"applied to a {n_qubits} qubit operator" ) n_qubits = num_qubits - if layout is not None and any(x >= n_qubits for x in layout): - raise QiskitError("Provided layout contains indices outside the number of qubits.") if layout is None: layout = list(range(self.num_qubits)) + else: + if any(x < 0 or x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indices outside the number of qubits.") + if len(set(layout)) != len(layout): + raise QiskitError("Provided layout contains duplicate indices.") + if self.num_qubits == 0: + return type(self)(["I" * n_qubits] * self.size, self.coeffs) new_op = type(self)("I" * n_qubits) return new_op.compose(self, qargs=layout) diff --git a/qiskit/quantum_info/quaternion.py b/qiskit/quantum_info/quaternion.py index 22508b5ceb1..69b9b61c8f9 100644 --- a/qiskit/quantum_info/quaternion.py +++ b/qiskit/quantum_info/quaternion.py @@ -43,7 +43,7 @@ def __mul__(self, r): out_data[3] = r(0) * q(3) - r(1) * q(2) + r(2) * q(1) + r(3) * q(0) return Quaternion(out_data) else: - raise Exception("Multiplication by other not supported.") + return NotImplemented def norm(self): """Norm of quaternion.""" diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 1c66d8bcf5c..7826e3f22fe 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -123,11 +123,9 @@ def __eq__(self, other): def __repr__(self): prefix = "DensityMatrix(" pad = len(prefix) * " " - return "{}{},\n{}dims={})".format( - prefix, - np.array2string(self._data, separator=", ", prefix=prefix), - pad, - self._op_shape.dims_l(), + return ( + f"{prefix}{np.array2string(self._data, separator=', ', prefix=prefix)},\n" + f"{pad}dims={self._op_shape.dims_l()})" ) @property @@ -771,9 +769,8 @@ def _append_instruction(self, other, qargs=None): raise QiskitError(f"Cannot apply Instruction: {other.name}") if not isinstance(other.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - other.name, type(other.definition) - ) + f"{other.name} instruction definition is {type(other.definition)};" + f" expected QuantumCircuit" ) qubit_indices = {bit: idx for idx, bit in enumerate(other.definition.qubits)} for instruction in other.definition: diff --git a/qiskit/quantum_info/states/stabilizerstate.py b/qiskit/quantum_info/states/stabilizerstate.py index 7f616bcff79..16abb67f223 100644 --- a/qiskit/quantum_info/states/stabilizerstate.py +++ b/qiskit/quantum_info/states/stabilizerstate.py @@ -297,7 +297,7 @@ def expectation_value(self, oper: Pauli, qargs: None | list = None) -> complex: # Otherwise pauli is (-1)^a prod_j S_j^b_j for Clifford stabilizers # If pauli anti-commutes with D_j then b_j = 1. - # Multiply pauli by stabilizers with anti-commuting destabilizers + # Multiply pauli by stabilizers with anti-commuting destabilisers pauli_z = (pauli.z).copy() # Make a copy of pauli.z for p in range(num_qubits): # Check if destabilizer anti-commutes @@ -386,8 +386,17 @@ def probabilities(self, qargs: None | list = None, decimals: None | int = None) return probs - def probabilities_dict(self, qargs: None | list = None, decimals: None | int = None) -> dict: - """Return the subsystem measurement probability dictionary. + def probabilities_dict_from_bitstring( + self, + outcome_bitstring: str, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary utilizing + a targeted outcome_bitstring to perform the measurement for. This + will calculate a probability for only a single targeted + outcome_bitstring value, giving a performance boost over calculating + all possible outcomes. Measurement probabilities are with respect to measurement in the computation (diagonal) basis. @@ -398,30 +407,44 @@ def probabilities_dict(self, qargs: None | list = None, decimals: None | int = N inserted between integers so that subsystems can be distinguished. Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) qargs (None or list): subsystems to return probabilities for, - if None return for all subsystems (Default: None). + if None return for all subsystems (Default: None). decimals (None or int): the number of decimal places to round - values. If None no rounding is done (Default: None). + values. If None no rounding is done (Default: None) Returns: - dict: The measurement probabilities in dict (ket) form. + dict[str, float]: The measurement probabilities in dict (ket) form. """ - if qargs is None: - qubits = range(self.clifford.num_qubits) - else: - qubits = qargs + return self._get_probabilities_dict( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ) - outcome = ["X"] * len(qubits) - outcome_prob = 1.0 - probs = {} # probabilities dictionary + def probabilities_dict( + self, qargs: None | list = None, decimals: None | int = None + ) -> dict[str, float]: + """Return the subsystem measurement probability dictionary. - self._get_probabilities(qubits, outcome, outcome_prob, probs) + Measurement probabilities are with respect to measurement in the + computation (diagonal) basis. - if decimals is not None: - for key, value in probs.items(): - probs[key] = round(value, decimals) + This dictionary representation uses a Ket-like notation where the + dictionary keys are qudit strings for the subsystem basis vectors. + If any subsystem has a dimension greater than 10 comma delimiters are + inserted between integers so that subsystems can be distinguished. - return probs + Args: + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + return self._get_probabilities_dict(outcome_bitstring=None, qargs=qargs, decimals=decimals) def reset(self, qargs: list | None = None) -> StabilizerState: """Reset state or subsystems to the 0-state. @@ -623,7 +646,7 @@ def _rowsum_nondeterministic(clifford, accum, row): @staticmethod def _rowsum_deterministic(clifford, aux_pauli, row): - """Updating an auxilary Pauli aux_pauli in the + """Updating an auxiliary Pauli aux_pauli in the deterministic rowsum calculation. The StabilizerState itself is not updated.""" @@ -644,22 +667,48 @@ def _rowsum_deterministic(clifford, aux_pauli, row): # ----------------------------------------------------------------------- # Helper functions for calculating the probabilities # ----------------------------------------------------------------------- - def _get_probabilities(self, qubits, outcome, outcome_prob, probs): - """Recursive helper function for calculating the probabilities""" + def _get_probabilities( + self, + qubits: range, + outcome: list[str], + outcome_prob: float, + probs: dict[str, float], + outcome_bitstring: str = None, + ): + """Recursive helper function for calculating the probabilities - qubit_for_branching = -1 - ret = self.copy() + Args: + qubits (range): range of qubits + outcome (list[str]): outcome being built + outcome_prob (float): probability of the outcome + probs (dict[str, float]): holds the outcomes and probability results + outcome_bitstring (str): target outcome to measure which reduces measurements, None + if not targeting a specific target + """ + qubit_for_branching: int = -1 + ret: StabilizerState = self.copy() + + # Find outcomes for each qubit for i in range(len(qubits)): - qubit = qubits[len(qubits) - i - 1] if outcome[i] == "X": - is_deterministic = not any(ret.clifford.stab_x[:, qubit]) - if is_deterministic: - single_qubit_outcome = ret._measure_and_update(qubit, 0) - if single_qubit_outcome: - outcome[i] = "1" + # Retrieve the qubit for the current measurement + qubit = qubits[(len(qubits) - i - 1)] + # Determine if the probability is deterministic + if not any(ret.clifford.stab_x[:, qubit]): + single_qubit_outcome: np.int64 = ret._measure_and_update(qubit, 0) + if outcome_bitstring is None or ( + int(outcome_bitstring[i]) == single_qubit_outcome + ): + # No outcome_bitstring target, or using outcome_bitstring target and + # the single_qubit_outcome equals the desired outcome_bitstring target value, + # then use current outcome_prob value + outcome[i] = str(single_qubit_outcome) else: - outcome[i] = "0" + # If the single_qubit_outcome does not equal the outcome_bitsring target + # then we know that the probability will be 0 + outcome[i] = str(outcome_bitstring[i]) + outcome_prob = 0 else: qubit_for_branching = i @@ -668,15 +717,57 @@ def _get_probabilities(self, qubits, outcome, outcome_prob, probs): probs[str_outcome] = outcome_prob return - for single_qubit_outcome in range(0, 2): + for single_qubit_outcome in ( + range(0, 2) + if (outcome_bitstring is None) + else [int(outcome_bitstring[qubit_for_branching])] + ): new_outcome = outcome.copy() - if single_qubit_outcome: - new_outcome[qubit_for_branching] = "1" - else: - new_outcome[qubit_for_branching] = "0" + new_outcome[qubit_for_branching] = str(single_qubit_outcome) stab_cpy = ret.copy() stab_cpy._measure_and_update( - qubits[len(qubits) - qubit_for_branching - 1], single_qubit_outcome + qubits[(len(qubits) - qubit_for_branching - 1)], single_qubit_outcome + ) + stab_cpy._get_probabilities( + qubits, new_outcome, (0.5 * outcome_prob), probs, outcome_bitstring ) - stab_cpy._get_probabilities(qubits, new_outcome, 0.5 * outcome_prob, probs) + + def _get_probabilities_dict( + self, + outcome_bitstring: None | str = None, + qargs: None | list = None, + decimals: None | int = None, + ) -> dict[str, float]: + """Helper Function for calculating the subsystem measurement probability dictionary. + When the targeted outcome_bitstring value is set, then only the single outcome_bitstring + probability will be calculated. + + Args: + outcome_bitstring (None or str): targeted outcome bitstring + to perform a measurement calculation for, this will significantly + reduce the number of calculation performed (Default: None) + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None). + + Returns: + dict: The measurement probabilities in dict (key) form. + """ + if qargs is None: + qubits = range(self.clifford.num_qubits) + else: + qubits = qargs + + outcome = ["X"] * len(qubits) + outcome_prob = 1.0 + probs: dict[str, float] = {} # Probabilities dict to return with the measured values + + self._get_probabilities(qubits, outcome, outcome_prob, probs, outcome_bitstring) + + if decimals is not None: + for key, value in probs.items(): + probs[key] = round(value, decimals) + + return probs diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index df39ba42f91..7fa14eaac9d 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -117,11 +117,9 @@ def __eq__(self, other): def __repr__(self): prefix = "Statevector(" pad = len(prefix) * " " - return "{}{},\n{}dims={})".format( - prefix, - np.array2string(self._data, separator=", ", prefix=prefix), - pad, - self._op_shape.dims_l(), + return ( + f"{prefix}{np.array2string(self._data, separator=', ', prefix=prefix)},\n{pad}" + f"dims={self._op_shape.dims_l()})" ) @property @@ -940,9 +938,7 @@ def _evolve_instruction(statevec, obj, qargs=None): raise QiskitError(f"Cannot apply Instruction: {obj.name}") if not isinstance(obj.definition, QuantumCircuit): raise QiskitError( - "{} instruction definition is {}; expected QuantumCircuit".format( - obj.name, type(obj.definition) - ) + f"{obj.name} instruction definition is {type(obj.definition)}; expected QuantumCircuit" ) if obj.definition.global_phase: diff --git a/qiskit/result/__init__.py b/qiskit/result/__init__.py index 08b43f70493..2eaa7803c5f 100644 --- a/qiskit/result/__init__.py +++ b/qiskit/result/__init__.py @@ -17,6 +17,9 @@ .. currentmodule:: qiskit.result +Core classes +============ + .. autosummary:: :toctree: ../stubs/ @@ -24,6 +27,9 @@ ResultError Counts +Marginalization +=============== + .. autofunction:: marginal_counts .. autofunction:: marginal_distribution .. autofunction:: marginal_memory diff --git a/qiskit/result/counts.py b/qiskit/result/counts.py index 8b90ff0f042..b34aa2373fb 100644 --- a/qiskit/result/counts.py +++ b/qiskit/result/counts.py @@ -101,7 +101,7 @@ def __init__(self, data, time_taken=None, creg_sizes=None, memory_slots=None): else: raise TypeError( "Invalid input key type %s, must be either an int " - "key or string key with hexademical value or bit string" + "key or string key with hexadecimal value or bit string" ) header = {} self.creg_sizes = creg_sizes @@ -130,7 +130,7 @@ def most_frequent(self): max_values_counts = [x[0] for x in self.items() if x[1] == max_value] if len(max_values_counts) != 1: raise exceptions.QiskitError( - "Multiple values have the same maximum counts: %s" % ",".join(max_values_counts) + f"Multiple values have the same maximum counts: {','.join(max_values_counts)}" ) return max_values_counts[0] diff --git a/qiskit/result/mitigation/correlated_readout_mitigator.py b/qiskit/result/mitigation/correlated_readout_mitigator.py index 06cc89b4c52..99e6f9ae414 100644 --- a/qiskit/result/mitigation/correlated_readout_mitigator.py +++ b/qiskit/result/mitigation/correlated_readout_mitigator.py @@ -54,8 +54,8 @@ def __init__(self, assignment_matrix: np.ndarray, qubits: Optional[Iterable[int] else: if len(qubits) != matrix_qubits_num: raise QiskitError( - "The number of given qubits ({}) is different than the number of " - "qubits inferred from the matrices ({})".format(len(qubits), matrix_qubits_num) + f"The number of given qubits ({len(qubits)}) is different than the number of " + f"qubits inferred from the matrices ({matrix_qubits_num})" ) self._qubits = qubits self._num_qubits = len(self._qubits) diff --git a/qiskit/result/mitigation/local_readout_mitigator.py b/qiskit/result/mitigation/local_readout_mitigator.py index ad71911c2d7..197c3f00d9b 100644 --- a/qiskit/result/mitigation/local_readout_mitigator.py +++ b/qiskit/result/mitigation/local_readout_mitigator.py @@ -68,8 +68,8 @@ def __init__( else: if len(qubits) != len(assignment_matrices): raise QiskitError( - "The number of given qubits ({}) is different than the number of qubits " - "inferred from the matrices ({})".format(len(qubits), len(assignment_matrices)) + f"The number of given qubits ({len(qubits)}) is different than the number of qubits " + f"inferred from the matrices ({len(assignment_matrices)})" ) self._qubits = qubits self._num_qubits = len(self._qubits) diff --git a/qiskit/result/mitigation/utils.py b/qiskit/result/mitigation/utils.py index 26b5ee37348..823e2b69a6c 100644 --- a/qiskit/result/mitigation/utils.py +++ b/qiskit/result/mitigation/utils.py @@ -120,9 +120,7 @@ def marganalize_counts( clbits_len = len(clbits) if not clbits is None else 0 if clbits_len not in (0, qubits_len): raise QiskitError( - "Num qubits ({}) does not match number of clbits ({}).".format( - qubits_len, clbits_len - ) + f"Num qubits ({qubits_len}) does not match number of clbits ({clbits_len})." ) counts = marginal_counts(counts, clbits) if clbits is None and qubits is not None: diff --git a/qiskit/result/models.py b/qiskit/result/models.py index 07286148f88..83d9e4e78d5 100644 --- a/qiskit/result/models.py +++ b/qiskit/result/models.py @@ -66,8 +66,7 @@ def __repr__(self): string_list = [] for field in self._data_attributes: string_list.append(f"{field}={getattr(self, field)}") - out = "ExperimentResultData(%s)" % ", ".join(string_list) - return out + return f"ExperimentResultData({', '.join(string_list)})" def to_dict(self): """Return a dictionary format representation of the ExperimentResultData @@ -157,25 +156,23 @@ def __init__( self._metadata.update(kwargs) def __repr__(self): - out = "ExperimentResult(shots={}, success={}, meas_level={}, data={}".format( - self.shots, - self.success, - self.meas_level, - self.data, + out = ( + f"ExperimentResult(shots={self.shots}, success={self.success}," + f" meas_level={self.meas_level}, data={self.data}" ) if hasattr(self, "header"): - out += ", header=%s" % self.header + out += f", header={self.header}" if hasattr(self, "status"): - out += ", status=%s" % self.status + out += f", status={self.status}" if hasattr(self, "seed"): - out += ", seed=%s" % self.seed + out += f", seed={self.seed}" if hasattr(self, "meas_return"): - out += ", meas_return=%s" % self.meas_return - for key in self._metadata: - if isinstance(self._metadata[key], str): - value_str = "'%s'" % self._metadata[key] + out += f", meas_return={self.meas_return}" + for key, value in self._metadata.items(): + if isinstance(value, str): + value_str = f"'{value}'" else: - value_str = repr(self._metadata[key]) + value_str = repr(value) out += f", {key}={value_str}" out += ")" return out diff --git a/qiskit/result/result.py b/qiskit/result/result.py index d99be996080..7df36578516 100644 --- a/qiskit/result/result.py +++ b/qiskit/result/result.py @@ -69,23 +69,16 @@ def __init__( def __repr__(self): out = ( - "Result(backend_name='%s', backend_version='%s', qobj_id='%s', " - "job_id='%s', success=%s, results=%s" - % ( - self.backend_name, - self.backend_version, - self.qobj_id, - self.job_id, - self.success, - self.results, - ) + f"Result(backend_name='{self.backend_name}', backend_version='{self.backend_version}'," + f" qobj_id='{self.qobj_id}', job_id='{self.job_id}', success={self.success}," + f" results={self.results}" ) out += f", date={self.date}, status={self.status}, header={self.header}" - for key in self._metadata: - if isinstance(self._metadata[key], str): - value_str = "'%s'" % self._metadata[key] + for key, value in self._metadata.items(): + if isinstance(value, str): + value_str = f"'{value}'" else: - value_str = repr(self._metadata[key]) + value_str = repr(value) out += f", {key}={value_str}" out += ")" return out @@ -236,10 +229,10 @@ def get_memory(self, experiment=None): except KeyError as ex: raise QiskitError( - 'No memory for experiment "{}". ' + f'No memory for experiment "{repr(experiment)}". ' "Please verify that you either ran a measurement level 2 job " 'with the memory flag set, eg., "memory=True", ' - "or a measurement level 0/1 job.".format(repr(experiment)) + "or a measurement level 0/1 job." ) from ex def get_counts(self, experiment=None): @@ -377,14 +370,14 @@ def _get_experiment(self, key=None): ] if len(exp) == 0: - raise QiskitError('Data for experiment "%s" could not be found.' % key) + raise QiskitError(f'Data for experiment "{key}" could not be found.') if len(exp) == 1: exp = exp[0] else: warnings.warn( - 'Result object contained multiple results matching name "%s", ' + f'Result object contained multiple results matching name "{key}", ' "only first match will be returned. Use an integer index to " - "retrieve results for all entries." % key + "retrieve results for all entries." ) exp = exp[0] diff --git a/qiskit/scheduler/__init__.py b/qiskit/scheduler/__init__.py index b33ececf5d6..7062e01a941 100644 --- a/qiskit/scheduler/__init__.py +++ b/qiskit/scheduler/__init__.py @@ -19,13 +19,22 @@ A circuit scheduler compiles a circuit program to a pulse program. +Core API +======== + .. autoclass:: ScheduleConfig .. currentmodule:: qiskit.scheduler.schedule_circuit .. autofunction:: schedule_circuit .. currentmodule:: qiskit.scheduler -.. automodule:: qiskit.scheduler.methods +Pulse scheduling methods +======================== + +.. currentmodule:: qiskit.scheduler.methods +.. autofunction:: as_soon_as_possible +.. autofunction:: as_late_as_possible +.. currentmodule:: qiskit.scheduler """ from qiskit.scheduler import schedule_circuit from qiskit.scheduler.config import ScheduleConfig diff --git a/qiskit/scheduler/lowering.py b/qiskit/scheduler/lowering.py index fa622b205d3..f0fb33957d9 100644 --- a/qiskit/scheduler/lowering.py +++ b/qiskit/scheduler/lowering.py @@ -145,9 +145,9 @@ def get_measure_schedule(qubit_mem_slots: Dict[int, int]) -> CircuitPulseDef: elif isinstance(instruction.operation, Measure): if len(inst_qubits) != 1 and len(instruction.clbits) != 1: raise QiskitError( - "Qubit '{}' or classical bit '{}' errored because the " + f"Qubit '{inst_qubits}' or classical bit '{instruction.clbits}' errored because the " "circuit Measure instruction only takes one of " - "each.".format(inst_qubits, instruction.clbits) + "each." ) qubit_mem_slots[inst_qubits[0]] = clbit_indices[instruction.clbits[0]] else: diff --git a/qiskit/scheduler/methods/__init__.py b/qiskit/scheduler/methods/__init__.py index 1fe4b301b7a..6df887d5499 100644 --- a/qiskit/scheduler/methods/__init__.py +++ b/qiskit/scheduler/methods/__init__.py @@ -10,13 +10,6 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -.. currentmodule:: qiskit.scheduler.methods - -Pulse scheduling methods. - -.. autofunction:: as_soon_as_possible -.. autofunction:: as_late_as_possible -""" +"""Scheduling methods.""" from qiskit.scheduler.methods.basic import as_soon_as_possible, as_late_as_possible diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index c191eac9747..b46c8eac545 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -51,6 +51,7 @@ .. autofunction:: synth_permutation_depth_lnn_kms .. autofunction:: synth_permutation_basic .. autofunction:: synth_permutation_acg +.. autofunction:: synth_permutation_reverse_lnn_kms Clifford Synthesis ================== @@ -98,12 +99,7 @@ .. autofunction:: qs_decomposition -The Approximate Quantum Compiler is available here: - -.. autosummary:: - :toctree: ../stubs/ - - qiskit.synthesis.unitary.aqc +The Approximate Quantum Compiler is available as the module :mod:`qiskit.synthesis.unitary.aqc`. One-Qubit Synthesis =================== @@ -140,6 +136,7 @@ synth_permutation_depth_lnn_kms, synth_permutation_basic, synth_permutation_acg, + synth_permutation_reverse_lnn_kms, ) from .linear import ( synth_cnot_count_full_pmh, diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index cbc54f16bb0..4800890a90d 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -76,11 +76,11 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: pos = [qubit, qubit + num_qubits] circ = _decompose_clifford_1q(clifford.tableau[pos][:, pos + [-1]]) if len(circ) > 0: - ret_circ.append(circ, [qubit]) + ret_circ.append(circ, [qubit], copy=False) # Add the inverse of the 2-qubit reductions circuit if len(inv_circuit) > 0: - ret_circ.append(inv_circuit.inverse(), range(num_qubits)) + ret_circ.append(inv_circuit.inverse(), range(num_qubits), copy=False) return ret_circ.decompose() @@ -192,7 +192,7 @@ def _cx_cost(clifford): return _cx_cost2(clifford) if clifford.num_qubits == 3: return _cx_cost3(clifford) - raise Exception("No Clifford CX cost function for num_qubits > 3.") + raise RuntimeError("No Clifford CX cost function for num_qubits > 3.") def _rank2(a, b, c, d): diff --git a/qiskit/synthesis/clifford/clifford_decompose_layers.py b/qiskit/synthesis/clifford/clifford_decompose_layers.py index f1a7c5cce13..8b745823dc1 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_layers.py +++ b/qiskit/synthesis/clifford/clifford_decompose_layers.py @@ -33,9 +33,10 @@ from qiskit.synthesis.linear_phase import synth_cz_depth_line_mr, synth_cx_cz_depth_line_my from qiskit.synthesis.linear.linear_matrix_utils import ( calc_inverse_matrix, - _compute_rank, - _gauss_elimination, - _gauss_elimination_with_perm, + compute_rank, + gauss_elimination, + gauss_elimination_with_perm, + binary_matmul, ) @@ -137,32 +138,32 @@ def synth_clifford_layers( cz_func_reverse_qubits=cz_func_reverse_qubits, ) - layeredCircuit.append(S2_circ, qubit_list) + layeredCircuit.append(S2_circ, qubit_list, copy=False) if cx_cz_synth_func is None: - layeredCircuit.append(CZ2_circ, qubit_list) + layeredCircuit.append(CZ2_circ, qubit_list, copy=False) CXinv = CX_circ.copy().inverse() - layeredCircuit.append(CXinv, qubit_list) + layeredCircuit.append(CXinv, qubit_list, copy=False) else: # note that CZ2_circ is None and built into the CX_circ when # cx_cz_synth_func is not None - layeredCircuit.append(CX_circ, qubit_list) + layeredCircuit.append(CX_circ, qubit_list, copy=False) - layeredCircuit.append(H2_circ, qubit_list) - layeredCircuit.append(S1_circ, qubit_list) - layeredCircuit.append(CZ1_circ, qubit_list) + layeredCircuit.append(H2_circ, qubit_list, copy=False) + layeredCircuit.append(S1_circ, qubit_list, copy=False) + layeredCircuit.append(CZ1_circ, qubit_list, copy=False) if cz_func_reverse_qubits: H1_circ = H1_circ.reverse_bits() - layeredCircuit.append(H1_circ, qubit_list) + layeredCircuit.append(H1_circ, qubit_list, copy=False) # Add Pauli layer to fix the Clifford phase signs clifford_target = Clifford(layeredCircuit) pauli_circ = _calc_pauli_diff(cliff, clifford_target) - layeredCircuit.append(pauli_circ, qubit_list) + layeredCircuit.append(pauli_circ, qubit_list, copy=False) return layeredCircuit @@ -203,24 +204,25 @@ def _create_graph_state(cliff, validate=False): """ num_qubits = cliff.num_qubits - rank = _compute_rank(cliff.stab_x) + rank = compute_rank(np.asarray(cliff.stab_x, dtype=bool)) H1_circ = QuantumCircuit(num_qubits, name="H1") cliffh = cliff.copy() if rank < num_qubits: stab = cliff.stab[:, :-1] - stab = _gauss_elimination(stab, num_qubits) + stab = stab.astype(bool, copy=True) + gauss_elimination(stab, num_qubits) Cmat = stab[rank:num_qubits, num_qubits:] Cmat = np.transpose(Cmat) - Cmat, perm = _gauss_elimination_with_perm(Cmat) + perm = gauss_elimination_with_perm(Cmat) perm = perm[0 : num_qubits - rank] # validate that the output matrix has the same rank if validate: - if _compute_rank(Cmat) != num_qubits - rank: + if compute_rank(Cmat) != num_qubits - rank: raise QiskitError("The matrix Cmat after Gauss elimination has wrong rank.") - if _compute_rank(stab[:, 0:num_qubits]) != rank: + if compute_rank(stab[:, 0:num_qubits]) != rank: raise QiskitError("The matrix after Gauss elimination has wrong rank.") # validate that we have a num_qubits - rank zero rows for i in range(rank, num_qubits): @@ -236,8 +238,8 @@ def _create_graph_state(cliff, validate=False): # validate that a layer of Hadamard gates and then appending cliff, provides a graph state. if validate: - stabh = cliffh.stab_x - if _compute_rank(stabh) != num_qubits: + stabh = (cliffh.stab_x).astype(bool, copy=False) + if compute_rank(stabh) != num_qubits: raise QiskitError("The state is not a graph state.") return H1_circ, cliffh @@ -267,7 +269,7 @@ def _decompose_graph_state(cliff, validate, cz_synth_func): """ num_qubits = cliff.num_qubits - rank = _compute_rank(cliff.stab_x) + rank = compute_rank(np.asarray(cliff.stab_x, dtype=bool)) cliff_cpy = cliff.copy() if rank < num_qubits: raise QiskitError("The stabilizer state is not a graph state.") @@ -278,7 +280,7 @@ def _decompose_graph_state(cliff, validate, cz_synth_func): stabx = cliff.stab_x stabz = cliff.stab_z stabx_inv = calc_inverse_matrix(stabx, validate) - stabz_update = np.matmul(stabx_inv, stabz) % 2 + stabz_update = binary_matmul(stabx_inv, stabz) # Assert that stabz_update is a symmetric matrix. if validate: @@ -340,7 +342,7 @@ def _decompose_hadamard_free( if not (stabx == np.zeros((num_qubits, num_qubits))).all(): raise QiskitError("The given Clifford is not Hadamard-free.") - destabz_update = np.matmul(calc_inverse_matrix(destabx), destabz) % 2 + destabz_update = binary_matmul(calc_inverse_matrix(destabx), destabz) # Assert that destabz_update is a symmetric matrix. if validate: if (destabz_update != destabz_update.T).any(): @@ -412,7 +414,7 @@ def _calc_pauli_diff(cliff, cliff_target): def synth_clifford_depth_lnn(cliff): - """Synthesis of a :class:`.Clifford` into layers for linear-nearest neighbour connectivity. + """Synthesis of a :class:`.Clifford` into layers for linear-nearest neighbor connectivity. The depth of the synthesized n-qubit circuit is bounded by :math:`7n+2`, which is not optimal. It should be replaced by a better algorithm that provides depth bounded by :math:`7n-4` [3]. diff --git a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py index 8131458b2d3..ae56f9926da 100644 --- a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py +++ b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_full.py @@ -40,7 +40,7 @@ def synth_cnotdihedral_full(elem: CNOTDihedral) -> QuantumCircuit: with optimal number of two qubit gates*, `Quantum 4(369), 2020 `_ 2. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ diff --git a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py index 83c63026a21..bedc5c735f0 100644 --- a/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py +++ b/qiskit/synthesis/cnotdihedral/cnotdihedral_decompose_general.py @@ -38,7 +38,7 @@ def synth_cnotdihedral_general(elem: CNOTDihedral) -> QuantumCircuit: References: 1. Andrew W. Cross, Easwar Magesan, Lev S. Bishop, John A. Smolin and Jay M. Gambetta, - *Scalable randomised benchmarking of non-Clifford gates*, + *Scalable randomized benchmarking of non-Clifford gates*, npj Quantum Inf 2, 16012 (2016). """ diff --git a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py index 07139b223b1..da9708c2455 100644 --- a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py +++ b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py @@ -137,7 +137,7 @@ def generate_basic_approximations( basis = [] for gate in basis_gates: if isinstance(gate, str): - if gate not in _1q_gates.keys(): + if gate not in _1q_gates: raise ValueError(f"Invalid gate identifier: {gate}") basis.append(gate) else: # gate is a qiskit.circuit.Gate @@ -156,7 +156,7 @@ def generate_basic_approximations( data = {} for sequence in sequences: gatestring = sequence.name - data[gatestring] = sequence.product + data[gatestring] = (sequence.product, sequence.global_phase) np.save(filename, data) diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 62ad50582d4..f367f6c0f0b 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -16,8 +16,6 @@ import numpy as np -from qiskit.circuit.gate import Gate - from .gate_sequence import GateSequence from .commutator_decompose import commutator_decompose from .generate_basis_approximations import generate_basic_approximations, _1q_gates, _1q_inverses @@ -53,14 +51,19 @@ def __init__( self.basic_approximations = self.load_basic_approximations(basic_approximations) - def load_basic_approximations(self, data: list | str | dict) -> list[GateSequence]: + @staticmethod + def load_basic_approximations(data: list | str | dict) -> list[GateSequence]: """Load basic approximations. Args: data: If a string, specifies the path to the file from where to load the data. - If a dictionary, directly specifies the decompositions as ``{gates: matrix}``. - There ``gates`` are the names of the gates producing the SO(3) matrix ``matrix``, - e.g. ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}``. + If a dictionary, directly specifies the decompositions as ``{gates: matrix}`` + or ``{gates: (matrix, global_phase)}``. There, ``gates`` are the names of the gates + producing the SO(3) matrix ``matrix``, e.g. + ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}`` + and the ``global_phase`` can be given to account for a global phase difference + between the U(2) matrix of the quantum gates and the stored SO(3) matrix. + If not given, the ``global_phase`` will be assumed to be 0. Returns: A list of basic approximations as type ``GateSequence``. @@ -74,13 +77,20 @@ def load_basic_approximations(self, data: list | str | dict) -> list[GateSequenc # if a file, load the dictionary if isinstance(data, str): - data = np.load(data, allow_pickle=True) + data = np.load(data, allow_pickle=True).item() sequences = [] - for gatestring, matrix in data.items(): + for gatestring, matrix_and_phase in data.items(): + if isinstance(matrix_and_phase, tuple): + matrix, global_phase = matrix_and_phase + else: + matrix, global_phase = matrix_and_phase, 0 + sequence = GateSequence() sequence.gates = [_1q_gates[element] for element in gatestring.split()] + sequence.labels = [gate.name for gate in sequence.gates] sequence.product = np.asarray(matrix) + sequence.global_phase = global_phase sequences.append(sequence) return sequences @@ -109,7 +119,7 @@ def run( gate_matrix_su2 = GateSequence.from_matrix(z * gate_matrix) global_phase = np.arctan2(np.imag(z), np.real(z)) - # get the decompositon as GateSequence type + # get the decomposition as GateSequence type decomposition = self._recurse(gate_matrix_su2, recursion_degree, check_input=check_input) # simplify @@ -157,14 +167,14 @@ def _recurse(self, sequence: GateSequence, n: int, check_input: bool = True) -> w_n1 = self._recurse(w_n, n - 1, check_input=check_input) return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1) - def find_basic_approximation(self, sequence: GateSequence) -> Gate: - """Finds gate in ``self._basic_approximations`` that best represents ``sequence``. + def find_basic_approximation(self, sequence: GateSequence) -> GateSequence: + """Find ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. Args: - sequence: The gate to find the approximation to. + sequence: ``GateSequence`` to find the approximation to. Returns: - Gate in basic approximations that is closest to ``sequence``. + ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. """ # TODO explore using a k-d tree here @@ -180,7 +190,7 @@ def _remove_inverse_follows_gate(sequence): while index < len(sequence.gates) - 1: curr_gate = sequence.gates[index] next_gate = sequence.gates[index + 1] - if curr_gate.name in _1q_inverses.keys(): + if curr_gate.name in _1q_inverses: remove = _1q_inverses[curr_gate.name] == next_gate.name else: remove = curr_gate.inverse() == next_gate diff --git a/qiskit/synthesis/linear/__init__.py b/qiskit/synthesis/linear/__init__.py index 115fc557bfa..f3537de9c3f 100644 --- a/qiskit/synthesis/linear/__init__.py +++ b/qiskit/synthesis/linear/__init__.py @@ -18,6 +18,7 @@ random_invertible_binary_matrix, calc_inverse_matrix, check_invertible_binary_matrix, + binary_matmul, ) # This is re-import is kept for compatibility with Terra 0.23. Eligible for deprecation in 0.25+. diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py index 5063577ed65..699523a7e75 100644 --- a/qiskit/synthesis/linear/cnot_synth.py +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -53,8 +53,7 @@ def synth_cnot_count_full_pmh( """ if not isinstance(state, (list, np.ndarray)): raise QiskitError( - "state should be of type list or numpy.ndarray, " - "but was of the type {}".format(type(state)) + f"state should be of type list or numpy.ndarray, but was of the type {type(state)}" ) state = np.array(state) # Synthesize lower triangular part diff --git a/qiskit/synthesis/linear/linear_depth_lnn.py b/qiskit/synthesis/linear/linear_depth_lnn.py index 2d544f37ef9..7c7360915e0 100644 --- a/qiskit/synthesis/linear/linear_depth_lnn.py +++ b/qiskit/synthesis/linear/linear_depth_lnn.py @@ -28,15 +28,15 @@ from qiskit.synthesis.linear.linear_matrix_utils import ( calc_inverse_matrix, check_invertible_binary_matrix, - _col_op, - _row_op, + col_op, + row_op, ) def _row_op_update_instructions(cx_instructions, mat, a, b): # Add a cx gate to the instructions and update the matrix mat cx_instructions.append((a, b)) - _row_op(mat, a, b) + row_op(mat, a, b) def _get_lower_triangular(n, mat, mat_inv): @@ -62,7 +62,7 @@ def _get_lower_triangular(n, mat, mat_inv): first_j = j else: # cx_instructions_cols (L instructions) are not needed - _col_op(mat, j, first_j) + col_op(mat, j, first_j) # Use row operations directed upwards to zero out all "1"s above the remaining "1" in row i for k in reversed(range(0, i)): if mat[k, first_j]: @@ -70,8 +70,8 @@ def _get_lower_triangular(n, mat, mat_inv): # Apply only U instructions to get the permuted L for inst in cx_instructions_rows: - _row_op(mat_t, inst[0], inst[1]) - _col_op(mat_inv_t, inst[0], inst[1]) + row_op(mat_t, inst[0], inst[1]) + col_op(mat_inv_t, inst[0], inst[1]) return mat_t, mat_inv_t @@ -210,7 +210,7 @@ def _north_west_to_identity(n, mat): def _optimize_cx_circ_depth_5n_line(mat): # Optimize CX circuit in depth bounded by 5n for LNN connectivity. # The algorithm [1] has two steps: - # a) transform the originl matrix to a north-west matrix (m2nw), + # a) transform the original matrix to a north-west matrix (m2nw), # b) transform the north-west matrix to identity (nw2id). # # A square n-by-n matrix A is called north-west if A[i][j]=0 for all i+j>=n @@ -222,7 +222,7 @@ def _optimize_cx_circ_depth_5n_line(mat): # According to [1] the synthesis is done on the inverse matrix # so the matrix mat is inverted at this step - mat_inv = mat.copy() + mat_inv = mat.astype(bool, copy=True) mat_cpy = calc_inverse_matrix(mat_inv) n = len(mat_cpy) diff --git a/qiskit/synthesis/linear/linear_matrix_utils.py b/qiskit/synthesis/linear/linear_matrix_utils.py index 7a5b6064147..a76efdbb8d7 100644 --- a/qiskit/synthesis/linear/linear_matrix_utils.py +++ b/qiskit/synthesis/linear/linear_matrix_utils.py @@ -12,164 +12,16 @@ """Utility functions for handling binary matrices.""" -from typing import Optional, Union -import numpy as np -from qiskit.exceptions import QiskitError - - -def check_invertible_binary_matrix(mat: np.ndarray): - """Check that a binary matrix is invertible. - - Args: - mat: a binary matrix. - - Returns: - bool: True if mat in invertible and False otherwise. - """ - if len(mat.shape) != 2 or mat.shape[0] != mat.shape[1]: - return False - - rank = _compute_rank(mat) - return rank == mat.shape[0] - - -def random_invertible_binary_matrix( - num_qubits: int, seed: Optional[Union[np.random.Generator, int]] = None -): - """Generates a random invertible n x n binary matrix. - - Args: - num_qubits: the matrix size. - seed: a random seed. - - Returns: - np.ndarray: A random invertible binary matrix of size num_qubits. - """ - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) - - rank = 0 - while rank != num_qubits: - mat = rng.integers(2, size=(num_qubits, num_qubits)) - rank = _compute_rank(mat) - return mat - - -def _gauss_elimination(mat, ncols=None, full_elim=False): - """Gauss elimination of a matrix mat with m rows and n columns. - If full_elim = True, it allows full elimination of mat[:, 0 : ncols] - Returns the matrix mat.""" - - mat, _ = _gauss_elimination_with_perm(mat, ncols, full_elim) - return mat - - -def _gauss_elimination_with_perm(mat, ncols=None, full_elim=False): - """Gauss elimination of a matrix mat with m rows and n columns. - If full_elim = True, it allows full elimination of mat[:, 0 : ncols] - Returns the matrix mat, and the permutation perm that was done on the rows during the process. - perm[0 : rank] represents the indices of linearly independent rows in the original matrix.""" - - # Treat the matrix A as containing integer values - mat = np.array(mat, dtype=int, copy=True) - - m = mat.shape[0] # no. of rows - n = mat.shape[1] # no. of columns - if ncols is not None: - n = min(n, ncols) # no. of active columns - - perm = np.array(range(m)) # permutation on the rows - - r = 0 # current rank - k = 0 # current pivot column - while (r < m) and (k < n): - is_non_zero = False - new_r = r - for j in range(k, n): - for i in range(r, m): - if mat[i][j]: - is_non_zero = True - k = j - new_r = i - break - if is_non_zero: - break - if not is_non_zero: - return mat, perm # A is in the canonical form - - if new_r != r: - mat[[r, new_r]] = mat[[new_r, r]] - perm[r], perm[new_r] = perm[new_r], perm[r] - - if full_elim: - for i in range(0, r): - if mat[i][k]: - mat[i] = mat[i] ^ mat[r] - - for i in range(r + 1, m): - if mat[i][k]: - mat[i] = mat[i] ^ mat[r] - r += 1 - - return mat, perm - - -def calc_inverse_matrix(mat: np.ndarray, verify: bool = False): - """Given a square numpy(dtype=int) matrix mat, tries to compute its inverse. - - Args: - mat: a boolean square matrix. - verify: if True asserts that the multiplication of mat and its inverse is the identity matrix. - - Returns: - np.ndarray: the inverse matrix. - - Raises: - QiskitError: if the matrix is not square. - QiskitError: if the matrix is not invertible. - """ - - if mat.shape[0] != mat.shape[1]: - raise QiskitError("Matrix to invert is a non-square matrix.") - - n = mat.shape[0] - # concatenate the matrix and identity - mat1 = np.concatenate((mat, np.eye(n, dtype=int)), axis=1) - mat1 = _gauss_elimination(mat1, None, full_elim=True) - - r = _compute_rank_after_gauss_elim(mat1[:, 0:n]) - - if r < n: - raise QiskitError("The matrix is not invertible.") - - matinv = mat1[:, n : 2 * n] - - if verify: - mat2 = np.dot(mat, matinv) % 2 - assert np.array_equal(mat2, np.eye(n)) - - return matinv - - -def _compute_rank_after_gauss_elim(mat): - """Given a matrix A after Gaussian elimination, computes its rank - (i.e. simply the number of nonzero rows)""" - return np.sum(mat.any(axis=1)) - - -def _compute_rank(mat): - """Given a matrix A computes its rank""" - mat = _gauss_elimination(mat) - return np.sum(mat.any(axis=1)) - - -def _row_op(mat, ctrl, trgt): - # Perform ROW operation on a matrix mat - mat[trgt] = mat[trgt] ^ mat[ctrl] - - -def _col_op(mat, ctrl, trgt): - # Perform COL operation on a matrix mat - mat[:, ctrl] = mat[:, trgt] ^ mat[:, ctrl] +# pylint: disable=unused-import +from qiskit._accelerate.synthesis.linear import ( + gauss_elimination, + gauss_elimination_with_perm, + compute_rank_after_gauss_elim, + compute_rank, + calc_inverse_matrix, + binary_matmul, + random_invertible_binary_matrix, + check_invertible_binary_matrix, + row_op, + col_op, +) diff --git a/qiskit/synthesis/linear_phase/cnot_phase_synth.py b/qiskit/synthesis/linear_phase/cnot_phase_synth.py index b107241310f..25320029ef5 100644 --- a/qiskit/synthesis/linear_phase/cnot_phase_synth.py +++ b/qiskit/synthesis/linear_phase/cnot_phase_synth.py @@ -123,7 +123,7 @@ def synth_cnot_phase_aam( # Implementation of the pseudo-code (Algorithm 1) in the aforementioned paper sta.append([cnots, range_list, epsilon]) - while sta != []: + while sta: [cnots, ilist, qubit] = sta.pop() if cnots == []: continue diff --git a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py index 23f24e07eab..c0956ea3bc7 100644 --- a/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cx_cz_depth_lnn.py @@ -39,7 +39,7 @@ def _initialize_phase_schedule(mat_z): """ Given a CZ layer (represented as an n*n CZ matrix Mz) - Return a scheudle of phase gates implementing Mz in a SWAP-only netwrok + Return a schedule of phase gates implementing Mz in a SWAP-only netwrok (c.f. Alg 1, [2]) """ n = len(mat_z) @@ -173,7 +173,7 @@ def _apply_phase_to_nw_circuit(n, phase_schedule, seq, swap_plus): of exactly n layers of boxes, each being either a SWAP or a SWAP+. That is, each northwest diagonalization circuit can be uniquely represented by which of its n(n-1)/2 boxes are SWAP+ and which are SWAP. - Return a QuantumCircuit that computes the phase scheudle S inside CX + Return a QuantumCircuit that computes the phase schedule S inside CX """ cir = QuantumCircuit(n) @@ -217,7 +217,7 @@ def _apply_phase_to_nw_circuit(n, phase_schedule, seq, swap_plus): def synth_cx_cz_depth_line_my(mat_x: np.ndarray, mat_z: np.ndarray) -> QuantumCircuit: """ - Joint synthesis of a -CZ-CX- circuit for linear nearest neighbour (LNN) connectivity, + Joint synthesis of a -CZ-CX- circuit for linear nearest neighbor (LNN) connectivity, with 2-qubit depth at most 5n, based on Maslov and Yang. This method computes the CZ circuit inside the CX circuit via phase gate insertions. diff --git a/qiskit/synthesis/linear_phase/cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cz_depth_lnn.py index b3931d07817..7a195f0caf9 100644 --- a/qiskit/synthesis/linear_phase/cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cz_depth_lnn.py @@ -24,24 +24,10 @@ import numpy as np from qiskit.circuit import QuantumCircuit - - -def _append_cx_stage1(qc, n): - """A single layer of CX gates.""" - for i in range(n // 2): - qc.cx(2 * i, 2 * i + 1) - for i in range((n + 1) // 2 - 1): - qc.cx(2 * i + 2, 2 * i + 1) - return qc - - -def _append_cx_stage2(qc, n): - """A single layer of CX gates.""" - for i in range(n // 2): - qc.cx(2 * i + 1, 2 * i) - for i in range((n + 1) // 2 - 1): - qc.cx(2 * i + 1, 2 * i + 2) - return qc +from qiskit.synthesis.permutation.permutation_reverse_lnn import ( + _append_cx_stage1, + _append_cx_stage2, +) def _odd_pattern1(n): @@ -133,7 +119,7 @@ def _create_patterns(n): def synth_cz_depth_line_mr(mat: np.ndarray) -> QuantumCircuit: - r"""Synthesis of a CZ circuit for linear nearest neighbour (LNN) connectivity, + r"""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, diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index 5ca44d43d5b..c84db761b7f 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -14,6 +14,7 @@ Decompose a single-qubit unitary via Euler angles. """ from __future__ import annotations +from typing import TYPE_CHECKING import numpy as np from qiskit._accelerate import euler_one_qubit_decomposer @@ -37,6 +38,9 @@ from qiskit.circuit.gate import Gate from qiskit.quantum_info.operators.operator import Operator +if TYPE_CHECKING: + from qiskit.dagcircuit import DAGCircuit + DEFAULT_ATOL = 1e-12 ONE_QUBIT_EULER_BASIS_GATES = { @@ -150,7 +154,7 @@ def __init__(self, basis: str = "U3", use_dag: bool = False): self.basis = basis # sets: self._basis, self._params, self._circuit self.use_dag = use_dag - def build_circuit(self, gates, global_phase): + def build_circuit(self, gates, global_phase) -> QuantumCircuit | DAGCircuit: """Return the circuit or dag object from a list of gates.""" qr = [Qubit()] lookup_gate = False @@ -186,7 +190,7 @@ def __call__( unitary: Operator | Gate | np.ndarray, simplify: bool = True, atol: float = DEFAULT_ATOL, - ) -> QuantumCircuit: + ) -> QuantumCircuit | DAGCircuit: """Decompose single qubit gate into a circuit. Args: diff --git a/qiskit/synthesis/permutation/__init__.py b/qiskit/synthesis/permutation/__init__.py index 7cc8d0174d7..5a8b9a7a13f 100644 --- a/qiskit/synthesis/permutation/__init__.py +++ b/qiskit/synthesis/permutation/__init__.py @@ -15,3 +15,4 @@ from .permutation_lnn import synth_permutation_depth_lnn_kms from .permutation_full import synth_permutation_basic, synth_permutation_acg +from .permutation_reverse_lnn import synth_permutation_reverse_lnn_kms diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index ff014cb3a05..c280065c2a5 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -16,8 +16,8 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit._accelerate.synthesis.permutation import _synth_permutation_basic from .permutation_utils import ( - _get_ordered_swap, _inverse_pattern, _pattern_to_cycles, _decompose_cycles, @@ -44,17 +44,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc Returns: The synthesized quantum circuit. """ - # This is the very original Qiskit algorithm for synthesizing permutations. - - num_qubits = len(pattern) - qc = QuantumCircuit(num_qubits) - - swaps = _get_ordered_swap(pattern) - - for swap in swaps: - qc.swap(swap[0], swap[1]) - - return qc + return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern)) def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit: diff --git a/qiskit/synthesis/permutation/permutation_reverse_lnn.py b/qiskit/synthesis/permutation/permutation_reverse_lnn.py new file mode 100644 index 00000000000..26287a06177 --- /dev/null +++ b/qiskit/synthesis/permutation/permutation_reverse_lnn.py @@ -0,0 +1,90 @@ +# 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. +""" +Synthesis of a reverse permutation for LNN connectivity. +""" + +from qiskit.circuit import QuantumCircuit + + +def _append_cx_stage1(qc, n): + """A single layer of CX gates.""" + for i in range(n // 2): + qc.cx(2 * i, 2 * i + 1) + for i in range((n + 1) // 2 - 1): + qc.cx(2 * i + 2, 2 * i + 1) + return qc + + +def _append_cx_stage2(qc, n): + """A single layer of CX gates.""" + for i in range(n // 2): + qc.cx(2 * i + 1, 2 * i) + for i in range((n + 1) // 2 - 1): + qc.cx(2 * i + 1, 2 * i + 2) + return qc + + +def _append_reverse_permutation_lnn_kms(qc: QuantumCircuit, num_qubits: int) -> None: + """ + Append reverse permutation to a QuantumCircuit 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`. + + Args: + qc: The original quantum circuit. + num_qubits: The number of qubits. + + Returns: + The quantum circuit with appended reverse permutation. + + 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 `_ + """ + + for _ in range((num_qubits + 1) // 2): + _append_cx_stage1(qc, num_qubits) + _append_cx_stage2(qc, num_qubits) + if (num_qubits % 2) == 0: + _append_cx_stage1(qc, num_qubits) + + +def synth_permutation_reverse_lnn_kms(num_qubits: int) -> QuantumCircuit: + """ + 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`. + + Args: + num_qubits: The number of qubits. + + Returns: + The synthesized quantum circuit. + + 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 `_ + """ + + qc = QuantumCircuit(num_qubits) + _append_reverse_permutation_lnn_kms(qc, num_qubits) + + return qc diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index 6c6d950dc38..4520e18f4d0 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -12,36 +12,11 @@ """Utility functions for handling permutations.""" - -def _get_ordered_swap(permutation_in): - """Sorts the input permutation by iterating through the permutation list - and putting each element to its correct position via a SWAP (if it's not - at the correct position already). If ``n`` is the length of the input - permutation, this requires at most ``n`` SWAPs. - - More precisely, if the input permutation is a cycle of length ``m``, - then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); - if the input permutation consists of several disjoint cycles, then each cycle - is essentially treated independently. - """ - permutation = list(permutation_in[:]) - swap_list = [] - index_map = _inverse_pattern(permutation_in) - for i, val in enumerate(permutation): - if val != i: - j = index_map[i] - swap_list.append((i, j)) - permutation[i], permutation[j] = permutation[j], permutation[i] - index_map[val] = j - index_map[i] = i - swap_list.reverse() - return swap_list - - -def _inverse_pattern(pattern): - """Finds inverse of a permutation pattern.""" - b_map = {pos: idx for idx, pos in enumerate(pattern)} - return [b_map[pos] for pos in range(len(pattern))] +# pylint: disable=unused-import +from qiskit._accelerate.synthesis.permutation import ( + _inverse_pattern, + _validate_permutation, +) def _pattern_to_cycles(pattern): diff --git a/qiskit/synthesis/qft/qft_decompose_lnn.py b/qiskit/synthesis/qft/qft_decompose_lnn.py index 4dd8d9d56d1..a54be481f51 100644 --- a/qiskit/synthesis/qft/qft_decompose_lnn.py +++ b/qiskit/synthesis/qft/qft_decompose_lnn.py @@ -15,7 +15,7 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.synthesis.linear_phase.cz_depth_lnn import _append_cx_stage1, _append_cx_stage2 +from qiskit.synthesis.permutation.permutation_reverse_lnn import _append_reverse_permutation_lnn_kms def synth_qft_line( @@ -65,10 +65,6 @@ def synth_qft_line( if not do_swaps: # Add a reversal network for LNN connectivity in depth 2*n+2, # based on Kutin at al., https://arxiv.org/abs/quant-ph/0701194, Section 5. - for _ in range((num_qubits + 1) // 2): - qc = _append_cx_stage1(qc, num_qubits) - qc = _append_cx_stage2(qc, num_qubits) - if (num_qubits % 2) == 0: - qc = _append_cx_stage1(qc, num_qubits) + _append_reverse_permutation_lnn_kms(qc, num_qubits) return qc diff --git a/qiskit/synthesis/stabilizer/stabilizer_circuit.py b/qiskit/synthesis/stabilizer/stabilizer_circuit.py index 3882676be7d..4a5d53a7322 100644 --- a/qiskit/synthesis/stabilizer/stabilizer_circuit.py +++ b/qiskit/synthesis/stabilizer/stabilizer_circuit.py @@ -68,8 +68,8 @@ def synth_circuit_from_stabilizers( circuit = QuantumCircuit(num_qubits) used = 0 - for i in range(len(stabilizer_list)): - curr_stab = stabilizer_list[i].evolve(Clifford(circuit), frame="s") + for i, stabilizer in enumerate(stabilizer_list): + curr_stab = stabilizer.evolve(Clifford(circuit), frame="s") # Find pivot. pivot = used @@ -81,17 +81,17 @@ def synth_circuit_from_stabilizers( if pivot == num_qubits: if curr_stab.x.any(): raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) anti-commutes with some of " + f"Stabilizer {i} ({stabilizer}) anti-commutes with some of " "the previous stabilizers." ) if curr_stab.phase == 2: raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) contradicts " + f"Stabilizer {i} ({stabilizer}) contradicts " "some of the previous stabilizers." ) if curr_stab.z.any() and not allow_redundant: raise QiskitError( - f"Stabilizer {i} ({stabilizer_list[i]}) is a product of the others " + f"Stabilizer {i} ({stabilizer}) is a product of the others " "and allow_redundant is False. Add allow_redundant=True " "to the function call if you want to allow redundant stabilizers." ) @@ -133,7 +133,7 @@ def synth_circuit_from_stabilizers( circuit.swap(pivot, used) # fix sign - curr_stab = stabilizer_list[i].evolve(Clifford(circuit), frame="s") + curr_stab = stabilizer.evolve(Clifford(circuit), frame="s") if curr_stab.phase == 2: circuit.x(used) used += 1 diff --git a/qiskit/synthesis/stabilizer/stabilizer_decompose.py b/qiskit/synthesis/stabilizer/stabilizer_decompose.py index c43747105d0..ef324bc3cad 100644 --- a/qiskit/synthesis/stabilizer/stabilizer_decompose.py +++ b/qiskit/synthesis/stabilizer/stabilizer_decompose.py @@ -143,7 +143,7 @@ def _calc_pauli_diff_stabilizer(cliff, cliff_target): phase.extend(phase_stab) phase = np.array(phase, dtype=int) - A = cliff.symplectic_matrix.astype(int) + A = cliff.symplectic_matrix.astype(bool, copy=False) Ainv = calc_inverse_matrix(A) # By carefully writing how X, Y, Z gates affect each qubit, all we need to compute @@ -166,7 +166,7 @@ def _calc_pauli_diff_stabilizer(cliff, cliff_target): def synth_stabilizer_depth_lnn(stab: StabilizerState) -> QuantumCircuit: - """Synthesis of an n-qubit stabilizer state for linear-nearest neighbour connectivity, + """Synthesis of an n-qubit stabilizer state for linear-nearest neighbor connectivity, in 2-qubit depth :math:`2n+2` and two distinct CX layers, using :class:`.CXGate`\\ s and phase gates (:class:`.SGate`, :class:`.SdgGate` or :class:`.ZGate`). diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 41ba75c6b23..3269797827e 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -116,7 +116,7 @@ def decompose_two_qubit_product_gate(special_unitary_matrix: np.ndarray): if deviation > 1.0e-13: raise QiskitError( "decompose_two_qubit_product_gate: decomposition failed: " - "deviation too large: {}".format(deviation) + f"deviation too large: {deviation}" ) return L, R, phase @@ -782,7 +782,7 @@ def __call__(self, mat): # This weird duplicated lazy structure is for backwards compatibility; Qiskit has historically # always made ``two_qubit_cnot_decompose`` available publicly immediately on import, but it's quite -# expensive to construct, and we want to defer the obejct's creation until it's actually used. We +# expensive to construct, and we want to defer the object's creation until it's actually used. We # only need to pass through the public methods that take `self` as a parameter. Using `__getattr__` # doesn't work because it is only called if the normal resolution methods fail. Using # `__getattribute__` is too messy for a simple one-off use object. diff --git a/qiskit/synthesis/unitary/aqc/cnot_structures.py b/qiskit/synthesis/unitary/aqc/cnot_structures.py index 8659f0c2c34..978b1fc84e6 100644 --- a/qiskit/synthesis/unitary/aqc/cnot_structures.py +++ b/qiskit/synthesis/unitary/aqc/cnot_structures.py @@ -133,7 +133,7 @@ def _get_connectivity(num_qubits: int, connectivity: str) -> dict: links = {i: list(range(num_qubits)) for i in range(num_qubits)} elif connectivity == "line": - # Every qubit is connected to its immediate neighbours only. + # Every qubit is connected to its immediate neighbors only. links = {i: [i - 1, i, i + 1] for i in range(1, num_qubits - 1)} # first qubit diff --git a/qiskit/synthesis/unitary/qsd.py b/qiskit/synthesis/unitary/qsd.py index 80a8afc1311..525daa3caf1 100644 --- a/qiskit/synthesis/unitary/qsd.py +++ b/qiskit/synthesis/unitary/qsd.py @@ -269,7 +269,7 @@ def _apply_a2(circ): # rolling over diagonals ind2 = None # lint for ind1, ind2 in zip(ind2q[0:-1:], ind2q[1::]): - # get neigboring 2q gates separated by controls + # get neighboring 2q gates separated by controls instr1 = ccirc.data[ind1] mat1 = Operator(instr1.operation).data instr2 = ccirc.data[ind2] diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index c09ee190e38..396f5cf4934 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -87,7 +87,7 @@ def __eq__(self, other): return hash(self) == hash(other) @abstractmethod - def run(self, dag: DAGCircuit): # pylint: disable=arguments-differ + def run(self, dag: DAGCircuit): # pylint:disable=arguments-renamed """Run a pass on the DAGCircuit. This is implemented by the pass developer. Args: diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 614e166050e..27f98da68e7 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -101,7 +101,7 @@ def add_physical_qubit(self, physical_qubit): raise CouplingError("Physical qubits should be integers.") if physical_qubit in self.physical_qubits: raise CouplingError( - "The physical qubit %s is already in the coupling graph" % physical_qubit + f"The physical qubit {physical_qubit} is already in the coupling graph" ) self.graph.add_node(physical_qubit) self._dist_matrix = None # invalidate @@ -188,9 +188,9 @@ def distance(self, physical_qubit1, physical_qubit2): CouplingError: if the qubits do not exist in the CouplingMap """ if physical_qubit1 >= self.size(): - raise CouplingError("%s not in coupling graph" % physical_qubit1) + raise CouplingError(f"{physical_qubit1} not in coupling graph") if physical_qubit2 >= self.size(): - raise CouplingError("%s not in coupling graph" % physical_qubit2) + raise CouplingError(f"{physical_qubit2} not in coupling graph") self.compute_distance_matrix() res = self._dist_matrix[physical_qubit1, physical_qubit2] if res == math.inf: diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 1bebc7b84dc..4117e2987bb 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -98,8 +98,8 @@ def order_based_on_type(value1, value2): virtual = value1 else: raise LayoutError( - "The map (%s -> %s) has to be a (Bit -> integer)" - " or the other way around." % (type(value1), type(value2)) + f"The map ({type(value1)} -> {type(value2)}) has to be a (Bit -> integer)" + " or the other way around." ) return virtual, physical @@ -137,7 +137,7 @@ def __delitem__(self, key): else: raise LayoutError( "The key to remove should be of the form" - " Qubit or integer) and %s was provided" % (type(key),) + f" Qubit or integer) and {type(key)} was provided" ) def __len__(self): diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index b2614624b41..400d9830495 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -46,6 +46,7 @@ StochasticSwap SabreSwap Commuting2qGateRouter + StarPreRouting Basis Change ============ @@ -87,6 +88,7 @@ EchoRZXWeylDecomposition ResetAfterMeasureSimplification OptimizeCliffords + ElidePermutations NormalizeRXAngle OptimizeAnnotated @@ -152,8 +154,10 @@ HLSConfig SolovayKitaev -Post Layout (Post transpile qubit selection) -============================================ +Post Layout +=========== + +These are post qubit selection. .. autosummary:: :toctree: ../stubs/ @@ -204,6 +208,7 @@ from .routing import StochasticSwap from .routing import SabreSwap from .routing import Commuting2qGateRouter +from .routing import StarPreRouting # basis change from .basis import Decompose @@ -236,6 +241,7 @@ from .optimization import CollectCliffords from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords +from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index c38d6581776..f2e752dd94f 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -97,8 +97,8 @@ class BasisTranslator(TransformationPass): When this error occurs it typically means that either the target basis is not universal or there are additional equivalence rules needed in the - :clas:~.EquivalenceLibrary` instance being used by the - :class:~.BasisTranslator` pass. You can refer to + :class:`~.EquivalenceLibrary` instance being used by the + :class:`~.BasisTranslator` pass. You can refer to :ref:`custom_basis_gates` for details on adding custom equivalence rules. """ @@ -148,12 +148,12 @@ def run(self, dag): # Names of instructions assumed to supported by any backend. if self._target is None: - basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay"] + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] target_basis = set(self._target_basis) source_basis = set(self._extract_basis(dag)) qargs_local_source_basis = {} else: - basic_instrs = ["barrier", "snapshot"] + basic_instrs = ["barrier", "snapshot", "store"] target_basis = self._target.keys() - set(self._non_global_operations) source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) @@ -207,7 +207,7 @@ def run(self, dag): "target basis is not universal or there are additional equivalence rules " "needed in the EquivalenceLibrary being used. For more details on this " "error see: " - "https://docs.quantum.ibm.com/api/qiskit/transpiler_passes." + "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." "BasisTranslator#translation-errors" ) @@ -225,7 +225,7 @@ def run(self, dag): f"basis: {list(target_basis)}. This likely means the target basis is not universal " "or there are additional equivalence rules needed in the EquivalenceLibrary being " "used. For more details on this error see: " - "https://docs.quantum.ibm.com/api/qiskit/transpiler_passes." + "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." "BasisTranslator#translation-errors" ) @@ -302,9 +302,7 @@ def _replace_node(self, dag, node, instr_map): if len(node.op.params) != len(target_params): raise TranspilerError( "Translation num_params not equal to op num_params." - "Op: {} {} Translation: {}\n{}".format( - node.op.params, node.op.name, target_params, target_dag - ) + f"Op: {node.op.params} {node.op.name} Translation: {target_params}\n{target_dag}" ) if node.op.params: parameter_map = dict(zip(target_params, node.op.params)) @@ -468,7 +466,7 @@ def discover_vertex(self, v, score): score, ) self._basis_transforms.append((gate.name, gate.num_qubits, rule.params, rule.circuit)) - # we can stop the search if we have found all gates in the original ciruit. + # we can stop the search if we have found all gates in the original circuit. if not self._source_gates_remain: # if we start from source gates and apply `basis_transforms` in reverse order, we'll end # up with gates in the target basis. Note though that `basis_transforms` may include @@ -550,7 +548,7 @@ def _basis_search(equiv_lib, source_basis, target_basis): if not source_basis: return [] - # This is only neccessary since gates in target basis are currently reported by + # This is only necessary since gates in target basis are currently reported by # their names and we need to have in addition the number of qubits they act on. target_basis_keys = [key for key in equiv_lib.keys() if key.name in target_basis] diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 701e87dd9cd..73e1d4ac548 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -78,7 +78,7 @@ def run(self, dag): continue raise QiskitError( "Cannot unroll all 3q or more gates. " - "No rule to expand instruction %s." % node.op.name + f"No rule to expand instruction {node.op.name}." ) decomposition = circuit_to_dag(node.op.definition, copy_operations=False) decomposition = self.run(decomposition) # recursively unroll diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 12e6811a2f0..99bf95147ae 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -60,9 +60,9 @@ def run(self, dag): if self._basis_gates is None and self._target is None: return dag + device_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} if self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} - device_insts = basic_insts | set(self._basis_gates) + device_insts |= set(self._basis_gates) for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): @@ -77,14 +77,14 @@ def run(self, dag): controlled_gate_open_ctrl = isinstance(node.op, ControlledGate) and node.op._open_ctrl if not controlled_gate_open_ctrl: - inst_supported = ( - self._target.instruction_supported( + if self._target is not None: + inst_supported = self._target.instruction_supported( operation_name=node.op.name, qargs=tuple(dag.find_bit(x).index for x in node.qargs), ) - if self._target is not None - else node.name in device_insts - ) + else: + inst_supported = node.name in device_insts + if inst_supported or self._equiv_lib.has_entry(node.op): continue try: @@ -95,9 +95,9 @@ def run(self, dag): if unrolled is None: # opaque node raise QiskitError( - "Cannot unroll the circuit to the given basis, %s. " - "Instruction %s not found in equivalence library " - "and no rule found to expand." % (str(self._basis_gates), node.op.name) + f"Cannot unroll the circuit to the given basis, {str(self._basis_gates)}. " + f"Instruction {node.op.name} not found in equivalence library " + "and no rule found to expand." ) decomposition = circuit_to_dag(unrolled, copy_operations=False) diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 4cb576a23bc..c153c3eeef3 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -204,7 +204,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: xgate = self._inst_map.get("x", qubits[0]) with builder.build( - default_alignment="sequential", name="rzx(%.3f)" % theta + default_alignment="sequential", name=f"rzx({theta:.3f})" ) as rzx_theta_native: for cr_tone, comp_tone in zip(cr_tones, comp_tones): with builder.align_left(): @@ -230,7 +230,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche builder.call(szt, name="szt") with builder.build( - default_alignment="sequential", name="rzx(%.3f)" % theta + default_alignment="sequential", name=f"rzx({theta:.3f})" ) as rzx_theta_flip: builder.call(hadamard, name="hadamard") for cr_tone, comp_tone in zip(cr_tones, comp_tones): @@ -297,7 +297,7 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche # RZXCalibrationNoEcho only good for forward CR direction if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with builder.build(default_alignment="left", name="rzx(%.3f)" % theta) as rzx_theta: + with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) self.rescale_cr_inst(comp_tones[0], 2 * theta) # Placeholder to make pulse gate work diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index c36a7e11107..9cbedcef5ab 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -56,11 +56,16 @@ def run(self, dag): raise TranspilerError("The 'layout' must be full (with ancilla).") post_layout = self.property_set["post_layout"] - q = QuantumRegister(len(layout), "q") new_dag = DAGCircuit() new_dag.add_qreg(q) + for var in dag.iter_input_vars(): + new_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + new_dag.add_captured_var(var) + for var in dag.iter_declared_vars(): + new_dag.add_declared_var(var) new_dag.metadata = dag.metadata new_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 92227f3c37d..2fb9a1890bd 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -144,7 +144,7 @@ def __init__( with the ``routing_pass`` argument and an error will be raised if both are used. layout_trials (int): The number of random seed trials to run - layout with. When > 1 the trial that resuls in the output with + layout with. When > 1 the trial that results in the output with the fewest swap gates will be selected. If this is not specified (and ``routing_pass`` is not set) then the number of local physical CPUs will be used as the default value. This option is @@ -308,6 +308,12 @@ def run(self, dag): mapped_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): mapped_dag.add_creg(creg) + for var in dag.iter_input_vars(): + mapped_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + 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 self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) @@ -414,7 +420,7 @@ def _inner_run(self, dag, coupling_map, starting_layouts=None): ) def _ancilla_allocation_no_pass_manager(self, dag): - """Run the ancilla-allocation and -enlargment passes on the DAG chained onto our + """Run the ancilla-allocation and -enlargement passes on the DAG chained onto our ``property_set``, skipping the DAG-to-circuit conversion cost of using a ``PassManager``.""" ancilla_pass = FullAncillaAllocation(self.coupling_map) ancilla_pass.property_set = self.property_set diff --git a/qiskit/transpiler/passes/layout/set_layout.py b/qiskit/transpiler/passes/layout/set_layout.py index cfdc6d630df..c4e5faa91fb 100644 --- a/qiskit/transpiler/passes/layout/set_layout.py +++ b/qiskit/transpiler/passes/layout/set_layout.py @@ -63,7 +63,7 @@ def run(self, dag): layout = None else: raise InvalidLayoutError( - f"SetLayout was intialized with the layout type: {type(self.layout)}" + f"SetLayout was initialized with the layout type: {type(self.layout)}" ) self.property_set["layout"] = layout return dag diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 4e3077eb1d4..2e799ffa4d9 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -104,15 +104,21 @@ def __init__( limit on the number of trials will be set. target (Target): A target representing the backend device to run ``VF2Layout`` on. If specified it will supersede a set value for ``properties`` and - ``coupling_map``. + ``coupling_map`` if the :class:`.Target` contains connectivity constraints. If the value + of ``target`` models an ideal backend without any constraints then the value of + ``coupling_map`` + will be used. Raises: TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. """ super().__init__() self.target = target - if target is not None: - self.coupling_map = self.target.build_coupling_map() + if ( + target is not None + and (target_coupling_map := self.target.build_coupling_map()) is not None + ): + self.coupling_map = target_coupling_map else: self.coupling_map = coupling_map self.properties = properties @@ -145,7 +151,7 @@ def run(self, dag): ) # Filter qubits without any supported operations. If they don't support any operations # They're not valid for layout selection - if self.target is not None: + if self.target is not None and self.target.qargs is not None: has_operations = set(itertools.chain.from_iterable(self.target.qargs)) to_remove = set(range(len(cm_nodes))).difference(has_operations) if to_remove: @@ -189,7 +195,7 @@ def mapping_to_layout(layout_mapping): if len(cm_graph) == len(im_graph): chosen_layout = mapping_to_layout(layout_mapping) break - # If there is no error map avilable we can just skip the scoring stage as there + # If there is no error map available we can just skip the scoring stage as there # is nothing to score with, so any match is the best we can find. if self.avg_error_map is None: chosen_layout = mapping_to_layout(layout_mapping) diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 99006017482..c5d420127f8 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -145,7 +145,7 @@ def score_layout( def build_average_error_map(target, properties, coupling_map): """Build an average error map used for scoring layouts pre-basis translation.""" num_qubits = 0 - if target is not None: + if target is not None and target.qargs is not None: num_qubits = target.num_qubits avg_map = ErrorMap(len(target.qargs)) elif coupling_map is not None: @@ -157,7 +157,7 @@ def build_average_error_map(target, properties, coupling_map): # object avg_map = ErrorMap(0) built = False - if target is not None: + if target is not None and target.qargs is not None: for qargs in target.qargs: if qargs is None: continue diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 40e877ec514..082cb3f67ec 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -35,5 +35,6 @@ from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords from .collect_cliffords import CollectCliffords +from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index 51b39d7e961..e0dd61ff6cf 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -218,8 +218,8 @@ def collect_key(x): prev = bit self.gate_groups[self.find_set(prev)].append(nd) # need to turn all groups that still exist into their own blocks - for index in self.parent: - if self.parent[index] == index and len(self.gate_groups[index]) != 0: + for index, item in self.parent.items(): + if item == index and len(self.gate_groups[index]) != 0: block_list.append(self.gate_groups[index][:]) self.property_set["block_list"] = block_list diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index 751e3d8d4f5..eddb659f0a2 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -47,7 +47,7 @@ def run(self, dag): # self.property_set['commutation_set'][wire][(node, wire)] will give the # commutation set that contains node. - for wire in dag.wires: + for wire in dag.qubits: self.property_set["commutation_set"][wire] = [] # Add edges to the dictionary for each qubit @@ -56,7 +56,7 @@ def run(self, dag): self.property_set["commutation_set"][(node, edge_wire)] = -1 # Construct the commutation set - for wire in dag.wires: + for wire in dag.qubits: for current_gate in dag.nodes_on_wire(wire): diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index b0eb6bd2413..4c6c487a0ea 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -16,7 +16,6 @@ import numpy as np from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passes.optimization.commutation_analysis import CommutationAnalysis @@ -72,14 +71,11 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. - - Raises: - TranspilerError: when the 1-qubit rotation gates are not found """ 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: - # priortize z gates in circuit + # 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] @@ -99,7 +95,7 @@ def run(self, dag): # - 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.wires: + 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): @@ -146,7 +142,7 @@ def run(self, dag): or len(current_node.qargs) != 1 or current_node.qargs[0] != run_qarg ): - raise TranspilerError("internal error") + raise RuntimeError("internal error") if current_node.name in ["p", "u1", "rz", "rx"]: current_angle = float(current_node.op.params[0]) @@ -156,6 +152,10 @@ def run(self, dag): 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 @@ -167,6 +167,8 @@ def run(self, dag): 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: diff --git a/qiskit/transpiler/passes/optimization/elide_permutations.py b/qiskit/transpiler/passes/optimization/elide_permutations.py new file mode 100644 index 00000000000..ca6902f15b4 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/elide_permutations.py @@ -0,0 +1,114 @@ +# 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. + + +"""Remove any swap gates in the circuit by pushing it through into a qubit permutation.""" + +import logging + +from qiskit.circuit.library.standard_gates import SwapGate +from qiskit.circuit.library.generalized_gates import PermutationGate +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.layout import Layout + +logger = logging.getLogger(__name__) + + +class ElidePermutations(TransformationPass): + r"""Remove permutation operations from a pre-layout circuit + + This pass is intended to be run before a layout (mapping virtual qubits + to physical qubits) is set during the transpilation pipeline. This + pass iterates over the :class:`~.DAGCircuit` and when a :class:`~.SwapGate` + or :class:`~.PermutationGate` are encountered it permutes the virtual qubits in + the circuit and removes the swap gate. This will effectively remove any + :class:`~SwapGate`\s or :class:`~PermutationGate` in the circuit prior to running + layout. If this pass is run after a layout has been set it will become a no-op + (and log a warning) as this optimization is not sound after physical qubits are + selected and there are connectivity constraints to adhere to. + + For tracking purposes this pass sets 3 values in the property set if there + are any :class:`~.SwapGate` or :class:`~.PermutationGate` objects in the circuit + and the pass isn't a no-op. + + * ``original_layout``: The trivial :class:`~.Layout` for the input to this pass being run + * ``original_qubit_indices``: The mapping of qubit objects to positional indices for the state + of the circuit as input to this pass. + * ``virtual_permutation_layout``: A :class:`~.Layout` object mapping input qubits to the output + state after eliding permutations. + + These three properties are needed for the transpiler to track the permutations in the out + :attr:`.QuantumCircuit.layout` attribute. The elision of permutations is equivalent to a + ``final_layout`` set by routing and all three of these attributes are needed in the case + """ + + def run(self, dag): + """Run the ElidePermutations pass on ``dag``. + + Args: + dag (DAGCircuit): the DAG to be optimized. + + Returns: + DAGCircuit: the optimized DAG. + """ + if self.property_set["layout"] is not None: + logger.warning( + "ElidePermutations is not valid after a layout has been set. This indicates " + "an invalid pass manager construction." + ) + return dag + + op_count = dag.count_ops(recurse=False) + if op_count.get("swap", 0) == 0 and op_count.get("permutation", 0) == 0: + return dag + + new_dag = dag.copy_empty_like() + qubit_mapping = list(range(len(dag.qubits))) + + def _apply_mapping(qargs): + return tuple(dag.qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) + + for node in dag.topological_op_nodes(): + if not isinstance(node.op, (SwapGate, PermutationGate)): + new_dag.apply_operation_back( + node.op, _apply_mapping(node.qargs), node.cargs, check=False + ) + elif getattr(node.op, "condition", None) is not None: + new_dag.apply_operation_back( + node.op, _apply_mapping(node.qargs), node.cargs, check=False + ) + elif isinstance(node.op, SwapGate): + index_0 = dag.find_bit(node.qargs[0]).index + index_1 = dag.find_bit(node.qargs[1]).index + qubit_mapping[index_1], qubit_mapping[index_0] = ( + qubit_mapping[index_0], + qubit_mapping[index_1], + ) + elif isinstance(node.op, PermutationGate): + starting_indices = [qubit_mapping[dag.find_bit(qarg).index] for qarg in node.qargs] + pattern = node.op.params[0] + pattern_indices = [qubit_mapping[idx] for idx in pattern] + for i, j in zip(starting_indices, pattern_indices): + qubit_mapping[i] = j + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + + new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) + if current_layout := self.property_set["virtual_permutation_layout"]: + self.property_set["virtual_permutation_layout"] = new_layout.compose( + current_layout.inverse(dag.qubits, dag.qubits), dag.qubits + ) + else: + self.property_set["virtual_permutation_layout"] = new_layout + return new_dag diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index c814f50d4a1..f5523432c26 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -53,8 +53,8 @@ def __init__(self, gates_to_cancel: List[Union[Gate, Tuple[Gate, Gate]]]): ) else: raise TranspilerError( - "InverseCancellation pass does not take input type {}. Input must be" - " a Gate.".format(type(gates)) + f"InverseCancellation pass does not take input type {type(gates)}. Input must be" + " a Gate." ) self.self_inverse_gates = [] @@ -112,15 +112,15 @@ def _run_on_self_inverse(self, dag: DAGCircuit): partitions = [] chunk = [] max_index = len(gate_cancel_run) - 1 - for i in range(len(gate_cancel_run)): - if gate_cancel_run[i].op == gate: - chunk.append(gate_cancel_run[i]) + 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 gate_cancel_run[i].qargs != gate_cancel_run[i + 1].qargs: + 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 diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py index 9370fe7409f..f8302b9232c 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py @@ -308,7 +308,7 @@ def run(self, dag): if "u3" in self.basis: new_op = U3Gate(*right_parameters) else: - raise TranspilerError("It was not possible to use the basis %s" % self.basis) + raise TranspilerError(f"It was not possible to use the basis {self.basis}") dag.global_phase += right_global_phase diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index 65d06436cc5..fe6fe7f49e7 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -12,12 +12,19 @@ """Optimize annotated operations on a circuit.""" -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.annotated_operation import AnnotatedOperation, _canonicalize_modifiers -from qiskit.circuit import EquivalenceLibrary, ControlledGate, Operation, ControlFlowOp +from qiskit.circuit import ( + QuantumCircuit, + Instruction, + EquivalenceLibrary, + ControlledGate, + Operation, + ControlFlowOp, +) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target @@ -43,6 +50,11 @@ class OptimizeAnnotated(TransformationPass): ``g2 = AnnotatedOperation(g1, ControlModifier(2))``, then ``g2`` can be replaced with ``AnnotatedOperation(SwapGate(), [InverseModifier(), ControlModifier(2)])``. + * Applies conjugate reduction to annotated operations. As an example, + ``control - [P -- Q -- P^{-1}]`` can be rewritten as ``P -- control - [Q] -- P^{-1}``, + that is, only the middle part needs to be controlled. This also works for inverse + and power modifiers. + """ def __init__( @@ -51,6 +63,7 @@ def __init__( equivalence_library: Optional[EquivalenceLibrary] = None, basis_gates: Optional[List[str]] = None, recurse: bool = True, + do_conjugate_reduction: bool = True, ): """ OptimizeAnnotated initializer. @@ -67,17 +80,19 @@ def __init__( not applied when neither is specified since such objects do not need to be synthesized). Setting this value to ``False`` precludes the recursion in every case. + do_conjugate_reduction: controls whether conjugate reduction should be performed. """ super().__init__() self._target = target self._equiv_lib = equivalence_library self._basis_gates = basis_gates + self._do_conjugate_reduction = do_conjugate_reduction self._top_level_only = not recurse or (self._basis_gates is None and self._target is None) if not self._top_level_only and self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit): @@ -122,7 +137,11 @@ def _run_inner(self, dag) -> Tuple[DAGCircuit, bool]: # as they may remove annotated gates. dag, opt2 = self._recurse(dag) - return dag, opt1 or opt2 + opt3 = False + if not self._top_level_only and self._do_conjugate_reduction: + dag, opt3 = self._conjugate_reduction(dag) + + return dag, opt1 or opt2 or opt3 def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: """ @@ -148,17 +167,219 @@ def _canonicalize(self, dag) -> Tuple[DAGCircuit, bool]: did_something = True return dag, did_something - def _recursively_process_definitions(self, op: Operation) -> bool: + def _conjugate_decomposition( + self, dag: DAGCircuit + ) -> Union[Tuple[DAGCircuit, DAGCircuit, DAGCircuit], None]: """ - Recursively applies optimizations to op's definition (or to op.base_op's - definition if op is an annotated operation). - Returns True if did something. + Decomposes a circuit ``A`` into 3 sub-circuits ``P``, ``Q``, ``R`` such that + ``A = P -- Q -- R`` and ``R = P^{-1}``. + + This is accomplished by iteratively finding inverse nodes at the front and at the back of the + circuit. """ - # If op is an annotated operation, we descend into its base_op - if isinstance(op, AnnotatedOperation): - return self._recursively_process_definitions(op.base_op) + front_block = [] # nodes collected from the front of the circuit (aka P) + back_block = [] # nodes collected from the back of the circuit (aka R) + + # Stores in- and out- degree for each node. These degrees are computed at the start of this + # function and are updated when nodes are collected into front_block or into back_block. + in_degree = {} + out_degree = {} + + # We use dicts to track for each qubit a DAG node at the front of the circuit that involves + # this qubit and a DAG node at the end of the circuit that involves this qubit (when exist). + # Note that for the DAGCircuit structure for each qubit there can be at most one such front + # and such back node. + # This allows for an efficient way to find an inverse pair of gates (one from the front and + # one from the back of the circuit). + # A qubit that was never examined does not appear in these dicts, and a qubit that was examined + # but currently is not involved at the front (resp. at the back) of the circuit has the value of + # None. + front_node_for_qubit = {} + back_node_for_qubit = {} + + # Keep the set of nodes that have been moved either to front_block or to back_block + processed_nodes = set() + + # Keep the set of qubits that are involved in nodes at the front or at the back of the circuit. + # When looking for inverse pairs of gates we will only iterate over these qubits. + active_qubits = set() + + # Keep pairs of nodes for which the inverse check was performed and the nodes + # were found to be not inverse to each other (memoization). + checked_node_pairs = set() + + # compute in- and out- degree for every node + # also update information for nodes at the start and at the end of the circuit + for node in dag.op_nodes(): + preds = list(dag.op_predecessors(node)) + in_degree[node] = len(preds) + if len(preds) == 0: + for q in node.qargs: + front_node_for_qubit[q] = node + active_qubits.add(q) + succs = list(dag.op_successors(node)) + out_degree[node] = len(succs) + if len(succs) == 0: + for q in node.qargs: + back_node_for_qubit[q] = node + active_qubits.add(q) + + # iterate while there is a possibility to find more inverse pairs + while len(active_qubits) > 0: + to_check = active_qubits.copy() + active_qubits.clear() + + # For each qubit q, check whether the gate at the front of the circuit that involves q + # and the gate at the end of the circuit that involves q are inverse + for q in to_check: + + if (front_node := front_node_for_qubit.get(q, None)) is None: + continue + if (back_node := back_node_for_qubit.get(q, None)) is None: + continue + + # front_node or back_node could be already collected when considering other qubits + if front_node in processed_nodes or back_node in processed_nodes: + continue + + # it is possible that the same node is both at the front and at the back, + # it should not be collected + if front_node == back_node: + continue + + # have been checked before + if (front_node, back_node) in checked_node_pairs: + continue + + # fast check based on the arguments + if front_node.qargs != back_node.qargs or front_node.cargs != back_node.cargs: + continue + + # in the future we want to include a more precise check whether a pair + # of nodes are inverse + if front_node.op == back_node.op.inverse(): + # update front_node_for_qubit and back_node_for_qubit + for q in front_node.qargs: + front_node_for_qubit[q] = None + for q in back_node.qargs: + back_node_for_qubit[q] = None + + # see which other nodes become at the front and update information + for node in dag.op_successors(front_node): + if node not in processed_nodes: + in_degree[node] -= 1 + if in_degree[node] == 0: + for q in node.qargs: + front_node_for_qubit[q] = node + active_qubits.add(q) + + # see which other nodes become at the back and update information + for node in dag.op_predecessors(back_node): + if node not in processed_nodes: + out_degree[node] -= 1 + if out_degree[node] == 0: + for q in node.qargs: + back_node_for_qubit[q] = node + active_qubits.add(q) + + # collect and mark as processed + front_block.append(front_node) + back_block.append(back_node) + processed_nodes.add(front_node) + processed_nodes.add(back_node) + + else: + checked_node_pairs.add((front_node, back_node)) + + # if nothing is found, return None + if len(front_block) == 0: + return None + + # create the output DAGs + front_circuit = dag.copy_empty_like() + middle_circuit = dag.copy_empty_like() + back_circuit = dag.copy_empty_like() + front_circuit.global_phase = 0 + back_circuit.global_phase = 0 + + for node in front_block: + front_circuit.apply_operation_back(node.op, node.qargs, node.cargs) + + for node in back_block: + back_circuit.apply_operation_front(node.op, node.qargs, node.cargs) + + for node in dag.op_nodes(): + if node not in processed_nodes: + middle_circuit.apply_operation_back(node.op, node.qargs, node.cargs) + + return front_circuit, middle_circuit, back_circuit + + def _conjugate_reduce_op( + self, op: AnnotatedOperation, base_decomposition: Tuple[DAGCircuit, DAGCircuit, DAGCircuit] + ) -> Operation: + """ + We are given an annotated-operation ``op = M [ B ]`` (where ``B`` is the base operation and + ``M`` is the list of modifiers) and the "conjugate decomposition" of the definition of ``B``, + i.e. ``B = P * Q * R``, with ``R = P^{-1}`` (with ``P``, ``Q`` and ``R`` represented as + ``DAGCircuit`` objects). + + Let ``IQ`` denote a new custom instruction with definitions ``Q``. + + We return the operation ``op_new`` which a new custom instruction with definition + ``P * A * R``, where ``A`` is a new annotated-operation with modifiers ``M`` and + base gate ``IQ``. + """ + p_dag, q_dag, r_dag = base_decomposition + + q_instr = Instruction( + name="iq", num_qubits=op.base_op.num_qubits, num_clbits=op.base_op.num_clbits, params=[] + ) + q_instr.definition = dag_to_circuit(q_dag) + + op_new = Instruction( + "optimized", num_qubits=op.num_qubits, num_clbits=op.num_clbits, params=[] + ) + num_control_qubits = op.num_qubits - op.base_op.num_qubits + + circ = QuantumCircuit(op.num_qubits, op.num_clbits) + qubits = circ.qubits + circ.compose( + dag_to_circuit(p_dag), qubits[num_control_qubits : op.num_qubits], inplace=True + ) + circ.append( + AnnotatedOperation(base_op=q_instr, modifiers=op.modifiers), range(op.num_qubits) + ) + circ.compose( + dag_to_circuit(r_dag), qubits[num_control_qubits : op.num_qubits], inplace=True + ) + op_new.definition = circ + return op_new + + def _conjugate_reduction(self, dag) -> Tuple[DAGCircuit, bool]: + """ + Looks for annotated operations whose base operation has a nontrivial conjugate decomposition. + In such cases, the modifiers of the annotated operation can be moved to the "middle" part of + the decomposition. + Returns the modified DAG and whether it did something. + """ + did_something = False + for node in dag.op_nodes(op=AnnotatedOperation): + base_op = node.op.base_op + if not self._skip_definition(base_op): + base_dag = circuit_to_dag(base_op.definition, copy_operations=False) + base_decomposition = self._conjugate_decomposition(base_dag) + if base_decomposition is not None: + new_op = self._conjugate_reduce_op(node.op, base_decomposition) + dag.substitute_node(node, new_op) + did_something = True + return dag, did_something + + def _skip_definition(self, op: Operation) -> bool: + """ + Returns True if we should not recurse into a gate's definition. + """ # Similar to HighLevelSynthesis transpiler pass, we do not recurse into a gate's # `definition` for a gate that is supported by the target or in equivalence library. @@ -170,7 +391,22 @@ def _recursively_process_definitions(self, op: Operation) -> bool: else op.name in self._device_insts ) if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)): - return False + return True + return False + + def _recursively_process_definitions(self, op: Operation) -> bool: + """ + Recursively applies optimizations to op's definition (or to op.base_op's + definition if op is an annotated operation). + Returns True if did something. + """ + + # If op is an annotated operation, we descend into its base_op + if isinstance(op, AnnotatedOperation): + return self._recursively_process_definitions(op.base_op) + + if self._skip_definition(op): + return False try: # extract definition diff --git a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py index a4b11a33de2..d194d1cbbdd 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py @@ -622,7 +622,7 @@ def run_backward_match(self): ) self.matching_list.append_scenario(new_matching_scenario) - # Third option: if blocking the succesors breaks a match, we consider + # Third option: if blocking the successors breaks a match, we consider # also the possibility to block all predecessors (push the gate to the left). if broken_matches and all(global_broken): diff --git a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py index 627db502d33..d8dd5bb2b9a 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py @@ -138,8 +138,8 @@ def _find_forward_candidates(self, node_id_t): """ matches = [] - for i in range(0, len(self.match)): - matches.append(self.match[i][0]) + for match in self.match: + matches.append(match[0]) pred = matches.copy() if len(pred) > 1: diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py index 06c5186d284..44689894176 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py +++ b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py @@ -507,7 +507,7 @@ def _attempt_bind(self, template_sublist, circuit_sublist): to_native_symbolic = lambda x: x circuit_params, template_params = [], [] - # Set of all parameter names that are present in the circuits to be optimised. + # Set of all parameter names that are present in the circuits to be optimized. circuit_params_set = set() template_dag_dep = copy.deepcopy(self.template_dag_dep) diff --git a/qiskit/transpiler/passes/routing/__init__.py b/qiskit/transpiler/passes/routing/__init__.py index 2316705b4a1..a1ac25fb414 100644 --- a/qiskit/transpiler/passes/routing/__init__.py +++ b/qiskit/transpiler/passes/routing/__init__.py @@ -19,3 +19,4 @@ from .sabre_swap import SabreSwap from .commuting_2q_gate_routing.commuting_2q_gate_router import Commuting2qGateRouter from .commuting_2q_gate_routing.swap_strategy import SwapStrategy +from .star_prerouting import StarPreRouting diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py index 402aa9146f0..501400f70ce 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_gate_router.py @@ -160,8 +160,13 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if len(dag.qubits) != next(iter(dag.qregs.values())).size: raise TranspilerError("Circuit has qubits not contained in the qubit register.") - new_dag = dag.copy_empty_like() + # Fix output permutation -- copied from ElidePermutations + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + new_dag = dag.copy_empty_like() current_layout = Layout.generate_trivial_layout(*dag.qregs.values()) # Used to keep track of nodes that do not decompose using swap strategies. @@ -183,6 +188,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._compose_non_swap_nodes(accumulator, current_layout, new_dag) + self.property_set["virtual_permutation_layout"] = current_layout + return new_dag def _compose_non_swap_nodes( diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 28ae67b321c..acb23f39ab0 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -218,7 +218,7 @@ def run(self, dag): elif self.heuristic == "decay": heuristic = Heuristic.Decay else: - raise TranspilerError("Heuristic %s not recognized." % self.heuristic) + raise TranspilerError(f"Heuristic {self.heuristic} not recognized.") disjoint_utils.require_layout_isolated_to_component( dag, self.coupling_map if self.target is None else self.target ) diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py new file mode 100644 index 00000000000..3679e8bfb8e --- /dev/null +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -0,0 +1,417 @@ +# 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. + +"""Search for star connectivity patterns and replace them with.""" +from typing import Iterable, Union, Optional, List, Tuple +from math import floor, log10 + +from qiskit.circuit import Barrier +from qiskit.dagcircuit import DAGOpNode, DAGDepNode, DAGDependency, DAGCircuit +from qiskit.transpiler import Layout +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.library import SwapGate + + +class StarBlock: + """Defines blocks representing star-shaped pieces of a circuit.""" + + def __init__(self, nodes=None, center=None, num2q=0): + self.center = center + self.num2q = num2q + self.nodes = [] if nodes is None else nodes + + def get_nodes(self): + """Returns the list of nodes used in the block.""" + return self.nodes + + def append_node(self, node): + """ + If node can be added to block while keeping the block star-shaped, and + return True. Otherwise, does not add node to block and returns False. + """ + + added = False + + if len(node.qargs) == 1: + self.nodes.append(node) + added = True + elif self.center is None: + self.center = set(node.qargs) + self.nodes.append(node) + self.num2q += 1 + added = True + elif isinstance(self.center, set): + if node.qargs[0] in self.center: + self.center = node.qargs[0] + self.nodes.append(node) + self.num2q += 1 + added = True + elif node.qargs[1] in self.center: + self.center = node.qargs[1] + self.nodes.append(node) + self.num2q += 1 + added = True + else: + if self.center in node.qargs: + self.nodes.append(node) + self.num2q += 1 + added = True + + return added + + def size(self): + """ + Returns the number of two-qubit quantum gates in this block. + """ + return self.num2q + + +class StarPreRouting(TransformationPass): + """Run star to linear pre-routing + + This pass is a logical optimization pass that rewrites any + solely 2q gate star connectivity subcircuit as a linear connectivity + equivalent with swaps. + + For example: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import StarPreRouting + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + StarPreRouting()(qc).draw("mpl") + + This pass was inspired by a similar pass described in Section IV of: + C. Campbell et al., "Superstaq: Deep Optimization of Quantum Programs," + 2023 IEEE International Conference on Quantum Computing and Engineering (QCE), + Bellevue, WA, USA, 2023, pp. 1020-1032, doi: 10.1109/QCE57702.2023.00116. + """ + + def __init__(self): + """StarPreRouting""" + + self._pending_nodes: Optional[list[Union[DAGOpNode, DAGDepNode]]] = None + self._in_degree: Optional[dict[Union[DAGOpNode, DAGDepNode], int]] = None + super().__init__() + + def _setup_in_degrees(self, dag): + """For an efficient implementation, for every node we keep the number of its + unprocessed immediate predecessors (called ``_in_degree``). This ``_in_degree`` + is set up at the start and updated throughout the algorithm. + A node is leaf (or input) node iff its ``_in_degree`` is 0. + When a node is (marked as) collected, the ``_in_degree`` of each of its immediate + successor is updated by subtracting 1. + Additionally, ``_pending_nodes`` explicitly keeps the list of nodes whose + ``_in_degree`` is 0. + """ + self._pending_nodes = [] + self._in_degree = {} + for node in self._op_nodes(dag): + deg = len(self._direct_preds(dag, node)) + self._in_degree[node] = deg + if deg == 0: + self._pending_nodes.append(node) + + def _op_nodes(self, dag) -> Iterable[Union[DAGOpNode, DAGDepNode]]: + """Returns DAG nodes.""" + if not isinstance(dag, DAGDependency): + return dag.op_nodes() + else: + return dag.get_nodes() + + def _direct_preds(self, dag, node): + """Returns direct predecessors of a node. This function takes into account the + direction of collecting blocks, that is node's predecessors when collecting + backwards are the direct successors of a node in the DAG. + """ + if not isinstance(dag, DAGDependency): + return [pred for pred in dag.predecessors(node) if isinstance(pred, DAGOpNode)] + else: + return [dag.get_node(pred_id) for pred_id in dag.direct_predecessors(node.node_id)] + + def _direct_succs(self, dag, node): + """Returns direct successors of a node. This function takes into account the + direction of collecting blocks, that is node's successors when collecting + backwards are the direct predecessors of a node in the DAG. + """ + if not isinstance(dag, DAGDependency): + return [succ for succ in dag.successors(node) if isinstance(succ, DAGOpNode)] + else: + return [dag.get_node(succ_id) for succ_id in dag.direct_successors(node.node_id)] + + def _have_uncollected_nodes(self): + """Returns whether there are uncollected (pending) nodes""" + return len(self._pending_nodes) > 0 + + def collect_matching_block(self, dag, filter_fn): + """Iteratively collects the largest block of input nodes (that is, nodes with + ``_in_degree`` equal to 0) that match a given filtering function. + Examples of this include collecting blocks of swap gates, + blocks of linear gates (CXs and SWAPs), blocks of Clifford gates, blocks of single-qubit gates, + blocks of two-qubit gates, etc. Here 'iteratively' means that once a node is collected, + the ``_in_degree`` of each of its immediate successor is decreased by 1, allowing more nodes + to become input and to be eligible for collecting into the current block. + Returns the block of collected nodes. + """ + unprocessed_pending_nodes = self._pending_nodes + self._pending_nodes = [] + + current_block = StarBlock() + + # Iteratively process unprocessed_pending_nodes: + # - any node that does not match filter_fn is added to pending_nodes + # - any node that match filter_fn is added to the current_block, + # and some of its successors may be moved to unprocessed_pending_nodes. + while unprocessed_pending_nodes: + new_pending_nodes = [] + for node in unprocessed_pending_nodes: + added = filter_fn(node) and current_block.append_node(node) + if added: + # update the _in_degree of node's successors + for suc in self._direct_succs(dag, node): + self._in_degree[suc] -= 1 + if self._in_degree[suc] == 0: + new_pending_nodes.append(suc) + else: + self._pending_nodes.append(node) + unprocessed_pending_nodes = new_pending_nodes + + return current_block + + def collect_all_matching_blocks( + self, + dag, + min_block_size=2, + ): + """Collects all blocks that match a given filtering function filter_fn. + This iteratively finds the largest block that does not match filter_fn, + then the largest block that matches filter_fn, and so on, until no more uncollected + nodes remain. Intuitively, finding larger blocks of non-matching nodes helps to + find larger blocks of matching nodes later on. The option ``min_block_size`` + specifies the minimum number of gates in the block for the block to be collected. + + By default, blocks are collected in the direction from the inputs towards the outputs + of the circuit. The option ``collect_from_back`` allows to change this direction, + that is collect blocks from the outputs towards the inputs of the circuit. + + Returns the list of matching blocks only. + """ + + def filter_fn(node): + """Specifies which nodes can be collected into star blocks.""" + return ( + len(node.qargs) <= 2 + and len(node.cargs) == 0 + and getattr(node.op, "condition", None) is None + and not isinstance(node.op, Barrier) + ) + + def not_filter_fn(node): + """Returns the opposite of filter_fn.""" + return not filter_fn(node) + + # Note: the collection direction must be specified before setting in-degrees + self._setup_in_degrees(dag) + + # Iteratively collect non-matching and matching blocks. + matching_blocks: list[StarBlock] = [] + processing_order = [] + while self._have_uncollected_nodes(): + self.collect_matching_block(dag, filter_fn=not_filter_fn) + matching_block = self.collect_matching_block(dag, filter_fn=filter_fn) + if matching_block.size() >= min_block_size: + matching_blocks.append(matching_block) + processing_order.append(matching_block) + + processing_order = [n for p in processing_order for n in p.nodes] + + return matching_blocks, processing_order + + def run(self, dag): + # Extract StarBlocks from DAGCircuit / DAGDependency / DAGDependencyV2 + star_blocks, processing_order = self.determine_star_blocks_processing(dag, min_block_size=2) + + if not star_blocks: + return dag + + if all(b.size() < 3 for b in star_blocks): + # we only process blocks with less than 3 two-qubit gates in this pre-routing pass + # if they occur in a collection of larger stars, otherwise we consider them to be 'lines' + return dag + + # Create a new DAGCircuit / DAGDependency / DAGDependencyV2, replacing each + # star block by a linear sequence of gates + new_dag, qubit_mapping = self.star_preroute(dag, star_blocks, processing_order) + + # Fix output permutation -- copied from ElidePermutations + input_qubit_mapping = {qubit: index for index, qubit in enumerate(dag.qubits)} + self.property_set["original_layout"] = Layout(input_qubit_mapping) + if self.property_set["original_qubit_indices"] is None: + self.property_set["original_qubit_indices"] = input_qubit_mapping + + new_layout = Layout({dag.qubits[out]: idx for idx, out in enumerate(qubit_mapping)}) + if current_layout := self.property_set["virtual_permutation_layout"]: + self.property_set["virtual_permutation_layout"] = new_layout.compose( + current_layout.inverse(dag.qubits, dag.qubits), dag.qubits + ) + else: + self.property_set["virtual_permutation_layout"] = new_layout + + return new_dag + + def determine_star_blocks_processing( + self, dag: Union[DAGCircuit, DAGDependency], min_block_size: int + ) -> Tuple[List[StarBlock], Union[List[DAGOpNode], List[DAGDepNode]]]: + """Returns star blocks in dag and the processing order of nodes within these star blocks + Args: + dag (DAGCircuit or DAGDependency): a dag on which star blocks should be determined. + min_block_size (int): minimum number of two-qubit gates in a star block. + + Returns: + List[StarBlock]: a list of star blocks in the given dag + Union[List[DAGOpNode], List[DAGDepNode]]: a list of operations specifying processing order + """ + blocks, processing_order = self.collect_all_matching_blocks( + dag, min_block_size=min_block_size + ) + return blocks, processing_order + + def star_preroute(self, dag, blocks, processing_order): + """Returns star blocks in dag and the processing order of nodes within these star blocks + Args: + dag (DAGCircuit or DAGDependency): a dag on which star prerouting should be performed. + blocks (List[StarBlock]): a list of star blocks in the given dag. + processing_order (Union[List[DAGOpNode], List[DAGDepNode]]): a list of operations specifying + processing order + + Returns: + new_dag: a dag specifying the pre-routed circuit + qubit_mapping: the final qubit mapping after pre-routing + """ + node_to_block_id = {} + for i, block in enumerate(blocks): + for node in block.get_nodes(): + node_to_block_id[node] = i + + new_dag = dag.copy_empty_like() + processed_block_ids = set() + qubit_mapping = list(range(len(dag.qubits))) + + def _apply_mapping(qargs, qubit_mapping, qubits): + return tuple(qubits[qubit_mapping[dag.find_bit(qubit).index]] for qubit in qargs) + + is_first_star = True + last_2q_gate = [ + op + for op in reversed(processing_order) + if ((len(op.qargs) > 1) and (op.name != "barrier")) + ] + if len(last_2q_gate) > 0: + last_2q_gate = last_2q_gate[0] + else: + last_2q_gate = None + + int_digits = floor(log10(len(processing_order))) + 1 + processing_order_index_map = { + node: f"a{str(index).zfill(int(int_digits))}" + for index, node in enumerate(processing_order) + } + + def tie_breaker_key(node): + return processing_order_index_map.get(node, node.sort_key) + + for node in dag.topological_op_nodes(key=tie_breaker_key): + block_id = node_to_block_id.get(node, None) + if block_id is not None: + if block_id in processed_block_ids: + continue + + processed_block_ids.add(block_id) + + # process the whole block + block = blocks[block_id] + sequence = block.nodes + center_node = block.center + + if len(sequence) == 2: + for inner_node in sequence: + new_dag.apply_operation_back( + inner_node.op, + _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), + inner_node.cargs, + check=False, + ) + continue + swap_source = None + prev = None + for inner_node in sequence: + if (len(inner_node.qargs) == 1) or (inner_node.qargs == prev): + new_dag.apply_operation_back( + inner_node.op, + _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), + inner_node.cargs, + check=False, + ) + continue + if is_first_star and swap_source is None: + swap_source = center_node + new_dag.apply_operation_back( + inner_node.op, + _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), + inner_node.cargs, + check=False, + ) + + prev = inner_node.qargs + continue + # place 2q-gate and subsequent swap gate + new_dag.apply_operation_back( + inner_node.op, + _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), + inner_node.cargs, + check=False, + ) + + if not inner_node is last_2q_gate and not isinstance(inner_node.op, Barrier): + new_dag.apply_operation_back( + SwapGate(), + _apply_mapping(inner_node.qargs, qubit_mapping, dag.qubits), + inner_node.cargs, + check=False, + ) + # Swap mapping + index_0 = dag.find_bit(inner_node.qargs[0]).index + index_1 = dag.find_bit(inner_node.qargs[1]).index + qubit_mapping[index_1], qubit_mapping[index_0] = ( + qubit_mapping[index_0], + qubit_mapping[index_1], + ) + + prev = inner_node.qargs + is_first_star = False + else: + # the node is not part of a block + new_dag.apply_operation_back( + node.op, + _apply_mapping(node.qargs, qubit_mapping, dag.qubits), + node.cargs, + check=False, + ) + return new_dag, qubit_mapping diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 3b80bf7b31a..3732802b770 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -33,7 +33,6 @@ ForLoopOp, SwitchCaseOp, ControlFlowOp, - Instruction, CASE_DEFAULT, ) from qiskit._accelerate import stochastic_swap as stochastic_swap_rs @@ -266,11 +265,15 @@ 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}) + dag.compose( + best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True + ) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer - dag.compose(layer["graph"], qubits=best_layout.reorder_bits(dag.qubits)) + dag.compose( + layer["graph"], qubits=best_layout.reorder_bits(dag.qubits), inline_captures=True + ) def _mapper(self, circuit_graph, coupling_graph, trials=20): """Map a DAGCircuit onto a CouplingMap using swap gates. @@ -354,9 +357,7 @@ def _mapper(self, circuit_graph, coupling_graph, trials=20): # Give up if we fail again if not success_flag: - raise TranspilerError( - "swap mapper failed: " + "layer %d, sublayer %d" % (i, j) - ) + raise TranspilerError(f"swap mapper failed: layer {i}, sublayer {j}") # Update the record of qubit positions # for each inner iteration @@ -438,7 +439,7 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout root_dag, self.coupling_map, layout, final_layout, seed=self._new_seed() ) if swap_dag.size(recurse=False): - updated_dag_block.compose(swap_dag, qubits=swap_qubits) + updated_dag_block.compose(swap_dag, qubits=swap_qubits, inline_captures=True) idle_qubits &= set(updated_dag_block.idle_wires()) # Now for each block, expand it to be full width over all active wires (all blocks of a @@ -504,10 +505,18 @@ def _dag_from_block(block, node, root_dag): out.add_qreg(qreg) # For clbits, we need to take more care. Nested control-flow might need registers to exist for # conditions on inner blocks. `DAGCircuit.substitute_node_with_dag` handles this register - # mapping when required, so we use that with a dummy block. + # mapping when required, so we use that with a dummy block that pretends to act on all variables + # in the DAG. out.add_clbits(node.cargs) + for var in block.iter_input_vars(): + out.add_input_var(var) + for var in block.iter_captured_vars(): + out.add_captured_var(var) + for var in block.iter_declared_vars(): + out.add_declared_var(var) + dummy = out.apply_operation_back( - Instruction("dummy", len(node.qargs), len(node.cargs), []), + IfElseOp(expr.lift(True), block.copy_empty_like(vars_mode="captures")), node.qargs, node.cargs, check=False, diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 513144937ab..8478f241c26 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -44,7 +44,7 @@ multiple of this value. Violation of this constraint may result in the backend execution failure. - In most of the senarios, the scheduled start time of ``DAGOpNode`` corresponds to the + In most of the scenarios, the scheduled start time of ``DAGOpNode`` corresponds to the start time of the underlying pulse instruction composing the node operation. However, this assumption can be intentionally broken by defining a pulse gate, i.e. calibration, with the schedule involving pre-buffer, i.e. some random pulse delay @@ -62,7 +62,7 @@ This value is reported by ``timing_constraints["granularity"]`` in the backend configuration in units of dt. This is the constraint for a single pulse :class:`Play` instruction that may constitute your pulse gate. - The length of waveform samples should be multipel of this constraint value. + The length of waveform samples should be multiple of this constraint value. Violation of this constraint may result in failue in backend execution. Minimum pulse length constraint diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 4085844a470..e9076c5c637 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -68,7 +68,7 @@ class BaseSchedulerTransform(TransformationPass): However, such optimization should be done by another pass, otherwise scheduling may break topological ordering of the original circuit. - Realistic control flow scheduling respecting for microarcitecture + Realistic control flow scheduling respecting for microarchitecture In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) followed by resonator ring-down (depopulation). This microwave signal is recorded diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 5b84b529e45..7be0e838ebf 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -20,6 +20,7 @@ from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.synthesis.one_qubit import OneQubitEulerDecomposer +from qiskit.transpiler import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -129,7 +130,7 @@ def __init__( will be used [d/2, d, d, ..., d, d, d/2]. skip_reset_qubits (bool): if True, does not insert DD on idle periods that immediately follow initialized/reset qubits (as - qubits in the ground state are less susceptile to decoherence). + qubits in the ground state are less susceptible to decoherence). target (Target): The :class:`~.Target` representing the target backend, if both ``durations`` and this are specified then this argument will take precedence and ``durations`` will be ignored. @@ -168,6 +169,8 @@ def run(self, dag): if dag.duration is None: raise TranspilerError("DD runs after circuit is scheduled.") + durations = self._update_inst_durations(dag) + num_pulses = len(self._dd_sequence) sequence_gphase = 0 if num_pulses != 1: @@ -208,7 +211,7 @@ def run(self, dag): for index, gate in enumerate(self._dd_sequence): gate = gate.to_mutable() self._dd_sequence[index] = gate - gate.duration = self._durations.get(gate, physical_qubit) + gate.duration = durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration @@ -277,6 +280,26 @@ def run(self, dag): return new_dag + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations is not None: + circ_durations.update(self._durations, getattr(self._durations, "dt", None)) + + return circ_durations + def __gate_supported(self, gate: Gate, qarg: int) -> bool: """A gate is supported on the qubit (qarg) or not.""" if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index a90f0c339ce..4ce17e7bc26 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -202,7 +202,7 @@ def _apply_scheduled_op( ): """Add new operation to DAG with scheduled information. - This is identical to apply_operation_back + updating the node_start_time propety. + This is identical to apply_operation_back + updating the node_start_time property. Args: dag: DAG circuit on which the sequence is applied. diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 42a1bdc80f1..45333de009b 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -128,7 +128,7 @@ def __init__( will be used [d/2, d, d, ..., d, d, d/2]. skip_reset_qubits: If True, does not insert DD on idle periods that immediately follow initialized/reset qubits - (as qubits in the ground state are less susceptile to decoherence). + (as qubits in the ground state are less susceptible to decoherence). pulse_alignment: The hardware constraints for gate timing allocation. This is usually provided from ``backend.configuration().timing_constraints``. If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to @@ -179,9 +179,31 @@ def __init__( f"{gate.name} in dd_sequence is not supported in the target" ) + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations is not None: + circ_durations.update(self._durations, getattr(self._durations, "dt", None)) + + return circ_durations + def _pre_runhook(self, dag: DAGCircuit): super()._pre_runhook(dag) + durations = self._update_inst_durations(dag) + num_pulses = len(self._dd_sequence) # Check if physical circuit is given @@ -245,7 +267,7 @@ def _pre_runhook(self, dag: DAGCircuit): f"is not acceptable in {self.__class__.__name__} pass." ) except KeyError: - gate_length = self._durations.get(gate, physical_index) + gate_length = durations.get(gate, physical_index) sequence_lengths.append(gate_length) # Update gate duration. This is necessary for current timeline drawer, i.e. scheduled. gate = gate.to_mutable() @@ -289,7 +311,7 @@ def _pad( # slack = 992 dt - 4 x 160 dt = 352 dt # # unconstraind sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt - # constraind sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt + # constrained sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt # # Now we evenly split extra slack into start and end of the sequence. # The distributed slack should be multiple of 16. @@ -339,17 +361,17 @@ def _pad( theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): # Absorb the inverse into the successor (from left in circuit) - theta_r, phi_r, lam_r = next_node.op.params - next_node.op.params = Optimize1qGates.compose_u3( - theta_r, phi_r, lam_r, theta, phi, lam - ) + op = next_node.op + theta_r, phi_r, lam_r = op.params + op.params = Optimize1qGates.compose_u3(theta_r, phi_r, lam_r, theta, phi, lam) + next_node.op = op sequence_gphase += phase elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): # Absorb the inverse into the predecessor (from right in circuit) - theta_l, phi_l, lam_l = prev_node.op.params - prev_node.op.params = Optimize1qGates.compose_u3( - theta, phi, lam, theta_l, phi_l, lam_l - ) + 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 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 3792a149fd7..69bea32acca 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,8 +70,9 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) - node.op = node.op.to_mutable() - node.op.duration = duration + op = node.op.to_mutable() + op.duration = duration + node.op = op else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index d53c3fc4ef6..08ac932d8ae 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -51,6 +51,7 @@ def __init__(self, inst_durations: InstructionDurations = None, target: Target = self.inst_durations = inst_durations or InstructionDurations() if target is not None: self.inst_durations = target.durations() + self._durations_provided = inst_durations is not None or target is not None def run(self, dag: DAGCircuit): """Run the TimeUnitAnalysis pass on `dag`. @@ -64,8 +65,11 @@ def run(self, dag: DAGCircuit): Raises: TranspilerError: if the units are not unifiable """ + + inst_durations = self._update_inst_durations(dag) + # Choose unit - if self.inst_durations.dt is not None: + if inst_durations.dt is not None: time_unit = "dt" else: # Check what units are used in delays and other instructions: dt or SI or mixed @@ -75,7 +79,7 @@ def run(self, dag: DAGCircuit): "Fail to unify time units in delays. SI units " "and dt unit must not be mixed when dt is not supplied." ) - units_other = self.inst_durations.units_used() + units_other = inst_durations.units_used() if self._unified(units_other) == "mixed": raise TranspilerError( "Fail to unify time units in instruction_durations. SI units " @@ -96,18 +100,39 @@ def run(self, dag: DAGCircuit): # Make units consistent for node in dag.op_nodes(): try: - duration = self.inst_durations.get( + duration = inst_durations.get( node.op, [dag.find_bit(qarg).index for qarg in node.qargs], unit=time_unit ) except TranspilerError: continue - node.op = node.op.to_mutable() - node.op.duration = duration - node.op.unit = time_unit + op = node.op.to_mutable() + op.duration = duration + op.unit = time_unit + node.op = op self.property_set["time_unit"] = time_unit return dag + def _update_inst_durations(self, dag): + """Update instruction durations with circuit information. If the dag contains gate + calibrations and no instruction durations were provided through the target or as a + standalone input, the circuit calibration durations will be used. + The priority order for instruction durations is: target > standalone > circuit. + """ + circ_durations = InstructionDurations() + + if dag.calibrations: + cal_durations = [] + for gate, gate_cals in dag.calibrations.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) + circ_durations.update(cal_durations, circ_durations.dt) + + if self._durations_provided: + circ_durations.update(self.inst_durations, getattr(self.inst_durations, "dt", None)) + + return circ_durations + @staticmethod def _units_used_in_delays(dag: DAGCircuit) -> Set[str]: units_used = set() diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 3d3e2a6851a..bbc98662105 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -133,7 +133,7 @@ TokenSwapperSynthesisPermutation """ -from typing import Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple, Callable import numpy as np import rustworkx as rx @@ -227,16 +227,34 @@ class HLSConfig: :ref:`using-high-level-synthesis-plugins`. """ - def __init__(self, use_default_on_unspecified=True, **kwargs): + def __init__( + self, + use_default_on_unspecified: bool = True, + plugin_selection: str = "sequential", + plugin_evaluation_fn: Optional[Callable[[QuantumCircuit], int]] = None, + **kwargs, + ): """Creates a high-level-synthesis config. Args: - use_default_on_unspecified (bool): if True, every higher-level-object without an + use_default_on_unspecified: if True, every higher-level-object without an explicitly specified list of methods will be synthesized using the "default" algorithm if it exists. + plugin_selection: if set to ``"sequential"`` (default), for every higher-level-object + the synthesis pass will consider the specified methods sequentially, stopping + at the first method that is able to synthesize the object. If set to ``"all"``, + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. + plugin_evaluation_fn: a callable that evaluates the quality of the synthesized + quantum circuit; a smaller value means a better circuit. If ``None``, the + quality of the circuit its size (i.e. the number of gates that it contains). kwargs: a dictionary mapping higher-level-objects to lists of synthesis methods. """ self.use_default_on_unspecified = use_default_on_unspecified + self.plugin_selection = plugin_selection + self.plugin_evaluation_fn = ( + plugin_evaluation_fn if plugin_evaluation_fn is not None else lambda qc: qc.size() + ) self.methods = {} for key, value in kwargs.items(): @@ -248,9 +266,6 @@ def set_methods(self, hls_name, hls_methods): self.methods[hls_name] = hls_methods -# ToDo: Do we have a way to specify optimization criteria (e.g., 2q gate count vs. depth)? - - class HighLevelSynthesis(TransformationPass): """Synthesize higher-level objects and unroll custom definitions. @@ -348,7 +363,7 @@ def __init__( # include path for when target exists but target.num_qubits is None (BasicSimulator) if not self._top_level_only and (self._target is None or self._target.num_qubits is None): - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -500,6 +515,9 @@ def _synthesize_op_using_plugins( else: methods = [] + best_decomposition = None + best_score = np.inf + for method in methods: # There are two ways to specify a synthesis method. The more explicit # way is to specify it as a tuple consisting of a synthesis algorithm and a @@ -522,8 +540,8 @@ def _synthesize_op_using_plugins( if isinstance(plugin_specifier, str): if plugin_specifier not in hls_plugin_manager.method_names(op.name): raise TranspilerError( - "Specified method: %s not found in available plugins for %s" - % (plugin_specifier, op.name) + f"Specified method: {plugin_specifier} not found in available " + f"plugins for {op.name}" ) plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) else: @@ -538,11 +556,22 @@ def _synthesize_op_using_plugins( ) # The synthesis methods that are not suited for the given higher-level-object - # will return None, in which case the next method in the list will be used. + # will return None. if decomposition is not None: - return decomposition + if self.hls_config.plugin_selection == "sequential": + # In the "sequential" mode the first successful decomposition is + # returned. + best_decomposition = decomposition + break - return None + # In the "run everything" mode we update the best decomposition + # discovered + current_score = self.hls_config.plugin_evaluation_fn(decomposition) + if current_score < best_score: + best_decomposition = decomposition + best_score = current_score + + return best_decomposition def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: """ @@ -732,9 +761,9 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): * 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 then the direct approach. + 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. + and to invert the order of CX gates in the synthesized circuit. In certain cases this provides a better decomposition than the direct approach. """ @@ -750,7 +779,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) - mat = high_level_object.linear.astype(int) + mat = high_level_object.linear.astype(bool, copy=False) if use_transposed: mat = np.transpose(mat) @@ -778,9 +807,9 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): * 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 then the direct approach. + 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. + 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: @@ -802,7 +831,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) - mat = high_level_object.linear.astype(int) + mat = high_level_object.linear.astype(bool, copy=False) if use_transposed: mat = np.transpose(mat) diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index f2485bfee53..c57c6d76f9f 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -698,13 +698,13 @@ def __init__(self): self.plugins_by_op = {} for plugin_name in self.plugins.names(): op_name, method_name = plugin_name.split(".") - if op_name not in self.plugins_by_op.keys(): + if op_name not in self.plugins_by_op: self.plugins_by_op[op_name] = [] self.plugins_by_op[op_name].append(method_name) def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 5d919661a83..7db48d6d139 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -39,6 +39,7 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, + GATE_NAME_MAP, ) from qiskit.quantum_info import Operator from qiskit.circuit import ControlFlowOp, Gate, Parameter @@ -293,7 +294,7 @@ def __init__( natural_direction: bool | None = None, synth_gates: list[str] | None = None, method: str = "default", - min_qubits: int = None, + min_qubits: int = 0, plugin_config: dict = None, target: Target = None, ): @@ -413,7 +414,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.method == "default": # If the method is the default, we only need to evaluate one set of keyword arguments. # To simplify later logic, and avoid cases where static analysis might complain that we - # haven't initialised the "default" handler, we rebind the names so they point to the + # haven't initialized the "default" handler, we rebind the names so they point to the # same object as the chosen method. default_method = plugin_method default_kwargs = plugin_kwargs @@ -499,27 +500,55 @@ def _run_main_loop( ] ) - for node in dag.named_nodes(*self._synth_gates): - if self._min_qubits is not None and len(node.qargs) < self._min_qubits: - continue - synth_dag = None - unitary = node.op.to_matrix() - n_qubits = len(node.qargs) - if (plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits) or ( - plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits - ): - method, kwargs = default_method, default_kwargs + out_dag = dag.copy_empty_like() + for node in dag.topological_op_nodes(): + if node.op.name == "unitary" and len(node.qargs) >= self._min_qubits: + synth_dag = None + unitary = node.op.to_matrix() + n_qubits = len(node.qargs) + if ( + plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits + ) or (plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits): + method, kwargs = default_method, default_kwargs + else: + method, kwargs = plugin_method, plugin_kwargs + if method.supports_coupling_map: + kwargs["coupling_map"] = ( + self._coupling_map, + [qubit_indices[x] for x in node.qargs], + ) + synth_dag = method.run(unitary, **kwargs) + if synth_dag is None: + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + continue + if isinstance(synth_dag, DAGCircuit): + qubit_map = dict(zip(synth_dag.qubits, node.qargs)) + for node in synth_dag.topological_op_nodes(): + out_dag.apply_operation_back( + node.op, (qubit_map[x] for x in node.qargs), check=False + ) + out_dag.global_phase += synth_dag.global_phase + else: + node_list, global_phase, gate = synth_dag + qubits = node.qargs + for ( + op_name, + params, + qargs, + ) in node_list: + if op_name == "USER_GATE": + op = gate + else: + op = GATE_NAME_MAP[op_name](*params) + out_dag.apply_operation_back( + op, + (qubits[x] for x in qargs), + check=False, + ) + out_dag.global_phase += global_phase else: - method, kwargs = plugin_method, plugin_kwargs - if method.supports_coupling_map: - kwargs["coupling_map"] = ( - self._coupling_map, - [qubit_indices[x] for x in node.qargs], - ) - synth_dag = method.run(unitary, **kwargs) - if synth_dag is not None: - dag.substitute_node_with_dag(node, synth_dag) - return dag + out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + return out_dag def _build_gate_lengths(props=None, target=None): @@ -893,6 +922,20 @@ def run(self, unitary, **options): decomposers2q = [decomposer2q] if decomposer2q is not None else [] # choose the cheapest output among synthesized circuits synth_circuits = [] + # If we have a single TwoQubitBasisDecomposer skip dag creation as we don't need to + # store and can instead manually create the synthesized gates directly in the output dag + if len(decomposers2q) == 1 and isinstance(decomposers2q[0], TwoQubitBasisDecomposer): + preferred_direction = _preferred_direction( + decomposers2q[0], + qubits, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + ) + return self._synth_su4_no_dag( + unitary, decomposers2q[0], preferred_direction, approximation_degree + ) for decomposer2q in decomposers2q: preferred_direction = _preferred_direction( decomposer2q, qubits, natural_direction, coupling_map, gate_lengths, gate_errors @@ -919,6 +962,24 @@ def run(self, unitary, **options): return synth_circuit return circuit_to_dag(synth_circuit) + def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approximation_degree): + approximate = not approximation_degree == 1.0 + synth_circ = decomposer2q._inner_decomposer(unitary, approximate=approximate) + if not preferred_direction: + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + + synth_direction = None + # if the gates in synthesis are in the opposite direction of the preferred direction + # resynthesize a new operator which is the original conjugated by swaps. + # this new operator is doubly mirrored from the original and is locally equivalent. + for op_name, _params, qubits in synth_circ: + if op_name in {"USER_GATE", "cx"}: + synth_direction = qubits + if synth_direction is not None and synth_direction != preferred_direction: + # TODO: Avoid using a dag to correct the synthesis direction + return self._reversed_synth_su4(unitary, decomposer2q, approximation_degree) + return (synth_circ, synth_circ.global_phase, decomposer2q.gate) + def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_degree): approximate = not approximation_degree == 1.0 synth_circ = decomposer2q(su4_mat, approximate=approximate, use_dag=True) @@ -932,16 +993,20 @@ def _synth_su4(self, su4_mat, decomposer2q, preferred_direction, approximation_d if inst.op.num_qubits == 2: synth_direction = [synth_circ.find_bit(q).index for q in inst.qargs] if synth_direction is not None and synth_direction != preferred_direction: - su4_mat_mm = su4_mat.copy() - su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] - su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] - synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) - out_dag = DAGCircuit() - out_dag.global_phase = synth_circ.global_phase - out_dag.add_qubits(list(reversed(synth_circ.qubits))) - flip_bits = out_dag.qubits[::-1] - for node in synth_circ.topological_op_nodes(): - qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - out_dag.apply_operation_back(node.op, qubits, check=False) - return out_dag + return self._reversed_synth_su4(su4_mat, decomposer2q, approximation_degree) return synth_circ + + def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): + approximate = not approximation_degree == 1.0 + su4_mat_mm = su4_mat.copy() + su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] + su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] + synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) + out_dag = DAGCircuit() + out_dag.global_phase = synth_circ.global_phase + out_dag.add_qubits(list(reversed(synth_circ.qubits))) + flip_bits = out_dag.qubits[::-1] + for node in synth_circ.topological_op_nodes(): + qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) + out_dag.apply_operation_back(node.op, qubits, check=False) + return out_dag diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index 61ddc71d131..437718ec27b 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -85,10 +85,8 @@ def _recurse(self, dag, wire_map) -> bool: 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"] = "{}({}, {}) failed".format( - node.name, - wire_map[node.qargs[0]], - wire_map[node.qargs[1]], + self.property_set["check_map_msg"] = ( + f"{node.name}({wire_map[node.qargs[0]]}, {wire_map[node.qargs[1]]}) failed" ) return False return True diff --git a/qiskit/transpiler/passes/utils/error.py b/qiskit/transpiler/passes/utils/error.py index f2659ec052f..44420582c1a 100644 --- a/qiskit/transpiler/passes/utils/error.py +++ b/qiskit/transpiler/passes/utils/error.py @@ -43,7 +43,7 @@ def __init__(self, msg=None, action="raise"): if action in ["raise", "warn", "log"]: self.action = action else: - raise TranspilerError("Unknown action: %s" % action) + raise TranspilerError(f"Unknown action: {action}") def run(self, _): """Run the Error pass on `dag`.""" @@ -66,4 +66,4 @@ def run(self, _): logger = logging.getLogger(__name__) logger.info(msg) else: - raise TranspilerError("Unknown action: %s" % self.action) + raise TranspilerError(f"Unknown action: {self.action}") diff --git a/qiskit/transpiler/passes/utils/fixed_point.py b/qiskit/transpiler/passes/utils/fixed_point.py index fbef9d0a85e..a85a7a8e6e7 100644 --- a/qiskit/transpiler/passes/utils/fixed_point.py +++ b/qiskit/transpiler/passes/utils/fixed_point.py @@ -37,12 +37,12 @@ def __init__(self, property_to_check): def run(self, dag): """Run the FixedPoint pass on `dag`.""" current_value = self.property_set[self._property] - fixed_point_previous_property = "_fixed_point_previous_%s" % self._property + fixed_point_previous_property = f"_fixed_point_previous_{self._property}" if self.property_set[fixed_point_previous_property] is None: - self.property_set["%s_fixed_point" % self._property] = False + self.property_set[f"{self._property}_fixed_point"] = False else: fixed_point_reached = self.property_set[fixed_point_previous_property] == current_value - self.property_set["%s_fixed_point" % self._property] = fixed_point_reached + self.property_set[f"{self._property}_fixed_point"] = fixed_point_reached self.property_set[fixed_point_previous_property] = deepcopy(current_value) diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 98b471f6f7f..79493ae8ad2 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -67,7 +67,7 @@ class GateDirection(TransformationPass): └──────┘ └───┘└──────┘└───┘ This pass assumes that the positions of the qubits in the :attr:`.DAGCircuit.qubits` attribute - are the physical qubit indicies. For example if ``dag.qubits[0]`` is qubit 0 in the + are the physical qubit indices. For example if ``dag.qubits[0]`` is qubit 0 in the :class:`.CouplingMap` or :class:`.Target`. """ diff --git a/qiskit/transpiler/passes/utils/gates_basis.py b/qiskit/transpiler/passes/utils/gates_basis.py index 657b1d13485..b1f004cc0df 100644 --- a/qiskit/transpiler/passes/utils/gates_basis.py +++ b/qiskit/transpiler/passes/utils/gates_basis.py @@ -32,7 +32,7 @@ def __init__(self, basis_gates=None, target=None): self._basis_gates = None if basis_gates is not None: self._basis_gates = set(basis_gates).union( - {"measure", "reset", "barrier", "snapshot", "delay"} + {"measure", "reset", "barrier", "snapshot", "delay", "store"} ) self._target = target @@ -46,8 +46,8 @@ def run(self, dag): def _visit_target(dag, wire_map): for gate in dag.op_nodes(): - # Barrier is universal and supported by all backends - if gate.name == "barrier": + # Barrier and store are assumed universal and supported by all backends + if gate.name in ("barrier", "store"): continue if not self._target.instruction_supported( gate.name, tuple(wire_map[bit] for bit in gate.qargs) diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 025c3ea9dfd..c905d614214 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -18,7 +18,7 @@ import re from collections.abc import Iterator, Iterable, Callable from functools import wraps -from typing import Union, List, Any +from typing import Union, List, Any, TypeVar from qiskit.circuit import QuantumCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -29,9 +29,9 @@ from qiskit.passmanager.exceptions import PassManagerError from .basepasses import BasePass from .exceptions import TranspilerError -from .layout import TranspileLayout +from .layout import TranspileLayout, Layout -_CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] +_CircuitsT = TypeVar("_CircuitsT", bound=Union[List[QuantumCircuit], QuantumCircuit]) class PassManager(BasePassManager): @@ -69,6 +69,7 @@ def _passmanager_backend( ) -> QuantumCircuit: out_program = dag_to_circuit(passmanager_ir, copy_operations=False) + self._finalize_layouts(passmanager_ir) out_name = kwargs.get("output_name", None) if out_name is not None: out_program.name = out_name @@ -96,7 +97,50 @@ def _passmanager_backend( return out_program - def append( + def _finalize_layouts(self, dag): + if (virtual_permutation_layout := self.property_set["virtual_permutation_layout"]) is None: + return + + self.property_set.pop("virtual_permutation_layout") + + # virtual_permutation_layout is usually created before extending the layout with ancillas, + # so we extend the permutation to be identity on ancilla qubits + original_qubit_indices = self.property_set.get("original_qubit_indices", None) + for oq in original_qubit_indices: + if oq not in virtual_permutation_layout: + virtual_permutation_layout[oq] = original_qubit_indices[oq] + + t_qubits = dag.qubits + + if (t_initial_layout := self.property_set.get("layout", None)) is None: + t_initial_layout = Layout(dict(enumerate(t_qubits))) + + if (t_final_layout := self.property_set.get("final_layout", None)) is None: + t_final_layout = Layout(dict(enumerate(t_qubits))) + + # Ordered list of original qubits + original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()} + original_qubits = [] + # pylint: disable-next=consider-using-enumerate + for i in range(len(original_qubits_reverse)): + original_qubits.append(original_qubits_reverse[i]) + + virtual_permutation_layout_inv = virtual_permutation_layout.inverse( + original_qubits, original_qubits + ) + + t_initial_layout_inv = t_initial_layout.inverse(original_qubits, t_qubits) + + # ToDo: this can possibly be made simpler + new_final_layout = t_initial_layout_inv + new_final_layout = new_final_layout.compose(virtual_permutation_layout_inv, original_qubits) + new_final_layout = new_final_layout.compose(t_initial_layout, original_qubits) + new_final_layout = new_final_layout.compose(t_final_layout, t_qubits) + + self.property_set["layout"] = t_initial_layout + self.property_set["final_layout"] = new_final_layout + + def append( # pylint:disable=arguments-renamed self, passes: Task | list[Task], ) -> None: @@ -110,7 +154,7 @@ def append( """ super().append(tasks=passes) - def replace( + def replace( # pylint:disable=arguments-renamed self, index: int, passes: Task | list[Task], @@ -124,7 +168,7 @@ def replace( super().replace(index, tasks=passes) # pylint: disable=arguments-differ - def run( + def run( # pylint:disable=arguments-renamed self, circuits: _CircuitsT, output_name: str | None = None, @@ -294,7 +338,7 @@ def __init__(self, stages: Iterable[str] | None = None, **kwargs) -> None: "scheduling", ] self._validate_stages(stages) - # Set through parent class since `__setattr__` requieres `expanded_stages` to be defined + # Set through parent class since `__setattr__` requires `expanded_stages` to be defined super().__setattr__("_stages", tuple(stages)) super().__setattr__("_expanded_stages", tuple(self._generate_expanded_stages())) super().__init__() diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 8d653ed3a1a..f2f011e486c 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 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 @@ -21,7 +21,8 @@ for the transpiler. The preset pass managers are instances of :class:`~.StagedPassManager` which are used to execute the circuit transformations as part of Qiskit's compiler inside the -:func:`~.transpile` function at the different optimization levels. +:func:`~.transpile` function at the different optimization levels, but +can also be used in a standalone manner. The functionality here is divided into two parts, the first includes the functions used generate the entire pass manager which is used by :func:`~.transpile` (:ref:`preset_pass_manager_generators`) and the @@ -56,13 +57,21 @@ .. autofunction:: generate_scheduling .. currentmodule:: qiskit.transpiler.preset_passmanagers """ +import copy -import warnings +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping +from qiskit.providers.backend_compat import BackendV2Converter + +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.target import target_to_backend_properties +from qiskit.transpiler.target import Target, target_to_backend_properties from qiskit.transpiler import CouplingMap +from qiskit.transpiler.exceptions import TranspilerError + from .level0 import level_0_pass_manager from .level1 import level_1_pass_manager from .level2 import level_2_pass_manager @@ -91,16 +100,43 @@ def generate_preset_pass_manager( hls_config=None, init_method=None, optimization_method=None, + dt=None, *, _skip_target=False, ): """Generate a preset :class:`~.PassManager` - This function is used to quickly generate a preset pass manager. A preset pass - manager are the default pass managers used by the :func:`~.transpile` + This function is used to quickly generate a preset pass manager. Preset pass + managers are the default pass managers used by the :func:`~.transpile` function. This function provides a convenient and simple method to construct - a standalone :class:`~.PassManager` object that mirrors what the transpile + a standalone :class:`~.PassManager` object that mirrors what the :func:`~.transpile` + function internally builds and uses. + + The target constraints for the pass manager construction can be specified through a :class:`.Target` + instance, a :class:`.BackendV1` or :class:`.BackendV2` instance, or via loose constraints + (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + ``dt`` or ``timing_constraints``). + The order of priorities for target constraints works as follows: if a ``target`` + input is provided, it will take priority over any ``backend`` input or loose constraints. + If a ``backend`` is provided together with any loose constraint + from the list above, the loose constraint will take priority over the corresponding backend + constraint. This behavior is independent of whether the ``backend`` instance is of type + :class:`.BackendV1` or :class:`.BackendV2`, as summarized in the table below. The first column + in the table summarizes the potential user-provided constraints, and each cell shows whether + the priority is assigned to that specific constraint input or another input + (`target`/`backend(V1)`/`backend(V2)`). + ============================ ========= ======================== ======================= + User Provided target backend(V1) backend(V2) + ============================ ========= ======================== ======================= + **basis_gates** target basis_gates basis_gates + **coupling_map** target coupling_map coupling_map + **instruction_durations** target instruction_durations instruction_durations + **inst_map** target inst_map inst_map + **dt** target dt dt + **timing_constraints** target timing_constraints timing_constraints + **backend_properties** target backend_properties backend_properties + ============================ ========= ======================== ======================= Args: optimization_level (int): The optimization level to generate a @@ -126,16 +162,57 @@ def generate_preset_pass_manager( and ``backend_properties``. basis_gates (list): List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). - inst_map (InstructionScheduleMap): Mapping object that maps gate to schedules. + inst_map (InstructionScheduleMap): Mapping object that maps gates to schedules. If any user defined calibration is found in the map and this is used in a circuit, transpiler attaches the custom gate definition to the circuit. This enables one to flexibly override the low-level instruction implementation. coupling_map (CouplingMap or list): Directed graph represented a coupling - map. - instruction_durations (InstructionDurations): Dictionary of duration - (in dt) for each instruction. + map. Multiple formats are supported: + + #. ``CouplingMap`` instance + #. List, must be given as an adjacency matrix, where each entry + specifies all directed two-qubit interactions supported by backend, + e.g: ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]`` + + instruction_durations (InstructionDurations or list): Dictionary of duration + (in dt) for each instruction. If specified, these durations overwrite the + gate lengths in ``backend.properties``. Applicable only if ``scheduling_method`` + is specified. + The format of ``instruction_durations`` must be as follows: + They must be given as an :class:`.InstructionDurations` instance or a list of tuples + + ``` + [(instruction_name, qubits, duration, unit), ...]. + | [('cx', [0, 1], 12.3, 'ns'), ('u3', [0], 4.56, 'ns')] + | [('cx', [0, 1], 1000), ('u3', [0], 300)] + ``` + + If ``unit`` is omitted, the default is ``'dt'``, which is a sample time depending on backend. + If the time unit is ``'dt'``, the duration must be an integer. + dt (float): Backend sample time (resolution) in seconds. + If provided, this value will overwrite the ``dt`` value in ``instruction_durations``. + If ``None`` (default) and a backend is provided, ``backend.dt`` is used. timing_constraints (TimingConstraints): Hardware time alignment restrictions. + A quantum computer backend may report a set of restrictions, namely: + + - granularity: An integer value representing minimum pulse gate + resolution in units of ``dt``. A user-defined pulse gate should have + duration of a multiple of this granularity value. + - min_length: An integer value representing minimum pulse gate + length in units of ``dt``. A user-defined pulse gate should be longer + than this length. + - pulse_alignment: An integer value representing a time resolution of gate + instruction starting time. Gate instruction should start at time which + is a multiple of the alignment value. + - acquire_alignment: An integer value representing a time resolution of measure + instruction starting time. Measure instruction should start at time which + is a multiple of the alignment value. + + This information will be provided by the backend configuration. + If the backend doesn't have any restriction on the instruction time allocation, + then ``timing_constraints`` is None and no adjustment will be performed. + initial_layout (Layout | List[int]): Initial position of virtual qubits on physical qubits. layout_method (str): The :class:`~.Pass` to use for choosing initial qubit @@ -205,8 +282,74 @@ def generate_preset_pass_manager( ValueError: if an invalid value for ``optimization_level`` is passed in. """ - if coupling_map is not None and not isinstance(coupling_map, CouplingMap): - coupling_map = CouplingMap(coupling_map) + if backend is not None and getattr(backend, "version", 0) <= 1: + # This is a temporary conversion step to allow for a smoother transition + # to a fully target-based transpiler pipeline while maintaining the behavior + # of `transpile` with BackendV1 inputs. + backend = BackendV2Converter(backend) + + # Check if a custom inst_map was specified before overwriting inst_map + _given_inst_map = bool(inst_map) + # If there are no loose constraints => use backend target if available + _no_loose_constraints = ( + basis_gates is None + and coupling_map is None + and dt is None + and instruction_durations is None + and backend_properties is None + and timing_constraints is None + ) + # If it's an edge case => do not build target + _skip_target = ( + target is None + and backend is None + and (basis_gates is None or coupling_map is None or instruction_durations is not None) + ) + + # Resolve loose constraints case-by-case against backend constraints. + # The order of priority is loose constraints > backend. + dt = _parse_dt(dt, backend) + instruction_durations = _parse_instruction_durations(backend, instruction_durations, dt) + timing_constraints = _parse_timing_constraints(backend, timing_constraints) + inst_map = _parse_inst_map(inst_map, backend) + # The basis gates parser will set _skip_target to True if a custom basis gate is found + # (known edge case). + basis_gates, name_mapping, _skip_target = _parse_basis_gates( + basis_gates, backend, inst_map, _skip_target + ) + coupling_map = _parse_coupling_map(coupling_map, backend) + + if target is None: + if backend is not None and _no_loose_constraints: + # If a backend is specified without loose constraints, use its target directly. + target = backend.target + elif not _skip_target: + # Only parse backend properties when the target isn't skipped to + # preserve the former behavior of transpile. + backend_properties = _parse_backend_properties(backend_properties, backend) + # Build target from constraints. + target = Target.from_configuration( + basis_gates=basis_gates, + num_qubits=backend.num_qubits if backend is not None else None, + coupling_map=coupling_map, + # If the instruction map has custom gates, do not give as config, the information + # will be added to the target with update_from_instruction_schedule_map + inst_map=inst_map if inst_map and not inst_map.has_custom_gate() else None, + backend_properties=backend_properties, + instruction_durations=instruction_durations, + concurrent_measurements=( + backend.target.concurrent_measurements if backend is not None else None + ), + dt=dt, + timing_constraints=timing_constraints, + custom_name_mapping=name_mapping, + ) + + # Update target with custom gate information. Note that this is an exception to the priority + # order (target > loose constraints), added to handle custom gates for scheduling passes. + if target is not None and _given_inst_map and inst_map.has_custom_gate(): + target = copy.deepcopy(target) + target.update_from_instruction_schedule_map(inst_map) if target is not None: if coupling_map is None: @@ -262,6 +405,119 @@ def generate_preset_pass_manager( return pm +def _parse_basis_gates(basis_gates, backend, inst_map, skip_target): + name_mapping = {} + standard_gates = get_standard_gate_name_mapping() + # Add control flow gates by default to basis set + default_gates = {"measure", "delay", "reset"}.union(CONTROL_FLOW_OP_NAMES) + + try: + instructions = set(basis_gates) + for name in default_gates: + if name not in instructions: + instructions.add(name) + except TypeError: + instructions = None + + if backend is None: + # Check for custom instructions + if instructions is None: + return None, name_mapping, skip_target + + for inst in instructions: + if inst not in standard_gates or inst not in default_gates: + skip_target = True + break + + return list(instructions), name_mapping, skip_target + + instructions = instructions or backend.operation_names + name_mapping.update( + {name: backend.target.operation_from_name(name) for name in backend.operation_names} + ) + + # Check for custom instructions before removing calibrations + for inst in instructions: + if inst not in standard_gates or inst not in default_gates: + skip_target = True + break + + # Remove calibrated instructions, as they will be added later from the instruction schedule map + if inst_map is not None and not skip_target: + for inst in inst_map.instructions: + for qubit in inst_map.qubits_with_instruction(inst): + entry = inst_map._get_calibration_entry(inst, qubit) + if entry.user_provided and inst in instructions: + instructions.remove(inst) + + return list(instructions) if instructions else None, name_mapping, skip_target + + +def _parse_inst_map(inst_map, backend): + # try getting inst_map from user, else backend + if inst_map is None and backend is not None: + inst_map = backend.target.instruction_schedule_map() + return inst_map + + +def _parse_backend_properties(backend_properties, backend): + # try getting backend_props from user, else backend + if backend_properties is None and backend is not None: + backend_properties = target_to_backend_properties(backend.target) + return backend_properties + + +def _parse_dt(dt, backend): + # try getting dt from user, else backend + if dt is None and backend is not None: + dt = backend.target.dt + return dt + + +def _parse_coupling_map(coupling_map, backend): + # try getting coupling_map from user, else backend + if coupling_map is None and backend is not None: + coupling_map = backend.coupling_map + + # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] + if coupling_map is None or isinstance(coupling_map, CouplingMap): + return coupling_map + if isinstance(coupling_map, list) and all( + isinstance(i, list) and len(i) == 2 for i in coupling_map + ): + return CouplingMap(coupling_map) + else: + raise TranspilerError( + "Only a single input coupling map can be used with generate_preset_pass_manager()." + ) + + +def _parse_instruction_durations(backend, inst_durations, dt): + """Create a list of ``InstructionDuration``s. If ``inst_durations`` is provided, + the backend will be ignored, otherwise, the durations will be populated from the + backend. + """ + final_durations = InstructionDurations() + if not inst_durations: + backend_durations = InstructionDurations() + if backend is not None: + backend_durations = backend.instruction_durations + final_durations.update(backend_durations, dt or backend_durations.dt) + else: + final_durations.update(inst_durations, dt or getattr(inst_durations, "dt", None)) + return final_durations + + +def _parse_timing_constraints(backend, timing_constraints): + if isinstance(timing_constraints, TimingConstraints): + return timing_constraints + if backend is None and timing_constraints is None: + timing_constraints = TimingConstraints() + elif backend is not None: + timing_constraints = backend.target.timing_constraints() + return timing_constraints + + __all__ = [ "level_0_pass_manager", "level_1_pass_manager", diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index fc01f6eface..f85b4d113c1 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -26,7 +26,7 @@ from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import CheckMap from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements -from qiskit.transpiler.passes import OptimizeSwapBeforeMeasure +from qiskit.transpiler.passes import ElidePermutations from qiskit.transpiler.passes import RemoveDiagonalGatesBeforeMeasure from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.preset_passmanagers.plugin import ( @@ -133,7 +133,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.unitary_synthesis_plugin_config, pass_manager_config.hls_config, ) - init.append(OptimizeSwapBeforeMeasure()) + init.append(ElidePermutations()) init.append(RemoveDiagonalGatesBeforeMeasure()) init.append( InverseCancellation( @@ -154,7 +154,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 1b77e7dbd26..ec479f9e006 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -518,7 +518,7 @@ def generate_translation_passmanager( ), ] else: - raise TranspilerError("Invalid translation method %s." % method) + raise TranspilerError(f"Invalid translation method {method}.") return PassManager(unroll) @@ -557,7 +557,7 @@ def generate_scheduling( try: scheduling.append(scheduler[scheduling_method](instruction_durations, target=target)) except KeyError as ex: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex + raise TranspilerError(f"Invalid scheduling method {scheduling_method}.") from ex elif instruction_durations: # No scheduling. But do unit conversion for delays. def _contains_delay(property_set): diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 8d609ce3b8a..8805deece50 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -411,7 +411,7 @@ def add_instruction(self, instruction, properties=None, name=None): if properties is None: properties = {None: None} if instruction_name in self._gate_map: - raise AttributeError("Instruction %s is already in the target" % instruction_name) + raise AttributeError(f"Instruction {instruction_name} is already in the target") self._gate_name_map[instruction_name] = instruction if is_class: qargs_val = {None: None} @@ -426,7 +426,9 @@ def add_instruction(self, instruction, properties=None, name=None): f"of qubits in the properties dictionary: {qarg}" ) if qarg is not None: - self.num_qubits = max(self.num_qubits, max(qarg) + 1) + self.num_qubits = max( + self.num_qubits if self.num_qubits is not None else 0, max(qarg) + 1 + ) qargs_val[qarg] = properties[qarg] self._qarg_gate_map[qarg].add(instruction_name) self._gate_map[instruction_name] = qargs_val @@ -814,7 +816,7 @@ def check_obj_params(parameters, obj): if qargs in self._gate_map[op_name]: return True if self._gate_map[op_name] is None or None in self._gate_map[op_name]: - return self._gate_name_map[op_name].num_qubits == len(qargs) and all( + return obj.num_qubits == len(qargs) and all( x < self.num_qubits for x in qargs ) return False @@ -890,7 +892,7 @@ def has_calibration( return False if qargs not in self._gate_map[operation_name]: return False - return getattr(self._gate_map[operation_name][qargs], "_calibration") is not None + return getattr(self._gate_map[operation_name][qargs], "_calibration", None) is not None def get_calibration( self, @@ -940,7 +942,9 @@ def instructions(self): is globally defined. """ return [ - (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op] + (self._gate_name_map[op], qarg) + for op, qargs in self._gate_map.items() + for qarg in qargs ] def instruction_properties(self, index): @@ -979,13 +983,14 @@ def instruction_properties(self, index): InstructionProperties: The instruction properties for the specified instruction tuple """ instruction_properties = [ - inst_props for op in self._gate_map for _, inst_props in self._gate_map[op].items() + inst_props for qargs in self._gate_map.values() for inst_props in qargs.values() ] return instruction_properties[index] def _build_coupling_graph(self): self._coupling_graph = rx.PyDiGraph(multigraph=False) - self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) + if self.num_qubits is not None: + self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) for gate, qarg_map in self._gate_map.items(): if qarg_map is None: if self._gate_name_map[gate].num_qubits == 2: @@ -1057,7 +1062,7 @@ def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): for qargs, properties in self._gate_map[two_q_gate].items(): if len(qargs) != 2: raise ValueError( - "Specified two_q_gate: %s is not a 2 qubit instruction" % two_q_gate + f"Specified two_q_gate: {two_q_gate} is not a 2 qubit instruction" ) coupling_graph.add_edge(*qargs, {two_q_gate: properties}) cmap = CouplingMap() diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 666bf53d962..0ca52fc5c8c 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -31,6 +31,7 @@ class UserConfig: circuit_mpl_style = default circuit_mpl_style_path = ~/.qiskit: circuit_reverse_bits = True + circuit_idle_wires = False transpile_optimization_level = 1 parallel = False num_processes = 4 @@ -62,9 +63,9 @@ def read_config_file(self): if circuit_drawer: if circuit_drawer not in ["text", "mpl", "latex", "latex_source", "auto"]: raise exceptions.QiskitUserConfigError( - "%s is not a valid circuit drawer backend. Must be " + f"{circuit_drawer} is not a valid circuit drawer backend. Must be " "either 'text', 'mpl', 'latex', 'latex_source', or " - "'auto'." % circuit_drawer + "'auto'." ) self.settings["circuit_drawer"] = circuit_drawer @@ -95,8 +96,8 @@ def read_config_file(self): if circuit_mpl_style: if not isinstance(circuit_mpl_style, str): warn( - "%s is not a valid mpl circuit style. Must be " - "a text string. Will not load style." % circuit_mpl_style, + f"{circuit_mpl_style} is not a valid mpl circuit style. Must be " + "a text string. Will not load style.", UserWarning, 2, ) @@ -111,8 +112,8 @@ def read_config_file(self): for path in cpath_list: if not os.path.exists(os.path.expanduser(path)): warn( - "%s is not a valid circuit mpl style path." - " Correct the path in ~/.qiskit/settings.conf." % path, + f"{path} is not a valid circuit mpl style path." + " Correct the path in ~/.qiskit/settings.conf.", UserWarning, 2, ) @@ -130,6 +131,18 @@ def read_config_file(self): if circuit_reverse_bits is not None: self.settings["circuit_reverse_bits"] = circuit_reverse_bits + # Parse circuit_idle_wires + try: + circuit_idle_wires = self.config_parser.getboolean( + "default", "circuit_idle_wires", fallback=None + ) + except ValueError as err: + raise exceptions.QiskitUserConfigError( + f"Value assigned to circuit_idle_wires is not valid. {str(err)}" + ) + if circuit_idle_wires is not None: + self.settings["circuit_idle_wires"] = circuit_idle_wires + # Parse transpile_optimization_level transpile_optimization_level = self.config_parser.getint( "default", "transpile_optimization_level", fallback=-1 @@ -191,6 +204,7 @@ def set_config(key, value, section=None, file_path=None): "circuit_mpl_style", "circuit_mpl_style_path", "circuit_reverse_bits", + "circuit_idle_wires", "transpile_optimization_level", "parallel", "num_processes", diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index f5256f6f11e..30935437ebf 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -44,7 +44,7 @@ .. autofunction:: local_hardware_info .. autofunction:: is_main_process -A helper function for calling a custom function with python +A helper function for calling a custom function with Python :class:`~concurrent.futures.ProcessPoolExecutor`. Tasks can be executed in parallel using this function. .. autofunction:: parallel_map @@ -70,7 +70,7 @@ from . import optionals -from .parallel import parallel_map +from .parallel import parallel_map, should_run_in_parallel __all__ = [ "LazyDependencyManager", @@ -85,4 +85,5 @@ "is_main_process", "apply_prefix", "parallel_map", + "should_run_in_parallel", ] diff --git a/qiskit/utils/classtools.py b/qiskit/utils/classtools.py index 1e58b1ad2b9..7dae35d1349 100644 --- a/qiskit/utils/classtools.py +++ b/qiskit/utils/classtools.py @@ -31,7 +31,7 @@ class _lift_to_method: # pylint: disable=invalid-name returned unchanged if so, otherwise it is turned into the default implementation for functions, which makes them bindable to instances. - Python-space functions and lambdas already have this behaviour, but builtins like ``print`` + Python-space functions and lambdas already have this behavior, but builtins like ``print`` don't; using this class allows us to do:: wrap_method(MyClass, "maybe_mutates_arguments", before=print, after=print) @@ -49,7 +49,7 @@ def __new__(cls, method): def __init__(self, method): if method is self: - # Prevent double-initialisation if we are passed an instance of this object to lift. + # Prevent double-initialization if we are passed an instance of this object to lift. return self._method = method @@ -118,7 +118,7 @@ def out(*args, **kwargs): def wrap_method(cls: Type, name: str, *, before: Callable = None, after: Callable = None): - """Wrap the functionality the instance- or class method ``cls.name`` with additional behaviour + """Wrap the functionality the instance- or class method ``cls.name`` with additional behavior ``before`` and ``after``. This mutates ``cls``, replacing the attribute ``name`` with the new functionality. This is diff --git a/qiskit/utils/lazy_tester.py b/qiskit/utils/lazy_tester.py index f2c4c380315..58c5931fd5e 100644 --- a/qiskit/utils/lazy_tester.py +++ b/qiskit/utils/lazy_tester.py @@ -174,7 +174,7 @@ def require_in_instance(self, feature_or_class: str) -> Callable[[Type], Type]: def require_in_instance(self, feature_or_class): """A class decorator that requires the dependency is available when the class is - initialised. This decorator can be used even if the class does not define an ``__init__`` + initialized. This decorator can be used even if the class does not define an ``__init__`` method. Args: @@ -186,7 +186,7 @@ def require_in_instance(self, feature_or_class): Returns: Callable: a class decorator that ensures that the wrapped feature is present if the - class is initialised. + class is initialized. """ if isinstance(feature_or_class, str): feature = feature_or_class diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index f2b1e56e112..f2e6c860faa 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -79,7 +79,7 @@ * - .. py:data:: HAS_IPYTHON - If `the IPython kernel `__ is available, certain additional - visualisations and line magics are made available. + visualizations and line magics are made available. * - .. py:data:: HAS_IPYWIDGETS - Monitoring widgets for jobs running on external backends can be provided if `ipywidgets @@ -94,7 +94,7 @@ interactivity features. * - .. py:data:: HAS_MATPLOTLIB - - Qiskit provides several visualisation tools in the :mod:`.visualization` module. + - 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. @@ -116,7 +116,7 @@ :class:`.DAGCircuit` in certain modes. * - .. py:data:: HAS_PYDOT - - For some graph visualisations, Qiskit uses `pydot `__ as an + - For some graph visualizations, Qiskit uses `pydot `__ as an interface to GraphViz (see :data:`HAS_GRAPHVIZ`). * - .. py:data:: HAS_PYGMENTS @@ -134,7 +134,7 @@ `__. * - .. py:data:: HAS_SEABORN - - Qiskit provides several visualisation tools in the :mod:`.visualization` module. Some + - 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. @@ -179,16 +179,16 @@ :widths: 25 75 * - .. py:data:: HAS_GRAPHVIZ - - For some graph visualisations, Qiskit uses the `GraphViz `__ - visualisation tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + - For some graph visualizations, Qiskit uses the `GraphViz `__ + visualization tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). * - .. py:data:: HAS_PDFLATEX - - Visualisation tools that use LaTeX in their output, such as the circuit drawers, require + - 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 - - Visualisation tools that convert LaTeX-generated files into rasterised images use the + - Visualization tools that convert LaTeX-generated files into rasterized images use the ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools `__. diff --git a/qiskit/utils/parallel.py b/qiskit/utils/parallel.py index d46036a478f..f87eeb81596 100644 --- a/qiskit/utils/parallel.py +++ b/qiskit/utils/parallel.py @@ -48,6 +48,8 @@ from the multiprocessing library. """ +from __future__ import annotations + import os from concurrent.futures import ProcessPoolExecutor import sys @@ -101,6 +103,21 @@ def _task_wrapper(param): return task(value, *task_args, **task_kwargs) +def should_run_in_parallel(num_processes: int | None = None) -> bool: + """Return whether the current parallelisation configuration suggests that we should run things + like :func:`parallel_map` in parallel (``True``) or degrade to serial (``False``). + + Args: + num_processes: the number of processes requested for use (if given). + """ + num_processes = CPU_COUNT if num_processes is None else num_processes + return ( + num_processes > 1 + and os.getenv("QISKIT_IN_PARALLEL", "FALSE") == "FALSE" + and CONFIG.get("parallel_enabled", PARALLEL_DEFAULT) + ) + + def parallel_map( # pylint: disable=dangerous-default-value task, values, task_args=(), task_kwargs={}, num_processes=CPU_COUNT ): @@ -110,21 +127,20 @@ def parallel_map( # pylint: disable=dangerous-default-value result = [task(value, *task_args, **task_kwargs) for value in values] - On Windows this function defaults to a serial implementation to avoid the - overhead from spawning processes in Windows. + This will parallelise the results if the number of ``values`` is greater than one, and the + current system configuration permits parallelization. Args: task (func): Function that is to be called for each value in ``values``. - values (array_like): List or array of values for which the ``task`` - function is to be evaluated. + values (array_like): List or array of values for which the ``task`` function is to be + evaluated. task_args (list): Optional additional arguments to the ``task`` function. task_kwargs (dict): Optional additional keyword argument to the ``task`` function. num_processes (int): Number of processes to spawn. Returns: - result: The result list contains the value of - ``task(value, *task_args, **task_kwargs)`` for - each value in ``values``. + result: The result list contains the value of ``task(value, *task_args, **task_kwargs)`` for + each value in ``values``. Raises: QiskitError: If user interrupts via keyboard. @@ -147,12 +163,7 @@ def func(_): if len(values) == 1: return [task(values[0], *task_args, **task_kwargs)] - # Run in parallel if not Win and not in parallel already - if ( - num_processes > 1 - and os.getenv("QISKIT_IN_PARALLEL") == "FALSE" - and CONFIG.get("parallel_enabled", PARALLEL_DEFAULT) - ): + if should_run_in_parallel(num_processes): os.environ["QISKIT_IN_PARALLEL"] = "TRUE" try: results = [] @@ -173,8 +184,6 @@ def func(_): os.environ["QISKIT_IN_PARALLEL"] = "FALSE" return results - # Cannot do parallel on Windows , if another parallel_map is running in parallel, - # or len(values) == 1. results = [] for _, value in enumerate(values): result = task(value, *task_args, **task_kwargs) diff --git a/qiskit/visualization/bloch.py b/qiskit/visualization/bloch.py index bae0633a811..2855c6ba965 100644 --- a/qiskit/visualization/bloch.py +++ b/qiskit/visualization/bloch.py @@ -290,7 +290,7 @@ def set_label_convention(self, convention): self.zlabel = ["$\\circlearrowleft$", "$\\circlearrowright$"] self.xlabel = ["$\\leftrightarrow$", "$\\updownarrow$"] else: - raise Exception("No such convention.") + raise ValueError("No such convention.") def __str__(self): string = "" @@ -396,7 +396,7 @@ def add_annotation(self, state_or_vector, text, **kwargs): if isinstance(state_or_vector, (list, np.ndarray, tuple)) and len(state_or_vector) == 3: vec = state_or_vector else: - raise Exception("Position needs to be specified by a qubit " + "state or a 3D vector.") + raise TypeError("Position needs to be specified by a qubit state or a 3D vector.") self.annotations.append({"position": vec, "text": text, "opts": kwargs}) def make_sphere(self): @@ -587,11 +587,11 @@ def plot_axes_labels(self): def plot_vectors(self): """Plot vector""" # -X and Y data are switched for plotting purposes - for k in range(len(self.vectors)): + for k, vector in enumerate(self.vectors): - xs3d = self.vectors[k][1] * np.array([0, 1]) - ys3d = -self.vectors[k][0] * np.array([0, 1]) - zs3d = self.vectors[k][2] * np.array([0, 1]) + xs3d = vector[1] * np.array([0, 1]) + ys3d = -vector[0] * np.array([0, 1]) + zs3d = vector[2] * np.array([0, 1]) color = self.vector_color[np.mod(k, len(self.vector_color))] @@ -617,15 +617,10 @@ def plot_vectors(self): def plot_points(self): """Plot points""" # -X and Y data are switched for plotting purposes - for k in range(len(self.points)): - num = len(self.points[k][0]) + for k, point in enumerate(self.points): + num = len(point[0]) dist = [ - np.sqrt( - self.points[k][0][j] ** 2 - + self.points[k][1][j] ** 2 - + self.points[k][2][j] ** 2 - ) - for j in range(num) + np.sqrt(point[0][j] ** 2 + point[1][j] ** 2 + point[2][j] ** 2) for j in range(num) ] if any(abs(dist - dist[0]) / dist[0] > 1e-12): # combine arrays so that they can be sorted together @@ -637,9 +632,9 @@ def plot_points(self): indperm = np.arange(num) if self.point_style[k] == "s": self.axes.scatter( - np.real(self.points[k][1][indperm]), - -np.real(self.points[k][0][indperm]), - np.real(self.points[k][2][indperm]), + np.real(point[1][indperm]), + -np.real(point[0][indperm]), + np.real(point[2][indperm]), s=self.point_size[np.mod(k, len(self.point_size))], alpha=1, edgecolor=None, @@ -656,9 +651,9 @@ def plot_points(self): marker = self.point_marker[np.mod(k, len(self.point_marker))] pnt_size = self.point_size[np.mod(k, len(self.point_size))] self.axes.scatter( - np.real(self.points[k][1][indperm]), - -np.real(self.points[k][0][indperm]), - np.real(self.points[k][2][indperm]), + np.real(point[1][indperm]), + -np.real(point[0][indperm]), + np.real(point[2][indperm]), s=pnt_size, alpha=1, edgecolor=None, @@ -670,9 +665,9 @@ def plot_points(self): elif self.point_style[k] == "l": color = self.point_color[np.mod(k, len(self.point_color))] self.axes.plot( - np.real(self.points[k][1]), - -np.real(self.points[k][0]), - np.real(self.points[k][2]), + np.real(point[1]), + -np.real(point[0]), + np.real(point[2]), alpha=0.75, zdir="z", color=color, diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index c14bb3d46c2..ca29794b962 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -112,7 +112,7 @@ def get_gate_ctrl_text(op, drawer, style=None, calibrations=None): gate_text = gate_text.replace("-", "\\mbox{-}") ctrl_text = f"$\\mathrm{{{ctrl_text}}}$" - # Only captitalize internally-created gate or instruction names + # Only capitalize internally-created gate or instruction names elif ( (gate_text == op.name and op_type not in (Gate, Instruction)) or (gate_text == base_name and base_type not in (Gate, Instruction)) diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index bea6021c23a..146de9d32de 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -63,7 +63,7 @@ def circuit_drawer( reverse_bits: bool | None = None, justify: str | None = None, vertical_compression: str | None = "medium", - idle_wires: bool = True, + idle_wires: bool | None = None, with_layout: bool = True, fold: int | None = None, # The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be @@ -115,7 +115,7 @@ def circuit_drawer( output: Select the output method to use for drawing the circuit. Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``. - By default the `text` drawer is used unless the user config file + By default, the ``text`` drawer is used unless the user config file (usually ``~/.qiskit/settings.conf``) has an alternative backend set as the default. For example, ``circuit_drawer = latex``. If the output kwarg is set, that backend will always be used over the default in @@ -141,7 +141,9 @@ def circuit_drawer( will take less vertical room. Default is ``medium``. Only used by the ``text`` output, will be silently ignored otherwise. idle_wires: Include idle wires (wires with no circuit elements) - in output visualization. Default is ``True``. + in output visualization. Default is ``True`` unless the + user config file (usually ``~/.qiskit/settings.conf``) has an + alternative value set. For example, ``circuit_idle_wires = False``. with_layout: Include layout information, with labels on the physical layout. Default is ``True``. fold: Sets pagination. It can be disabled using -1. In ``text``, @@ -200,6 +202,7 @@ def circuit_drawer( # Get default from config file else use text default_output = "text" default_reverse_bits = False + default_idle_wires = config.get("circuit_idle_wires", True) if config: default_output = config.get("circuit_drawer", "text") if default_output == "auto": @@ -215,6 +218,9 @@ def circuit_drawer( if reverse_bits is None: reverse_bits = default_reverse_bits + if idle_wires is None: + idle_wires = default_idle_wires + if wire_order is not None and reverse_bits: raise VisualizationError( "The wire_order option cannot be set when the reverse_bits option is True." @@ -339,8 +345,8 @@ def check_clbit_in_inst(circuit, cregbundle): ) else: raise VisualizationError( - "Invalid output type %s selected. The only valid choices " - "are text, latex, latex_source, and mpl" % output + f"Invalid output type {output} selected. The only valid choices " + "are text, latex, latex_source, and mpl" ) if image and interactive: image.show() diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index ad4b8e070e1..4cb233277c0 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -213,17 +213,22 @@ def _initialize_latex_array(self): self._latex.append([" "] * (self._img_depth + 1)) # display the bit/register labels - for wire in self._wire_map: + for wire, index in self._wire_map.items(): if isinstance(wire, ClassicalRegister): register = wire - index = self._wire_map[wire] + wire_label = get_wire_label( + "latex", register, index, layout=self._layout, cregbundle=self._cregbundle + ) else: register, bit_index, reg_index = get_bit_reg_index(self._circuit, wire) - index = bit_index if register is None else reg_index + wire_label = get_wire_label( + "latex", + register, + bit_index if register is None else reg_index, + layout=self._layout, + cregbundle=self._cregbundle, + ) - wire_label = get_wire_label( - "latex", register, index, layout=self._layout, cregbundle=self._cregbundle - ) wire_label += " : " if self._initial_state: wire_label += "\\ket{{0}}" if isinstance(wire, Qubit) else "0" @@ -234,7 +239,7 @@ def _initialize_latex_array(self): self._latex[pos][1] = "\\lstick{/_{_{" + str(register.size) + "}}} \\cw" wire_label = f"\\mathrm{{{wire_label}}}" else: - pos = self._wire_map[wire] + pos = index self._latex[pos][0] = "\\nghost{" + wire_label + " & " + "\\lstick{" + wire_label def _get_image_depth(self): @@ -410,7 +415,7 @@ def _build_latex_array(self): cwire_list = [] if len(wire_list) == 1 and not node.cargs: - self._latex[wire_list[0]][column] = "\\gate{%s}" % gate_text + self._latex[wire_list[0]][column] = f"\\gate{{{gate_text}}}" elif isinstance(op, ControlledGate): num_cols_op = self._build_ctrl_gate(op, gate_text, wire_list, column) @@ -438,20 +443,20 @@ def _build_multi_gate(self, op, gate_text, wire_list, cwire_list, col): self._latex[wire_min][col] = ( f"\\multigate{{{wire_max - wire_min}}}{{{gate_text}}}_" + "<" * (len(str(wire_ind)) + 2) - + "{%s}" % wire_ind + + f"{{{wire_ind}}}" ) for wire in range(wire_min + 1, wire_max + 1): if wire < cwire_start: - ghost_box = "\\ghost{%s}" % gate_text + ghost_box = f"\\ghost{{{gate_text}}}" if wire in wire_list: wire_ind = wire_list.index(wire) else: - ghost_box = "\\cghost{%s}" % gate_text + ghost_box = f"\\cghost{{{gate_text}}}" if wire in cwire_list: wire_ind = cwire_list.index(wire) if wire in wire_list + cwire_list: self._latex[wire][col] = ( - ghost_box + "_" + "<" * (len(str(wire_ind)) + 2) + "{%s}" % wire_ind + ghost_box + "_" + "<" * (len(str(wire_ind)) + 2) + f"{{{wire_ind}}}" ) else: self._latex[wire][col] = ghost_box @@ -479,7 +484,7 @@ def _build_ctrl_gate(self, op, gate_text, wire_list, col): elif isinstance(op.base_gate, (U1Gate, PhaseGate)): num_cols_op = self._build_symmetric_gate(op, gate_text, wire_list, col) else: - self._latex[wireqargs[0]][col] = "\\gate{%s}" % gate_text + self._latex[wireqargs[0]][col] = f"\\gate{{{gate_text}}}" else: # Treat special cases of swap and rzz gates if isinstance(op.base_gate, (SwapGate, RZZGate)): @@ -522,7 +527,7 @@ def _build_symmetric_gate(self, op, gate_text, wire_list, col): ) self._latex[wire_last][col] = "\\control \\qw" # Put side text to the right between bottom wire in wire_list and the one above it - self._latex[wire_max - 1][col + 1] = "\\dstick{\\hspace{2.0em}%s} \\qw" % gate_text + self._latex[wire_max - 1][col + 1] = f"\\dstick{{\\hspace{{2.0em}}{gate_text}}} \\qw" return 4 # num_cols for side text gates def _build_measure(self, node, col): @@ -539,11 +544,9 @@ def _build_measure(self, node, col): idx_str = str(self._circuit.find_bit(node.cargs[0]).registers[0][1]) else: wire2 = self._wire_map[node.cargs[0]] - - self._latex[wire2][col] = "\\dstick{_{_{\\hspace{%sem}%s}}} \\cw \\ar @{<=} [-%s,0]" % ( - cond_offset, - idx_str, - str(wire2 - wire1), + self._latex[wire2][col] = ( + f"\\dstick{{_{{_{{\\hspace{{{cond_offset}em}}{idx_str}}}}}}} " + f"\\cw \\ar @{{<=}} [-{str(wire2 - wire1)},0]" ) else: wire2 = self._wire_map[node.cargs[0]] @@ -568,7 +571,7 @@ def _build_barrier(self, node, col): if node.op.label is not None: pos = indexes[0] label = node.op.label.replace(" ", "\\,") - self._latex[pos][col] = "\\cds{0}{^{\\mathrm{%s}}}" % label + self._latex[pos][col] = f"\\cds{{0}}{{^{{\\mathrm{{{label}}}}}}}" def _add_controls(self, wire_list, ctrlqargs, ctrl_state, col): """Add one or more controls to a gate""" @@ -610,21 +613,20 @@ def _add_condition(self, op, wire_list, col): ) gap = cwire - max(wire_list) control = "\\control" if op.condition[1] else "\\controlo" - self._latex[cwire][col] = f"{control}" + " \\cw^(%s){^{\\mathtt{%s}}} \\cwx[-%s]" % ( - meas_offset, - label, - str(gap), - ) + self._latex[cwire][ + col + ] = f"{control} \\cw^({meas_offset}){{^{{\\mathtt{{{label}}}}}}} \\cwx[-{str(gap)}]" + # If condition is a register and cregbundle is false else: # First sort the val_bits in the order of the register bits in the circuit cond_wires = [] cond_bits = [] - for wire in self._wire_map: + for wire, index in self._wire_map.items(): reg, _, reg_index = get_bit_reg_index(self._circuit, wire) if reg == cond_reg: cond_bits.append(reg_index) - cond_wires.append(self._wire_map[wire]) + cond_wires.append(index) gap = cond_wires[0] - max(wire_list) prev_wire = cond_wires[0] diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 8d83fecb896..9c4fa25309f 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -33,6 +33,7 @@ IfElseOp, ForLoopOp, SwitchCaseOp, + CircuitError, ) from qiskit.circuit.controlflow import condition_resources from qiskit.circuit.classical import expr @@ -46,7 +47,8 @@ XGate, ZGate, ) -from qiskit.qasm3.exporter import QASM3Builder +from qiskit.qasm3 import ast +from qiskit.qasm3.exporter import _ExprBuilder from qiskit.qasm3.printer import BasicPrinter from qiskit.circuit.tools.pi_check import pi_check @@ -369,7 +371,7 @@ def draw(self, filename=None, verbose=False): # Once the scaling factor has been determined, the global phase, register names # and numbers, wires, and gates are drawn if self._global_phase: - plt_mod.text(xl, yt, "Global Phase: %s" % pi_check(self._global_phase, output="mpl")) + plt_mod.text(xl, yt, f"Global Phase: {pi_check(self._global_phase, output='mpl')}") self._draw_regs_wires(num_folds, xmax, max_x_index, qubits_dict, clbits_dict, glob_data) self._draw_ops( self._nodes, @@ -393,7 +395,7 @@ def draw(self, filename=None, verbose=False): matplotlib_close_if_inline(mpl_figure) return mpl_figure - def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, builder=None): + def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data): """Compute the layer_widths for the layers""" layer_widths = {} @@ -482,18 +484,41 @@ def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, build if (isinstance(op, SwitchCaseOp) and isinstance(op.target, expr.Expr)) or ( getattr(op, "condition", None) and isinstance(op.condition, expr.Expr) ): - condition = op.target if isinstance(op, SwitchCaseOp) else op.condition - if builder is None: - builder = QASM3Builder( - outer_circuit, - includeslist=("stdgates.inc",), - basis_gates=("U",), - disable_constants=False, - allow_aliasing=False, + + def lookup_var(var): + """Look up a classical-expression variable or register/bit in our + internal symbol table, and return an OQ3-like identifier.""" + # We don't attempt to disambiguate anything like register/var naming + # collisions; we already don't really show classical variables. + if isinstance(var, expr.Var): + return ast.Identifier(var.name) + if isinstance(var, ClassicalRegister): + return ast.Identifier(var.name) + # Single clbit. This is not actually the correct way to lookup a bit on + # the circuit (it doesn't handle bit bindings fully), but the mpl + # drawer doesn't completely track inner-outer _bit_ bindings, only + # inner-indices, so we can't fully recover the information losslessly. + # Since most control-flow uses the control-flow builders, we should + # decay to something usable most of the time. + try: + register, bit_index, reg_index = get_bit_reg_index( + outer_circuit, var + ) + except CircuitError: + # We failed to find the bit due to binding problems - fall back to + # something that's probably wrong, but at least disambiguating. + return ast.Identifier(f"bit{wire_map[var]}") + if register is None: + return ast.Identifier(f"bit{bit_index}") + return ast.SubscriptedIdentifier( + register.name, ast.IntegerLiteral(reg_index) ) - builder.build_classical_declarations() + + condition = op.target if isinstance(op, SwitchCaseOp) else op.condition stream = StringIO() - BasicPrinter(stream, indent=" ").visit(builder.build_expression(condition)) + BasicPrinter(stream, indent=" ").visit( + condition.accept(_ExprBuilder(lookup_var)) + ) expr_text = stream.getvalue() # Truncate expr_text so that first gate is no more than about 3 x_index's over if len(expr_text) > self._expr_len: @@ -570,7 +595,7 @@ def _get_layer_widths(self, node_data, wire_map, outer_circuit, glob_data, build # Recursively call _get_layer_widths for the circuit inside the ControlFlowOp flow_widths = flow_drawer._get_layer_widths( - node_data, flow_wire_map, outer_circuit, glob_data, builder + node_data, flow_wire_map, outer_circuit, glob_data ) layer_widths.update(flow_widths) @@ -868,7 +893,7 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic this_clbit_dict = {} for clbit in clbits_dict.values(): y = clbit["y"] - fold_num * (glob_data["n_lines"] + 1) - if y not in this_clbit_dict.keys(): + if y not in this_clbit_dict: this_clbit_dict[y] = { "val": 1, "wire_label": clbit["wire_label"], @@ -1243,6 +1268,11 @@ def _condition(self, node, node_data, wire_map, outer_circuit, cond_xy, glob_dat self._ax.add_patch(box) xy_plot.append(xy) + if not xy_plot: + # Expression that's only on new-style `expr.Var` nodes, and doesn't need any vertical + # line drawing. + return + qubit_b = min(node_data[node].q_xy, key=lambda xy: xy[1]) clbit_b = min(xy_plot, key=lambda xy: xy[1]) @@ -1554,6 +1584,8 @@ def _flow_op_gate(self, node, node_data, glob_data): flow_text = " For" elif isinstance(node.op, SwitchCaseOp): flow_text = "Switch" + else: + flow_text = node.op.name # Some spacers. op_spacer moves 'Switch' back a bit for alignment, # expr_spacer moves the expr over to line up with 'Switch' and diff --git a/qiskit/visualization/circuit/qcstyle.py b/qiskit/visualization/circuit/qcstyle.py index a8432ca86a9..67ae9faaf24 100644 --- a/qiskit/visualization/circuit/qcstyle.py +++ b/qiskit/visualization/circuit/qcstyle.py @@ -72,7 +72,7 @@ class StyleDict(dict): def __setitem__(self, key: Any, value: Any) -> None: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] if key not in self.VALID_FIELDS: @@ -85,7 +85,7 @@ def __setitem__(self, key: Any, value: Any) -> None: def __getitem__(self, key: Any) -> Any: # allow using field abbreviations - if key in self.ABBREVIATIONS.keys(): + if key in self.ABBREVIATIONS: key = self.ABBREVIATIONS[key] return super().__getitem__(key) diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index 1e6137275a9..e9b7aa819e9 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -20,7 +20,7 @@ import collections import sys -from qiskit.circuit import Qubit, Clbit, ClassicalRegister +from qiskit.circuit import Qubit, Clbit, ClassicalRegister, CircuitError from qiskit.circuit import ControlledGate, Reset, Measure from qiskit.circuit import ControlFlowOp, WhileLoopOp, IfElseOp, ForLoopOp, SwitchCaseOp from qiskit.circuit.classical import expr @@ -28,8 +28,9 @@ from qiskit.circuit.library.standard_gates import IGate, RZZGate, SwapGate, SXGate, SXdgGate from qiskit.circuit.annotated_operation import _canonicalize_modifiers, ControlModifier from qiskit.circuit.tools.pi_check import pi_check -from qiskit.qasm3.exporter import QASM3Builder +from qiskit.qasm3 import ast from qiskit.qasm3.printer import BasicPrinter +from qiskit.qasm3.exporter import _ExprBuilder from ._utils import ( get_gate_ctrl_text, @@ -738,17 +739,10 @@ def __init__( self._wire_map = {} self.cregbundle = cregbundle - if encoding: - self.encoding = encoding - else: - if sys.stdout.encoding: - self.encoding = sys.stdout.encoding - else: - self.encoding = "utf8" + self.encoding = encoding or sys.stdout.encoding or "utf8" self._nest_depth = 0 # nesting depth for control flow ops self._expr_text = "" # expression text to display - self._builder = None # QASM3Builder class instance for expressions # Because jupyter calls both __repr__ and __repr_html__ for some backends, # the entire drawer can be run twice which can result in different output @@ -765,7 +759,7 @@ def _repr_html_(self): "background: #fff0;" "line-height: 1.1;" 'font-family: "Courier New",Courier,monospace">' - "%s" % self.single_string() + f"{self.single_string()}" ) def __repr__(self): @@ -786,8 +780,9 @@ def single_string(self): ) except (UnicodeEncodeError, UnicodeDecodeError): warn( - "The encoding %s has a limited charset. Consider a different encoding in your " - "environment. UTF-8 is being used instead" % self.encoding, + f"The encoding {self.encoding} has a limited charset." + " Consider a different encoding in your " + "environment. UTF-8 is being used instead", RuntimeWarning, ) self.encoding = "utf-8" @@ -867,7 +862,7 @@ def lines(self, line_length=None): lines = [] if self.global_phase: - lines.append("global phase: %s" % pi_check(self.global_phase, ndigits=5)) + lines.append(f"global phase: {pi_check(self.global_phase, ndigits=5)}") for layer_group in layer_groups: wires = list(zip(*layer_group)) @@ -894,10 +889,9 @@ def wire_names(self, with_initial_state=False): self._wire_map = get_wire_map(self._circuit, (self.qubits + self.clbits), self.cregbundle) wire_labels = [] - for wire in self._wire_map: + for wire, index in self._wire_map.items(): if isinstance(wire, ClassicalRegister): register = wire - index = self._wire_map[wire] else: register, bit_index, reg_index = get_bit_reg_index(self._circuit, wire) index = bit_index if register is None else reg_index @@ -1175,7 +1169,7 @@ def add_connected_gate(node, gates, layer, current_cons, gate_wire_map): elif isinstance(op, RZZGate): # rzz - connection_label = "ZZ%s" % params + connection_label = f"ZZ{params}" gates = [Bullet(conditional=conditional), Bullet(conditional=conditional)] add_connected_gate(node, gates, layer, current_cons, gate_wire_map) @@ -1218,7 +1212,7 @@ def add_connected_gate(node, gates, layer, current_cons, gate_wire_map): add_connected_gate(node, gates, layer, current_cons, gate_wire_map) elif base_gate.name == "rzz": # crzz - connection_label = "ZZ%s" % params + connection_label = f"ZZ{params}" gates += [Bullet(conditional=conditional), Bullet(conditional=conditional)] elif len(rest) > 1: top_connect = "┴" if controlled_top else None @@ -1306,25 +1300,44 @@ def add_control_flow(self, node, layers, wire_map): if (isinstance(node.op, SwitchCaseOp) and isinstance(node.op.target, expr.Expr)) or ( getattr(node.op, "condition", None) and isinstance(node.op.condition, expr.Expr) ): + + def lookup_var(var): + """Look up a classical-expression variable or register/bit in our internal symbol + table, and return an OQ3-like identifier.""" + # We don't attempt to disambiguate anything like register/var naming collisions; we + # already don't really show classical variables. + if isinstance(var, expr.Var): + return ast.Identifier(var.name) + if isinstance(var, ClassicalRegister): + return ast.Identifier(var.name) + # Single clbit. This is not actually the correct way to lookup a bit on the + # circuit (it doesn't handle bit bindings fully), but the text drawer doesn't + # completely track inner-outer _bit_ bindings, only inner-indices, so we can't fully + # recover the information losslessly. Since most control-flow uses the control-flow + # builders, we should decay to something usable most of the time. + try: + register, bit_index, reg_index = get_bit_reg_index(self._circuit, var) + except CircuitError: + # We failed to find the bit due to binding problems - fall back to something + # that's probably wrong, but at least disambiguating. + return ast.Identifier(f"_bit{wire_map[var]}") + if register is None: + return ast.Identifier(f"_bit{bit_index}") + return ast.SubscriptedIdentifier(register.name, ast.IntegerLiteral(reg_index)) + condition = node.op.target if isinstance(node.op, SwitchCaseOp) else node.op.condition - if self._builder is None: - self._builder = QASM3Builder( - self._circuit, - includeslist=("stdgates.inc",), - basis_gates=("U",), - disable_constants=False, - allow_aliasing=False, - ) - self._builder.build_classical_declarations() + draw_conditional = bool(node_resources(condition).clbits) stream = StringIO() - BasicPrinter(stream, indent=" ").visit(self._builder.build_expression(condition)) + BasicPrinter(stream, indent=" ").visit(condition.accept(_ExprBuilder(lookup_var))) self._expr_text = stream.getvalue() # Truncate expr_text at 30 chars or user-set expr_len if len(self._expr_text) > self.expr_len: self._expr_text = self._expr_text[: self.expr_len] + "..." + else: + draw_conditional = not isinstance(node.op, ForLoopOp) # # Draw a left box such as If, While, For, and Switch - flow_layer = self.draw_flow_box(node, wire_map, CF_LEFT) + flow_layer = self.draw_flow_box(node, wire_map, CF_LEFT, conditional=draw_conditional) layers.append(flow_layer.full_layer) # Get the list of circuits in the ControlFlowOp from the node blocks @@ -1351,7 +1364,9 @@ def add_control_flow(self, node, layers, wire_map): if circ_num > 0: # Draw a middle box such as Else and Case - flow_layer = self.draw_flow_box(node, flow_wire_map, CF_MID, circ_num - 1) + flow_layer = self.draw_flow_box( + node, flow_wire_map, CF_MID, circ_num - 1, conditional=False + ) layers.append(flow_layer.full_layer) _, _, nodes = _get_layered_instructions(circuit, wire_map=flow_wire_map) @@ -1380,14 +1395,13 @@ def add_control_flow(self, node, layers, wire_map): layers.append(flow_layer2.full_layer) # Draw the right box for End - flow_layer = self.draw_flow_box(node, flow_wire_map, CF_RIGHT) + flow_layer = self.draw_flow_box(node, flow_wire_map, CF_RIGHT, conditional=False) layers.append(flow_layer.full_layer) - def draw_flow_box(self, node, flow_wire_map, section, circ_num=0): + def draw_flow_box(self, node, flow_wire_map, section, circ_num=0, conditional=False): """Draw the left, middle, or right of a control flow box""" op = node.op - conditional = section == CF_LEFT and not isinstance(op, ForLoopOp) depth = str(self._nest_depth) if section == CF_LEFT: etext = "" diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index 73b9c30f6dc..ad2fca6e9bc 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -152,7 +152,7 @@ def node_attr_func(node): n["fillcolor"] = "lightblue" return n else: - raise VisualizationError("Unrecognized style %s for the dag_drawer." % style) + raise VisualizationError(f"Unrecognized style {style} for the dag_drawer.") edge_attr_func = None @@ -197,7 +197,7 @@ def node_attr_func(node): n["fillcolor"] = "red" return n else: - raise VisualizationError("Invalid style %s" % style) + raise VisualizationError(f"Invalid style {style}") def edge_attr_func(edge): e = {} diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index d8ffb6e1038..b950c84c902 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -1039,7 +1039,9 @@ def plot_coupling_map( graph = CouplingMap(coupling_map).graph if not plot_directed: + line_color_map = dict(zip(graph.edge_list(), line_color)) graph = graph.to_undirected(multigraph=False) + line_color = [line_color_map[edge] for edge in graph.edge_list()] for node in graph.node_indices(): graph[node] = node @@ -1122,7 +1124,13 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None Args: circuit (QuantumCircuit): Input quantum circuit. backend (Backend): Target backend. - view (str): Layout view: either 'virtual' or 'physical'. + view (str): How to label qubits in the layout. Options: + + - ``"virtual"``: Label each qubit with the index of the virtual qubit that + mapped to it. + - ``"physical"``: Label each qubit with the index of the physical qubit that it + corresponds to on the device. + qubit_coordinates (Sequence): An optional sequence input (list or array being the most common) of 2d coordinates for each qubit. The length of the sequence must match the number of qubits on the backend. The sequence diff --git a/qiskit/visualization/pulse_v2/core.py b/qiskit/visualization/pulse_v2/core.py index d60f2db030d..20686f6fb4f 100644 --- a/qiskit/visualization/pulse_v2/core.py +++ b/qiskit/visualization/pulse_v2/core.py @@ -220,7 +220,7 @@ def load_program( elif isinstance(program, (pulse.Waveform, pulse.SymbolicPulse)): self._waveform_loader(program) else: - raise VisualizationError("Data type %s is not supported." % type(program)) + raise VisualizationError(f"Data type {type(program)} is not supported.") # update time range self.set_time_range(0, program.duration, seconds=False) diff --git a/qiskit/visualization/pulse_v2/device_info.py b/qiskit/visualization/pulse_v2/device_info.py index 1e809c43abd..7898f978772 100644 --- a/qiskit/visualization/pulse_v2/device_info.py +++ b/qiskit/visualization/pulse_v2/device_info.py @@ -40,7 +40,7 @@ class :py:class:``DrawerBackendInfo`` with necessary methods to generate drawing from qiskit import pulse from qiskit.providers import BackendConfigurationError -from qiskit.providers.backend import Backend +from qiskit.providers.backend import Backend, BackendV2 class DrawerBackendInfo(ABC): @@ -106,40 +106,67 @@ def create_from_backend(cls, backend: Backend): Returns: OpenPulseBackendInfo: New configured instance. """ - configuration = backend.configuration() - defaults = backend.defaults() - - # load name - name = backend.name() - - # load cycle time - dt = configuration.dt - - # load frequencies chan_freqs = {} - - chan_freqs.update( - {pulse.DriveChannel(qind): freq for qind, freq in enumerate(defaults.qubit_freq_est)} - ) - chan_freqs.update( - {pulse.MeasureChannel(qind): freq for qind, freq in enumerate(defaults.meas_freq_est)} - ) - for qind, u_lo_mappers in enumerate(configuration.u_channel_lo): - temp_val = 0.0 + 0.0j - for u_lo_mapper in u_lo_mappers: - temp_val += defaults.qubit_freq_est[u_lo_mapper.q] * u_lo_mapper.scale - chan_freqs[pulse.ControlChannel(qind)] = temp_val.real - - # load qubit channel mapping qubit_channel_map = defaultdict(list) - for qind in range(configuration.n_qubits): - qubit_channel_map[qind].append(configuration.drive(qubit=qind)) - qubit_channel_map[qind].append(configuration.measure(qubit=qind)) - for tind in range(configuration.n_qubits): + + if hasattr(backend, "configuration") and hasattr(backend, "defaults"): + configuration = backend.configuration() + defaults = backend.defaults() + + name = configuration.backend_name + dt = configuration.dt + + # load frequencies + chan_freqs.update( + { + pulse.DriveChannel(qind): freq + for qind, freq in enumerate(defaults.qubit_freq_est) + } + ) + chan_freqs.update( + { + pulse.MeasureChannel(qind): freq + for qind, freq in enumerate(defaults.meas_freq_est) + } + ) + for qind, u_lo_mappers in enumerate(configuration.u_channel_lo): + temp_val = 0.0 + 0.0j + for u_lo_mapper in u_lo_mappers: + temp_val += defaults.qubit_freq_est[u_lo_mapper.q] * u_lo_mapper.scale + chan_freqs[pulse.ControlChannel(qind)] = temp_val.real + + # load qubit channel mapping + for qind in range(configuration.n_qubits): + qubit_channel_map[qind].append(configuration.drive(qubit=qind)) + qubit_channel_map[qind].append(configuration.measure(qubit=qind)) + for tind in range(configuration.n_qubits): + try: + qubit_channel_map[qind].extend(configuration.control(qubits=(qind, tind))) + except BackendConfigurationError: + pass + elif isinstance(backend, BackendV2): + # Pure V2 model doesn't contain channel frequency information. + name = backend.name + dt = backend.dt + + # load qubit channel mapping + for qind in range(backend.num_qubits): + # channels are NotImplemented by default so we must catch arbitrary error. + try: + qubit_channel_map[qind].append(backend.drive_channel(qind)) + except Exception: # pylint: disable=broad-except + pass try: - qubit_channel_map[qind].extend(configuration.control(qubits=(qind, tind))) - except BackendConfigurationError: + qubit_channel_map[qind].append(backend.measure_channel(qind)) + except Exception: # pylint: disable=broad-except pass + for tind in range(backend.num_qubits): + try: + qubit_channel_map[qind].extend(backend.control_channel(qubits=(qind, tind))) + except Exception: # pylint: disable=broad-except + pass + else: + raise RuntimeError("Backend object not yet supported") return OpenPulseBackendInfo( name=name, dt=dt, channel_frequency_map=chan_freqs, qubit_channel_map=qubit_channel_map diff --git a/qiskit/visualization/pulse_v2/events.py b/qiskit/visualization/pulse_v2/events.py index 4bb59cd8626..74da24b1db2 100644 --- a/qiskit/visualization/pulse_v2/events.py +++ b/qiskit/visualization/pulse_v2/events.py @@ -196,7 +196,7 @@ def get_waveforms(self) -> Iterator[PulseInstruction]: def get_frame_changes(self) -> Iterator[PulseInstruction]: """Return frame change type instructions with total frame change amount.""" - # TODO parse parametrised FCs correctly + # TODO parse parametrized FCs correctly sorted_frame_changes = sorted(self._frames.items(), key=lambda x: x[0]) diff --git a/qiskit/visualization/pulse_v2/generators/frame.py b/qiskit/visualization/pulse_v2/generators/frame.py index 394f4b4aaf8..8b71b8596bb 100644 --- a/qiskit/visualization/pulse_v2/generators/frame.py +++ b/qiskit/visualization/pulse_v2/generators/frame.py @@ -264,10 +264,9 @@ def gen_raw_operand_values_compact( freq_sci_notation = "0.0" else: abs_freq = np.abs(data.frame.freq) - freq_sci_notation = "{base:.1f}e{exp:d}".format( - base=data.frame.freq / (10 ** int(np.floor(np.log10(abs_freq)))), - exp=int(np.floor(np.log10(abs_freq))), - ) + base = data.frame.freq / (10 ** int(np.floor(np.log10(abs_freq)))) + exponent = int(np.floor(np.log10(abs_freq))) + freq_sci_notation = f"{base:.1f}e{exponent:d}" frame_info = f"{data.frame.phase:.2f}\n{freq_sci_notation}" text = drawings.TextData( diff --git a/qiskit/visualization/pulse_v2/generators/waveform.py b/qiskit/visualization/pulse_v2/generators/waveform.py index b0d90b895c7..e770f271c45 100644 --- a/qiskit/visualization/pulse_v2/generators/waveform.py +++ b/qiskit/visualization/pulse_v2/generators/waveform.py @@ -203,11 +203,10 @@ def gen_ibmq_latex_waveform_name( if frac.numerator == 1: angle = rf"\pi/{frac.denominator:d}" else: - angle = r"{num:d}/{denom:d} \pi".format( - num=frac.numerator, denom=frac.denominator - ) + angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" else: # single qubit pulse + # pylint: disable-next=consider-using-f-string op_name = r"{{\rm {}}}".format(match_dict["op"]) angle_val = match_dict["angle"] if angle_val is None: @@ -217,9 +216,7 @@ def gen_ibmq_latex_waveform_name( if frac.numerator == 1: angle = rf"\pi/{frac.denominator:d}" else: - angle = r"{num:d}/{denom:d} \pi".format( - num=frac.numerator, denom=frac.denominator - ) + angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" latex_name = rf"{op_name}({sign}{angle})" else: latex_name = None @@ -490,7 +487,7 @@ def _draw_opaque_waveform( fill_objs.append(box_obj) # parameter name - func_repr = "{func}({params})".format(func=pulse_shape, params=", ".join(pnames)) + func_repr = f"{pulse_shape}({', '.join(pnames)})" text_style = { "zorder": formatter["layer.annotate"], @@ -630,8 +627,7 @@ def _parse_waveform( meta.update(acq_data) else: raise VisualizationError( - "Unsupported instruction {inst} by " - "filled envelope.".format(inst=inst.__class__.__name__) + f"Unsupported instruction {inst.__class__.__name__} by " "filled envelope." ) meta.update( diff --git a/qiskit/visualization/pulse_v2/layouts.py b/qiskit/visualization/pulse_v2/layouts.py index 6b39dceaf56..13b42e394e9 100644 --- a/qiskit/visualization/pulse_v2/layouts.py +++ b/qiskit/visualization/pulse_v2/layouts.py @@ -373,11 +373,7 @@ def detail_title(program: Union[pulse.Waveform, pulse.Schedule], device: DrawerB # add program duration dt = device.dt * 1e9 if device.dt else 1.0 - title_str.append( - "Duration: {dur:.1f} {unit}".format( - dur=program.duration * dt, unit="ns" if device.dt else "dt" - ) - ) + title_str.append(f"Duration: {program.duration * dt:.1f} {'ns' if device.dt else 'dt'}") # add device name if device.backend_name != "no-backend": diff --git a/qiskit/visualization/pulse_v2/plotters/matplotlib.py b/qiskit/visualization/pulse_v2/plotters/matplotlib.py index e92a3418999..1788a125489 100644 --- a/qiskit/visualization/pulse_v2/plotters/matplotlib.py +++ b/qiskit/visualization/pulse_v2/plotters/matplotlib.py @@ -119,8 +119,7 @@ def draw(self): self.ax.add_patch(box) else: raise VisualizationError( - "Data {name} is not supported " - "by {plotter}".format(name=data, plotter=self.__class__.__name__) + f"Data {data} is not supported " f"by {self.__class__.__name__}" ) # axis break for pos in axis_config.axis_break_pos: diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index e862b0208e2..0e47a5fe6d7 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -971,10 +971,10 @@ def plot_state_qsphere( if show_state_phases: element_angle = (np.angle(state[i]) + (np.pi * 4)) % (np.pi * 2) if use_degrees: - element_text += "\n$%.1f^\\circ$" % (element_angle * 180 / np.pi) + element_text += f"\n${element_angle * 180 / np.pi:.1f}^\\circ$" else: element_angle = pi_check(element_angle, ndigits=3).replace("pi", "\\pi") - element_text += "\n$%s$" % (element_angle) + element_text += f"\n${element_angle}$" ax.text( xvalue_text, yvalue_text, @@ -1463,11 +1463,10 @@ def state_drawer(state, output=None, **drawer_args): return draw_func(state, **drawer_args) except KeyError as err: raise ValueError( - """'{}' is not a valid option for drawing {} objects. Please choose from: + f"""'{output}' is not a valid option for drawing {type(state).__name__} + objects. Please choose from: 'text', 'latex', 'latex_source', 'qsphere', 'hinton', - 'bloch', 'city' or 'paulivec'.""".format( - output, type(state).__name__ - ) + 'bloch', 'city' or 'paulivec'.""" ) from err diff --git a/qiskit/visualization/timeline/plotters/matplotlib.py b/qiskit/visualization/timeline/plotters/matplotlib.py index 126d0981fed..daae6fe2558 100644 --- a/qiskit/visualization/timeline/plotters/matplotlib.py +++ b/qiskit/visualization/timeline/plotters/matplotlib.py @@ -132,8 +132,7 @@ def draw(self): else: raise VisualizationError( - "Data {name} is not supported by {plotter}" - "".format(name=data, plotter=self.__class__.__name__) + f"Data {data} is not supported by {self.__class__.__name__}" ) def _time_bucket_outline( diff --git a/qiskit/visualization/transition_visualization.py b/qiskit/visualization/transition_visualization.py index f322be64a4f..a2ff7479999 100644 --- a/qiskit/visualization/transition_visualization.py +++ b/qiskit/visualization/transition_visualization.py @@ -72,10 +72,10 @@ def __mul__(self, b): return self._multiply_with_quaternion(b) elif isinstance(b, (list, tuple, np.ndarray)): if len(b) != 3: - raise Exception(f"Input vector has invalid length {len(b)}") + raise ValueError(f"Input vector has invalid length {len(b)}") return self._multiply_with_vector(b) else: - raise Exception(f"Multiplication with unknown type {type(b)}") + return NotImplemented def _multiply_with_quaternion(self, q_2): """Multiplication of quaternion with quaternion""" diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 2467665e0d0..edff5997c8f 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -28,7 +28,6 @@ notifications: ".*\\.rs$|^Cargo": - "`@mtreinish`" - "`@kevinhartman`" - - "@Eric-Arellano" "(?!.*pulse.*)\\bvisualization\\b": - "@enavarro51" "^docs/": diff --git a/releasenotes/config.yaml b/releasenotes/config.yaml index bea33ef99a1..0c621662ca8 100644 --- a/releasenotes/config.yaml +++ b/releasenotes/config.yaml @@ -87,7 +87,7 @@ template: | New features related to the qiskit.qpy module. features_quantum_info: - | - New features releated to the qiskit.quantum_info module. + New features related to the qiskit.quantum_info module. features_synthesis: - | New features related to the qiskit.synthesis module. @@ -178,7 +178,7 @@ template: | Deprecations related to the qiskit.qpy module. deprecations_quantum_info: - | - Deprecations releated to the qiskit.quantum_info module. + Deprecations related to the qiskit.quantum_info module. deprecations_synthesis: - | Deprecations related to the qiskit.synthesis module. diff --git a/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml b/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml index 9dd4a241a09..0e778de9665 100644 --- a/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml +++ b/releasenotes/notes/0.12/operator-dot-fd90e7e5ad99ff9b.yaml @@ -22,4 +22,4 @@ upgrade: from the right hand side of the operation if the left does not have ``__mul__`` defined) implements scalar multiplication (i.e. :meth:`qiskit.quantum_info.Operator.multiply`). Previously both methods - implemented scalar multiplciation. + implemented scalar multiplication. diff --git a/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml b/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml index 30561a9aadc..71fe512aa77 100644 --- a/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml +++ b/releasenotes/notes/0.13/0.13.0-release-a92553cf72c203aa.yaml @@ -8,7 +8,7 @@ prelude: | structure behind all operations to be based on `retworkx `_ for greatly improved performance. Circuit transpilation speed in the 0.13.0 release should - be significanlty faster than in previous releases. + be significantly faster than in previous releases. There has been a significant simplification to the style in which Pulse instructions are built. Now, ``Command`` s are deprecated and a unified diff --git a/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml b/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml index 59956851a81..96fb649b07d 100644 --- a/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml +++ b/releasenotes/notes/0.13/add-base-job-status-methods-3ab9646c5f5470a6.yaml @@ -8,4 +8,4 @@ features: * :meth:`~qiskit.providers.BaseJob.cancelled` * :meth:`~qiskit.providers.BaseJob.in_final_state` - These methods are used to check wheter a job is in a given job status. + These methods are used to check whether a job is in a given job status. diff --git a/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml b/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml index d8df9b53fdc..24b00a262e0 100644 --- a/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml +++ b/releasenotes/notes/0.13/default-schedule-name-51ba198cf08978cd.yaml @@ -2,6 +2,6 @@ fixes: - | Fixes a case in :meth:`qiskit.result.Result.get_counts`, where the results - for an expirement could not be referenced if the experiment was initialized + for an experiment could not be referenced if the experiment was initialized as a Schedule without a name. Fixes `#2753 `_ diff --git a/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml b/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml index 8384df8d205..ba4f07afd89 100644 --- a/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml +++ b/releasenotes/notes/0.13/qinfo-operators-0193871295190bad.yaml @@ -11,7 +11,7 @@ features: the number of two-qubit gates. - | Adds :class:`qiskit.quantum_info.SparsePauliOp` operator class. This is an - efficient representaiton of an N-qubit matrix that is sparse in the Pauli + efficient representation of an N-qubit matrix that is sparse in the Pauli basis and uses a :class:`qiskit.quantum_info.PauliTable` and vector of complex coefficients for its data structure. @@ -23,7 +23,7 @@ features: Numpy arrays or :class:`~qiskit.quantum_info.Operator` objects can be converted to a :class:`~qiskit.quantum_info.SparsePauliOp` using the `:class:`~qiskit.quantum_info.SparsePauliOp.from_operator` method. - :class:`~qiskit.quantum_info.SparsePauliOp` can be convered to a sparse + :class:`~qiskit.quantum_info.SparsePauliOp` can be converted to a sparse csr_matrix or dense Numpy array using the :class:`~qiskit.quantum_info.SparsePauliOp.to_matrix` method, or to an :class:`~qiskit.quantum_info.Operator` object using the @@ -54,7 +54,7 @@ features: :meth:`~qiskit.quantum_info.PauliTable.tensor`) between each element of the first table, with each element of the second table. - * Addition of two tables acts as list concatination of the terms in each + * Addition of two tables acts as list concatenation of the terms in each table (``+``). * Pauli tables can be sorted by lexicographic (tensor product) order or @@ -148,7 +148,7 @@ upgrade: n_qubits = 10 ham = ScalarOp(2 ** n_qubits, coeff=0) - # Add 2-body nearest neighbour terms + # Add 2-body nearest neighbor terms for j in range(n_qubits - 1): ham = ham + ZZ([j, j+1]) - | diff --git a/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml b/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml index 9a8a8453083..df465a05c43 100644 --- a/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml +++ b/releasenotes/notes/0.13/qinfo-states-7f67e2432cf0c12c.yaml @@ -175,7 +175,7 @@ deprecations: The ``add``, ``subtract``, and ``multiply`` methods of the :class:`qiskit.quantum_info.Statevector` and :class:`qiskit.quantum_info.DensityMatrix` classes are deprecated and will - be removed in a future release. Instead you shoulde use ``+``, ``-``, ``*`` + be removed in a future release. Instead you should use ``+``, ``-``, ``*`` binary operators instead. - | Deprecates :meth:`qiskit.quantum_info.Statevector.to_counts`, diff --git a/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml b/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml index 12c69ef47c5..21e14b6ef54 100644 --- a/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml +++ b/releasenotes/notes/0.13/quibit-transition-visualization-a62d0d119569fa05.yaml @@ -6,7 +6,7 @@ features: single qubit gate transitions has been added. It takes in a single qubit circuit and returns an animation of qubit state transitions on a Bloch sphere. To use this function you must have installed - the dependencies for and configured globally a matplotlib animtion + the dependencies for and configured globally a matplotlib animation writer. You can refer to the `matplotlib documentation `_ for more details on this. However, in the default case simply ensuring diff --git a/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml b/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml index e30386b5dfd..3fbb8558afc 100644 --- a/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml +++ b/releasenotes/notes/0.15/parameter-conjugate-a16fd7ae0dc18ede.yaml @@ -5,4 +5,4 @@ features: been added to the :class:`~qiskit.circuit.ParameterExpression` class. This enables calling ``numpy.conj()`` without raising an error. Since a :class:`~qiskit.circuit.ParameterExpression` object is real, it will - return itself. This behaviour is analogous to Python floats/ints. + return itself. This behavior is analogous to Python floats/ints. diff --git a/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml b/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml index a9ffc4d508d..bc60745c71d 100644 --- a/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml +++ b/releasenotes/notes/0.16/delay-in-circuit-33f0d81783ac12ea.yaml @@ -43,7 +43,7 @@ features: of scheduled circuits. - | - A new fuction :func:`qiskit.compiler.sequence` has been also added so that + A new function :func:`qiskit.compiler.sequence` has been also added so that we can convert a scheduled circuit into a :class:`~qiskit.pulse.Schedule` to make it executable on a pulse-enabled backend. diff --git a/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml b/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml index 9b441e252c1..158550d9a09 100644 --- a/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml +++ b/releasenotes/notes/0.16/fix-bug-in-controlled-unitary-when-setting-ctrl_state-2f9af3b9f0f7903f.yaml @@ -6,4 +6,4 @@ fixes: in the creation of the matrix for the controlled unitary and again when calling the :meth:`~qiskit.circuit.ControlledGate.definition` method of the :class:`qiskit.circuit.ControlledGate` class. This would give the - appearence that setting ``ctrl_state`` had no effect. + appearance that setting ``ctrl_state`` had no effect. diff --git a/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml b/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml index a0b69d5333c..42f69635155 100644 --- a/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml +++ b/releasenotes/notes/0.16/remove-dagnode-dict-32fa35479c0a8331.yaml @@ -3,7 +3,7 @@ upgrade: - | The previously deprecated support for passing in a dictionary as the first positional argument to :class:`~qiskit.dagcircuit.DAGNode` constructor - has been removed. Using a dictonary for the first positional argument + has been removed. Using a dictionary for the first positional argument was deprecated in the 0.13.0 release. To create a :class:`~qiskit.dagcircuit.DAGNode` object now you should directly pass the attributes as kwargs on the constructor. diff --git a/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml b/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml index 0cde632a36b..fc317417747 100644 --- a/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml +++ b/releasenotes/notes/0.17/add-schedule-block-c37527f3205b7b62.yaml @@ -49,7 +49,7 @@ deprecations: constructing parameterized pulse programs. - | The :attr:`~qiskit.pulse.channels.Channel.parameters` attribute for - the following clasess: + the following classes: * :py:class:`~qiskit.pulse.channels.Channel` * :py:class:`~qiskit.pulse.instructions.Instruction`. diff --git a/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml b/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml index 85b6a4cd37b..a688665aea5 100644 --- a/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml +++ b/releasenotes/notes/0.17/basicaer-new-provider-ea7cf756df231c2b.yaml @@ -34,7 +34,7 @@ upgrade: until the simulation finishes executing. If you want to restore the previous async behavior you'll need to wrap the :meth:`~qiskit.providers.basicaer.QasmSimulatorPy.run` with something that - will run in a seperate thread or process like ``futures.ThreadPoolExecutor`` + will run in a separate thread or process like ``futures.ThreadPoolExecutor`` or ``futures.ProcessPoolExecutor``. - | The ``allow_sample_measuring`` option for the diff --git a/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml b/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml index dd9b8052e5a..d627ecfe47c 100644 --- a/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml +++ b/releasenotes/notes/0.17/deprecate-schemas-424c29fbd35c90de.yaml @@ -11,7 +11,7 @@ deprecations: deprecation warning). The schema files have been moved to the `Qiskit/ibmq-schemas `__ repository and those should be treated as the canonical versions of the - API schemas. Moving forward only those schemas will recieve updates and + API schemas. Moving forward only those schemas will receive updates and will be used as the source of truth for the schemas. If you were relying on the schemas bundled in qiskit-terra you should update to use that repository instead. diff --git a/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml b/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml index 5ddd2771072..42aeec0b513 100644 --- a/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml +++ b/releasenotes/notes/0.17/ecr-gate-45cfda1b84ac792c.yaml @@ -25,7 +25,7 @@ features: - | Two new transpiler passess, :class:`~qiskit.transpiler.GateDirection` and class:`qiskit.transpiler.CheckGateDirection`, were added to the - :mod:`qiskit.transpiler.passes` module. These new passes are inteded to + :mod:`qiskit.transpiler.passes` module. These new passes are intended to be more general replacements for :class:`~qiskit.transpiler.passes.CXDirection` and :class:`~qiskit.transpiler.passes.CheckCXDirection` (which are both now diff --git a/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml b/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml index 1dcd1982955..4adadd4a7c8 100644 --- a/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml +++ b/releasenotes/notes/0.17/fix-nlocal-circular-entanglement-0acf0195138b6aa2.yaml @@ -5,6 +5,6 @@ fixes: :class:`qiskit.circuit.library.NLocal` circuit class for the edge case where the circuit has the same size as the entanglement block (e.g. a two-qubit circuit and CZ entanglement gates). In this case there should only be one entanglement - gate, but there was accidentially added a second one in the inverse direction as the + gate, but there was accidentally added a second one in the inverse direction as the first. Fixed `Qiskit/qiskit-aqua#1452 `__ diff --git a/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml b/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml index e306e5a1558..824a9556308 100644 --- a/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml +++ b/releasenotes/notes/0.17/idle-time-visualization-b5404ad875cbdae4.yaml @@ -2,7 +2,7 @@ fixes: - | Fixed an issue with the :func:`qiskit.visualization.timeline_drawer` - function where classical bits were inproperly handled. + function where classical bits were improperly handled. Fixed `#5361 `__ - | Fixed an issue in the :func:`qiskit.visualization.circuit_drawer` function diff --git a/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml b/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml index e5553896c76..d7a8d21f974 100644 --- a/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml +++ b/releasenotes/notes/0.17/issue-5751-1b6249f6263c9c30.yaml @@ -17,5 +17,5 @@ features: the :class:`~qiskit.transpiler.passes.TemplateOptimization` pass with the :py:class:`qiskit.transpiler.passes.RZXCalibrationBuilder` pass to automatically find and replace gate sequences, such as - ``CNOT - P(theta) - CNOT``, with more efficent circuits based on + ``CNOT - P(theta) - CNOT``, with more efficient circuits based on :class:`qiskit.circuit.library.RZXGate` with a calibration. diff --git a/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml b/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml index 0bc3fbf78ff..f30cea6b27f 100644 --- a/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml +++ b/releasenotes/notes/0.17/qiskit-version-wrapper-90cb7fcffeaafd6a.yaml @@ -13,7 +13,7 @@ upgrade: this change. - | The ``qiskit.execute`` module has been renamed to - :mod:`qiskit.execute_function`. This was necessary to avoid a potentical + :mod:`qiskit.execute_function`. This was necessary to avoid a potential name conflict between the :func:`~qiskit.execute_function.execute` function which is re-exported as ``qiskit.execute``. ``qiskit.execute`` the function in some situations could conflict with ``qiskit.execute`` the module which @@ -30,7 +30,7 @@ upgrade: been renamed to ``qiskit.compiler.transpiler``, ``qiskit.compiler.assembler``, ``qiskit.compiler.scheduler``, and ``qiskit.compiler.sequence`` respectively. This was necessary to avoid a - potentical name conflict between the modules and the re-exported function + potential name conflict between the modules and the re-exported function paths :func:`qiskit.compiler.transpile`, :func:`qiskit.compiler.assemble`, :func:`qiskit.compiler.schedule`, and :func:`qiskit.compiler.sequence`. In some situations this name conflict between the module path and diff --git a/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml b/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml index ef5a733d2a0..a5a75974ed1 100644 --- a/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml +++ b/releasenotes/notes/0.17/replace-pulse-drawer-f9f667c8f71e1e02.yaml @@ -15,7 +15,7 @@ features: * Specifying ``axis`` objects for plotting to allow further extension of generated plots, i.e., for publication manipulations. - New stylesheets can take callback functions that dynamically modify the apperance of + New stylesheets can take callback functions that dynamically modify the appearance of the output image, for example, reassembling a collection of channels, showing details of instructions, updating appearance of pulse envelopes, etc... You can create custom callback functions and feed them into a stylesheet instance to diff --git a/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml b/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml index bd614d766f5..1c70b64a91f 100644 --- a/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml +++ b/releasenotes/notes/0.18/add-pauli-list-5644d695f91de808.yaml @@ -4,7 +4,7 @@ features: A new class, :class:`~qiskit.quantum_info.PauliList`, has been added to the :mod:`qiskit.quantum_info` module. This class is used to efficiently represent a list of :class:`~qiskit.quantum_info.Pauli` - operators. This new class inherets from the same parent class as the + operators. This new class inherits from the same parent class as the existing :class:`~qiskit.quantum_info.PauliTable` (and therefore can be mostly used interchangeably), however it differs from the :class:`~qiskit.quantum_info.PauliTable` diff --git a/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml b/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml index b4b2bbbd8b3..c37fe6bbcdc 100644 --- a/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml +++ b/releasenotes/notes/0.19/fix-infinite-job-submissions-d6f6a583535ca798.yaml @@ -5,7 +5,7 @@ features: to limit the number of times a job will attempt to be executed on a backend. Previously the submission and fetching of results would be attempted infinitely, even if the job was cancelled or errored on the backend. The - default is now 50, and the previous behaviour can be achieved by setting + default is now 50, and the previous behavior can be achieved by setting ``max_job_tries=-1``. Fixes `#6872 `__ and `#6821 `__. diff --git a/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml b/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml index 2da1ce1a40d..fbde9d704a1 100644 --- a/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml +++ b/releasenotes/notes/0.19/gates-in-basis-pass-337f6637e61919db.yaml @@ -13,7 +13,7 @@ features: from qiskit.circuit import QuantumCircuit from qiskit.transpiler.passes import GatesInBasis - # Instatiate Pass + # Instantiate Pass basis_gates = ["cx", "h"] basis_check_pass = GatesInBasis(basis_gates) # Build circuit diff --git a/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml b/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml index d5737bba8e5..bdfca2b5c57 100644 --- a/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml +++ b/releasenotes/notes/0.19/measure_all-add_bits-8525317935197b90.yaml @@ -2,7 +2,7 @@ features: - | Added a new parameter, ``add_bits``, to :meth:`.QuantumCircuit.measure_all`. - By default it is set to ``True`` to maintain the previous behaviour of adding a new :obj:`.ClassicalRegister` of the same size as the number of qubits to store the measurements. + By default it is set to ``True`` to maintain the previous behavior of adding a new :obj:`.ClassicalRegister` of the same size as the number of qubits to store the measurements. If set to ``False``, the measurements will be stored in the already existing classical bits. For example, if you created a circuit with existing classical bits like:: diff --git a/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml b/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml index 16e636ab7c9..c553ee8f0ca 100644 --- a/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml +++ b/releasenotes/notes/0.19/mpl-bump-33a1240266e66508.yaml @@ -10,5 +10,5 @@ upgrade: deprecated the use of APIs around 3D visualizations that were compatible with older releases and second installing older versions of Matplotlib was becoming increasingly difficult as matplotlib's upstream dependencies - have caused incompatiblities that made testing moving forward more + have caused incompatibilities that made testing moving forward more difficult. diff --git a/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml b/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml index 69d60ee1545..26058e03f3d 100644 --- a/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml +++ b/releasenotes/notes/0.19/readout-mitigation-classes-2ef175e232d791ae.yaml @@ -25,7 +25,7 @@ features: - | Added the :class:`~qiskit.result.LocalReadoutMitigator` class for performing measurement readout error mitigation of local measurement - errors. Local measuerment errors are those that are described by a + errors. Local measurement errors are those that are described by a tensor-product of single-qubit measurement errors. This class can be initialized with a list of :math:`N` single-qubit of @@ -40,7 +40,7 @@ features: performing measurement readout error mitigation of correlated measurement errors. This class can be initialized with a single :math:`2^N \times 2^N` measurement error assignment matrix that descirbes the error probabilities. - Mitigation is implemented via inversion of assigment matrix which has + Mitigation is implemented via inversion of assignment matrix which has mitigation complexity of :math:`O(4^N)` of :class:`~qiskit.result.QuasiDistribution` and expectation values. - | diff --git a/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml b/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml index 15ee5cc07ce..1ec1774e680 100644 --- a/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml +++ b/releasenotes/notes/0.19/remove-manual-warning-filters-028646b73bb86860.yaml @@ -2,12 +2,12 @@ upgrade: - | An internal filter override that caused all Qiskit deprecation warnings to - be displayed has been removed. This means that the behaviour will now - revert to the standard Python behaviour for deprecations; you should only + be displayed has been removed. This means that the behavior will now + revert to the standard Python behavior for deprecations; you should only see a ``DeprecationWarning`` if it was triggered by code in the main script file, interpreter session or Jupyter notebook. The user will no longer be blamed with a warning if internal Qiskit functions call deprecated - behaviour. If you write libraries, you should occasionally run with the + behavior. If you write libraries, you should occasionally run with the default warning filters disabled, or have tests which always run with them disabled. See the `Python documentation on warnings`_, and in particular the `section on testing for deprecations`_ for more information on how to do this. @@ -16,7 +16,7 @@ upgrade: .. _section on testing for deprecations: https://docs.python.org/3/library/warnings.html#updating-code-for-new-versions-of-dependencies - | Certain warnings used to be only issued once, even if triggered from - multiple places. This behaviour has been removed, so it is possible that if + multiple places. This behavior has been removed, so it is possible that if you call deprecated functions, you may see more warnings than you did before. You should change any deprecated function calls to the suggested versions, because the deprecated forms will be removed in future Qiskit diff --git a/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml b/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml index 4ce25f21146..a9623206ecd 100644 --- a/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml +++ b/releasenotes/notes/0.19/sparse-pauli-internal-8226b4f57a61b982.yaml @@ -12,5 +12,5 @@ upgrade: The return type of :func:`~qiskit.quantum_info.pauli_basis` will change from :class:`~qiskit.quantum_info.PauliTable` to :class:`~qiskit.quantum_info.PauliList` in a future release of Qiskit Terra. - To immediately swap to the new behaviour, pass the keyword argument + To immediately swap to the new behavior, pass the keyword argument ``pauli_list=True``. diff --git a/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml b/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml index 07c0ce9178d..74d66c7558c 100644 --- a/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml +++ b/releasenotes/notes/0.19/vf2layout-4cea88087c355769.yaml @@ -7,7 +7,7 @@ features: `__ to find a perfect layout (a layout which would not require additional routing) if one exists. The functionality exposed by this new pass is very - similar to exisiting :class:`~qiskit.transpiler.passes.CSPLayout` but + similar to existing :class:`~qiskit.transpiler.passes.CSPLayout` but :class:`~qiskit.transpiler.passes.VF2Layout` is significantly faster. .. _VF2 algorithm: https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.101.5342&rep=rep1&type=pdf diff --git a/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml b/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml index e6dc2829ca0..db9b15d8625 100644 --- a/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml +++ b/releasenotes/notes/0.20/expose-tolerances-z2symmetries-9c444a7b1237252e.yaml @@ -35,7 +35,7 @@ features: SparsePauliOp(['X', 'Y'], coeffs=[1.+0.j, 0.+1.j]) - Note that the chop method does not accumulate the coefficents of the same Paulis, e.g. + Note that the chop method does not accumulate the coefficients of the same Paulis, e.g. .. code-block:: diff --git a/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml b/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml index af5f158ecb1..5f79ace148e 100644 --- a/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml +++ b/releasenotes/notes/0.20/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml @@ -12,7 +12,7 @@ features: Previously, the pass chain would have been implemented as ``scheduling -> alignment`` which were both transform passes thus there were multiple :class:`~.DAGCircuit` - instances recreated during each pass. In addition, scheduling occured in each pass + instances recreated during each pass. In addition, scheduling occurred in each pass to obtain instruction start time. Now the required pass chain becomes ``scheduling -> alignment -> padding`` where the :class:`~.DAGCircuit` update only occurs at the end with the ``padding`` pass. @@ -59,7 +59,7 @@ features: The :class:`~.ConstrainedReschedule` pass considers both hardware alignment constraints that can be definied in a :class:`.BackendConfiguration` object, ``pulse_alignment`` and ``acquire_alignment``. This new class superscedes - the previosuly existing :class:`~.AlignMeasures` as it performs the same alignment + the previously existing :class:`~.AlignMeasures` as it performs the same alignment (via the property set) for measurement instructions in addition to general instruction alignment. By setting the ``acquire_alignment`` constraint argument for the :class:`~.ConstrainedReschedule` pass it is a drop-in replacement of @@ -67,7 +67,7 @@ features: - | Added two new transpiler passes :class:`~.ALAPScheduleAnalysis` and :class:`~.ASAPScheduleAnalysis` which superscede the :class:`~.ALAPSchedule` and :class:`~.ASAPSchedule` as part of the - reworked transpiler workflow for schedling. The new passes perform the same scheduling but + reworked transpiler workflow for scheduling. The new passes perform the same scheduling but in the property set and relying on a :class:`~.BasePadding` pass to adjust the circuit based on all the scheduling alignment analysis. @@ -155,5 +155,5 @@ features: Added a new transpiler pass :class:`~.PadDynamicalDecoupling` which superscedes the :class:`~.DynamicalDecoupling` pass as part of the reworked transpiler workflow for scheduling. This new pass will insert dynamical decoupling - sequences into the circuit per any scheduling and alignment analysis that occured in earlier + sequences into the circuit per any scheduling and alignment analysis that occurred in earlier passes. diff --git a/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml b/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml index 86e9c23612a..93d39e48184 100644 --- a/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml +++ b/releasenotes/notes/0.20/vf2layout-preset-passmanager-db46513a24e79aa9.yaml @@ -18,7 +18,7 @@ upgrade: (where ``circuit.qubits[0]`` is mapped to physical qubit 0, ``circuit.qubits[1]`` is mapped to physical qubit 1, etc) assuming the trivial layout is perfect. If your use case was dependent on the - trivial layout you can explictly request it when transpiling by specifying + trivial layout you can explicitly request it when transpiling by specifying ``layout_method="trivial"`` when calling :func:`~qiskit.compiler.transpile`. - | The preset pass manager for optimization level 1 (when calling diff --git a/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml b/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml index 361f90cf35f..182ccd32a79 100644 --- a/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml +++ b/releasenotes/notes/0.21/marginal-memory-29d9d6586ae78590.yaml @@ -3,7 +3,7 @@ features: - | Added a new function :func:`~.marginal_memory` which is used to marginalize shot memory arrays. Provided with the shot memory array and the indices - of interest, the function will return a maginized shot memory array. This + of interest, the function will return a marginalized shot memory array. This function differs from the memory support in the :func:`~.marginal_counts` method which only works on the ``memory`` field in a :class:`~.Results` object. diff --git a/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml b/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml index 5446d12e9ee..e508f5b734f 100644 --- a/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml +++ b/releasenotes/notes/0.21/vf2-post-layout-f0213e2c7ebb645c.yaml @@ -15,7 +15,7 @@ features: This pass is similar to the :class:`~.VF2Layout` pass and both internally use the same VF2 implementation from `retworkx `__. However, - :class:`~.VF2PostLayout` is deisgned to run after initial layout, routing, + :class:`~.VF2PostLayout` is designed to run after initial layout, routing, basis translation, and any optimization passes run and will only work if a layout has already been applied, the circuit has been routed, and all gates are in the target basis. This is required so that when a new layout diff --git a/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml b/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml index a6a8cbd06bd..f79c4ade2c7 100644 --- a/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml +++ b/releasenotes/notes/0.21/vqd-implementation-details-09b0ead8b42cacda.yaml @@ -2,7 +2,7 @@ features: - | The algorithm iteratively computes each eigenstate by starting from the ground - state (which is computed as in VQE) and then optimising a modified cost function + state (which is computed as in VQE) and then optimizing a modified cost function that tries to compute eigen states that are orthogonal to the states computed in the previous iterations and have the lowest energy when computed over the ansatz. The interface implemented is very similar to that of VQE and is of the form: diff --git a/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml b/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml index 5606c830b41..65d64dda08d 100644 --- a/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml +++ b/releasenotes/notes/0.22/add-reverse-linear-entanglement-nlocal-38581e4ffb7a7c68.yaml @@ -13,5 +13,5 @@ upgrade: :class:`~.RealAmplitudes` and :class:`~.EfficientSU2` classes has changed from ``"full"`` to ``"reverse_linear"``. This change was made because the output circuit is equivalent but uses only :math:`n-1` instead of :math:`\frac{n(n-1)}{2}` :class:`~.CXGate` gates. If you - desire the previous default you can explicity set ``entanglement="full"`` when calling either + desire the previous default you can explicitly set ``entanglement="full"`` when calling either constructor. diff --git a/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml b/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml index 7f1e448fd18..9f535dcd409 100644 --- a/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml +++ b/releasenotes/notes/0.22/fix-target-control-flow-representation-09520e2838f0657e.yaml @@ -93,7 +93,7 @@ upgrade: and no edges. This change was made to better reflect the actual connectivity constraints of the :class:`~.Target` because in this case there are no connectivity constraints on the backend being modeled by - the :class:`~.Target`, not a lack of connecitvity. If you desire the + the :class:`~.Target`, not a lack of connectivity. If you desire the previous behavior for any reason you can reproduce it by checking for a ``None`` return and manually building a coupling map, for example:: diff --git a/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml b/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml index a0a56ff0d85..e471b6dc371 100644 --- a/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml +++ b/releasenotes/notes/0.22/gate-direction-target-a9f0acd0cf30ed66.yaml @@ -2,5 +2,5 @@ fixes: - | The :class:`.GateDirection` transpiler pass will now respect the available - values for gate parameters when handling parametrised gates with a + values for gate parameters when handling parametrized gates with a :class:`.Target`. diff --git a/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml b/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml index d6267404c24..4ef32d600fc 100644 --- a/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml +++ b/releasenotes/notes/0.22/primitive-run-5d1afab3655330a6.yaml @@ -3,7 +3,7 @@ features: - | Added new methods for executing primitives: :meth:`.BaseSampler.run` and :meth:`.BaseEstimator.run`. These methods execute asynchronously and return :class:`.JobV1` objects which - provide a handle to the exections. These new run methods can be passed :class:`~.QuantumCircuit` + provide a handle to the exceptions. These new run methods can be passed :class:`~.QuantumCircuit` objects (and observables for :class:`~.BaseEstimator`) that are not registered in the constructor. For example:: diff --git a/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml b/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml index 0162dfcad6a..15bb0769884 100644 --- a/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml +++ b/releasenotes/notes/0.22/remove-symbolic-pulse-subclasses-77314a1654521852.yaml @@ -5,7 +5,7 @@ features: :class:`.Drag` and :class:`.Constant` have been upgraded to instantiate :class:`SymbolicPulse` rather than the subclass itself. All parametric pulse objects in pulse programs must be symbolic pulse instances, - because subclassing is no longer neccesary. Note that :class:`SymbolicPulse` can + because subclassing is no longer necessary. Note that :class:`SymbolicPulse` can uniquely identify a particular envelope with the symbolic expression object defined in :attr:`SymbolicPulse.envelope`. upgrade: @@ -15,7 +15,7 @@ upgrade: these pulse subclasses are no longer instantiated. They will still work in Terra 0.22, but you should begin transitioning immediately. Instead of using type information, :attr:`SymbolicPulse.pulse_type` should be used. - This is assumed to be a unique string identifer for pulse envelopes, + This is assumed to be a unique string identifier for pulse envelopes, and we can use string equality to investigate the pulse types. For example, .. code-block:: python diff --git a/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml b/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml index 9e5c3916c78..659b7312bb1 100644 --- a/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml +++ b/releasenotes/notes/0.22/steppable-optimizers-9d9b48ba78bd58bb.yaml @@ -72,7 +72,7 @@ features: evaluated_gradient = grad(ask_data.x_center) optimizer.state.njev += 1 - optmizer.state.nit += 1 + optimizer.state.nit += 1 cf = TellData(eval_jac=evaluated_gradient) optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml b/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml index 6061ff04b3d..3da0de9d6db 100644 --- a/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml +++ b/releasenotes/notes/0.22/tensored-subset-fitter-bd28e6e6ec5bdaae.yaml @@ -5,5 +5,5 @@ features: class. The implementation is restricted to mitigation patterns in which each qubit is mitigated individually, e.g. ``[[0], [1], [2]]``. This is, however, the most widely used case. It allows the :class:`.TensoredMeasFitter` to - be used in cases where the numberical order of the physical qubits does not + be used in cases where the numerical order of the physical qubits does not match the index of the classical bit. diff --git a/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml b/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml index 009984fbd7f..92c07b4e245 100644 --- a/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml +++ b/releasenotes/notes/0.22/visualization-reorganisation-9e302239705c7842.yaml @@ -2,7 +2,7 @@ upgrade: - | The visualization module :mod:`qiskit.visualization` has seen some internal - reorganisation. This should not have affected the public interface, but if + reorganization. This should not have affected the public interface, but if you were accessing any internals of the circuit drawers, they may now be in different places. The only parts of the visualization module that are considered public are the components that are documented in this online diff --git a/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml b/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml index a9fa4703d67..f28178eb320 100644 --- a/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml +++ b/releasenotes/notes/0.23/fix-qpy-loose-bits-5283dc4ad3823ce3.yaml @@ -1,9 +1,9 @@ --- fixes: - | - QPY deserialisation will no longer add extra :class:`.Clbit` instances to the + QPY deserialization will no longer add extra :class:`.Clbit` instances to the circuit if there are both loose :class:`.Clbit`\ s in the circuit and more :class:`~qiskit.circuit.Qubit`\ s than :class:`.Clbit`\ s. - | - QPY deserialisation will no longer add registers named `q` and `c` if the + QPY deserialization will no longer add registers named `q` and `c` if the input circuit contained only loose bits. diff --git a/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml b/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml index e1d083a4a2d..716726dcbe5 100644 --- a/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml +++ b/releasenotes/notes/0.23/fix_8897-2a90c4b0857c19c2.yaml @@ -2,7 +2,7 @@ fixes: - | Fixes issue where :meth:`.Statevector.evolve` and :meth:`.DensityMatrix.evolve` - would raise an exeception for nested subsystem evolution for non-qubit + would raise an exception for nested subsystem evolution for non-qubit subsystems. Fixes `issue #8897 `_ - | diff --git a/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml b/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml index 365c5858610..d7dbd7edcb3 100644 --- a/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml +++ b/releasenotes/notes/0.23/initial_state-8e20b04fc2ec2f4b.yaml @@ -3,4 +3,4 @@ upgrade: - | The ``initial_state`` argument of the :class:`~NLocal` class should be a :class:`~.QuantumCircuit`. Passing any other type was deprecated as of Qiskit - Terra 0.18.0 (July 2021) and that posibility is now removed. + Terra 0.18.0 (July 2021) and that possibility is now removed. diff --git a/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml b/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml index 969b2aa2a24..6d1d608aab7 100644 --- a/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml +++ b/releasenotes/notes/0.23/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml @@ -3,6 +3,6 @@ features: - | The :class:`~.Optimize1qGatesDecomposition` transpiler pass has a new keyword argument, ``target``, on its constructor. This argument can be used to - specify a :class:`~.Target` object that represnts the compilation target. + specify a :class:`~.Target` object that represents the compilation target. If used it superscedes the ``basis`` argument to determine if an instruction in the circuit is present on the target backend. diff --git a/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml b/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml index c019f1329d0..a755d3b9506 100644 --- a/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml +++ b/releasenotes/notes/0.23/target-aware-unroll-custom-definitions-a1b839de199ca048.yaml @@ -3,6 +3,6 @@ features: - | The :class:`~.UnrollCustomDefinitions` transpiler pass has a new keyword argument, ``target``, on its constructor. This argument can be used to - specify a :class:`~.Target` object that represnts the compilation target. - If used it superscedes the ``basis_gates`` argument to determine if an + specify a :class:`~.Target` object that represents the compilation target. + If used it supersedes the ``basis_gates`` argument to determine if an instruction in the circuit is present on the target backend. diff --git a/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml b/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml index ea98eddd2d6..7f4e7a64fcd 100644 --- a/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml +++ b/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml @@ -65,7 +65,7 @@ features: qc.append(lin_fun, [0, 1, 2]) qc.append(cliff, [1, 2, 3]) - # Choose synthesis methods that adhere to linear-nearest-neighbour connectivity + # Choose synthesis methods that adhere to linear-nearest-neighbor connectivity hls_config = HLSConfig(linear_function=["kms"], clifford=["lnn"]) # Synthesize diff --git a/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml b/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml index 4072e863538..55bd079d0f8 100644 --- a/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml +++ b/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml @@ -9,5 +9,5 @@ features: * :class:``~qiskit.pulse.library.Triangle`` The new functions return a ``ScalableSymbolicPulse``. With the exception of the ``Sawtooth`` phase, - behaviour is identical to that of the corresponding waveform generators (:class:``~qiskit.pulse.library.sin`` etc). + behavior is identical to that of the corresponding waveform generators (:class:``~qiskit.pulse.library.sin`` etc). The ``Sawtooth`` phase is defined such that a phase of :math:``2\\pi`` shifts by a full cycle. diff --git a/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml b/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml index 93a0523397b..85637311dc4 100644 --- a/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml +++ b/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml @@ -10,5 +10,5 @@ deprecations: The pass was made into a separate plugin package for two reasons, first the dependency on CPLEX makes it harder to use and secondly the plugin - packge more cleanly integrates with :func:`~.transpile`. + package more cleanly integrates with :func:`~.transpile`. diff --git a/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml b/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml index e06fec8772f..c992160b740 100644 --- a/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml +++ b/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml @@ -5,4 +5,4 @@ fixes: to be any object that implements :class:`.Operation`, not just a :class:`.circuit.Instruction`. Note that any manual mutation of :attr:`.QuantumCircuit.data` is discouraged; it is not *usually* any more efficient than building a new circuit object, as checking the invariants - surrounding parametrised objects can be surprisingly expensive. + surrounding parametrized objects can be surprisingly expensive. diff --git a/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml b/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml index 000dd81ee98..c217852c33c 100644 --- a/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml +++ b/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml @@ -2,6 +2,6 @@ fixes: - | Fixed a bug in :meth:`.TensoredOp.to_matrix` where the global coefficient of the operator - was multiplied to the final matrix more than once. Now, the global coefficient is correclty + was multiplied to the final matrix more than once. Now, the global coefficient is correctly applied, independent of the number of tensored operators or states. Fixed `#9398 `__. diff --git a/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml b/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml index f33b3d240ba..79fe381d896 100644 --- a/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml +++ b/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml @@ -5,4 +5,4 @@ features: and RZXCalibrationBuilderNoEcho to consume `ecr` entangling gates from the backend, in addition to the `cx` gates they were build for. These native gates contain the calibrated pulse schedules that the pulse scaling passes use to - generate arbitraty rotations of the :class:`~RZXGate` operation. + generate arbitrary rotations of the :class:`~RZXGate` operation. diff --git a/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml b/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml index c6170b62a8f..c2003139d67 100644 --- a/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml +++ b/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml @@ -21,7 +21,7 @@ fixes: `#7769 `__ and `#7773 `__. - | - Standard gates defined by Qiskit, such as :class:`.RZXGate`, will now have properly parametrised + Standard gates defined by Qiskit, such as :class:`.RZXGate`, will now have properly parametrized definitions when exported using the OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`). See `#7172 `__. - | diff --git a/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml b/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml index f38d694e77a..0fc7dd6b69a 100644 --- a/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml +++ b/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml @@ -17,7 +17,7 @@ features: This new parser is approximately 10x faster than the existing ones at :meth:`.QuantumCircuit.from_qasm_file` and :meth:`.QuantumCircuit.from_qasm_str` for large files, - and has less overhead on each call as well. The new parser is more extensible, customisable and + and has less overhead on each call as well. The new parser is more extensible, customizable and generally also more type-safe; it will not attempt to output custom Qiskit objects when the definition in the OpenQASM 2 file clashes with the Qiskit object, unlike the current exporter. See the :mod:`qiskit.qasm2` module documentation for full details and more examples. diff --git a/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml b/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml index fadd99c8023..9af85d8572d 100644 --- a/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml +++ b/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml @@ -5,6 +5,6 @@ features: to pass a list of optimizers and initial points for the different minimization runs. For example, the ``k``-th initial point and ``k``-th optimizer will be used for the optimization of the - ``k-1``-th exicted state. + ``k-1``-th excited state. diff --git a/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml b/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml index d5dfd69f54a..999d30095d1 100644 --- a/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml +++ b/releasenotes/notes/0.25/dag-substitute-node-propagate-condition-898052b53edb1f17.yaml @@ -3,7 +3,7 @@ features: - | :meth:`.DAGCircuit.substitute_node` gained a ``propagate_condition`` keyword argument that is analogous to the same argument in :meth:`~.DAGCircuit.substitute_node_with_dag`. Setting this - to ``False`` opts out of the legacy behaviour of copying a condition on the ``node`` onto the + to ``False`` opts out of the legacy behavior of copying a condition on the ``node`` onto the new ``op`` that is replacing it. This option is ignored for general control-flow operations, which will never propagate their diff --git a/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml b/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml index c8f7e3ddd8c..4902fb85a2f 100644 --- a/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml +++ b/releasenotes/notes/0.25/faster-parameter-rebind-3c799e74456469d9.yaml @@ -15,6 +15,6 @@ features: reduce the overhead of input normalisation in this function. fixes: - | - A parametrised circuit that contains a custom gate whose definition has a parametrised global phase + A parametrized circuit that contains a custom gate whose definition has a parametrized global phase can now successfully bind the parameter in the inner global phase. See `#10283 `__ for more detail. diff --git a/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml b/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml index aa89abeb662..30d76baa436 100644 --- a/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml +++ b/releasenotes/notes/0.25/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml @@ -4,4 +4,4 @@ fixes: Fixed the gate decomposition of multi-controlled Z rotation gates added via :meth:`.QuantumCircuit.mcrz`. Previously, this method implemented a multi-controlled phase gate, which has a relative phase difference to the Z rotation. To obtain the - previous `.QuantumCircuit.mcrz` behaviour, use `.QuantumCircuit.mcp`. + previous `.QuantumCircuit.mcrz` behavior, use `.QuantumCircuit.mcp`. diff --git a/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml b/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml index 890223e82b8..40975c31d1d 100644 --- a/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml +++ b/releasenotes/notes/0.25/fix_9016-2e8bc2cb10b5e204.yaml @@ -3,5 +3,5 @@ fixes: - | When the parameter ``conditional=True`` is set in ``qiskit.circuit.random.random_circuit``, the conditional operations will - be preceded by a full mid-circuit measurment. + be preceded by a full mid-circuit measurement. Fixes `#9016 `__ diff --git a/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml b/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml index ebb699e8c7b..246387384ca 100644 --- a/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml +++ b/releasenotes/notes/0.25/flatten-nlocal-family-292b23b99947f3c9.yaml @@ -17,6 +17,6 @@ features: :class:`~.circuit.Instruction` objects. While this isn't optimal for visualization it typically results in much better runtime performance, especially with :meth:`.QuantumCircuit.bind_parameters` and - :meth:`.QuantumCircuit.assign_parameters` which can see a substatial + :meth:`.QuantumCircuit.assign_parameters` which can see a substantial runtime improvement with a flattened output compared to the nested wrapped default output. diff --git a/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml b/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml index e460b6e55ee..6615b11605e 100644 --- a/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml +++ b/releasenotes/notes/0.25/normalize-stateprep-e21972dce8695509.yaml @@ -4,5 +4,5 @@ features: The instructions :class:`.StatePreparation` and :class:`~.extensions.Initialize`, and their associated circuit methods :meth:`.QuantumCircuit.prepare_state` and :meth:`~.QuantumCircuit.initialize`, gained a keyword argument ``normalize``, which can be set to ``True`` to automatically normalize - an array target. By default this is ``False``, which retains the current behaviour of + an array target. By default this is ``False``, which retains the current behavior of raising an exception when given non-normalized input. diff --git a/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml b/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml index 35ce0ce4ca5..9ca56d961bc 100644 --- a/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml +++ b/releasenotes/notes/0.25/qpy-layout-927ab34f2b47f4aa.yaml @@ -7,6 +7,6 @@ upgrade: fixes: - | Fixed the :mod:`~qiskit.qpy` serialization of :attr:`.QuantumCircuit.layout` - attribue. Previously, the :attr:`~.QuantumCircuit.layout` attribute would + attribute. Previously, the :attr:`~.QuantumCircuit.layout` attribute would have been dropped when serializing a circuit to QPY. Fixed `#10112 `__ diff --git a/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml b/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml index fdfaf394e8b..844deea0b80 100644 --- a/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml +++ b/releasenotes/notes/0.25/token-swapper-rustworkx-9e02c0ab67a59fe8.yaml @@ -2,5 +2,5 @@ upgrade: - | The :meth:`~.ApproximateTokenSwapper.map` has been modified to use the new ``rustworkx`` version - of :func:`~graph_token_swapper` for performance reasons. Qiskit Terra 0.25 now requires versison + of :func:`~graph_token_swapper` for performance reasons. Qiskit Terra 0.25 now requires version 0.13.0 of ``rustworkx``. diff --git a/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml b/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml index 4126b91bb86..17bf77f51c1 100644 --- a/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml +++ b/releasenotes/notes/0.45/dag-appenders-check-84d4ef20c1e20fd0.yaml @@ -4,5 +4,5 @@ features: The :class:`.DAGCircuit` methods :meth:`~.DAGCircuit.apply_operation_back` and :meth:`~.DAGCircuit.apply_operation_front` have gained a ``check`` keyword argument that can be set ``False`` to skip validation that the inputs uphold the :class:`.DAGCircuit` data-structure - invariants. This is useful as a performance optimisation when the DAG is being built from + invariants. This is useful as a performance optimization when the DAG is being built from known-good data, such as during transpiler passes. diff --git a/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml b/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml index a62ed79a047..a55582f9476 100644 --- a/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml +++ b/releasenotes/notes/0.45/deprecate-duplicates-a871f83bbbe1c96f.yaml @@ -13,5 +13,5 @@ deprecations: * :meth:`.QuantumCircuit.i` in favor of :meth:`.QuantumCircuit.id` Note that :meth:`.QuantumCircuit.i` is the only exception to the rule above, but since - :meth:`.QuantumCircuit.id` more intuively represents the identity and is used more, we chose + :meth:`.QuantumCircuit.id` more intuitively represents the identity and is used more, we chose it over its counterpart. \ No newline at end of file diff --git a/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml b/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml index c3d4bbe6386..2819be565c1 100644 --- a/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml +++ b/releasenotes/notes/0.45/discrete-basis-gatedirection-bdffad3b47c1c532.yaml @@ -4,5 +4,5 @@ fixes: The :class:`.GateDirection` transpiler pass will now use discrete-basis translations rather than relying on a continuous :class:`.RYGate`, which should help make some discrete-basis-set targets slightly more reliable. In general, :func:`.transpile` only has partial support for basis sets - that do not contain a continuously-parametrised operation, and so it may not always succeed in + that do not contain a continuously-parametrized operation, and so it may not always succeed in these situations, and will almost certainly not produce optimal results. diff --git a/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml b/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml index 234888b2ac9..335c819dc8c 100644 --- a/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml +++ b/releasenotes/notes/0.45/expr-rvalue-conditions-8b5d5f7c015658c0.yaml @@ -60,7 +60,7 @@ features: and :class:`.Clbit` instances. All these classical expressions are fully supported through the Qiskit transpiler stack, through - QPY serialisation (:mod:`qiskit.qpy`) and for export to OpenQASM 3 (:mod:`qiskit.qasm3`). Import + QPY serialization (:mod:`qiskit.qpy`) and for export to OpenQASM 3 (:mod:`qiskit.qasm3`). Import from OpenQASM 3 is currently managed by `a separate package `__ (which is re-exposed via :mod:`qiskit.qasm3`), which we hope will be extended to match the new features in Qiskit. diff --git a/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml b/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml index e03fa8555a4..8f04a25141e 100644 --- a/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml +++ b/releasenotes/notes/0.45/fix-parameter-hash-d22c270090ffc80e.yaml @@ -3,8 +3,8 @@ features: - | :class:`.Parameter` now has an advanced-usage keyword argument ``uuid`` in its constructor, which can be used to make the :class:`.Parameter` compare equal to another of the same name. - This should not typically be used by users, and is most useful for custom serialisation and - deserialisation. + This should not typically be used by users, and is most useful for custom serialization and + deserialization. fixes: - | The hash of a :class:`.Parameter` is now equal to the hashes of any diff --git a/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml b/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml index e93e62c963a..e75c8e09606 100644 --- a/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml +++ b/releasenotes/notes/0.45/fix-timeline-draw-unscheduled-warning-873f7a24c6b51e2c.yaml @@ -4,7 +4,7 @@ deprecations: Passing a circuit to :func:`qiskit.visualization.timeline_drawer` that does not have scheduled node start-time information is deprecated. Only circuits that have gone through one of the scheduling analysis passes (for example :class:`.ALAPScheduleAnalysis` or - :class:`.ASAPScheduleAnalysis`) can be visualised. If you have used one of the old-style + :class:`.ASAPScheduleAnalysis`) can be visualized. If you have used one of the old-style scheduling passes (for example :class:`.ALAPSchedule` or :class:`.ASAPSchedule`), you can propagate the scheduling information by running:: @@ -18,5 +18,5 @@ deprecations: instruction_durations=InstructionDurations(), ) - This behaviour was previously intended to be deprecated in Qiskit 0.37, but due to a bug in the - warning, it was not displayed to users until now. The behaviour will be removed in Qiskit 1.0. + This behavior was previously intended to be deprecated in Qiskit 0.37, but due to a bug in the + warning, it was not displayed to users until now. The behavior will be removed in Qiskit 1.0. diff --git a/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml b/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml index befe3011c70..a1e4a549266 100644 --- a/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml +++ b/releasenotes/notes/0.45/qasm2-new-api-4e1e4803d6a5a175.yaml @@ -18,7 +18,7 @@ features: (:mod:`qiskit.qasm3`) and QPY (:mod:`qiskit.qpy`) modules. This is particularly important since the method name :meth:`~.QuantumCircuit.qasm` gave no indication of the OpenQASM version, and since it was originally - added, Qiskit has gained several serialisation modules that could easily + added, Qiskit has gained several serialization modules that could easily become confused. deprecations: - | diff --git a/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml b/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml index 7b8313e6b08..ac38751d9ce 100644 --- a/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml +++ b/releasenotes/notes/0.45/singletons-83782de8bd062cbc.yaml @@ -103,7 +103,7 @@ features: :attr:`.Instruction.mutable` which is used to get a mutable copy and check whether an :class:`~.circuit.Instruction` object is mutable. With the introduction of :class:`~.SingletonGate` these methods can be used to have a unified interface - to deal with the mutablitiy of instruction objects. + to deal with the mutability of instruction objects. - | Added an attribute :attr:`.Instruction.base_class`, which gets the "base" type of an instruction. Many instructions will satisfy ``type(obj) == obj.base_class``, however the diff --git a/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml b/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml index 151127a0c8d..a6a8745d175 100644 --- a/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml +++ b/releasenotes/notes/0.9/changes-on-upgrade-6fcd573269a8ebc5.yaml @@ -121,7 +121,7 @@ other: ``qiskit.execute()`` has been changed to optimization level 1 pass manager defined at ``qiskit.transpile.preset_passmanagers.level1_pass_manager``. - | - All the circuit drawer backends now willl express gate parameters in a + All the circuit drawer backends now will express gate parameters in a circuit as common fractions of pi in the output visualization. If the value of a parameter can be expressed as a fraction of pi that will be used instead of the numeric equivalent. diff --git a/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml b/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml index 925da8840f5..30b7baca783 100644 --- a/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml +++ b/releasenotes/notes/0.9/new-features-0.9-159645f977a139f7.yaml @@ -38,7 +38,7 @@ features: Two new functions, ``sech()`` and ``sech_deriv()`` were added to the pulse library module ``qiskit.pulse.pulse_lib`` for creating an unnormalized hyperbolic secant ``SamplePulse`` object and an unnormalized hyperbolic - secant derviative ``SamplePulse`` object respectively. + secant derivative ``SamplePulse`` object respectively. - | A new kwarg option ``vertical_compression`` was added to the ``QuantumCircuit.draw()`` method and the @@ -61,7 +61,7 @@ features: - | When creating a PassManager you can now specify a callback function that if specified will be run after each pass is executed. This function gets - passed a set of kwargs on each call with the state of the pass maanger after + passed a set of kwargs on each call with the state of the pass manager after each pass execution. Currently these kwargs are: * pass\_ (Pass): the pass being run diff --git a/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml b/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml index 62626e80995..1b11fe94542 100644 --- a/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml +++ b/releasenotes/notes/1.0/remove-opflow-qi-utils-3debd943c65b17da.yaml @@ -8,7 +8,7 @@ upgrade_algorithms: - | - A series of legacy quantum execution utililties have been removed, following their deprecation in Qiskit 0.44. + A series of legacy quantum execution utilities have been removed, following their deprecation in Qiskit 0.44. These include the ``qiskit.utils.QuantumInstance`` class, as well the modules: - ``qiskit.utils.backend_utils`` diff --git a/releasenotes/notes/abstract-commutation-analysis-3518129e91a33599.yaml b/releasenotes/notes/1.1/abstract-commutation-analysis-3518129e91a33599.yaml similarity index 100% rename from releasenotes/notes/abstract-commutation-analysis-3518129e91a33599.yaml rename to releasenotes/notes/1.1/abstract-commutation-analysis-3518129e91a33599.yaml diff --git a/releasenotes/notes/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml b/releasenotes/notes/1.1/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml similarity index 100% rename from releasenotes/notes/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml rename to releasenotes/notes/1.1/add-annotated-arg-to-power-4afe90e89fa50f5a.yaml diff --git a/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml b/releasenotes/notes/1.1/add-backend-estimator-v2-26cf14a3612bb81a.yaml similarity index 100% rename from releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml rename to releasenotes/notes/1.1/add-backend-estimator-v2-26cf14a3612bb81a.yaml diff --git a/releasenotes/notes/add-backend-sampler-v2-5e40135781eebc7f.yaml b/releasenotes/notes/1.1/add-backend-sampler-v2-5e40135781eebc7f.yaml similarity index 100% rename from releasenotes/notes/add-backend-sampler-v2-5e40135781eebc7f.yaml rename to releasenotes/notes/1.1/add-backend-sampler-v2-5e40135781eebc7f.yaml diff --git a/releasenotes/notes/1.1/add-bitarray-utilities-c85261138d5a1a97.yaml b/releasenotes/notes/1.1/add-bitarray-utilities-c85261138d5a1a97.yaml new file mode 100644 index 00000000000..089a7bcd113 --- /dev/null +++ b/releasenotes/notes/1.1/add-bitarray-utilities-c85261138d5a1a97.yaml @@ -0,0 +1,84 @@ +--- +features_primitives: + - | + Added methods to join multiple :class:`~.BitArray` objects along various axes. + + - :meth:`~.BitArray.concatenate`: join arrays along an existing axis of the arrays. + - :meth:`~.BitArray.concatenate_bits`: join arrays along the bit axis. + - :meth:`~.BitArray.concatenate_shots`: join arrays along the shots axis. + + .. code-block:: + + ba = BitArray.from_samples(['00', '11']) + print(ba) + # BitArray() + + # reshape the bit array because `concatenate` requires an axis. + ba_ = ba.reshape(1, 2) + print(ba_) + # BitArray() + + ba2 = BitArray.concatenate([ba_, ba_]) + print(ba2.get_bitstrings()) + # ['00', '11', '00', '11'] + + # `concatenate_bits` and `concatenates_shots` do not require any axis. + + ba3 = BitArray.concatenate_bits([ba, ba]) + print(ba3.get_bitstrings()) + # ['0000', '1111'] + + ba4 = BitArray.concatenate_shots([ba, ba]) + print(ba4.get_bitstrings()) + # ['00', '11', '00', '11'] + + - | + Added methods to generate a subset of :class:`~.BitArray` object by slicing along various axes. + + - :meth:`~.BitArray.__getitem__`: slice the array along an existing axis of the array. + - :meth:`~.BitArray.slice_bits`: slice the array along the bit axis. + - :meth:`~.BitArray.slice_shots`: slice the array along the shot axis. + + .. code-block:: + + ba = BitArray.from_samples(['0000', '0001', '0010', '0011'], 4) + print(ba) + # BitArray() + print(ba.get_bitstrings()) + # ['0000', '0001', '0010', '0011'] + + ba2 = ba.reshape(2, 2) + print(ba2) + # BitArray() + print(ba2[0].get_bitstrings()) + # ['0000', '0001'] + print(ba2[1].get_bitstrings()) + # ['0010', '0011'] + + ba3 = ba.slice_bits([0, 2]) + print(ba3.get_bitstrings()) + # ['00', '01', '00', '01'] + + ba4 = ba.slice_shots([0, 2]) + print(ba3.get_bitstrings()) + # ['0000', '0010'] + + - | + Added a method :meth:`~.BitArray.transpose` to transpose a :class:`~.BitArray`. + + .. code-block:: + + ba = BitArray.from_samples(['00', '11']).reshape(2, 1, 1) + print(ba) + # BitArray() + print(ba.transpose()) + # BitArray() + + - | + Added a method :meth:`~.BitArray.expectation_values` to compute expectation values of diagonal operators. + + .. code-block:: + + ba = BitArray.from_samples(['01', '11']) + print(ba.expectation_values(["IZ", "ZI", "01"])) + # [-1. 0. 0.5] diff --git a/releasenotes/notes/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml b/releasenotes/notes/1.1/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml similarity index 100% rename from releasenotes/notes/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml rename to releasenotes/notes/1.1/add-ctrl_state-mcp-parameter-b23562aa7047665a.yaml diff --git a/releasenotes/notes/1.1/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml b/releasenotes/notes/1.1/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml new file mode 100644 index 00000000000..ddc35ddcb98 --- /dev/null +++ b/releasenotes/notes/1.1/add-elide-permutations-to-pipeline-077dad03bd55ab9c.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The transpiler pass :class:`~.ElidePermutations` + runs by default with optimization level 2 and 3. Intuitively, removing + :class:`~.SwapGate`\s and :class:`~qiskit.circuit.library.PermutationGate`\s + in a virtual circuit is almost always beneficial, as it makes the circuit shorter + and easier to route. As :class:`~.OptimizeSwapBeforeMeasure` is a special case + of :class:`~.ElidePermutations`, it has been removed from optimization level 3. diff --git a/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml b/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml new file mode 100644 index 00000000000..a8da2921990 --- /dev/null +++ b/releasenotes/notes/1.1/add-elide-swaps-b0a4c373c9af1efd.yaml @@ -0,0 +1,41 @@ +--- +features: + - | + Added a new optimization transpiler pass, :class:`~.ElidePermutations`, + which is designed to run prior to the :ref:`layout_stage` and will + optimize away any :class:`~.SwapGate`\s and + :class:`~qiskit.circuit.library.PermutationGate`\s + in a circuit by permuting virtual + qubits. For example, taking a circuit with :class:`~.SwapGate`\s: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 1) + qc.swap(2, 0) + qc.cx(1, 0) + qc.measure_all() + qc.draw("mpl") + + will remove the swaps when the pass is run: + + .. plot:: + :include-source: + + from qiskit.transpiler.passes import ElidePermutations + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 1) + qc.swap(2, 0) + qc.cx(1, 0) + qc.measure_all() + + ElidePermutations()(qc).draw("mpl") + + The pass also sets the ``virtual_permutation_layout`` property set, storing + the permutation of the virtual qubits that was optimized away. diff --git a/releasenotes/notes/add-linear-plugin-options-b8a0ffe70dfe1676.yaml b/releasenotes/notes/1.1/add-linear-plugin-options-b8a0ffe70dfe1676.yaml similarity index 100% rename from releasenotes/notes/add-linear-plugin-options-b8a0ffe70dfe1676.yaml rename to releasenotes/notes/1.1/add-linear-plugin-options-b8a0ffe70dfe1676.yaml diff --git a/releasenotes/notes/1.1/add-run-all-plugins-option-ba8806a269e5713c.yaml b/releasenotes/notes/1.1/add-run-all-plugins-option-ba8806a269e5713c.yaml new file mode 100644 index 00000000000..2ab34c61fb3 --- /dev/null +++ b/releasenotes/notes/1.1/add-run-all-plugins-option-ba8806a269e5713c.yaml @@ -0,0 +1,51 @@ +--- +features: + - | + The :class:`~.HLSConfig` now has two additional optional arguments. The argument + ``plugin_selection`` can be set either to ``"sequential"`` or to ``"all"``. + If set to "sequential" (default), for every higher-level-object + the :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass will consider the + specified methods sequentially, in the order they appear in the list, stopping + at the first method that is able to synthesize the object. If set to "all", + all the specified methods will be considered, and the best synthesized circuit, + according to ``plugin_evaluation_fn`` will be chosen. The argument + ``plugin_evaluation_fn`` is an optional callable that evaluates the quality of + the synthesized quantum circuit; a smaller value means a better circuit. When + set to ``None``, the quality of the circuit is its size (i.e. the number of gates + that it contains). + + The following example illustrates the new functionality:: + + from qiskit import QuantumCircuit + from qiskit.circuit.library import LinearFunction + from qiskit.synthesis.linear import random_invertible_binary_matrix + from qiskit.transpiler.passes import HighLevelSynthesis, HLSConfig + + # Create a circuit with a linear function + mat = random_invertible_binary_matrix(7, seed=37) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + # Run different methods with different parameters, + # choosing the best result in terms of depth. + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ("pmh", {"section_size": 1}), + ("pmh", {"section_size": 3}), + ("kms", {}), + ("kms", {"use_inverted": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda circuit: circuit.depth(), + ) + + # synthesize + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + + In the example, we run multiple synthesis methods with different parameters, + choosing the best circuit in terms of depth. Note that optimizing + ``circuit.size()`` instead would pick a different circuit. diff --git a/releasenotes/notes/add-scheduler-warnings-da6968a39fd8e6e7.yaml b/releasenotes/notes/1.1/add-scheduler-warnings-da6968a39fd8e6e7.yaml similarity index 100% rename from releasenotes/notes/add-scheduler-warnings-da6968a39fd8e6e7.yaml rename to releasenotes/notes/1.1/add-scheduler-warnings-da6968a39fd8e6e7.yaml diff --git a/releasenotes/notes/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml b/releasenotes/notes/1.1/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml similarity index 100% rename from releasenotes/notes/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml rename to releasenotes/notes/1.1/add-use-dag-flag-two-qubit-basis-decomposer-024a9ced9833289c.yaml diff --git a/releasenotes/notes/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml b/releasenotes/notes/1.1/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml similarity index 100% rename from releasenotes/notes/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml rename to releasenotes/notes/1.1/added-parameter-ctrl_state-mcx-816dcd80e459a5ed.yaml diff --git a/releasenotes/notes/classical-store-e64ee1286219a862.yaml b/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml similarity index 79% rename from releasenotes/notes/classical-store-e64ee1286219a862.yaml rename to releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml index 9de8affebe4..6718cd66f1f 100644 --- a/releasenotes/notes/classical-store-e64ee1286219a862.yaml +++ b/releasenotes/notes/1.1/classical-store-e64ee1286219a862.yaml @@ -54,3 +54,16 @@ features_circuits: Variables can be used wherever classical expressions (see :mod:`qiskit.circuit.classical.expr`) are valid. Currently this is the target expressions of control-flow operations, though we plan to expand this to gate parameters in the future, as the type and expression system are expanded. + + See :ref:`circuit-repr-real-time-classical` for more discussion of these variables, and the + associated data model. + + These are supported throughout the transpiler, through QPY serialization (:mod:`qiskit.qpy`), + OpenQASM 3 export (:mod:`qiskit.qasm3`), and have initial support through the circuit visualizers + (see :meth:`.QuantumCircuit.draw`). + + .. note:: + + The new classical variables and storage will take some time to become supported on hardware + and simulator backends. They are not supported in the primitives interfaces + (:mod:`qiskit.primitives`), but will likely inform those interfaces as they evolve. diff --git a/releasenotes/notes/commutation-checker-utf8-47b13b78a40af196.yaml b/releasenotes/notes/1.1/commutation-checker-utf8-47b13b78a40af196.yaml similarity index 100% rename from releasenotes/notes/commutation-checker-utf8-47b13b78a40af196.yaml rename to releasenotes/notes/1.1/commutation-checker-utf8-47b13b78a40af196.yaml diff --git a/releasenotes/notes/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml b/releasenotes/notes/1.1/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml similarity index 100% rename from releasenotes/notes/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml rename to releasenotes/notes/1.1/commutative-cancellation-preset-passmanager-c137ce516a10eae5.yaml diff --git a/releasenotes/notes/1.1/databin-construction-72ec041075410cb2.yaml b/releasenotes/notes/1.1/databin-construction-72ec041075410cb2.yaml new file mode 100644 index 00000000000..a9dbd60298f --- /dev/null +++ b/releasenotes/notes/1.1/databin-construction-72ec041075410cb2.yaml @@ -0,0 +1,16 @@ +--- +features_primitives: + - | + `qiskit.primitives.containers.DataBin` now satisfies the `qiskit.primitives.containers.Shaped` + protocol. This means that every `DataBin` instance now has the additional attributes + * `shape: tuple[int, ...]` the leading shape of every entry in the instance + * `ndim: int` the length of `shape` + * `size: int` the product of the entries of `shape` + The shape can be passed to the constructor. +upgrade_primitives: + - | + The function `qiskit.primitives.containers.make_data_bin()` no longer creates and returns a + `qiskit.primitives.containers.DataBin` subclass. It instead always returns the `DataBin` class. + However, it continues to exist for backwards compatibility, though will eventually be deprecated. + All users should migrate to construct `DataBin` instances directly, instead of instantiating + subclasses as output by `make_data_bin()`. diff --git a/releasenotes/notes/databin-mapping-45d24d71f9bb4eda.yaml b/releasenotes/notes/1.1/databin-mapping-45d24d71f9bb4eda.yaml similarity index 100% rename from releasenotes/notes/databin-mapping-45d24d71f9bb4eda.yaml rename to releasenotes/notes/1.1/databin-mapping-45d24d71f9bb4eda.yaml diff --git a/releasenotes/notes/deprecate-3.8-a9db071fa3c85b1a.yaml b/releasenotes/notes/1.1/deprecate-3.8-a9db071fa3c85b1a.yaml similarity index 100% rename from releasenotes/notes/deprecate-3.8-a9db071fa3c85b1a.yaml rename to releasenotes/notes/1.1/deprecate-3.8-a9db071fa3c85b1a.yaml diff --git a/releasenotes/notes/deprecate_providerV1-ba17d7b4639d1cc5.yaml b/releasenotes/notes/1.1/deprecate_providerV1-ba17d7b4639d1cc5.yaml similarity index 100% rename from releasenotes/notes/deprecate_providerV1-ba17d7b4639d1cc5.yaml rename to releasenotes/notes/1.1/deprecate_providerV1-ba17d7b4639d1cc5.yaml diff --git a/releasenotes/notes/1.1/expr-bitshift-index-e9cfc6ea8729ef5e.yaml b/releasenotes/notes/1.1/expr-bitshift-index-e9cfc6ea8729ef5e.yaml new file mode 100644 index 00000000000..78d31f8238a --- /dev/null +++ b/releasenotes/notes/1.1/expr-bitshift-index-e9cfc6ea8729ef5e.yaml @@ -0,0 +1,20 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + indexing and bitshifting of unsigned integers and bitlikes (e.g. :class:`.ClassicalRegister`). + For example, it is now possible to compare one register with the bitshift of another:: + + from qiskit.circuit import QuantumCircuit, ClassicalRegister + from qiskit.circuit.classical import expr + + cr1 = ClassicalRegister(4, "cr1") + cr2 = ClassicalRegister(4, "cr2") + qc = QuantumCircuit(cr1, cr2) + with qc.if_test(expr.equal(cr1, expr.shift_left(cr2, 2))): + pass + + Qiskit can also represent a condition that dynamically indexes into a register:: + + with qc.if_test(expr.index(cr1, cr2)): + pass diff --git a/releasenotes/notes/faster-lie-trotter-ba8f6dd84fe4cae4.yaml b/releasenotes/notes/1.1/faster-lie-trotter-ba8f6dd84fe4cae4.yaml similarity index 100% rename from releasenotes/notes/faster-lie-trotter-ba8f6dd84fe4cae4.yaml rename to releasenotes/notes/1.1/faster-lie-trotter-ba8f6dd84fe4cae4.yaml diff --git a/releasenotes/notes/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml b/releasenotes/notes/1.1/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml similarity index 100% rename from releasenotes/notes/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml rename to releasenotes/notes/1.1/fix-backend-primitives-performance-1409b08ccc2a5ce9.yaml diff --git a/releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml b/releasenotes/notes/1.1/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml similarity index 100% rename from releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml rename to releasenotes/notes/1.1/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml diff --git a/releasenotes/notes/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml b/releasenotes/notes/1.1/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml similarity index 100% rename from releasenotes/notes/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml rename to releasenotes/notes/1.1/fix-control-flow-fold-minus-one-f2af168a5313385f.yaml diff --git a/releasenotes/notes/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml b/releasenotes/notes/1.1/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml similarity index 100% rename from releasenotes/notes/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml rename to releasenotes/notes/1.1/fix-custom-pulse-qobj-conversion-5d6041b36356cfd1.yaml diff --git a/releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml b/releasenotes/notes/1.1/fix-custom-transpile-constraints-5defa36d540d1608.yaml similarity index 100% rename from releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml rename to releasenotes/notes/1.1/fix-custom-transpile-constraints-5defa36d540d1608.yaml diff --git a/releasenotes/notes/fix-equivalence-setentry-5a30b0790666fcf2.yaml b/releasenotes/notes/1.1/fix-equivalence-setentry-5a30b0790666fcf2.yaml similarity index 100% rename from releasenotes/notes/fix-equivalence-setentry-5a30b0790666fcf2.yaml rename to releasenotes/notes/1.1/fix-equivalence-setentry-5a30b0790666fcf2.yaml diff --git a/releasenotes/notes/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml b/releasenotes/notes/1.1/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml similarity index 100% rename from releasenotes/notes/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml rename to releasenotes/notes/1.1/fix-evolved-operator-ansatz-empty-ops-bf8ecfae8f1e1001.yaml diff --git a/releasenotes/notes/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml b/releasenotes/notes/1.1/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml similarity index 100% rename from releasenotes/notes/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml rename to releasenotes/notes/1.1/fix-instruction-repeat-conditional-dfe4d7ced54a7bb6.yaml diff --git a/releasenotes/notes/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml b/releasenotes/notes/1.1/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml similarity index 100% rename from releasenotes/notes/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml rename to releasenotes/notes/1.1/fix-inverse-cancellation-self-inverse-e09a5553331e1b0b.yaml diff --git a/releasenotes/notes/fix-mcx-mcp-performance-b00040804b47b200.yaml b/releasenotes/notes/1.1/fix-mcx-mcp-performance-b00040804b47b200.yaml similarity index 100% rename from releasenotes/notes/fix-mcx-mcp-performance-b00040804b47b200.yaml rename to releasenotes/notes/1.1/fix-mcx-mcp-performance-b00040804b47b200.yaml diff --git a/releasenotes/notes/fix-missing-qubit-properties-35137aa5250d9368.yaml b/releasenotes/notes/1.1/fix-missing-qubit-properties-35137aa5250d9368.yaml similarity index 100% rename from releasenotes/notes/fix-missing-qubit-properties-35137aa5250d9368.yaml rename to releasenotes/notes/1.1/fix-missing-qubit-properties-35137aa5250d9368.yaml diff --git a/releasenotes/notes/fix-passmanager-reuse-151877e1905d49df.yaml b/releasenotes/notes/1.1/fix-passmanager-reuse-151877e1905d49df.yaml similarity index 100% rename from releasenotes/notes/fix-passmanager-reuse-151877e1905d49df.yaml rename to releasenotes/notes/1.1/fix-passmanager-reuse-151877e1905d49df.yaml diff --git a/releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml b/releasenotes/notes/1.1/fix-pauli-evolve-ecr-and-name-bugs.yaml similarity index 100% rename from releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml rename to releasenotes/notes/1.1/fix-pauli-evolve-ecr-and-name-bugs.yaml diff --git a/releasenotes/notes/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml b/releasenotes/notes/1.1/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml similarity index 100% rename from releasenotes/notes/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml rename to releasenotes/notes/1.1/fix-performance-scaling-num-bits-qpy-37b5109a40cccc54.yaml diff --git a/releasenotes/notes/fix-pub-coerce-5d13700e15126421.yaml b/releasenotes/notes/1.1/fix-pub-coerce-5d13700e15126421.yaml similarity index 100% rename from releasenotes/notes/fix-pub-coerce-5d13700e15126421.yaml rename to releasenotes/notes/1.1/fix-pub-coerce-5d13700e15126421.yaml diff --git a/releasenotes/notes/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml b/releasenotes/notes/1.1/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml similarity index 100% rename from releasenotes/notes/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml rename to releasenotes/notes/1.1/fix-pulse-builder-default-alingment-52f81224d90c21e2.yaml diff --git a/releasenotes/notes/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml b/releasenotes/notes/1.1/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml similarity index 100% rename from releasenotes/notes/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml rename to releasenotes/notes/1.1/fix-pulse-parameter-formatter-2ee3fb91efb2794c.yaml diff --git a/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml b/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml new file mode 100644 index 00000000000..62e7e945161 --- /dev/null +++ b/releasenotes/notes/1.1/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml @@ -0,0 +1,3 @@ +fixes: + - | + Fix incorrect implemention of `qDRIFT`, negative coefficients of the Hamiltonian are now added back whereas they were always forced to be positive. diff --git a/releasenotes/notes/fix-scheduling-units-59477912b47d3dc1.yaml b/releasenotes/notes/1.1/fix-scheduling-units-59477912b47d3dc1.yaml similarity index 100% rename from releasenotes/notes/fix-scheduling-units-59477912b47d3dc1.yaml rename to releasenotes/notes/1.1/fix-scheduling-units-59477912b47d3dc1.yaml diff --git a/releasenotes/notes/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml b/releasenotes/notes/1.1/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml similarity index 100% rename from releasenotes/notes/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml rename to releasenotes/notes/1.1/fix-transpile-control-flow-no-hardware-7c00ad733a569bb9.yaml diff --git a/releasenotes/notes/fix_soft_compare-3f4148aab3a4606b.yaml b/releasenotes/notes/1.1/fix_soft_compare-3f4148aab3a4606b.yaml similarity index 100% rename from releasenotes/notes/fix_soft_compare-3f4148aab3a4606b.yaml rename to releasenotes/notes/1.1/fix_soft_compare-3f4148aab3a4606b.yaml diff --git a/releasenotes/notes/1.1/fixes_10852-e197344c5f44b4f1.yaml b/releasenotes/notes/1.1/fixes_10852-e197344c5f44b4f1.yaml new file mode 100644 index 00000000000..755403d98a3 --- /dev/null +++ b/releasenotes/notes/1.1/fixes_10852-e197344c5f44b4f1.yaml @@ -0,0 +1,5 @@ +--- +features_providers: + - | + The :class:`.BasicSimulator` python-based simulator included in :mod:`qiskit.providers.basic_provider` + now includes all the standard gates (:mod:`qiskit.circuit.library .standard_gates`) up to 3 qubits. diff --git a/releasenotes/notes/fixes_11212-d6de3c007ce6d697.yaml b/releasenotes/notes/1.1/fixes_11212-d6de3c007ce6d697.yaml similarity index 100% rename from releasenotes/notes/fixes_11212-d6de3c007ce6d697.yaml rename to releasenotes/notes/1.1/fixes_11212-d6de3c007ce6d697.yaml diff --git a/releasenotes/notes/followup_11468-61c6181e62531796.yaml b/releasenotes/notes/1.1/followup_11468-61c6181e62531796.yaml similarity index 100% rename from releasenotes/notes/followup_11468-61c6181e62531796.yaml rename to releasenotes/notes/1.1/followup_11468-61c6181e62531796.yaml diff --git a/releasenotes/notes/histogram-style-03807965c3cc2e8a.yaml b/releasenotes/notes/1.1/histogram-style-03807965c3cc2e8a.yaml similarity index 100% rename from releasenotes/notes/histogram-style-03807965c3cc2e8a.yaml rename to releasenotes/notes/1.1/histogram-style-03807965c3cc2e8a.yaml diff --git a/releasenotes/notes/layout-compose-0b9a9a72359638d8.yaml b/releasenotes/notes/1.1/layout-compose-0b9a9a72359638d8.yaml similarity index 100% rename from releasenotes/notes/layout-compose-0b9a9a72359638d8.yaml rename to releasenotes/notes/1.1/layout-compose-0b9a9a72359638d8.yaml diff --git a/releasenotes/notes/1.1/macos-arm64-tier-1-c5030f009be6adcb.yaml b/releasenotes/notes/1.1/macos-arm64-tier-1-c5030f009be6adcb.yaml new file mode 100644 index 00000000000..b59f5b9844c --- /dev/null +++ b/releasenotes/notes/1.1/macos-arm64-tier-1-c5030f009be6adcb.yaml @@ -0,0 +1,11 @@ +--- +other: + - | + Support for the arm64 macOS platform has been promoted from Tier 3 + to Tier 1. Previously the platform was at Tier 3 because there was + no available CI environment for testing Qiskit on the platform. Now + that Github has made an arm64 macOS environment available to open source + projects [#]_ we're testing the platform along with the other Tier 1 + supported platforms. + + .. [#] https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/ diff --git a/releasenotes/notes/nlocal-perf-3b8ebd9be1b2f4b3.yaml b/releasenotes/notes/1.1/nlocal-perf-3b8ebd9be1b2f4b3.yaml similarity index 100% rename from releasenotes/notes/nlocal-perf-3b8ebd9be1b2f4b3.yaml rename to releasenotes/notes/1.1/nlocal-perf-3b8ebd9be1b2f4b3.yaml diff --git a/releasenotes/notes/numpy-2.0-2f3e35bd42c48518.yaml b/releasenotes/notes/1.1/numpy-2.0-2f3e35bd42c48518.yaml similarity index 100% rename from releasenotes/notes/numpy-2.0-2f3e35bd42c48518.yaml rename to releasenotes/notes/1.1/numpy-2.0-2f3e35bd42c48518.yaml diff --git a/releasenotes/notes/obs-array-coerce-0d-28b192fb3d004d4a.yaml b/releasenotes/notes/1.1/obs-array-coerce-0d-28b192fb3d004d4a.yaml similarity index 100% rename from releasenotes/notes/obs-array-coerce-0d-28b192fb3d004d4a.yaml rename to releasenotes/notes/1.1/obs-array-coerce-0d-28b192fb3d004d4a.yaml diff --git a/releasenotes/notes/1.1/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml b/releasenotes/notes/1.1/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml new file mode 100644 index 00000000000..759f023efc8 --- /dev/null +++ b/releasenotes/notes/1.1/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an issue with the :meth:`.Operator.from_circuit` constructor method where it would incorrectly + interpret the final layout permutation resulting in an invalid `Operator` being constructed. + Previously, the final layout was processed without regards for the initial layout, i.e. the + initialization was incorrect for all quantum circuits that have a non-trivial initial layout. diff --git a/releasenotes/notes/optimization-level2-2c8c1488173aed31.yaml b/releasenotes/notes/1.1/optimization-level2-2c8c1488173aed31.yaml similarity index 100% rename from releasenotes/notes/optimization-level2-2c8c1488173aed31.yaml rename to releasenotes/notes/1.1/optimization-level2-2c8c1488173aed31.yaml diff --git a/releasenotes/notes/1.1/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml b/releasenotes/notes/1.1/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml new file mode 100644 index 00000000000..231f0e7c8f3 --- /dev/null +++ b/releasenotes/notes/1.1/optimize-annotated-conjugate-reduction-656438d3642f27dc.yaml @@ -0,0 +1,24 @@ +features: + - | + Added a new reduction to the :class:`.OptimizeAnnotated` transpiler pass. + This reduction looks for annotated operations (objects of type :class:`.AnnotatedOperation` + that consist of a base operation ``B`` and a list ``M`` of control, inverse and power + modifiers) with the following properties: + + * the base operation ``B`` needs to be synthesized (i.e. it's not already supported + by the target or belongs to the equivalence library) + + * the definition circuit for ``B`` can be expressed as ``P -- Q -- R`` with :math:`R = P^{-1}` + + In this case the modifiers can be moved to the ``Q``-part only. As a specific example, + controlled QFT-based adders have the form ``control - [QFT -- U -- IQFT]``, which can be + simplified to ``QFT -- control-[U] -- IQFT``. By removing the controls over ``QFT`` and + ``IQFT`` parts of the circuit, one obtains significantly fewer gates in the transpiled + circuit. + - | + Added two new methods to the :class:`~qiskit.dagcircuit.DAGCircuit` class: + :meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to + :class:`.DAGOpNode` successors of a node, and + :meth:`qiskit.dagcircuit.DAGCircuit.op_successors` returns an iterator to + :class:`.DAGOpNode` predecessors of a node. + diff --git a/releasenotes/notes/parameter-hash-eq-645f9de55aa78d02.yaml b/releasenotes/notes/1.1/parameter-hash-eq-645f9de55aa78d02.yaml similarity index 100% rename from releasenotes/notes/parameter-hash-eq-645f9de55aa78d02.yaml rename to releasenotes/notes/1.1/parameter-hash-eq-645f9de55aa78d02.yaml diff --git a/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml new file mode 100644 index 00000000000..551ea9e918c --- /dev/null +++ b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml @@ -0,0 +1,8 @@ +--- +features_pulse: + - | + It is now possible to assign parameters to pulse :class:`.Schedule`and :class:`.ScheduleBlock` objects by specifying + the parameter name as a string. The parameter name can be used to assign values to all parameters within the + `Schedule` or `ScheduleBlock` that have the same name. Moreover, the parameter name of a `ParameterVector` + can be used to assign all values of the vector simultaneously (the list of values should therefore match the + length of the vector). diff --git a/releasenotes/notes/pauli-apply-layout-cdcbc1bce724a150.yaml b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml similarity index 100% rename from releasenotes/notes/pauli-apply-layout-cdcbc1bce724a150.yaml rename to releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml diff --git a/releasenotes/notes/public-noncommutation-graph-dd31c931b7045a4f.yaml b/releasenotes/notes/1.1/public-noncommutation-graph-dd31c931b7045a4f.yaml similarity index 100% rename from releasenotes/notes/public-noncommutation-graph-dd31c931b7045a4f.yaml rename to releasenotes/notes/1.1/public-noncommutation-graph-dd31c931b7045a4f.yaml diff --git a/releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml b/releasenotes/notes/1.1/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml similarity index 100% rename from releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml rename to releasenotes/notes/1.1/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml diff --git a/releasenotes/notes/1.1/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml b/releasenotes/notes/1.1/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml new file mode 100644 index 00000000000..217fbc46412 --- /dev/null +++ b/releasenotes/notes/1.1/qasm3-parameter-gate-clash-34ef7b0383849a78.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :class:`.Parameter` instances used as stand-ins for ``input`` variables in + OpenQASM 3 programs will now have their names escaped to avoid collisions + with built-in gates during the export to OpenQASM 3. Previously there + could be a naming clash, and the exporter would generate invalid OpenQASM 3. diff --git a/releasenotes/notes/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml b/releasenotes/notes/1.1/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml similarity index 100% rename from releasenotes/notes/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml rename to releasenotes/notes/1.1/qcstyle-bug-custom-style-dicts-22deab6c602ccd6a.yaml diff --git a/releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml b/releasenotes/notes/1.1/quantumcircuit-append-copy-8a9b71ad4b789490.yaml similarity index 100% rename from releasenotes/notes/quantumcircuit-append-copy-8a9b71ad4b789490.yaml rename to releasenotes/notes/1.1/quantumcircuit-append-copy-8a9b71ad4b789490.yaml diff --git a/releasenotes/notes/qv-perf-be76290f472e4777.yaml b/releasenotes/notes/1.1/qv-perf-be76290f472e4777.yaml similarity index 100% rename from releasenotes/notes/qv-perf-be76290f472e4777.yaml rename to releasenotes/notes/1.1/qv-perf-be76290f472e4777.yaml diff --git a/releasenotes/notes/remove-final-reset-488247c01c4e147d.yaml b/releasenotes/notes/1.1/remove-final-reset-488247c01c4e147d.yaml similarity index 100% rename from releasenotes/notes/remove-final-reset-488247c01c4e147d.yaml rename to releasenotes/notes/1.1/remove-final-reset-488247c01c4e147d.yaml diff --git a/releasenotes/notes/removed_deprecated_0.21-741d08a01a7ed527.yaml b/releasenotes/notes/1.1/removed_deprecated_0.21-741d08a01a7ed527.yaml similarity index 100% rename from releasenotes/notes/removed_deprecated_0.21-741d08a01a7ed527.yaml rename to releasenotes/notes/1.1/removed_deprecated_0.21-741d08a01a7ed527.yaml diff --git a/releasenotes/notes/1.1/reverse-permutation-lnn-409a07c7f6d0eed9.yaml b/releasenotes/notes/1.1/reverse-permutation-lnn-409a07c7f6d0eed9.yaml new file mode 100644 index 00000000000..357345adfa2 --- /dev/null +++ b/releasenotes/notes/1.1/reverse-permutation-lnn-409a07c7f6d0eed9.yaml @@ -0,0 +1,8 @@ +--- +features_synthesis: + - | + Add a new synthesis method :func:`.synth_permutation_reverse_lnn_kms` + of reverse permutations for linear nearest-neighbor architectures using + Kutin, Moulton, Smithline method. + 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`. diff --git a/releasenotes/notes/1.1/rework-inst-durations-passes-28c78401682e22c0.yaml b/releasenotes/notes/1.1/rework-inst-durations-passes-28c78401682e22c0.yaml new file mode 100644 index 00000000000..2ccd92f19c1 --- /dev/null +++ b/releasenotes/notes/1.1/rework-inst-durations-passes-28c78401682e22c0.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + The internal handling of custom circuit calibrations and :class:`.InstructionDurations` + has been offloaded from the :func:`.transpile` function to the individual transpiler passes: + :class:`qiskit.transpiler.passes.scheduling.DynamicalDecoupling`, + :class:`qiskit.transpiler.passes.scheduling.padding.DynamicalDecoupling`. Before, + instruction durations from circuit calibrations would not be taken into account unless + they were manually incorporated into `instruction_durations` input argument, but the passes + that need it now analyze the circuit and pick the most relevant duration value according + to the following priority order: target > custom input > circuit calibrations. + + - | + Fixed a bug in :func:`.transpile` where the ``num_processes`` argument would only be used + if ``dt`` or ``instruction_durations`` were provided. \ No newline at end of file diff --git a/releasenotes/notes/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml b/releasenotes/notes/1.1/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml similarity index 100% rename from releasenotes/notes/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml rename to releasenotes/notes/1.1/rust-two-qubit-basis-decomposer-329ead588fa7526d.yaml diff --git a/releasenotes/notes/rust-two-qubit-weyl-ec551f3f9c812124.yaml b/releasenotes/notes/1.1/rust-two-qubit-weyl-ec551f3f9c812124.yaml similarity index 100% rename from releasenotes/notes/rust-two-qubit-weyl-ec551f3f9c812124.yaml rename to releasenotes/notes/1.1/rust-two-qubit-weyl-ec551f3f9c812124.yaml diff --git a/releasenotes/notes/1.1/sampler-pub-result-e64e7de1bae2d35e.yaml b/releasenotes/notes/1.1/sampler-pub-result-e64e7de1bae2d35e.yaml new file mode 100644 index 00000000000..2c5c2a6e10c --- /dev/null +++ b/releasenotes/notes/1.1/sampler-pub-result-e64e7de1bae2d35e.yaml @@ -0,0 +1,17 @@ +--- +features_primitives: + - | + The subclass :class:`~.SamplerPubResult` of :class:`~.PubResult` was added, + which :class:`~.BaseSamplerV2` implementations can return. The main feature + added in this new subclass is :meth:`~.SamplerPubResult.join_data`, which + joins together (a subset of) the contents of :attr:`~.PubResult.data` into + a single object. This enables the following patterns: + + .. code:: python + + job_result = sampler.run([pub1, pub2, pub3], shots=123).result() + + # assuming all returned data entries are BitArrays + counts1 = job_result[0].join_data().get_counts() + bistrings2 = job_result[1].join_data().get_bitstrings() + array3 = job_result[2].join_data().array \ No newline at end of file diff --git a/releasenotes/notes/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml b/releasenotes/notes/1.1/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml similarity index 100% rename from releasenotes/notes/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml rename to releasenotes/notes/1.1/show_idle_and_show_barrier-6e77e1f9d6f55599.yaml diff --git a/releasenotes/notes/spo-to-matrix-26445a791e24f62a.yaml b/releasenotes/notes/1.1/spo-to-matrix-26445a791e24f62a.yaml similarity index 100% rename from releasenotes/notes/spo-to-matrix-26445a791e24f62a.yaml rename to releasenotes/notes/1.1/spo-to-matrix-26445a791e24f62a.yaml diff --git a/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml b/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml new file mode 100644 index 00000000000..ff83deee939 --- /dev/null +++ b/releasenotes/notes/1.1/star-prerouting-0998b59880c20cef.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Added a new transpiler pass :class:`.StarPreRouting` which is designed to identify star connectivity subcircuits + and then replace them with an optimal linear routing. This is useful for certain circuits that are composed of + this circuit connectivity such as Bernstein Vazirani and QFT. For example: + + .. plot: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + qc.draw("mpl") + + .. plot: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import StarPreRouting + + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + qc.measure_all() + StarPreRouting()(qc).draw("mpl") diff --git a/releasenotes/notes/update-gate-dictionary-c0c017be67bb2f29.yaml b/releasenotes/notes/1.1/update-gate-dictionary-c0c017be67bb2f29.yaml similarity index 100% rename from releasenotes/notes/update-gate-dictionary-c0c017be67bb2f29.yaml rename to releasenotes/notes/1.1/update-gate-dictionary-c0c017be67bb2f29.yaml diff --git a/releasenotes/notes/use-target-in-transpile-7c04b14549a11f40.yaml b/releasenotes/notes/1.1/use-target-in-transpile-7c04b14549a11f40.yaml similarity index 100% rename from releasenotes/notes/use-target-in-transpile-7c04b14549a11f40.yaml rename to releasenotes/notes/1.1/use-target-in-transpile-7c04b14549a11f40.yaml diff --git a/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml new file mode 100644 index 00000000000..7f2e20db652 --- /dev/null +++ b/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml @@ -0,0 +1,14 @@ +--- +features_circuits: + - | + Added a new function to ``qiskit.circuit.random`` that allows to generate a pseudo-random + Clifford circuit with gates from the standard library: :func:`.random_clifford_circuit`. + Example usage: + + .. plot:: + :include-source: + + from qiskit.circuit.random import random_clifford_circuit + + circ = random_clifford_circuit(num_qubits=2, num_gates=6) + circ.draw(output='mpl') diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml new file mode 100644 index 00000000000..d826bc15e48 --- /dev/null +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -0,0 +1,79 @@ +--- +features_circuits: + - | + A native rust representation of Qiskit's standard gate library has been added. When a standard gate + is added to a :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` it is now represented in a more + efficient manner directly in Rust seamlessly. Accessing that gate object from a circuit or dag will + return a new Python object representing the standard gate. This leads to faster and more efficient + transpilation and manipulation of circuits for functionality written in Rust. +features_misc: + - | + Added a new build-time environment variable ``QISKIT_NO_CACHE_GATES`` which + when set to a value of ``1`` (i.e. ``QISKIT_NO_CACHE_GATES=1``) which + decreases the memory overhead of a :class:`.CircuitInstruction` and + :class:`.DAGOpNode` object at the cost of decreased runtime on multiple + accesses to :attr:`.CircuitInstruction.operation` and :attr:`.DAGOpNode.op`. + If this environment variable is set when building the Qiskit python package + from source the caching of the return of these attributes will be disabled. +upgrade_circuits: + - | + The :class:`.Operation` instances of :attr:`.DAGOpNode.op` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.DAGOpNode.op` directly by reference + was unsound and always likely to corrupt the dag's internal state tracking + Due to the internal refactor of the :class:`.QuantumCircuit` and + :class:`.DAGCircuit` to store standard gates in rust the output object from + :attr:`.DAGOpNode.op` will now likely be a copy instead of a shared instance. If you + need to mutate an element should ensure that you either do:: + + op = dag_node.op + op.params[0] = 3.14159 + dag_node.op = op + + or:: + + op = dag_node.op + op.params[0] = 3.14159 + dag.substitute_node(dag_node, op) + + instead of doing something like:: + + dag_node.op.params[0] = 3.14159 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. + - | + The :class:`.Operation` instances of :attr:`.CircuitInstruction.operation` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.CircuitInstruction.operation` directly by reference + was unsound and always likely to corrupt the circuit, especially when + parameters were in use. Due to the internal refactor of the QuantumCircuit + to store standard gates in rust the output object from + :attr:`.CircuitInstruction.operation` will now likely be a copy instead + of a shared instance. If you need to mutate an element in the circuit (which + is strongly **not** recommended as it's inefficient and error prone) you + should ensure that you do:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + op = qc.data[0].operation + op.params[0] = 3.14 + + qc.data[0] = qc.data[0].replace(operation=op) + + instead of doing something like:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + qc.data[0].operation.params[0] = 3.14 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. diff --git a/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml new file mode 100644 index 00000000000..d656ee5cb82 --- /dev/null +++ b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml @@ -0,0 +1,23 @@ +--- +deprecations_circuits: + - | + Treating :class:`.CircuitInstruction` as a tuple-like iterable is deprecated, and this legacy + path way will be removed in Qiskit 2.0. You should use the attribute-access fields + :attr:`~.CircuitInstruction.operation`, :attr:`~.CircuitInstruction.qubits`, and + :attr:`~.CircuitInstruction.clbits` instead. For example:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + + # Deprecated. + for op, qubits, clbits in qc.data: + pass + # New style. + for instruction in qc.data: + op = instruction.operation + qubits = instruction.qubits + clbits = instruction.clbits diff --git a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml new file mode 100644 index 00000000000..f4bb585053b --- /dev/null +++ b/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml @@ -0,0 +1,21 @@ +--- +features_circuits: + - | + The `random_circuit` function from `qiskit.circuit.random.utils` has a new feature where + users can specify a distribution `num_operand_distribution` (a dict) that specifies the + ratio of 1-qubit, 2-qubit, 3-qubit, and 4-qubit gates in the random circuit. For example, + if `num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}` is passed to the function + then the generated circuit will have approximately 25% of 1-qubit, 2-qubit, 3-qubit, and + 4-qubit gates (The order in which the dictionary is passed does not matter i.e. you can specify + `num_operand_distribution = {3: 0.5, 1: 0.0, 4: 0.3, 2: 0.2}` and the function will still work + as expected). Also it should be noted that the if `num_operand_distribution` is not specified + then `max_operands` will default to 4 and a random circuit with a random gate distribution will + be generated. If both `num_operand_distribution` and `max_operands` are specified at the same + time then `num_operand_distribution` will be used to generate the random circuit. + Example usage:: + + from qiskit.circuit.random import random_circuit + + circ = random_circuit(num_qubits=6, depth=5, num_operand_distribution = {1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}) + circ.draw(output='mpl') + diff --git a/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml b/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml new file mode 100644 index 00000000000..9fbe0ffd9c7 --- /dev/null +++ b/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed :meth:`.SparsePauliOp.apply_layout` and :meth:`.Pauli.apply_layout` + to raise :exc:`.QiskitError` if duplicate indices or negative indices are provided + as part of a layout. diff --git a/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml b/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml new file mode 100644 index 00000000000..4eeaa9aa3d7 --- /dev/null +++ b/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix a bug in :class:`~.library.Isometry` due to an unnecessary assertion, + that led to an error in :meth:`.UnitaryGate.control` + when :class:`~.library.UnitaryGate` had more that two qubits. diff --git a/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml b/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml new file mode 100644 index 00000000000..8cee3356ac4 --- /dev/null +++ b/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Improve the decomposition of the gate generated by :meth:`.QuantumCircuit.mcx` + without using ancilla qubits, so that the number of :class:`.CXGate` will grow + quadratically in the number of qubits and not exponentially. diff --git a/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml new file mode 100644 index 00000000000..05ac759569f --- /dev/null +++ b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The :attr:`.QuantumCircuit.parameters` attribute will now correctly be empty + when using :meth:`.QuantumCircuit.copy_empty_like` on a parametric circuit. + Previously, an internal cache would be copied over without invalidation. + Fix `#12617 `__. diff --git a/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml new file mode 100644 index 00000000000..a0744b3dd89 --- /dev/null +++ b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.depth` will now correctly handle operations that + do not have operands, such as :class:`.GlobalPhaseGate`. + - | + :meth:`.QuantumCircuit.depth` will now count the variables and clbits + used in real-time expressions as part of the depth calculation. diff --git a/releasenotes/notes/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml b/releasenotes/notes/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml deleted file mode 100644 index a86869a4e54..00000000000 --- a/releasenotes/notes/fix-qdrift-evolution-bceb9c4f182ab0f5.yaml +++ /dev/null @@ -1,3 +0,0 @@ -fixes: - - | - Fix incorrect implemention of `qDRIFT`, negative coeffients of the Hamiltonian are now added back whereas they were always forced to be positive. diff --git a/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml new file mode 100644 index 00000000000..d995af06bcc --- /dev/null +++ b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix the :class:`.SolovayKitaev` transpiler pass when loading basic + approximations from an exising ``.npy`` file. Previously, loading + a stored approximation which allowed for further reductions (e.g. due + to gate cancellations) could cause a runtime failure. + Additionally, the global phase difference of the U(2) gate product + and SO(3) representation was lost during a save-reload procedure. + Fixes `Qiskit/qiskit#12576 `_. diff --git a/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml b/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml new file mode 100644 index 00000000000..117230aee53 --- /dev/null +++ b/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml @@ -0,0 +1,10 @@ +fixes: + - | + Fixed :meth:`.SparsePauliOp.apply_layout` to work correctly with zero-qubit operators. + For example, if you previously created a 0 qubit and applied a layout like:: + + op = SparsePauliOp("") + op.apply_layout(None, 3) + + this would have previously raised an error. Now this will correctly return an operator of the form: + ``SparsePauliOp(['III'], coeffs=[1.+0.j])`` diff --git a/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml new file mode 100644 index 00000000000..834d7986ab8 --- /dev/null +++ b/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an oversight in the :class:`.Commuting2qGateRouter` transpiler pass where the qreg permutations + were not added to the pass property set, so they would have to be tracked manually by the user. Now it's + possible to access the permutation through the output circuit's ``layout`` property and plug the pass + into any transpilation pipeline without loss of information. diff --git a/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml b/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml new file mode 100644 index 00000000000..5ca00904a9a --- /dev/null +++ b/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a floating-point imprecision when scaling certain pulse units + between seconds and nanoseconds. If the pulse was symbolically defined, + an unnecessary floating-point error could be introduced by the scaling + for certain builds of ``symengine``, which could manifest in unexpected + results once the symbols were fully bound. See `#12392 `__. diff --git a/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml b/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml new file mode 100644 index 00000000000..b158703c6b8 --- /dev/null +++ b/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug in :func:`qiskit.visualization.pulse_v2.interface.draw` that didn't + draw pulse schedules when the draw function was called with a :class:`.BackendV2` argument. + Because the V2 backend doesn't report hardware channel frequencies, + the generated drawing will show 'no freq.' below each channel label. diff --git a/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml b/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml new file mode 100644 index 00000000000..52ea96d0984 --- /dev/null +++ b/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml @@ -0,0 +1,4 @@ +fixes: + - | + The :class:`.VF2Layout` pass would raise an exception when provided with a :class:`.Target` instance without connectivity constraints. + This would be the case with targets from Aer 0.13. The issue is now fixed. diff --git a/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml new file mode 100644 index 00000000000..9d297125e3c --- /dev/null +++ b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + The constructor :class:`.GenericBackendV2` was allowing to create malformed backends because it accepted basis gates that couldn't be allocated in the backend size . That is, a backend with a single qubit should not accept a basis with two-qubit gates. diff --git a/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml new file mode 100644 index 00000000000..5a072f481ab --- /dev/null +++ b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml @@ -0,0 +1,5 @@ +--- +features_circuits: + - | + Improved performance of the method :meth:`.DAGCircuit.quantum_causal_cone` by not examining + the same non-directive node multiple times when reached from different paths. diff --git a/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml b/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml new file mode 100644 index 00000000000..a8e9ec74380 --- /dev/null +++ b/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml @@ -0,0 +1,8 @@ +--- +features_synthesis: + - | + Port internal binary matrix utils from Python to Rust, including + binary matrix multiplication, gaussian elimination, rank calculation, + binary matrix inversion, and random invertible binary matrix generation. + These functions are not part of the Qiskit API, and porting them to rust + improves the performance of certain synthesis methods. diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml new file mode 100644 index 00000000000..6fa548d9245 --- /dev/null +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + The :class:'.StabilizerState' class now has a new method + :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + user to pass single bitstring to measure an outcome for. Previouslly the + :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + at worst case calculate (2^n) number of probability calculations (depending + on the state), even if a user wanted a single result. With this new method + the user can calculate just the single outcome bitstring value a user passes + to measure the probability for. As the number of qubits increases, the more + prevelant the performance enhancement may be (depending on the state) as only + 1 bitstring result is measured. diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml new file mode 100644 index 00000000000..e770aa1ca31 --- /dev/null +++ b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml @@ -0,0 +1,4 @@ +--- +upgrade_synthesis: + - | + Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust. diff --git a/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml b/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml new file mode 100644 index 00000000000..d3266b2aa5f --- /dev/null +++ b/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + :meth:`.PassManager.run` will no longer waste time serializing itself when given multiple inputs + if it is only going to work in serial. diff --git a/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml new file mode 100644 index 00000000000..075de45b3b2 --- /dev/null +++ b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + :class:`.ParameterExpression` was updated so that fully bound instances + that compare equal to instances of Python's built-in numeric types (like + ``float`` and ``int``) also have hash values that match those of the other + instances. This change ensures that these types can be used interchangeably + as dictionary keys. See `#12488 `__. diff --git a/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml new file mode 100644 index 00000000000..72f2c95962a --- /dev/null +++ b/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :func:`plot_coupling_map` that caused the edges of the coupling map to be colored incorrectly. + See https://github.com/Qiskit/qiskit/pull/12369 for details. diff --git a/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml new file mode 100644 index 00000000000..2fb1b4dcc5a --- /dev/null +++ b/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The OpenQASM 2.0 parser (:func:`.qasm2.load` and :func:`.qasm2.loads`) can now evaluate + gate-angle expressions including integer operands that would overflow the system-size integer. + These will be evaluated in a double-precision floating-point context, just like the rest of the + expression always has been. Beware: an arbitrarily large integer will not necessarily be + exactly representable in double-precision floating-point, so there is a chance that however the + circuit was generated, it had already lost all numerical precision modulo :math:`2\pi`. diff --git a/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml new file mode 100644 index 00000000000..84cfd1a1d35 --- /dev/null +++ b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Custom gates (those stemming from a ``gate`` statement) in imported OpenQASM 2 programs will now + have an :meth:`.Gate.to_matrix` implementation. Previously they would have no matrix definition, + meaning that roundtrips through OpenQASM 2 could needlessly lose the ability to derive the gate + matrix. Note, though, that the matrix is calculated by recursively finding the matrices of the + inner gate definitions, as :class:`.Operator` does, which might be less performant than before + the round-trip. diff --git a/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml new file mode 100644 index 00000000000..5bf8e7a80b4 --- /dev/null +++ b/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml @@ -0,0 +1,7 @@ +--- +features_circuits: + - | + Replacing the internal synthesis algorithm of :class:`~.library.StatePreparation` + and :class:`~.library.Initialize` of Shende et al. by the algorithm given in + :class:`~.library.Isometry` of Iten et al. + The new algorithm reduces the number of CX gates and the circuit depth by a factor of 2. diff --git a/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml b/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml new file mode 100644 index 00000000000..b3b18be2fc1 --- /dev/null +++ b/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml @@ -0,0 +1,122 @@ +--- +features_circuits: + - | + :class:`.QuantumCircuit` has several new methods to work with and inspect manual :class:`.Var` + variables. + + See :ref:`circuit-real-time-methods` for more in-depth discussion on all of these. + + The new methods are: + + * :meth:`~.QuantumCircuit.add_var` + * :meth:`~.QuantumCircuit.add_input` + * :meth:`~.QuantumCircuit.add_capture` + * :meth:`~.QuantumCircuit.add_uninitialized_var` + * :meth:`~.QuantumCircuit.get_var` + * :meth:`~.QuantumCircuit.has_var` + * :meth:`~.QuantumCircuit.iter_vars` + * :meth:`~.QuantumCircuit.iter_declared_vars` + * :meth:`~.QuantumCircuit.iter_captured_vars` + * :meth:`~.QuantumCircuit.iter_input_vars` + * :meth:`~.QuantumCircuit.store` + + In addition, there are several new dynamic attributes on :class:`.QuantumCircuit` surrounding + these variables: + + * :attr:`~.QuantumCircuit.num_vars` + * :attr:`~.QuantumCircuit.num_input_vars` + * :attr:`~.QuantumCircuit.num_captured_vars` + * :attr:`~.QuantumCircuit.num_declared_vars` + - | + :class:`.ControlFlowOp` and its subclasses now have a :meth:`~.ControlFlowOp.iter_captured_vars` + method, which will return an iterator over the unique variables captured in any of its immediate + blocks. + - | + :class:`.DAGCircuit` has several new methods to work with and inspect manual :class:`.Var` + variables. These are largely equivalent to their :class:`.QuantumCircuit` counterparts, except + that the :class:`.DAGCircuit` ones are optimized for programmatic access with already defined + objects, while the :class:`.QuantumCircuit` methods are more focussed on interactive human use. + + The new methods are: + + * :meth:`~.DAGCircuit.add_input_var` + * :meth:`~.DAGCircuit.add_captured_var` + * :meth:`~.DAGCircuit.add_declared_var` + * :meth:`~.DAGCircuit.has_var` + * :meth:`~.DAGCircuit.iter_vars` + * :meth:`~.DAGCircuit.iter_declared_vars` + * :meth:`~.DAGCircuit.iter_captured_vars` + * :meth:`~.DAGCircuit.iter_input_vars` + + There are also new public attributes: + + * :attr:`~.DAGCircuit.num_vars` + * :attr:`~.DAGCircuit.num_input_vars` + * :attr:`~.DAGCircuit.num_captured_vars` + * :attr:`~.DAGCircuit.num_declared_vars` + - | + :attr:`.DAGCircuit.wires` will now also contain any :class:`.Var` manual variables in the + circuit as well, as these are also classical data flow. + - | + A new method, :meth:`.Var.new`, is added to manually construct a real-time classical variable + that owns its memory. + - | + :meth:`.QuantumCircuit.compose` has two need keyword arguments, ``var_remap`` and ``inline_captures`` + to better support real-time classical variables. + + ``var_remap`` can be used to rewrite :class:`.Var` nodes in the circuit argument as its + instructions are inlined onto the base circuit. This can be used to avoid naming conflicts. + + ``inline_captures`` can be set to ``True`` (defaults to ``False``) to link all :class:`.Var` + nodes tracked as "captures" in the argument circuit with the same :class:`.Var` nodes in the + base circuit, without attempting to redeclare the variables. This can be used, in combination + with :meth:`.QuantumCircuit.copy_empty_like`'s ``vars_mode="captures"`` handling, to build up + a circuit layer by layer, containing variables. + - | + :meth:`.DAGCircuit.compose` has a new keyword argument, ``inline_captures``, which can be set to + ``True`` to inline "captured" :class:`.Var` nodes on the argument circuit onto the base circuit + without redeclaring them. In conjunction with the ``vars_mode="captures"`` option to several + :class:`.DAGCircuit` methods, this can be used to combine DAGs that operate on the same variables. + - | + :meth:`.QuantumCircuit.copy_empty_like` and :meth:`.DAGCircuit.copy_empty_like` have a new + keyword argument, ``vars_mode`` which controls how any memory-owning :class:`.Var` nodes are + tracked in the output. By default (``"alike"``), the variables are declared in the same + input/captured/local mode as the source. This can be set to ``"captures"`` to convert all + variables to captures (useful with :meth:`~.QuantumCircuit.compose`) or ``"drop"`` to remove + them. + - | + A new ``vars_mode`` keyword argument has been added to the :class:`.DAGCircuit` methods: + + * :meth:`~.DAGCircuit.separable_circuits` + * :meth:`~.DAGCircuit.layers` + * :meth:`~.DAGCircuit.serial_layers` + + which has the same meaning as it does for :meth:`~.DAGCircuit.copy_empty_like`. +features_qasm: + - | + The OpenQASM 3 exporter supports manual-storage :class:`.Var` nodes on circuits. +features_qpy: + - | + QPY (:mod:`qiskit.qpy`) format version 12 has been added, which includes support for memory-owning + :class:`.Var` variables. See :ref:`qpy_version_12` for more detail on the format changes. +features_visualization: + - | + The text and `Matplotlib `__ circuit drawers (:meth:`.QuantumCircuit.draw`) + have minimal support for displaying expressions involving manual real-time variables. The + :class:`.Store` operation and the variable initializations are not yet supported; for large-scale + dynamic circuits, we recommend using the OpenQASM 3 export capabilities (:func:`.qasm3.dumps`) to + get a textual representation of a circuit. +upgrade_qpy: + - | + The value of :attr:`qiskit.qpy.QPY_VERSION` is now 12. :attr:`.QPY_COMPATIBILITY_VERSION` is + unchanged at 10. +upgrade_providers: + - | + Implementations of :class:`.BackendV2` (and :class:`.BackendV1`) may desire to update their + :meth:`~.BackendV2.run` methods to eagerly reject inputs containing typed + classical variables (see :mod:`qiskit.circuit.classical`) and the :class:`.Store` instruction, + if they do not have support for them. The new :class:`.Store` instruction is treated by the + transpiler as an always-available "directive" (like :class:`.Barrier`); if your backends do not + support this won't be caught by the :mod:`~qiskit.transpiler`. + + See :ref:`providers-guide-real-time-variables` for more information. diff --git a/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml b/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml new file mode 100644 index 00000000000..07970679722 --- /dev/null +++ b/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:`.Target.has_calibration` has been updated so that it does not raise + an exception for an instruction that has been added to the target with + ``None`` for its instruction properties. Fixes + `#12525 `__. diff --git a/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml b/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml new file mode 100644 index 00000000000..92fc63d4989 --- /dev/null +++ b/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml @@ -0,0 +1,4 @@ +--- +features_circuits: + - | + :class:`.ParameterExpression` now supports the unary ``+`` operator. diff --git a/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml b/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml new file mode 100644 index 00000000000..4857bb1bda1 --- /dev/null +++ b/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml @@ -0,0 +1,14 @@ +--- +features_transpiler: + - | + A new ``dt`` argument has been added to :func:`.generate_preset_pass_manager` to match + the set of arguments of :func:`.transpile`. This will allow for the internal conversion + of transpilation constraints to a :class:`.Target` representation. + +upgrade_transpiler: + - | + The :func:`.generate_preset_pass_manager` function has been upgraded to, when possible, + internally convert transpiler constraints into a :class:`.Target` instance. + If a `backend` input of type :class:`.BackendV1` is provided, it will be + converted to :class:`.BackendV2` to expose its :class:`.Target`. This change does + not require any user action. diff --git a/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml b/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml new file mode 100644 index 00000000000..9c19be117ed --- /dev/null +++ b/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml @@ -0,0 +1,14 @@ +--- +features_visualization: + - | + The user configuration file has a new option ``circuit_idle_wires``, which takes a Boolean + value. This allows users to set their preferred default behavior of the ``idle_wires`` option + of the circuit drawers :meth:`.QuantumCircuit.draw` and :func:`.circuit_drawer`. For example, + adding a section to ``~/.qiskit/settings.conf`` with: + + .. code-block:: text + + [default] + circuit_idle_wires = false + + will change the default to display the bits in reverse order. diff --git a/requirements-dev.txt b/requirements-dev.txt index c75237e77ed..7c5a909bd39 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,8 +17,8 @@ black[jupyter]~=24.1 # # These versions are pinned precisely because pylint frequently includes new # on-by-default lint failures in new versions, which breaks our CI. -astroid==2.14.2 -pylint==2.16.2 +astroid==3.2.2 +pylint==3.2.3 ruff==0.0.267 diff --git a/requirements-optional.txt b/requirements-optional.txt index 36985cdd7cd..3dfc2031d02 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -19,7 +19,7 @@ seaborn>=0.9.0 # Functionality and accelerators. qiskit-aer -qiskit-qasm3-import +qiskit-qasm3-import>=0.5.0 python-constraint>=1.4 cvxpy scikit-learn>=0.20.0 diff --git a/setup.py b/setup.py index 9bb5b04ae6e..61168050547 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ # # python setup.py build_rust --inplace --release # -# to make optimised Rust components even for editable releases, which would otherwise be quite +# to make optimized Rust components even for editable releases, which would otherwise be quite # unergonomic to do otherwise. @@ -30,6 +30,17 @@ # it's an editable installation. rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None +# If QISKIT_NO_CACHE_GATES is set then don't enable any features while building +# +# TODO: before final release we should reverse this by default once the default transpiler pass +# is all in rust (default to no caching and make caching an opt-in feature). This is opt-out +# right now to avoid the runtime overhead until we are leveraging the rust gates infrastructure. +if os.getenv("QISKIT_NO_CACHE_GATES") == "1": + features = [] +else: + features = ["cache_pygates"] + + setup( rust_extensions=[ RustExtension( @@ -37,6 +48,7 @@ "crates/pyext/Cargo.toml", binding=Binding.PyO3, debug=rust_debug, + features=features, ) ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, diff --git a/test/benchmarks/circuit_construction.py b/test/benchmarks/circuit_construction.py index 71c079476cd..6c6c8733d25 100644 --- a/test/benchmarks/circuit_construction.py +++ b/test/benchmarks/circuit_construction.py @@ -52,7 +52,7 @@ def time_circuit_copy(self, _, __): def build_parameterized_circuit(width, gates, param_count): - params = [Parameter("param-%s" % x) for x in range(param_count)] + params = [Parameter(f"param-{x}") for x in range(param_count)] param_iter = itertools.cycle(params) qr = QuantumRegister(width) diff --git a/test/benchmarks/mapping_passes.py b/test/benchmarks/mapping_passes.py index 180925905d2..4f87323f33a 100644 --- a/test/benchmarks/mapping_passes.py +++ b/test/benchmarks/mapping_passes.py @@ -124,12 +124,12 @@ def time_dense_layout(self, _, __): def time_layout_2q_distance(self, _, __): layout = Layout2qDistance(self.coupling_map) layout.property_set["layout"] = self.layout - layout.run(self.dag) + layout.run(self.enlarge_dag) def time_apply_layout(self, _, __): layout = ApplyLayout() layout.property_set["layout"] = self.layout - layout.run(self.dag) + layout.run(self.enlarge_dag) def time_full_ancilla_allocation(self, _, __): ancilla = FullAncillaAllocation(self.coupling_map) @@ -232,12 +232,6 @@ def setup(self, n_qubits, depth): self.backend_props = Fake20QV1().properties() self.routed_dag = StochasticSwap(self.coupling_map, seed=42).run(self.dag) - def time_cxdirection(self, _, __): - CXDirection(self.coupling_map).run(self.routed_dag) - - def time_check_cx_direction(self, _, __): - CheckCXDirection(self.coupling_map).run(self.routed_dag) - def time_gate_direction(self, _, __): GateDirection(self.coupling_map).run(self.routed_dag) diff --git a/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm b/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm index 4910c35dfec..ba5db139704 100644 --- a/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm +++ b/test/benchmarks/qasm/54QBT_25CYC_QSE_3.qasm @@ -1,7 +1,7 @@ // Originally source from the QUEKO benchmark suite // https://github.com/UCLA-VAST/QUEKO-benchmark // A benchmark that is near-term feasible for Google Sycamore with a optimal -// soluation depth of 25 +// solution depth of 25 OPENQASM 2.0; include "qelib1.inc"; qreg q[54]; diff --git a/test/benchmarks/random_circuit_hex.py b/test/benchmarks/random_circuit_hex.py index 952c651df9f..92f1cb5843b 100644 --- a/test/benchmarks/random_circuit_hex.py +++ b/test/benchmarks/random_circuit_hex.py @@ -41,7 +41,7 @@ def make_circuit_ring(nq, depth, seed): for i in range(nq): # round of single-qubit unitaries u = random_unitary(2, seed).data angles = decomposer.angles(u) - qc.u3(angles[0], angles[1], angles[2], q[i]) + qc.u(angles[0], angles[1], angles[2], q[i]) # insert the final measurements qcm = copy.deepcopy(qc) diff --git a/test/benchmarks/randomized_benchmarking.py b/test/benchmarks/randomized_benchmarking.py index f3c3d18e9f9..9847c928ad7 100644 --- a/test/benchmarks/randomized_benchmarking.py +++ b/test/benchmarks/randomized_benchmarking.py @@ -105,6 +105,7 @@ def clifford_2_qubit_circuit(num): qc = QuantumCircuit(2) if vals[0] == 0 or vals[0] == 3: (form, i0, i1, j0, j1, p0, p1) = vals + k0, k1 = (None, None) else: (form, i0, i1, j0, j1, k0, k1, p0, p1) = vals if i0 == 1: diff --git a/test/benchmarks/scheduling_passes.py b/test/benchmarks/scheduling_passes.py index a4c25dc46bc..34d40ea97e6 100644 --- a/test/benchmarks/scheduling_passes.py +++ b/test/benchmarks/scheduling_passes.py @@ -37,7 +37,7 @@ class SchedulingPassBenchmarks: def setup(self, n_qubits, depth): seed = 42 self.circuit = random_circuit( - n_qubits, depth, measure=True, conditional=True, reset=True, seed=seed, max_operands=2 + n_qubits, depth, measure=True, conditional=False, reset=False, seed=seed, max_operands=2 ) self.basis_gates = ["rz", "sx", "x", "cx", "id", "reset"] self.cmap = [ @@ -108,15 +108,6 @@ def setup(self, n_qubits, depth): ], dt=1e-9, ) - self.timed_dag = TimeUnitConversion(self.durations).run(self.dag) - dd_sequence = [XGate(), XGate()] - pm = PassManager( - [ - ALAPScheduleAnalysis(self.durations), - PadDynamicalDecoupling(self.durations, dd_sequence), - ] - ) - self.scheduled_dag = pm.run(self.timed_dag) def time_time_unit_conversion_pass(self, _, __): TimeUnitConversion(self.durations).run(self.dag) @@ -129,7 +120,7 @@ def time_alap_schedule_pass(self, _, __): PadDynamicalDecoupling(self.durations, dd_sequence), ] ) - pm.run(self.timed_dag) + pm.run(self.transpiled_circuit) def time_asap_schedule_pass(self, _, __): dd_sequence = [XGate(), XGate()] @@ -139,9 +130,4 @@ def time_asap_schedule_pass(self, _, __): PadDynamicalDecoupling(self.durations, dd_sequence), ] ) - pm.run(self.timed_dag) - - def time_dynamical_decoupling_pass(self, _, __): - PadDynamicalDecoupling(self.durations, dd_sequence=[XGate(), XGate()]).run( - self.scheduled_dag - ) + pm.run(self.transpiled_circuit) diff --git a/test/benchmarks/isometry.py b/test/benchmarks/statepreparation.py similarity index 72% rename from test/benchmarks/isometry.py rename to test/benchmarks/statepreparation.py index c3cf13e4d0e..67dc1178fc2 100644 --- a/test/benchmarks/isometry.py +++ b/test/benchmarks/statepreparation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2023 +# (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 @@ -14,26 +14,23 @@ # pylint: disable=attribute-defined-outside-init # pylint: disable=unused-argument +import numpy as np from qiskit import QuantumRegister, QuantumCircuit from qiskit.compiler import transpile -from qiskit.quantum_info.random import random_unitary -from qiskit.circuit.library.generalized_gates import Isometry +from qiskit.circuit.library.data_preparation import StatePreparation -class IsometryTranspileBench: - params = ([0, 1, 2, 3], [3, 4, 5, 6]) - param_names = ["number of input qubits", "number of output qubits"] +class StatePreparationTranspileBench: + params = [4, 5, 6, 7, 8] + param_names = ["number of qubits in state"] - def setup(self, m, n): + def setup(self, n): q = QuantumRegister(n) qc = QuantumCircuit(q) - if not hasattr(qc, "iso"): - raise NotImplementedError - iso = random_unitary(2**n, seed=0).data[:, 0 : 2**m] - if len(iso.shape) == 1: - iso = iso.reshape((len(iso), 1)) - iso_gate = Isometry(iso, 0, 0) - qc.append(iso_gate, q) + state = np.random.rand(2**n) + np.random.rand(2**n) * 1j + state = state / np.linalg.norm(state) + state_gate = StatePreparation(state) + qc.append(state_gate, q) self.circuit = qc diff --git a/test/benchmarks/utils.py b/test/benchmarks/utils.py index af8f3318074..13350346b82 100644 --- a/test/benchmarks/utils.py +++ b/test/benchmarks/utils.py @@ -71,7 +71,7 @@ def random_circuit( Exception: when invalid options given """ if max_operands < 1 or max_operands > 3: - raise Exception("max_operands must be between 1 and 3") + raise ValueError("max_operands must be between 1 and 3") one_q_ops = [ IGate, @@ -126,6 +126,8 @@ def random_circuit( operation = rng.choice(two_q_ops) elif num_operands == 3: operation = rng.choice(three_q_ops) + else: + raise RuntimeError("not supported number of operands") if operation in one_param: num_angles = 1 elif operation in two_param: @@ -213,7 +215,7 @@ def unmajority(p, a, b, c): qc.x(a[0]) # Set input a = 0...0001 qc.x(b) # Set input b = 1...1111 # Apply the adder - qc += adder_subcircuit + qc &= adder_subcircuit # Measure the output register in the computational basis for j in range(n): diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index b655acf5749..012697a17dd 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -224,7 +224,7 @@ def test_binary_bitwise_explicit(self, function, opcode): ) @ddt.unpack def test_binary_bitwise_uint_inference(self, function, opcode): - """The binary bitwise functions have specialised inference for the widths of integer + """The binary bitwise functions have specialized inference for the widths of integer literals, since the bitwise functions require the operands to already be of exactly the same width without promotion.""" cr = ClassicalRegister(8, "c") @@ -247,7 +247,7 @@ def test_binary_bitwise_uint_inference(self, function, opcode): ), ) - # Inference between two integer literals is "best effort". This behaviour isn't super + # Inference between two integer literals is "best effort". This behavior isn't super # important to maintain if we want to change the expression system. self.assertEqual( function(5, 255), @@ -387,3 +387,57 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + + def test_index_explicit(self): + cr = ClassicalRegister(4, "c") + a = expr.Var.new("a", types.Uint(8)) + + self.assertEqual( + expr.index(cr, 3), + expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()), + ) + self.assertEqual( + expr.index(a, cr), + expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), + ) + + def test_index_forbidden(self): + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(Clbit(), 3) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), False) + + @ddt.data( + (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), + (expr.shift_right, expr.Binary.Op.SHIFT_RIGHT), + ) + @ddt.unpack + def test_shift_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + a = expr.Var.new("a", types.Uint(4)) + + self.assertEqual( + function(cr, 5), + expr.Binary( + opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + ), + ) + self.assertEqual( + function(a, cr), + expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), + ) + self.assertEqual( + function(3, 5, types.Uint(8)), + expr.Binary( + opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + ), + ) + + @ddt.data(expr.shift_left, expr.shift_right) + def test_shift_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), Clbit()) diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index f52a896df46..5264e55a52d 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -30,6 +30,8 @@ class TestStructurallyEquivalent(QiskitTestCase): expr.logic_not(Clbit()), expr.bit_and(5, ClassicalRegister(3, "a")), expr.logic_and(expr.less(2, ClassicalRegister(3, "a")), expr.lift(Clbit())), + expr.shift_left(expr.shift_right(255, 3), 3), + expr.index(expr.Var.new("a", types.Uint(8)), 0), ) def test_equivalent_to_self(self, node): self.assertTrue(expr.structurally_equivalent(node, node)) @@ -124,6 +126,7 @@ class TestIsLValue(QiskitTestCase): expr.Var.new("b", types.Uint(8)), expr.Var(Clbit(), types.Bool()), expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)), + expr.index(expr.Var.new("a", types.Uint(8)), 0), ) def test_happy_cases(self, lvalue): self.assertTrue(expr.is_lvalue(lvalue)) @@ -139,6 +142,7 @@ def test_happy_cases(self, lvalue): expr.Var.new("b", types.Bool()), types.Bool(), ), + expr.index(expr.bit_not(expr.Var.new("a", types.Uint(8))), 0), ) def test_bad_cases(self, not_an_lvalue): self.assertFalse(expr.is_lvalue(not_an_lvalue)) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 56726b3342d..625db22cc12 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -51,6 +51,21 @@ def test_types_can_be_cloned(self, obj): expr.Value(True, types.Bool()), types.Bool(), ), + expr.Index( + expr.Var.new("a", types.Uint(3)), + expr.Binary( + expr.Binary.Op.SHIFT_LEFT, + expr.Binary( + expr.Binary.Op.SHIFT_RIGHT, + expr.Var.new("b", types.Uint(3)), + expr.Value(1, types.Uint(1)), + types.Uint(3), + ), + expr.Value(1, types.Uint(1)), + types.Uint(3), + ), + types.Bool(), + ), ) def test_expr_can_be_cloned(self, obj): """Test that various ways of cloning an `Expr` object are valid and produce equal output.""" @@ -70,7 +85,7 @@ def test_var_equality(self): self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool())) # Manually constructing the same object with the same UUID should cause it compare equal, - # though, for serialisation ease. + # though, for serialization ease. self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a")) # This is a badly constructed variable because it's using a different type to refer to the diff --git a/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 2a5070e8ac7..5f0a2814872 100644 --- a/test/python/circuit/library/test_blueprintcircuit.py +++ b/test/python/circuit/library/test_blueprintcircuit.py @@ -77,17 +77,17 @@ def test_invalidate_rebuild(self): with self.subTest(msg="after building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) mock._invalidate() with self.subTest(msg="after invalidating"): self.assertFalse(mock._is_built) - self.assertEqual(len(mock._parameter_table), 0) + self.assertEqual(mock._data.num_params(), 0) mock._build() with self.subTest(msg="after re-building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) def test_calling_attributes_works(self): """Test that the circuit is constructed when attributes are called.""" diff --git a/test/python/circuit/library/test_diagonal.py b/test/python/circuit/library/test_diagonal.py index 7dde0d62d8f..b1158fdab3f 100644 --- a/test/python/circuit/library/test_diagonal.py +++ b/test/python/circuit/library/test_diagonal.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the digonal circuit.""" +"""Test the diagonal circuit.""" import unittest from ddt import ddt, data diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index a2d868fbc1b..a3df1e9664a 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -86,7 +86,8 @@ def random_linear_circuit( elif name == "linear": nqargs = rng.choice(range(1, num_qubits + 1)) qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - mat = random_invertible_binary_matrix(nqargs, seed=rng) + seed = rng.integers(100000, size=1, dtype=np.uint64)[0] + mat = random_invertible_binary_matrix(nqargs, seed=seed) circ.append(LinearFunction(mat), qargs) elif name == "permutation": nqargs = rng.choice(range(1, num_qubits + 1)) @@ -140,10 +141,11 @@ def test_conversion_to_matrix_and_back(self, num_qubits): and then synthesizing this linear function to a quantum circuit.""" rng = np.random.default_rng(1234) - for _ in range(10): - for num_gates in [0, 5, 5 * num_qubits]: + for num_gates in [0, 5, 5 * num_qubits]: + seeds = rng.integers(100000, size=10, dtype=np.uint64) + for seed in seeds: # create a random linear circuit - linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=rng) + linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=seed) self.assertIsInstance(linear_circuit, QuantumCircuit) # convert it to a linear function @@ -168,10 +170,11 @@ def test_conversion_to_linear_function_and_back(self, num_qubits): """Test correctness of first synthesizing a linear circuit from a linear function, and then converting this linear circuit to a linear function.""" rng = np.random.default_rng(5678) + seeds = rng.integers(100000, size=10, dtype=np.uint64) - for _ in range(10): + for seed in seeds: # create a random invertible binary matrix - binary_matrix = random_invertible_binary_matrix(num_qubits, seed=rng) + binary_matrix = random_invertible_binary_matrix(num_qubits, seed=seed) # create a linear function with this matrix linear_function = LinearFunction(binary_matrix, validate_input=True) diff --git a/test/python/circuit/library/test_overlap.py b/test/python/circuit/library/test_overlap.py index a603f28037b..1a95e3ba915 100644 --- a/test/python/circuit/library/test_overlap.py +++ b/test/python/circuit/library/test_overlap.py @@ -131,6 +131,21 @@ def test_mismatching_qubits(self): with self.assertRaises(CircuitError): _ = UnitaryOverlap(unitary1, unitary2) + def test_insert_barrier(self): + """Test inserting barrier between circuits""" + unitary1 = EfficientSU2(1, reps=1) + unitary2 = EfficientSU2(1, reps=1) + overlap = UnitaryOverlap(unitary1, unitary2, insert_barrier=True) + self.assertEqual(overlap.count_ops()["barrier"], 1) + self.assertEqual( + str(overlap.draw(fold=-1, output="text")).strip(), + """ + ┌───────────────────────────────────────┐ ░ ┌──────────────────────────────────────────┐ +q: ┤ EfficientSU2(p1[0],p1[1],p1[2],p1[3]) ├─░─┤ EfficientSU2_dg(p2[0],p2[1],p2[2],p2[3]) ├ + └───────────────────────────────────────┘ ░ └──────────────────────────────────────────┘ +""".strip(), + ) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_qft.py b/test/python/circuit/library/test_qft.py index 3d85bb526dc..078b5af04ea 100644 --- a/test/python/circuit/library/test_qft.py +++ b/test/python/circuit/library/test_qft.py @@ -183,7 +183,7 @@ def __init__(self, *_args, **_kwargs): raise self # We don't want to issue a warning on mutation until we know that the values are - # finalised; this is because a user might want to mutate the number of qubits and the + # finalized; this is because a user might want to mutate the number of qubits and the # approximation degree. In these cases, wait until we try to build the circuit. with warnings.catch_warnings(record=True) as caught_warnings: warnings.filterwarnings( diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 73398e4316b..55028c8e883 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -187,12 +187,20 @@ def test_foreach_op_indexed(self): def test_map_ops(self): """Test all operations are replaced.""" qr = QuantumRegister(5) + + # Use a custom gate to ensure we get a gate class returned and not + # a standard gate. + class CustomXGate(XGate): + """A custom X gate that doesn't have rust native representation.""" + + _standard_gate = None + data_list = [ - CircuitInstruction(XGate(), [qr[0]], []), - CircuitInstruction(XGate(), [qr[1]], []), - CircuitInstruction(XGate(), [qr[2]], []), - CircuitInstruction(XGate(), [qr[3]], []), - CircuitInstruction(XGate(), [qr[4]], []), + CircuitInstruction(CustomXGate(), [qr[0]], []), + CircuitInstruction(CustomXGate(), [qr[1]], []), + CircuitInstruction(CustomXGate(), [qr[2]], []), + CircuitInstruction(CustomXGate(), [qr[3]], []), + CircuitInstruction(CustomXGate(), [qr[4]], []), ] data = CircuitData(qubits=list(qr), data=data_list) data.map_ops(lambda op: op.to_mutable()) @@ -395,6 +403,26 @@ class TestQuantumCircuitInstructionData(QiskitTestCase): # but are included as tests to maintain compatability with the previous # list interface of circuit.data. + def test_iteration_of_data_entry(self): + """Verify that the base types of the legacy tuple iteration are correct, since they're + different to attribute access.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.measure([0, 1, 2], [0, 1, 2]) + + def to_legacy(instruction): + return (instruction.operation, list(instruction.qubits), list(instruction.clbits)) + + expected = [to_legacy(instruction) for instruction in qc.data] + + with self.assertWarnsRegex( + DeprecationWarning, "Treating CircuitInstruction as an iterable is deprecated" + ): + actual = [tuple(instruction) for instruction in qc.data] + self.assertEqual(actual, expected) + def test_getitem_by_insertion_order(self): """Verify one can get circuit.data items in insertion order.""" qr = QuantumRegister(2) @@ -828,6 +856,9 @@ def test_param_gate_instance(self): qc0.append(rx, [0]) qc1.append(rx, [0]) qc0.assign_parameters({a: b}, inplace=True) - qc0_instance = next(iter(qc0._parameter_table[b]))[0] - qc1_instance = next(iter(qc1._parameter_table[a]))[0] + # A fancy way of doing qc0_instance = qc0.data[0] and qc1_instance = qc1.data[0] + # but this at least verifies the parameter table is point from the parameter to + # the correct instruction (which is the only one) + qc0_instance = qc0._data[next(iter(qc0._data._get_param(b.uuid.int)))[0]] + qc1_instance = qc1._data[next(iter(qc1._data._get_param(a.uuid.int)))[0]] self.assertNotEqual(qc0_instance, qc1_instance) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 766d555bda5..04e71a0dd4d 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -22,7 +22,7 @@ import numpy as np from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, pulse -from qiskit.circuit import CASE_DEFAULT +from qiskit.circuit import CASE_DEFAULT, IfElseOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.classical import expr, types from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.quantumregister import Qubit @@ -57,7 +57,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parametervector import ParameterVector from qiskit.synthesis import LieTrotter, SuzukiTrotter -from qiskit.qpy import dump, load +from qiskit.qpy import dump, load, UnsupportedFeatureForVersion, QPY_COMPATIBILITY_VERSION from qiskit.quantum_info import Pauli, SparsePauliOp, Clifford from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate @@ -84,6 +84,26 @@ def assertDeprecatedBitProperties(self, original, roundtripped): original_clbits, roundtripped_clbits = zip(*owned_clbits) self.assertEqual(original_clbits, roundtripped_clbits) + def assertMinimalVarEqual(self, left, right): + """Replacement for asserting `QuantumCircuit` equality for use in `Var` tests, for use while + the `DAGCircuit` does not yet allow full equality checks. This should be removed and the + tests changed to directly call `assertEqual` once possible. + + This filters out instructions that have `QuantumCircuit` parameters in the data comparison + (such as control-flow ops), which need to be handled separately.""" + self.assertEqual(list(left.iter_input_vars()), list(right.iter_input_vars())) + self.assertEqual(list(left.iter_declared_vars()), list(right.iter_declared_vars())) + self.assertEqual(list(left.iter_captured_vars()), list(right.iter_captured_vars())) + + def filter_ops(data): + return [ + ins + for ins in data + if not any(isinstance(x, QuantumCircuit) for x in ins.operation.params) + ] + + self.assertEqual(filter_ops(left.data), filter_ops(right.data)) + def test_qpy_full_path(self): """Test full path qpy serialization for basic circuit.""" qr_a = QuantumRegister(4, "a") @@ -1143,7 +1163,7 @@ def test_qpy_with_for_loop_iterator(self): self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_clbit_switch(self): - """Test QPY serialisation for a switch statement with a Clbit target.""" + """Test QPY serialization for a switch statement with a Clbit target.""" case_t = QuantumCircuit(2, 1) case_t.x(0) case_f = QuantumCircuit(2, 1) @@ -1160,7 +1180,7 @@ def test_qpy_clbit_switch(self): self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_register_switch(self): - """Test QPY serialisation for a switch statement with a ClassicalRegister target.""" + """Test QPY serialization for a switch statement with a ClassicalRegister target.""" qreg = QuantumRegister(2, "q") creg = ClassicalRegister(3, "c") @@ -1760,6 +1780,190 @@ def test_annotated_operations_iterative(self): new_circuit = load(fptr)[0] self.assertEqual(circuit, new_circuit) + def test_load_empty_vars(self): + """Test loading empty circuits with variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + all_vars = { + a: expr.lift(False), + b: expr.lift(3, type=b.type), + expr.Var.new("θψφ", types.Bool()): expr.logic_not(a), + expr.Var.new("🐍🐍🐍", types.Uint(8)): expr.bit_and(b, b), + } + + inputs = QuantumCircuit(inputs=list(all_vars)) + with io.BytesIO() as fptr: + dump(inputs, fptr) + fptr.seek(0) + new_inputs = load(fptr)[0] + self.assertMinimalVarEqual(inputs, new_inputs) + self.assertDeprecatedBitProperties(inputs, new_inputs) + + # Reversed order just to check there's no sorting shenanigans. + captures = QuantumCircuit(captures=list(all_vars)[::-1]) + with io.BytesIO() as fptr: + dump(captures, fptr) + fptr.seek(0) + new_captures = load(fptr)[0] + self.assertMinimalVarEqual(captures, new_captures) + self.assertDeprecatedBitProperties(captures, new_captures) + + declares = QuantumCircuit(declarations=all_vars) + with io.BytesIO() as fptr: + dump(declares, fptr) + fptr.seek(0) + new_declares = load(fptr)[0] + self.assertMinimalVarEqual(declares, new_declares) + self.assertDeprecatedBitProperties(declares, new_declares) + + def test_load_empty_vars_if(self): + """Test loading circuit with vars in if/else closures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("θψφ", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + d = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a]) + qc.add_var(b, expr.logic_not(a)) + qc.add_var(c, expr.lift(0, c.type)) + with qc.if_test(b) as else_: + qc.store(c, expr.lift(3, c.type)) + with else_: + qc.add_var(d, expr.lift(7, d.type)) + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_if_else = qc.data[-1].operation + new_if_else = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_if_else, IfElseOp) + self.assertIsInstance(new_if_else, IfElseOp) + self.assertEqual(len(old_if_else.blocks), len(new_if_else.blocks)) + + for old, new in zip(old_if_else.blocks, new_if_else.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + def test_load_empty_vars_while(self): + """Test loading circuit with vars in while closures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("θψφ", types.Bool()) + c = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a]) + qc.add_var(b, expr.logic_not(a)) + with qc.while_loop(b): + qc.add_var(c, expr.lift(7, c.type)) + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_while = qc.data[-1].operation + new_while = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_while, WhileLoopOp) + self.assertIsInstance(new_while, WhileLoopOp) + self.assertEqual(len(old_while.blocks), len(new_while.blocks)) + + for old, new in zip(old_while.blocks, new_while.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + def test_load_empty_vars_switch(self): + """Test loading circuit with vars in switch closures.""" + a = expr.Var.new("🐍🐍🐍", types.Uint(8)) + + qc = QuantumCircuit(1, 1, inputs=[a]) + qc.measure(0, 0) + b_outer = qc.add_var("b", False) + with qc.switch(a) as case: + with case(0): + qc.store(b_outer, True) + with case(1): + qc.store(qc.clbits[0], False) + with case(2): + # Explicit shadowing. + qc.add_var("b", True) + with case(3): + qc.store(a, expr.lift(1, a.type)) + with case(case.DEFAULT): + pass + + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertMinimalVarEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + old_switch = qc.data[-1].operation + new_switch = new_qc.data[-1].operation + # Sanity check for test. + self.assertIsInstance(old_switch, SwitchCaseOp) + self.assertIsInstance(new_switch, SwitchCaseOp) + self.assertEqual(len(old_switch.blocks), len(new_switch.blocks)) + + for old, new in zip(old_switch.blocks, new_switch.blocks): + self.assertMinimalVarEqual(old, new) + self.assertDeprecatedBitProperties(old, new) + + def test_roundtrip_index_expr(self): + """Test that the `Index` node round-trips.""" + a = expr.Var.new("a", types.Uint(8)) + cr = ClassicalRegister(4, "cr") + qc = QuantumCircuit(cr, inputs=[a]) + qc.store(expr.index(cr, 0), expr.index(a, a)) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + def test_roundtrip_bitshift_expr(self): + """Test that bit-shift expressions can round-trip.""" + a = expr.Var.new("a", types.Uint(8)) + cr = ClassicalRegister(4, "cr") + qc = QuantumCircuit(cr, inputs=[a]) + with qc.if_test(expr.equal(expr.shift_right(expr.shift_left(a, 1), 1), a)): + pass + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_qc = load(fptr)[0] + self.assertEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) + + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 12)) + 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" + ): + dump(qc, fptr, version=version) + + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 12)) + def test_pre_v12_rejects_index(self, version): + """Test that dumping to older QPY versions rejects the `Index` node.""" + # 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" + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 48322419679..517a7093e81 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -485,6 +485,69 @@ def test_copy_empty_variables(self): self.assertEqual({b, d}, set(copied.iter_captured_vars())) self.assertEqual({b}, set(qc.iter_captured_vars())) + def test_copy_empty_variables_alike(self): + """Test that an empty copy of circuits including variables copies them across, but does not + initialise them. This is the same as the default, just spelled explicitly.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="alike") + self.assertEqual({a}, set(copied.iter_input_vars())) + self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_input(b) + copied.add_var(d, 0xFF) + self.assertEqual({a, b}, set(copied.iter_input_vars())) + self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({c}, set(qc.iter_declared_vars())) + + qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) + copied = qc.copy_empty_like(vars_mode="alike") + self.assertEqual({b}, set(copied.iter_captured_vars())) + self.assertEqual({a, c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_capture(d) + self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_captured_vars())) + + def test_copy_empty_variables_to_captures(self): + """``vars_mode="captures"`` should convert all variables to captures.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="captures") + self.assertEqual({a, b, c}, set(copied.iter_captured_vars())) + self.assertEqual({a, b, c}, set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + + qc = QuantumCircuit(captures=[c, d]) + copied = qc.copy_empty_like(vars_mode="captures") + self.assertEqual({c, d}, set(copied.iter_captured_vars())) + self.assertEqual({c, d}, set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + + def test_copy_empty_variables_drop(self): + """``vars_mode="drop"`` should not have variables in the output.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + + qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like(vars_mode="drop") + self.assertEqual(set(), set(copied.iter_vars())) + self.assertEqual([], list(copied.data)) + def test_copy_empty_like_parametric_phase(self): """Test that the parameter table of an empty circuit remains valid after copying a circuit with a parametric global phase.""" @@ -530,7 +593,7 @@ def test_clear_circuit(self): qc.clear() self.assertEqual(len(qc.data), 0) - self.assertEqual(len(qc._parameter_table), 0) + self.assertEqual(qc._data.num_params(), 0) def test_barrier(self): """Test multiple argument forms of barrier.""" @@ -865,7 +928,7 @@ def test_remove_final_measurements_7089(self): self.assertEqual(circuit.clbits, []) def test_remove_final_measurements_bit_locations(self): - """Test remove_final_measurements properly recalculates clbit indicies + """Test remove_final_measurements properly recalculates clbit indices and preserves order of remaining cregs and clbits. """ c0 = ClassicalRegister(1) @@ -939,6 +1002,31 @@ def test_reverse(self): self.assertEqual(qc.reverse_ops(), expected) + def test_reverse_with_standlone_vars(self): + """Test that instruction-reversing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + with qc.if_test(a): + # We don't really comment on what should happen within control-flow operations in this + # method - it's not really defined in a non-linear CFG. This deliberately uses a body + # of length 1 (a single `Store`), so there's only one possibility. + qc.add_var(c, 12) + + expected = qc.copy_empty_like() + with expected.if_test(a): + expected.add_var(c, 12) + expected.cx(0, 1) + expected.h(0) + expected.store(b, 12) + + self.assertEqual(qc.reverse_ops(), expected) + def test_repeat(self): """Test repeating the circuit works.""" qr = QuantumRegister(2) diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index 481f2fe3ca5..d51dd0c7561 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -17,7 +17,8 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse from qiskit.circuit import Clbit -from qiskit.circuit.library import RXGate, RYGate +from qiskit.circuit.classical import expr, types +from qiskit.circuit.library import RXGate, RYGate, GlobalPhaseGate from qiskit.circuit.exceptions import CircuitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -638,6 +639,58 @@ def test_circuit_depth_first_qubit(self): circ.measure(1, 0) self.assertEqual(circ.depth(lambda x: circ.qubits[0] in x.qubits), 3) + def test_circuit_depth_0_operands(self): + """Test that the depth can be found even with zero-bit operands.""" + qc = QuantumCircuit(2, 2) + qc.append(GlobalPhaseGate(0.0), [], []) + qc.append(GlobalPhaseGate(0.0), [], []) + qc.append(GlobalPhaseGate(0.0), [], []) + self.assertEqual(qc.depth(), 0) + qc.measure([0, 1], [0, 1]) + self.assertEqual(qc.depth(), 1) + + def test_circuit_depth_expr_condition(self): + """Test that circuit depth respects `Expr` conditions in `IfElseOp`.""" + # Note that the "depth" of control-flow operations is not well defined, so the assertions + # here are quite weak. We're mostly aiming to match legacy behaviour of `c_if` for cases + # where there's a single instruction within the conditional. + qc = QuantumCircuit(2, 2) + a = qc.add_input("a", types.Bool()) + with qc.if_test(a): + qc.x(0) + with qc.if_test(expr.logic_and(a, qc.clbits[0])): + qc.x(1) + self.assertEqual(qc.depth(), 2) + qc.measure([0, 1], [0, 1]) + self.assertEqual(qc.depth(), 3) + + def test_circuit_depth_expr_store(self): + """Test that circuit depth respects `Store`.""" + qc = QuantumCircuit(3, 3) + a = qc.add_input("a", types.Bool()) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + # Note that `Store` is a "directive", so doesn't increase the depth by default, but does + # cause qubits 0,1; clbits 0,1 and 'a' to all be depth 3 at this point. + qc.store(a, qc.clbits[0]) + qc.store(a, expr.logic_and(a, qc.clbits[1])) + # ... so this use of 'a' should make it depth 4. + with qc.if_test(a): + qc.x(2) + self.assertEqual(qc.depth(), 4) + + def test_circuit_depth_switch(self): + """Test that circuit depth respects the `target` of `SwitchCaseOp`.""" + qc = QuantumCircuit(QuantumRegister(3, "q"), ClassicalRegister(3, "c")) + a = qc.add_input("a", types.Uint(3)) + + with qc.switch(expr.bit_and(a, qc.cregs[0])) as case: + with case(case.DEFAULT): + qc.x(0) + qc.measure(1, 0) + self.assertEqual(qc.depth(), 2) + def test_circuit_size_empty(self): """Circuit.size should return 0 for an empty circuit.""" size = 4 diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 121ad7222f4..13882281cff 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -167,7 +167,7 @@ def test_circuit_qasm_with_multiple_composite_circuits_with_same_name(self): my_gate_inst2_id = id(circuit.data[-1].operation) circuit.append(my_gate_inst3, [qr[0]]) my_gate_inst3_id = id(circuit.data[-1].operation) - + # pylint: disable-next=consider-using-f-string expected_qasm = """OPENQASM 2.0; include "qelib1.inc"; gate my_gate q0 {{ h q0; }} @@ -394,12 +394,14 @@ 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 - expected_qasm = """OPENQASM 2.0; + pattern = r"""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; } -qreg q[4]; -mcx q[0],q[1],q[2],q[3];""" - self.assertEqual(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; } +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) def test_circuit_qasm_with_mcx_gate_variants(self): """Test circuit qasm() method with MCXGrayCode, MCXRecursive, MCXVChain""" diff --git a/test/python/circuit/test_circuit_registers.py b/test/python/circuit/test_circuit_registers.py index 3de2a451877..7ef8751da0e 100644 --- a/test/python/circuit/test_circuit_registers.py +++ b/test/python/circuit/test_circuit_registers.py @@ -97,7 +97,7 @@ def test_numpy_array_of_registers(self): """Test numpy array of Registers . See https://github.com/Qiskit/qiskit-terra/issues/1898 """ - qrs = [QuantumRegister(2, name="q%s" % i) for i in range(5)] + qrs = [QuantumRegister(2, name=f"q{i}") for i in range(5)] qreg_array = np.array([], dtype=object, ndmin=1) qreg_array = np.append(qreg_array, qrs) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index 0da54108536..f6916dcb72d 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -14,7 +14,7 @@ from test import QiskitTestCase -from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister +from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister, Store from qiskit.circuit.classical import expr, types @@ -76,7 +76,7 @@ def test_initialise_declarations_mapping(self): ) def test_initialise_declarations_dependencies(self): - """Test that the cirucit initialiser can take in declarations with dependencies between + """Test that the circuit initializer can take in declarations with dependencies between them, provided they're specified in a suitable order.""" a = expr.Var.new("a", types.Bool()) vars_ = [ @@ -241,6 +241,30 @@ def test_initialise_declarations_equal_to_add_var(self): self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) self.assertEqual(qc_init.data, qc_manual.data) + def test_declarations_widen_integer_literals(self): + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(16)) + qc = QuantumCircuit(declarations=[(a, 3)]) + qc.add_var(b, 5) + actual_initializers = [ + (op.lvalue, op.rvalue) + for instruction in qc + if isinstance((op := instruction.operation), Store) + ] + expected_initializers = [ + (a, expr.Value(3, types.Uint(8))), + (b, expr.Value(5, types.Uint(16))), + ] + self.assertEqual(actual_initializers, expected_initializers) + + def test_declaration_does_not_widen_bool_literal(self): + # `bool` is a subclass of `int` in Python (except some arithmetic operations have different + # semantics...). It's not in Qiskit's value type system, though. + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "explicit cast is required"): + qc.add_var(a, True) + def test_cannot_shadow_vars(self): """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are detected and rejected.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 03301899a6a..7bb36a1401f 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -34,7 +34,7 @@ CircuitError, ) from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -357,7 +357,8 @@ def test_compose_copy(self): self.assertIsNot(should_copy.data[-1].operation, parametric.data[-1].operation) self.assertEqual(should_copy.data[-1].operation, parametric.data[-1].operation) forbid_copy = base.compose(parametric, qubits=[0], copy=False) - self.assertIs(forbid_copy.data[-1].operation, parametric.data[-1].operation) + # For standard gates a fresh copy is returned from the data list each time + self.assertEqual(forbid_copy.data[-1].operation, parametric.data[-1].operation) conditional = QuantumCircuit(1, 1) conditional.x(0).c_if(conditional.clbits[0], True) @@ -820,13 +821,16 @@ def test_expr_condition_is_mapped(self): b_src = ClassicalRegister(2, "b_src") c_src = ClassicalRegister(name="c_src", bits=list(a_src) + list(b_src)) source = QuantumCircuit(QuantumRegister(1), a_src, b_src, c_src) + target_var = source.add_input("target_var", types.Uint(2)) test_1 = lambda: expr.lift(a_src[0]) test_2 = lambda: expr.logic_not(b_src[1]) test_3 = lambda: expr.logic_and(expr.bit_and(b_src, 2), expr.less(c_src, 7)) + test_4 = lambda: expr.bit_xor(expr.index(target_var, 0), expr.index(target_var, 1)) source.if_test(test_1(), inner.copy(), [0], []) source.if_else(test_2(), inner.copy(), inner.copy(), [0], []) source.while_loop(test_3(), inner.copy(), [0], []) + source.if_test(test_4(), inner.copy(), [0], []) a_dest = ClassicalRegister(2, "a_dest") b_dest = ClassicalRegister(2, "b_dest") @@ -840,12 +844,19 @@ def test_expr_condition_is_mapped(self): self.assertEqual(len(dest.cregs), 3) mapped_reg = dest.cregs[-1] - expected = QuantumCircuit(dest.qregs[0], a_dest, b_dest, mapped_reg) + expected = QuantumCircuit(dest.qregs[0], a_dest, b_dest, mapped_reg, inputs=[target_var]) expected.if_test(expr.lift(a_dest[0]), inner.copy(), [0], []) expected.if_else(expr.logic_not(b_dest[1]), inner.copy(), inner.copy(), [0], []) expected.while_loop( expr.logic_and(expr.bit_and(b_dest, 2), expr.less(mapped_reg, 7)), inner.copy(), [0], [] ) + # `Var` nodes aren't remapped, but this should be passed through fine. + expected.if_test( + expr.bit_xor(expr.index(target_var, 0), expr.index(target_var, 1)), + inner.copy(), + [0], + [], + ) self.assertEqual(dest, expected) def test_expr_target_is_mapped(self): @@ -901,6 +912,118 @@ def test_expr_target_is_mapped(self): self.assertEqual(dest, expected) + def test_join_unrelated_vars(self): + """Composing disjoint sets of vars should produce an additive output.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + base = QuantumCircuit(inputs=[a]) + other = QuantumCircuit(inputs=[b]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a, b}, set(out.iter_input_vars())) + # Assert that base was unaltered. + self.assertEqual({a}, set(base.iter_vars())) + + base = QuantumCircuit(captures=[a]) + other = QuantumCircuit(captures=[b]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a, b}, set(out.iter_captured_vars())) + self.assertEqual({a}, set(base.iter_vars())) + + base = QuantumCircuit(inputs=[a]) + other = QuantumCircuit(declarations=[(b, 255)]) + out = base.compose(other) + self.assertEqual({a, b}, set(out.iter_vars())) + self.assertEqual({a}, set(out.iter_input_vars())) + self.assertEqual({b}, set(out.iter_declared_vars())) + + def test_var_remap_to_avoid_collisions(self): + """We can use `var_remap` to avoid a variable collision.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + base = QuantumCircuit(inputs=[a1]) + other = QuantumCircuit(inputs=[a2]) + + out = base.compose(other, var_remap={a2: b}) + self.assertEqual([a1, b], list(out.iter_input_vars())) + self.assertEqual([a1, b], list(out.iter_vars())) + + out = base.compose(other, var_remap={"a": b}) + self.assertEqual([a1, b], list(out.iter_input_vars())) + self.assertEqual([a1, b], list(out.iter_vars())) + + out = base.compose(other, var_remap={"a": "c"}) + self.assertTrue(out.has_var("c")) + c = out.get_var("c") + self.assertEqual(c.name, "c") + self.assertEqual([a1, c], list(out.iter_input_vars())) + self.assertEqual([a1, c], list(out.iter_vars())) + + def test_simple_inline_captures(self): + """We should be able to inline captures onto other variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + base = QuantumCircuit(inputs=[a, b]) + base.add_var(c, 255) + base.store(a, expr.logic_or(a, b)) + other = QuantumCircuit(captures=[a, b, c]) + other.store(c, 254) + other.store(b, expr.logic_or(a, b)) + new = base.compose(other, inline_captures=True) + + expected = QuantumCircuit(inputs=[a, b]) + expected.add_var(c, 255) + expected.store(a, expr.logic_or(a, b)) + expected.store(c, 254) + expected.store(b, expr.logic_or(a, b)) + self.assertEqual(new, expected) + + def test_can_inline_a_capture_after_remapping(self): + """We can use `var_remap` to redefine a capture variable _and then_ inline it in deeply + nested scopes. This is a stress test of capture inlining.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + # We shouldn't be able to inline `qc`'s variable use as-is because it closes over the wrong + # variable, but it should work after variable remapping. (This isn't expected to be super + # useful, it's just a consequence of how the order between `var_remap` and `inline_captures` + # is defined). + base = QuantumCircuit(inputs=[a]) + qc = QuantumCircuit(declarations=[(c, 255)], captures=[b]) + qc.store(b, expr.logic_and(b, b)) + with qc.if_test(expr.logic_not(b)): + with qc.while_loop(b): + qc.store(b, expr.logic_not(b)) + # Note that 'c' is captured in this scope, so this is also a test that 'inline_captures' + # doesn't do something silly in nested scopes. + with qc.switch(c) as case: + with case(0): + qc.store(c, expr.bit_and(c, 255)) + with case(case.DEFAULT): + qc.store(b, expr.equal(c, 255)) + base.compose(qc, inplace=True, inline_captures=True, var_remap={b: a}) + + expected = QuantumCircuit(inputs=[a], declarations=[(c, 255)]) + expected.store(a, expr.logic_and(a, a)) + with expected.if_test(expr.logic_not(a)): + with expected.while_loop(a): + expected.store(a, expr.logic_not(a)) + # Note that 'c' is not remapped. + with expected.switch(c) as case: + with case(0): + expected.store(c, expr.bit_and(c, 255)) + with case(case.DEFAULT): + expected.store(a, expr.equal(c, 255)) + + self.assertEqual(base, expected) + def test_rejects_duplicate_bits(self): """Test that compose rejects duplicates in either qubits or clbits.""" base = QuantumCircuit(5, 5) @@ -911,6 +1034,55 @@ def test_rejects_duplicate_bits(self): with self.assertRaisesRegex(CircuitError, "Duplicate clbits"): base.compose(attempt, [0, 1], [1, 1]) + def test_cannot_mix_inputs_and_captures(self): + """The rules about mixing `input` and `capture` vars should still apply.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "circuits with input variables cannot be"): + QuantumCircuit(inputs=[a]).compose(QuantumCircuit(captures=[b])) + with self.assertRaisesRegex(CircuitError, "circuits to be enclosed with captures cannot"): + QuantumCircuit(captures=[a]).compose(QuantumCircuit(inputs=[b])) + + def test_reject_var_naming_collision(self): + """We can't have multiple vars with the same name.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + self.assertNotEqual(a1, a2) + + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(inputs=[a1]).compose(QuantumCircuit(inputs=[a2])) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(captures=[a1]).compose(QuantumCircuit(declarations=[(a2, False)])) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(declarations=[(a1, True)]).compose( + QuantumCircuit(inputs=[b]), var_remap={b: a2} + ) + + def test_reject_remap_var_to_bad_type(self): + """Can't map a var to a different type.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "mismatched types"): + QuantumCircuit().compose(qc, var_remap={a: b}) + qc = QuantumCircuit(captures=[b]) + with self.assertRaisesRegex(CircuitError, "mismatched types"): + QuantumCircuit().compose(qc, var_remap={b: a}) + + def test_reject_inlining_missing_var(self): + """Can't inline a var that doesn't exist.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Variable '.*' to be inlined is not in the base"): + QuantumCircuit().compose(qc, inline_captures=True) + + # 'a' _would_ be present, except we also say to remap it before attempting the inline. + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Replacement '.*' for variable '.*' is not in"): + QuantumCircuit(inputs=[a]).compose(qc, var_remap={a: b}, inline_captures=True) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 0aeeb084f47..e41b5c1f9f3 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -1438,7 +1438,7 @@ def test_break_continue_deeply_nested(self, loop_operation): These are the deepest tests, hitting all parts of the deferred builder scopes. We test ``if``, ``if/else`` and ``switch`` paths at various levels of the scoping to try and account - for as many weird edge cases with the deferred behaviour as possible. We try to make sure, + for as many weird edge cases with the deferred behavior as possible. We try to make sure, particularly in the most complicated examples, that there are resources added before and after every single scope, to try and catch all possibilities of where resources may be missed. @@ -2943,7 +2943,7 @@ def test_inplace_compose_within_builder(self): self.assertEqual(canonicalize_control_flow(outer), canonicalize_control_flow(expected)) def test_global_phase_of_blocks(self): - """It should be possible to set a global phase of a scope independantly of the containing + """It should be possible to set a global phase of a scope independently of the containing scope and other sibling scopes.""" qr = QuantumRegister(3) cr = ClassicalRegister(3) @@ -3335,7 +3335,7 @@ def test_if_rejects_break_continue_if_not_in_loop(self): def test_for_rejects_reentry(self): """Test that the ``for``-loop context manager rejects attempts to re-enter it. Since it holds some forms of state during execution (the loop variable, which may be generated), we - can't safely re-enter it and get the expected behaviour.""" + can't safely re-enter it and get the expected behavior.""" for_manager = QuantumCircuit(2, 2).for_loop(range(2)) with for_manager: @@ -3584,7 +3584,7 @@ def test_reject_c_if_from_outside_scope(self): # As a side-effect of how the lazy building of 'if' statements works, we actually # *could* add a condition to the gate after the 'if' block as long as we were still # within the 'for' loop. It should actually manage the resource correctly as well, but - # it's "undefined behaviour" than something we specifically want to forbid or allow. + # it's "undefined behavior" than something we specifically want to forbid or allow. test = QuantumCircuit(bits) with test.for_loop(range(2)): with test.if_test(cond): diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index f0d6dd3a8f7..f26ab987f4f 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -764,9 +764,9 @@ def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits): @data(1, 2, 3, 4) def test_mcxgraycode_gates_yield_explicit_gates(self, num_ctrl_qubits): - """Test creating an mcx gate calls MCXGrayCode and yeilds explicit definition.""" + """Test an MCXGrayCode yields explicit definition.""" qc = QuantumCircuit(num_ctrl_qubits + 1) - qc.mcx(list(range(num_ctrl_qubits)), [num_ctrl_qubits]) + qc.append(MCXGrayCode(num_ctrl_qubits), list(range(qc.num_qubits)), []) explicit = {1: CXGate, 2: CCXGate, 3: C3XGate, 4: C4XGate} self.assertEqual(type(qc[0].operation), explicit[num_ctrl_qubits]) @@ -852,10 +852,9 @@ def test_controlled_unitary(self, num_ctrl_qubits): self.assertTrue(is_unitary_matrix(base_mat)) self.assertTrue(matrix_equal(cop_mat, test_op.data)) - @data(1, 2, 3, 4, 5) - def test_controlled_random_unitary(self, num_ctrl_qubits): + @combine(num_ctrl_qubits=(1, 2, 3, 4, 5), num_target=(2, 3)) + def test_controlled_random_unitary(self, num_ctrl_qubits, num_target): """Test the matrix data of an Operator based on a random UnitaryGate.""" - num_target = 2 base_gate = random_unitary(2**num_target).to_instruction() base_mat = base_gate.to_matrix() cgate = base_gate.control(num_ctrl_qubits) @@ -1263,7 +1262,7 @@ def test_modify_cugate_params_slice(self): self.assertEqual(cu.base_gate.params, [0.4, 0.3, 0.2]) def test_assign_nested_controlled_cu(self): - """Test assignment of an arbitrary controlled parametrised gate that appears through the + """Test assignment of an arbitrary controlled parametrized gate that appears through the `Gate.control()` method on an already-controlled gate.""" theta = Parameter("t") qc_c = QuantumCircuit(2) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 1b7faae6592..c5df22a0e8a 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -21,6 +21,7 @@ from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Operator from qiskit.circuit import ParameterVector, Gate, ControlledGate +from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate from qiskit.circuit.library import standard_gates from qiskit.circuit.library import ( HGate, @@ -260,7 +261,12 @@ class TestGateEquivalenceEqual(QiskitTestCase): """Test the decomposition of a gate in terms of other gates yields the same matrix as the hardcoded matrix definition.""" - class_list = Gate.__subclasses__() + ControlledGate.__subclasses__() + class_list = ( + SingletonGate.__subclasses__() + + SingletonControlledGate.__subclasses__() + + Gate.__subclasses__() + + ControlledGate.__subclasses__() + ) exclude = { "ControlledGate", "DiagonalGate", @@ -314,7 +320,11 @@ def test_equivalence_phase(self, gate_class): with self.subTest(msg=gate.name + "_" + str(ieq)): op1 = Operator(gate) op2 = Operator(equivalency) - self.assertEqual(op1, op2) + msg = ( + f"Equivalence entry from '{gate.name}' to:\n" + f"{str(equivalency.draw('text'))}\nfailed" + ) + self.assertEqual(op1, op2, msg) @ddt diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index edd01c5cc1c..170b47632c4 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -423,17 +423,15 @@ def test_repr_of_instructions(self): ins1 = Instruction("test_instruction", 3, 5, [0, 1, 2, 3]) self.assertEqual( repr(ins1), - "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - ins1.name, ins1.num_qubits, ins1.num_clbits, ins1.params - ), + f"Instruction(name='{ins1.name}', num_qubits={ins1.num_qubits}, " + f"num_clbits={ins1.num_clbits}, params={ins1.params})", ) ins2 = random_circuit(num_qubits=4, depth=4, measure=True).to_instruction() self.assertEqual( repr(ins2), - "Instruction(name='{}', num_qubits={}, num_clbits={}, params={})".format( - ins2.name, ins2.num_qubits, ins2.num_clbits, ins2.params - ), + f"Instruction(name='{ins2.name}', num_qubits={ins2.num_qubits}, " + f"num_clbits={ins2.num_clbits}, params={ins2.params})", ) def test_instruction_condition_bits(self): @@ -568,7 +566,7 @@ def case(specifier, message): case(1.0, r"Unknown classical resource specifier: .*") def test_instructionset_c_if_with_no_requester(self): - """Test that using a raw :obj:`.InstructionSet` with no classical-resource resoluer accepts + """Test that using a raw :obj:`.InstructionSet` with no classical-resource resolver accepts arbitrary :obj:`.Clbit` and `:obj:`.ClassicalRegister` instances, but rejects integers.""" with self.subTest("accepts arbitrary register"): @@ -577,14 +575,14 @@ def test_instructionset_c_if_with_no_requester(self): instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) instructions.c_if(register, 0) - self.assertIs(instruction.condition[0], register) + self.assertIs(instructions[0].operation.condition[0], register) with self.subTest("accepts arbitrary bit"): instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) bit = Clbit() instructions.c_if(bit, 0) - self.assertIs(instruction.condition[0], bit) + self.assertIs(instructions[0].operation.condition[0], bit) with self.subTest("rejects index"): instruction = RZGate(0) instructions = InstructionSet() @@ -617,7 +615,7 @@ def dummy_requester(specifier): bit = Clbit() instructions.c_if(bit, 0) dummy_requester.assert_called_once_with(bit) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -626,7 +624,7 @@ def dummy_requester(specifier): index = 0 instructions.c_if(index, 0) dummy_requester.assert_called_once_with(index) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -635,7 +633,7 @@ def dummy_requester(specifier): register = ClassicalRegister(2) instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) with self.subTest("calls requester only once when broadcast"): dummy_requester.reset_mock() instruction_list = [RZGate(0), RZGate(0), RZGate(0)] @@ -646,7 +644,7 @@ def dummy_requester(specifier): instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) for instruction in instruction_list: - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) def test_label_type_enforcement(self): """Test instruction label type enforcement.""" diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index a09ff331e02..35ff639cedd 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -102,7 +102,6 @@ def test_isometry_tolerance(self, iso): # Simulate the decomposed gate unitary = Operator(qc).data iso_from_circuit = unitary[::, 0 : 2**num_q_input] - self.assertTrue(np.allclose(iso_from_circuit, iso)) @data( diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 7bcc2cd35f3..f580416eccf 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -26,7 +26,7 @@ from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Instruction, Parameter, ParameterExpression, ParameterVector -from qiskit.circuit.parametertable import ParameterReferences, ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.exceptions import CircuitError from qiskit.compiler import assemble, transpile from qiskit import pulse @@ -45,8 +45,6 @@ def raise_if_parameter_table_invalid(circuit): CircuitError: if QuantumCircuit and ParameterTable are inconsistent. """ - table = circuit._parameter_table - # Assert parameters present in circuit match those in table. circuit_parameters = { parameter @@ -55,50 +53,53 @@ def raise_if_parameter_table_invalid(circuit): for parameter in param.parameters if isinstance(param, ParameterExpression) } - table_parameters = set(table._table.keys()) + table_parameters = set(circuit._data.get_params_unsorted()) if circuit_parameters != table_parameters: raise CircuitError( "Circuit/ParameterTable Parameter mismatch. " - "Circuit parameters: {}. " - "Table parameters: {}.".format(circuit_parameters, table_parameters) + f"Circuit parameters: {circuit_parameters}. " + f"Table parameters: {table_parameters}." ) # Assert parameter locations in table are present in circuit. circuit_instructions = [instr.operation for instr in circuit._data] - for parameter, instr_list in table.items(): - for instr, param_index in instr_list: + for parameter in table_parameters: + instr_list = circuit._data._get_param(parameter.uuid.int) + for instr_index, param_index in instr_list: + instr = circuit.data[instr_index].operation if instr not in circuit_instructions: raise CircuitError(f"ParameterTable instruction not present in circuit: {instr}.") if not isinstance(instr.params[param_index], ParameterExpression): raise CircuitError( "ParameterTable instruction does not have a " - "ParameterExpression at param_index {}: {}." - "".format(param_index, instr) + f"ParameterExpression at param_index {param_index}: {instr}." ) if parameter not in instr.params[param_index].parameters: raise CircuitError( "ParameterTable instruction parameters does " "not match ParameterTable key. Instruction " - "parameters: {} ParameterTable key: {}." - "".format(instr.params[param_index].parameters, parameter) + f"parameters: {instr.params[param_index].parameters}" + f" ParameterTable key: {parameter}." ) # Assert circuit has no other parameter locations other than those in table. - for instruction in circuit._data: + for instr_index, instruction in enumerate(circuit._data): for param_index, param in enumerate(instruction.operation.params): if isinstance(param, ParameterExpression): parameters = param.parameters for parameter in parameters: - if (instruction.operation, param_index) not in table[parameter]: + if (instr_index, param_index) not in circuit._data._get_param( + parameter.uuid.int + ): raise CircuitError( "Found parameterized instruction not " - "present in table. Instruction: {} " - "param_index: {}".format(instruction.operation, param_index) + f"present in table. Instruction: {instruction.operation} " + f"param_index: {param_index}" ) @@ -158,15 +159,19 @@ def test_append_copies_parametric(self): self.assertIsNot(qc.data[-1].operation, gate_param) self.assertEqual(qc.data[-1].operation, gate_param) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_param, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_param) + self.assertEqual(qc.data[-1].operation, gate_param) qc.append(gate_expr, [0], copy=True) self.assertIsNot(qc.data[-1].operation, gate_expr) self.assertEqual(qc.data[-1].operation, gate_expr) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_expr, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_expr) + self.assertEqual(qc.data[-1].operation, gate_expr) def test_parameters_property(self): """Test instantiating gate with variable parameters""" @@ -177,10 +182,9 @@ def test_parameters_property(self): qc = QuantumCircuit(qr) rxg = RXGate(theta) qc.append(rxg, [qr[0]], []) - vparams = qc._parameter_table - self.assertEqual(len(vparams), 1) - self.assertIs(theta, next(iter(vparams))) - self.assertEqual(rxg, next(iter(vparams[theta]))[0]) + self.assertEqual(qc._data.num_params(), 1) + self.assertIs(theta, next(iter(qc._data.get_params_unsorted()))) + self.assertEqual(rxg, qc.data[next(iter(qc._data._get_param(theta.uuid.int)))[0]].operation) def test_parameters_property_by_index(self): """Test getting parameters by index""" @@ -199,6 +203,29 @@ def test_parameters_property_by_index(self): for i, vi in enumerate(v): self.assertEqual(vi, qc.parameters[i]) + def test_parameters_property_independent_after_copy(self): + """Test that any `parameters` property caching is invalidated after a copy operation.""" + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + + qc1 = QuantumCircuit(1) + qc1.rz(a, 0) + self.assertEqual(set(qc1.parameters), {a}) + + qc2 = qc1.copy_empty_like() + self.assertEqual(set(qc2.parameters), set()) + + qc3 = qc1.copy() + self.assertEqual(set(qc3.parameters), {a}) + qc3.rz(b, 0) + self.assertEqual(set(qc3.parameters), {a, b}) + self.assertEqual(set(qc1.parameters), {a}) + + qc1.rz(c, 0) + self.assertEqual(set(qc1.parameters), {a, c}) + self.assertEqual(set(qc3.parameters), {a, b}) + def test_get_parameter(self): """Test the `get_parameter` method.""" x = Parameter("x") @@ -310,7 +337,7 @@ def test_assign_parameters_by_name(self): ) def test_bind_parameters_custom_definition_global_phase(self): - """Test that a custom gate with a parametrised `global_phase` is assigned correctly.""" + """Test that a custom gate with a parametrized `global_phase` is assigned correctly.""" x = Parameter("x") custom = QuantumCircuit(1, global_phase=x).to_gate() base = QuantumCircuit(1) @@ -553,12 +580,12 @@ def test_two_parameter_expression_binding(self): qc.rx(theta, 0) qc.ry(phi, 0) - self.assertEqual(len(qc._parameter_table[theta]), 1) - self.assertEqual(len(qc._parameter_table[phi]), 1) + self.assertEqual(qc._data._get_entry_count(theta), 1) + self.assertEqual(qc._data._get_entry_count(phi), 1) qc.assign_parameters({theta: -phi}, inplace=True) - self.assertEqual(len(qc._parameter_table[phi]), 2) + self.assertEqual(qc._data._get_entry_count(phi), 2) def test_expression_partial_binding_zero(self): """Verify that binding remains possible even if a previous partial bind @@ -580,7 +607,6 @@ def test_expression_partial_binding_zero(self): fbqc = pqc.assign_parameters({phi: 1}) self.assertEqual(fbqc.parameters, set()) - self.assertIsInstance(fbqc.data[0].operation.params[0], int) self.assertEqual(float(fbqc.data[0].operation.params[0]), 0) def test_raise_if_assigning_params_not_in_circuit(self): @@ -614,7 +640,7 @@ def test_gate_multiplicity_binding(self): qc.append(gate, [0], []) qc.append(gate, [0], []) qc2 = qc.assign_parameters({theta: 1.0}) - self.assertEqual(len(qc2._parameter_table), 0) + self.assertEqual(qc2._data.num_params(), 0) for instruction in qc2.data: self.assertEqual(float(instruction.operation.params[0]), 1.0) @@ -1091,7 +1117,7 @@ def test_decompose_propagates_bound_parameters(self, target_type, parameter_type if target_type == "gate": inst = qc.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": inst = qc.to_instruction() qc2 = QuantumCircuit(1) @@ -1132,7 +1158,7 @@ def test_decompose_propagates_deeply_bound_parameters(self, target_type, paramet if target_type == "gate": inst = qc1.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": inst = qc1.to_instruction() qc2 = QuantumCircuit(1) @@ -1188,7 +1214,7 @@ def test_executing_parameterized_instruction_bound_early(self, target_type): if target_type == "gate": sub_inst = sub_qc.to_gate() - elif target_type == "instruction": + else: # target_type == "instruction": sub_inst = sub_qc.to_instruction() unbound_qc = QuantumCircuit(2, 1) @@ -1368,12 +1394,25 @@ def test_parametervector_resize(self): with self.subTest("enlargen"): vec.resize(3) self.assertEqual(len(vec), 3) - # ensure we still have the same instance not a copy with the same name - # this is crucial for adding parameters to circuits since we cannot use the same - # name if the instance is not the same - self.assertIs(element, vec[1]) + # ensure we still have an element with the same uuid + self.assertEqual(element, vec[1]) self.assertListEqual([param.name for param in vec], _paramvec_names("x", 3)) + def test_parametervector_repr(self): + """Test the __repr__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(repr(vec), "ParameterVector(name='x', length=2)") + + def test_parametervector_str(self): + """Test the __str__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(str(vec), "x, ['x[0]', 'x[1]']") + + def test_parametervector_index(self): + """Test the index method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(vec.index(vec[1]), 1) + def test_raise_if_sub_unknown_parameters(self): """Verify we raise if asked to sub a parameter not in self.""" x = Parameter("x") @@ -1407,6 +1446,7 @@ def _paramvec_names(prefix, length): @ddt class TestParameterExpressions(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Test expressions of Parameters.""" # supported operations dictionary operation : accuracy (0=exact match) @@ -1425,6 +1465,7 @@ def test_compare_to_value_when_bound(self): x = Parameter("x") bound_expr = x.bind({x: 2.3}) self.assertEqual(bound_expr, 2.3) + self.assertEqual(hash(bound_expr), hash(2.3)) def test_abs_function_when_bound(self): """Verify expression can be used with @@ -1482,7 +1523,7 @@ def test_cast_to_float_when_underlying_expression_bound(self): def test_cast_to_float_intermediate_complex_value(self): """Verify expression can be cast to a float when it is fully bound, but an intermediate part of the expression evaluation involved complex types. Sympy is generally more permissive - than symengine here, and sympy's tends to be the expected behaviour for our users.""" + than symengine here, and sympy's tends to be the expected behavior for our users.""" x = Parameter("x") bound_expr = (x + 1.0 + 1.0j).bind({x: -1.0j}) self.assertEqual(float(bound_expr), 1.0) @@ -1755,6 +1796,13 @@ def test_negated_expression(self): self.assertEqual(float(bound_expr2), 3) + def test_positive_expression(self): + """This tests parameter unary plus.""" + x = Parameter("x") + y = +x + self.assertEqual(float(y.bind({x: 1})), 1.0) + self.assertIsInstance(+x, type(-x)) + def test_standard_cu3(self): """This tests parameter negation in standard extension gate cu3.""" from qiskit.circuit.library import CU3Gate @@ -2156,162 +2204,13 @@ def test_parameter_equal_to_identical_expression(self): self.assertEqual(theta, expr) def test_parameter_symbol_equal_after_ufunc(self): - """Verfiy ParameterExpression phi + """Verify ParameterExpression phi and ParameterExpression cos(phi) have the same symbol map""" phi = Parameter("phi") cos_phi = numpy.cos(phi) self.assertEqual(phi._parameter_symbols, cos_phi._parameter_symbols) -class TestParameterReferences(QiskitTestCase): - """Test the ParameterReferences class.""" - - def test_equal_inst_diff_instance(self): - """Different value equal instructions are treated as distinct.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - # test __contains__ - self.assertIn((gate1, 0), refs) - self.assertIn((gate2, 0), refs) - - gate_ids = {id(gate1), id(gate2)} - self.assertEqual(gate_ids, {id(gate) for gate, _ in refs}) - self.assertTrue(all(idx == 0 for _, idx in refs)) - - def test_pickle_unpickle(self): - """Membership testing after pickle/unpickle.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - to_pickle = (gate1, refs) - pickled = pickle.dumps(to_pickle) - (gate1_new, refs_new) = pickle.loads(pickled) - - self.assertEqual(len(refs_new), len(refs)) - self.assertNotIn((gate1, 0), refs_new) - self.assertIn((gate1_new, 0), refs_new) - - def test_equal_inst_same_instance(self): - """Referentially equal instructions are treated as same.""" - - theta = Parameter("theta") - gate = RZGate(theta) - - refs = ParameterReferences(((gate, 0), (gate, 0))) - - self.assertIn((gate, 0), refs) - self.assertEqual(len(refs), 1) - self.assertIs(next(iter(refs))[0], gate) - self.assertEqual(next(iter(refs))[1], 0) - - def test_extend_refs(self): - """Extending references handles duplicates.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0,)) - refs |= ParameterReferences((ref0, ref1, ref2, ref1, ref0)) - - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - def test_copy_param_refs(self): - """Copy of parameter references is a shallow copy.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - ref3 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0, ref1)) - refs_copy = refs.copy() - - # Check same gate instances in copy - gate_ids = {id(ref0[0]), id(ref1[0])} - self.assertEqual({id(gate) for gate, _ in refs_copy}, gate_ids) - - # add new ref to original and check copy not modified - refs.add(ref2) - self.assertNotIn(ref2, refs_copy) - self.assertEqual(refs_copy, ParameterReferences((ref0, ref1))) - - # add new ref to copy and check original not modified - refs_copy.add(ref3) - self.assertNotIn(ref3, refs) - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - -class TestParameterTable(QiskitTestCase): - """Test the ParameterTable class.""" - - def test_init_param_table(self): - """Parameter table init from mapping.""" - - p1 = Parameter("theta") - p2 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p2), 0) - - mapping = {p1: ParameterReferences((ref0, ref1)), p2: ParameterReferences((ref2,))} - - table = ParameterTable(mapping) - - # make sure editing mapping doesn't change `table` - del mapping[p1] - - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - self.assertEqual(table[p2], ParameterReferences((ref2,))) - - def test_set_references(self): - """References replacement by parameter key.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - - table = ParameterTable() - table[p1] = ParameterReferences((ref0, ref1)) - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - - table[p1] = ParameterReferences((ref1,)) - self.assertEqual(table[p1], ParameterReferences((ref1,))) - - def test_set_references_from_iterable(self): - """Parameter table init from iterable.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p1), 0) - - table = ParameterTable({p1: ParameterReferences((ref0, ref1))}) - table[p1] = (ref2, ref1, ref0) - - self.assertEqual(table[p1], ParameterReferences((ref2, ref1, ref0))) - - class TestParameterView(QiskitTestCase): """Test the ParameterView object.""" diff --git a/test/python/circuit/test_random_circuit.py b/test/python/circuit/test_random_circuit.py index deadcd09d69..ebbdfd28d64 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -12,6 +12,7 @@ """Test random circuit generation utility.""" +import numpy as np from qiskit.circuit import QuantumCircuit, ClassicalRegister, Clbit from qiskit.circuit import Measure from qiskit.circuit.random import random_circuit @@ -71,7 +72,7 @@ def test_large_conditional(self): def test_random_mid_circuit_measure_conditional(self): """Test random circuit with mid-circuit measurements for conditionals.""" num_qubits = depth = 2 - circ = random_circuit(num_qubits, depth, conditional=True, seed=4) + circ = random_circuit(num_qubits, depth, conditional=True, seed=16) self.assertEqual(circ.width(), 2 * num_qubits) op_names = [instruction.operation.name for instruction in circ] # Before a condition, there needs to be measurement in all the qubits. @@ -81,3 +82,81 @@ def test_random_mid_circuit_measure_conditional(self): bool(getattr(instruction.operation, "condition", None)) for instruction in circ ] self.assertEqual([False, False, False, True], conditions) + + def test_random_circuit_num_operand_distribution(self): + """Test that num_operand_distribution argument generates gates in correct proportion""" + num_qubits = 50 + depth = 300 + num_op_dist = {2: 0.25, 3: 0.25, 1: 0.25, 4: 0.25} + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=123456789 + ) + total_gates = circ.size() + self.assertEqual(circ.width(), num_qubits) + self.assertEqual(circ.depth(), depth) + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + + def test_random_circuit_2and3_qubit_gates_only(self): + """ + Test that the generated random circuit only has 2 and 3 qubit gates, + while disallowing 1-qubit and 4-qubit gates if + num_operand_distribution = {2: some_prob, 3: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {2: 0.5, 3: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=200) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 2 and 3 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 4-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[4], 0.0) + + def test_random_circuit_3and4_qubit_gates_only(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {3: 0.5, 4: 0.5} + circ = random_circuit( + num_qubits, depth, num_operand_distribution=num_op_dist, seed=11111111 + ) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) + + def test_random_circuit_with_zero_distribution(self): + """ + Test that the generated random circuit only has 3 and 4 qubit gates, + while disallowing 1-qubit and 2-qubit gates if + num_operand_distribution = {1: 0.0, 2: 0.0, 3: some_prob, 4: some_prob} + """ + num_qubits = 10 + depth = 200 + num_op_dist = {1: 0.0, 2: 0.0, 3: 0.5, 4: 0.5} + circ = random_circuit(num_qubits, depth, num_operand_distribution=num_op_dist, seed=12) + total_gates = circ.size() + gate_qubits = [instruction.operation.num_qubits for instruction in circ] + gate_type_counter = np.bincount(gate_qubits, minlength=5) + # Testing that the distribution of 3 and 4 qubit gate matches with given distribution + for gate_type, prob in sorted(num_op_dist.items()): + self.assertAlmostEqual(prob, gate_type_counter[gate_type] / total_gates, delta=0.1) + # Testing that there are no 1-qubit gate and 2-qubit in the generated random circuit + self.assertEqual(gate_type_counter[1], 0.0) + self.assertEqual(gate_type_counter[2], 0.0) diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py new file mode 100644 index 00000000000..6c0cc977e58 --- /dev/null +++ b/test/python/circuit/test_rust_equivalence.py @@ -0,0 +1,177 @@ +# 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. + +"""Rust gate definition tests""" + +from math import pi + +from test import QiskitTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit, CircuitInstruction +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping + +SKIP_LIST = {"rx", "ry", "ecr"} +CUSTOM_MAPPING = {"x", "rz"} + + +class TestRustGateEquivalence(QiskitTestCase): + """Tests that compile time rust gate definitions is correct.""" + + def setUp(self): + super().setUp() + self.standard_gates = get_standard_gate_name_mapping() + # Pre-warm gate mapping cache, this is needed so rust -> py conversion is done + qc = QuantumCircuit(3) + for gate in self.standard_gates.values(): + if getattr(gate, "_standard_gate", None): + if gate.params: + gate = gate.base_class(*[pi] * len(gate.params)) + qc.append(gate, list(range(gate.num_qubits))) + + def test_gate_cross_domain_conversion(self): + """Test the rust -> python conversion returns the right class.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # Gate not in rust yet or no constructor method + continue + with self.subTest(name=name): + qc = QuantumCircuit(standard_gate.num_qubits) + qc._append( + CircuitInstruction(standard_gate, qubits=qc.qubits, params=gate_class.params) + ) + self.assertEqual(qc.data[0].operation.base_class, gate_class.base_class) + self.assertEqual(qc.data[0].operation, gate_class) + + def test_definitions(self): + """Test definitions are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if name in SKIP_LIST: + # gate does not have a rust definition yet + continue + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [pi] * standard_gate._num_params() + py_def = gate_class.base_class(*params).definition + rs_def = standard_gate._get_definition(params) + if py_def is None: + self.assertIsNone(rs_def) + else: + rs_def = QuantumCircuit._from_circuit_data(rs_def) + + for rs_inst, py_inst in zip(rs_def._data, py_def._data): + # Rust uses U but python still uses U3 and u2 + if rs_inst.operation.name == "u": + if py_inst.operation.name == "u3": + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + elif py_inst.operation.name == "u2": + self.assertEqual( + rs_inst.operation.params, + [ + pi / 2, + py_inst.operation.params[0], + py_inst.operation.params[1], + ], + ) + + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + # Rust uses p but python still uses u1/u3 in some cases + elif rs_inst.operation.name == "p" and not name in ["cp", "cs", "csdg"]: + if py_inst.operation.name == "u1": + self.assertEqual(py_inst.operation.name, "u1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, "u3") + self.assertEqual( + rs_inst.operation.params[0], py_inst.operation.params[2] + ) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + # Rust uses cp but python still uses cu1 in some cases + elif rs_inst.operation.name == "cp": + self.assertEqual(py_inst.operation.name, "cu1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, rs_inst.operation.name) + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + + def test_matrix(self): + """Test matrices are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [0.1] * 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) + + def test_name(self): + """Test that the gate name properties match in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.name, standard_gate.name) + + def test_num_qubits(self): + """Test the number of qubits are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.num_qubits, standard_gate.num_qubits) + + def test_num_params(self): + """Test the number of parameters are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual( + len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal" + ) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index b44aac51f7a..139192745d2 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -29,6 +29,14 @@ def test_happy_path_construction(self): self.assertEqual(constructed.lvalue, lvalue) self.assertEqual(constructed.rvalue, rvalue) + def test_store_to_index(self): + lvalue = expr.index(expr.Var.new("a", types.Uint(8)), 3) + rvalue = expr.lift(False) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, rvalue) + def test_implicit_cast(self): lvalue = expr.Var.new("a", types.Bool()) rvalue = expr.Var.new("b", types.Uint(8)) @@ -45,6 +53,11 @@ def test_rejects_non_lvalue(self): with self.assertRaisesRegex(CircuitError, "not an l-value"): Store(not_an_lvalue, rvalue) + not_an_lvalue = expr.index(expr.shift_right(expr.Var.new("a", types.Uint(8)), 1), 2) + rvalue = expr.lift(True) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + Store(not_an_lvalue, rvalue) + def test_rejects_explicit_cast(self): lvalue = expr.Var.new("a", types.Uint(16)) rvalue = expr.Var.new("b", types.Uint(8)) @@ -122,6 +135,21 @@ def test_allows_stores_with_cregs(self): actual = [instruction.operation for instruction in qc.data] self.assertEqual(actual, expected) + def test_allows_stores_with_index(self): + cr = ClassicalRegister(8, "cr") + a = expr.Var.new("a", types.Uint(3)) + qc = QuantumCircuit(cr, inputs=[a]) + qc.store(expr.index(cr, 0), False) + qc.store(expr.index(a, 3), True) + qc.store(expr.index(cr, a), expr.index(cr, 0)) + expected = [ + Store(expr.index(cr, 0), expr.lift(False)), + Store(expr.index(a, 3), expr.lift(True)), + Store(expr.index(cr, a), expr.index(cr, 0)), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + def test_lifts_values(self): a = expr.Var.new("a", types.Bool()) qc = QuantumCircuit(captures=[a]) @@ -133,6 +161,22 @@ def test_lifts_values(self): qc.store(b, 0xFFFF) self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) + def test_lifts_integer_literals_to_full_width(self): + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + qc.store(a, 1) + self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(1, a.type))) + qc.store(a, 255) + self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(255, a.type))) + + def test_does_not_widen_bool_literal(self): + # `bool` is a subclass of `int` in Python (except some arithmetic operations have different + # semantics...). It's not in Qiskit's value type system, though. + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "explicit cast is required"): + qc.store(a, True) + def test_rejects_vars_not_in_circuit(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 23aec666cbd..c5c9344ad7e 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -178,7 +178,7 @@ def test_qobj_with_unitary_matrix(self): class NumpyEncoder(json.JSONEncoder): """Class for encoding json str with complex and numpy arrays.""" - def default(self, obj): + def default(self, obj): # pylint:disable=arguments-renamed if isinstance(obj, numpy.ndarray): return obj.tolist() if isinstance(obj, complex): diff --git a/test/python/classical_function_compiler/test_boolean_expression.py b/test/python/classical_function_compiler/test_boolean_expression.py index afdc91fdd04..40a01b154c6 100644 --- a/test/python/classical_function_compiler/test_boolean_expression.py +++ b/test/python/classical_function_compiler/test_boolean_expression.py @@ -28,6 +28,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") @ddt class TestBooleanExpression(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Test boolean expression.""" @data( diff --git a/test/python/classical_function_compiler/test_classical_function.py b/test/python/classical_function_compiler/test_classical_function.py index d4a0bf66d49..e385745952e 100644 --- a/test/python/classical_function_compiler/test_classical_function.py +++ b/test/python/classical_function_compiler/test_classical_function.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestOracleDecomposition(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests ClassicalFunction.decomposition.""" def test_grover_oracle(self): diff --git a/test/python/classical_function_compiler/test_parse.py b/test/python/classical_function_compiler/test_parse.py index 15862ca71b3..9da93873c70 100644 --- a/test/python/classical_function_compiler/test_parse.py +++ b/test/python/classical_function_compiler/test_parse.py @@ -25,6 +25,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestParseFail(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests bad_examples with the classicalfunction parser.""" def assertExceptionMessage(self, context, message): diff --git a/test/python/classical_function_compiler/test_simulate.py b/test/python/classical_function_compiler/test_simulate.py index 65399de82d0..f7c6ef3dd16 100644 --- a/test/python/classical_function_compiler/test_simulate.py +++ b/test/python/classical_function_compiler/test_simulate.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") @ddt class TestSimulate(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests LogicNetwork.simulate method""" @data(*utils.example_list()) diff --git a/test/python/classical_function_compiler/test_synthesis.py b/test/python/classical_function_compiler/test_synthesis.py index 3b8890d986c..1d44b58882f 100644 --- a/test/python/classical_function_compiler/test_synthesis.py +++ b/test/python/classical_function_compiler/test_synthesis.py @@ -26,6 +26,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestSynthesis(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests ClassicalFunction.synth method.""" def test_grover_oracle(self): diff --git a/test/python/classical_function_compiler/test_tweedledum2qiskit.py b/test/python/classical_function_compiler/test_tweedledum2qiskit.py index 32bd9485fdc..ff9b73a5b55 100644 --- a/test/python/classical_function_compiler/test_tweedledum2qiskit.py +++ b/test/python/classical_function_compiler/test_tweedledum2qiskit.py @@ -29,6 +29,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTweedledum2Qiskit(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests qiskit.transpiler.classicalfunction.utils.tweedledum2qiskit function.""" def test_x(self): diff --git a/test/python/classical_function_compiler/test_typecheck.py b/test/python/classical_function_compiler/test_typecheck.py index 36b64ce4fd4..ffe57cc3d4b 100644 --- a/test/python/classical_function_compiler/test_typecheck.py +++ b/test/python/classical_function_compiler/test_typecheck.py @@ -25,6 +25,7 @@ @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTypeCheck(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests classicalfunction compiler type checker (good examples).""" def test_id(self): @@ -74,6 +75,7 @@ def test_bool_or(self): @unittest.skipUnless(HAS_TWEEDLEDUM, "Tweedledum is required for these tests.") class TestTypeCheckFail(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Tests classicalfunction compiler type checker (bad examples).""" def assertExceptionMessage(self, context, message): diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 630db1c0c1c..1a6e5b6b8fe 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -233,7 +233,7 @@ def test_assemble_opaque_inst(self): self.assertEqual(qobj.experiments[0].instructions[0].params, [0.5, 0.4]) def test_assemble_unroll_parametervector(self): - """Verfiy that assemble unrolls parametervectors ref #5467""" + """Verify that assemble unrolls parametervectors ref #5467""" pv1 = ParameterVector("pv1", 3) pv2 = ParameterVector("pv2", 3) qc = QuantumCircuit(2, 2) @@ -609,7 +609,7 @@ def test_pulse_gates_common_cals(self): self.assertFalse(hasattr(qobj.experiments[1].config, "calibrations")) def test_assemble_adds_circuit_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the exeriment header.""" + """Verify that any circuit metadata is added to the experiment header.""" circ = QuantumCircuit(2, metadata={"experiment_type": "gst", "execution_number": "1234"}) qobj = assemble(circ, shots=100, memory=False, seed_simulator=6) self.assertEqual( @@ -943,7 +943,7 @@ def setUp(self): self.header = {"backend_name": "FakeOpenPulse2Q", "backend_version": "0.0.0"} def test_assemble_adds_schedule_metadata_to_experiment_header(self): - """Verify that any circuit metadata is added to the exeriment header.""" + """Verify that any circuit metadata is added to the experiment header.""" self.schedule.metadata = {"experiment_type": "gst", "execution_number": "1234"} qobj = assemble( self.schedule, diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 943de7b932e..77b63a3098b 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -42,13 +42,13 @@ SwitchCaseOp, WhileLoopOp, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.annotated_operation import ( AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier, ) -from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay from qiskit.circuit.measure import Measure from qiskit.circuit.reset import Reset @@ -499,7 +499,7 @@ def test_transpile_bell(self): self.assertIsInstance(circuits, QuantumCircuit) def test_transpile_bell_discrete_basis(self): - """Test that it's possible to transpile a very simple circuit to a discrete stabiliser-like + """Test that it's possible to transpile a very simple circuit to a discrete stabilizer-like basis. In general, we do not make any guarantees about the possibility or quality of transpilation in these situations, but this is at least useful as a check that stuff that _could_ be possible remains so.""" @@ -1750,8 +1750,11 @@ def test_translate_ecr_basis(self, optimization_level): optimization_level=optimization_level, seed_transpiler=42, ) - self.assertEqual(res.count_ops()["ecr"], 9) - self.assertTrue(Operator(res).equiv(circuit)) + + # Swap gates get optimized away in opt. level 2, 3 + expected_num_ecr_gates = 6 if optimization_level in (2, 3) else 9 + self.assertEqual(res.count_ops()["ecr"], expected_num_ecr_gates) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) def test_optimize_ecr_basis(self): """Test highest optimization level can optimize over ECR.""" @@ -1760,8 +1763,13 @@ def test_optimize_ecr_basis(self): circuit.iswap(0, 1) res = transpile(circuit, basis_gates=["u", "ecr"], optimization_level=3, seed_transpiler=42) - self.assertEqual(res.count_ops()["ecr"], 1) - self.assertTrue(Operator(res).equiv(circuit)) + + # an iswap gate is equivalent to (swap, CZ) up to single-qubit rotations. Normally, the swap gate + # in the circuit would cancel with the swap gate of the (swap, CZ), leaving a single CZ gate that + # can be realized via one ECR gate. However, with the introduction of ElideSwap, the swap gate + # cancellation can not occur anymore, thus requiring two ECR gates for the iswap gate. + self.assertEqual(res.count_ops()["ecr"], 2) + self.assertEqual(Operator(circuit), Operator.from_circuit(res)) def test_approximation_degree_invalid(self): """Test invalid approximation degree raises.""" @@ -1821,7 +1829,7 @@ def test_synthesis_translation_method_with_single_qubit_gates(self, optimization @data(0, 1, 2, 3) def test_synthesis_translation_method_with_gates_outside_basis(self, optimization_level): - """Test that synthesis translation works for circuits with single gates outside bassis""" + """Test that synthesis translation works for circuits with single gates outside basis""" qc = QuantumCircuit(2) qc.swap(0, 1) res = transpile( @@ -1882,7 +1890,7 @@ def test_transpile_control_flow_no_backend(self, opt_level): @data(0, 1, 2, 3) def test_transpile_with_custom_control_flow_target(self, opt_level): - """Test transpile() with a target and constrol flow ops.""" + """Test transpile() with a target and control flow ops.""" target = GenericBackendV2(num_qubits=8, control_flow=True).target circuit = QuantumCircuit(6, 1) @@ -1919,7 +1927,7 @@ def test_transpile_with_custom_control_flow_target(self, opt_level): transpiled = transpile( circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 ) - # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # Tests of the complete validity of a circuit are mostly done at the individual pass level; # here we're just checking that various passes do appear to have run. self.assertIsInstance(transpiled, QuantumCircuit) # Assert layout ran. @@ -2175,6 +2183,38 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [3, 4]) return base + def _standalone_var_circuit(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(5, 5, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + with qc.if_test(a) as else_: + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 2) + with else_: + qc.add_var(c, 12) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cz(1, 0) + qc.cz(4, 1) + qc.store(a, False) + with qc.switch(expr.bit_and(b, 7)) as case: + with case(0): + qc.cz(0, 1) + qc.cx(1, 2) + qc.cy(2, 0) + with case(case.DEFAULT): + qc.store(b, expr.bit_and(b, 7)) + return qc + @data(0, 1, 2, 3) def test_qpy_roundtrip(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" @@ -2300,6 +2340,46 @@ def test_qpy_roundtrip_control_flow_expr_backendv2(self, optimization_level): round_tripped = qpy.load(buffer)[0] self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=7) + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + basis_gates=backend.operation_names + + ["if_else", "for_loop", "while_loop", "switch_case"], + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var_target(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=11) + backend.target.add_instruction(IfElseOp, name="if_else") + backend.target.add_instruction(ForLoopOp, name="for_loop") + backend.target.add_instruction(WhileLoopOp, name="while_loop") + backend.target.add_instruction(SwitchCaseOp, name="switch_case") + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) def test_qasm3_output(self, optimization_level): """Test that the output of a transpiled circuit can be dumped into OpenQASM 3.""" @@ -2350,6 +2430,21 @@ def test_qasm3_output_control_flow_expr(self, optimization_level): str, ) + @data(0, 1, 2, 3) + def test_qasm3_output_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow and standalone `Var` nodes + can be dumped into OpenQASM 3.""" + transpiled = transpile( + self._standalone_var_circuit(), + backend=GenericBackendV2(num_qubits=13, control_flow=True), + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + # TODO: There's not a huge amount we can sensibly test for the output here until we can + # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump + # itself doesn't throw an error, though. + self.assertIsInstance(qasm3.dumps(transpiled), str) + @data(0, 1, 2, 3) def test_transpile_target_no_measurement_error(self, opt_level): """Test that transpile with a target which contains ideal measurement works @@ -2686,12 +2781,14 @@ def test_backend_and_custom_gate(self, opt_level): backend = GenericBackendV2( num_qubits=5, coupling_map=[[0, 1], [1, 0], [1, 2], [1, 3], [2, 1], [3, 1], [3, 4], [4, 3]], + seed=42, ) inst_map = InstructionScheduleMap() inst_map.add("newgate", [0, 1], pulse.ScheduleBlock()) newgate = Gate("newgate", 2, []) circ = QuantumCircuit(2) circ.append(newgate, [0, 1]) + tqc = transpile( circ, backend, @@ -2702,8 +2799,8 @@ def test_backend_and_custom_gate(self, opt_level): ) self.assertEqual(len(tqc.data), 1) self.assertEqual(tqc.data[0].operation, newgate) - qubits = tuple(tqc.find_bit(x).index for x in tqc.data[0].qubits) - self.assertIn(qubits, backend.target.qargs) + for x in tqc.data[0].qubits: + self.assertIn((tqc.find_bit(x).index,), backend.target.qargs) @ddt @@ -2767,7 +2864,7 @@ def max_circuits(self): def _default_options(cls): return Options(shots=1024) - def run(self, circuit, **kwargs): + def run(self, circuit, **kwargs): # pylint:disable=arguments-renamed raise NotImplementedError self.backend = FakeMultiChip() diff --git a/test/python/converters/test_circuit_to_dag.py b/test/python/converters/test_circuit_to_dag.py index 4f2f52d0378..0bded9c0f4a 100644 --- a/test/python/converters/test_circuit_to_dag.py +++ b/test/python/converters/test_circuit_to_dag.py @@ -15,9 +15,9 @@ import unittest from qiskit.dagcircuit import DAGCircuit -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Clbit +from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Clbit, SwitchCaseOp from qiskit.circuit.library import HGate, Measure -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.converters import dag_to_circuit, circuit_to_dag from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -106,6 +106,38 @@ def test_wires_from_expr_nodes_target(self): for original, test in zip(outer, roundtripped): self.assertEqual(original.operation.target, test.operation.target) + def test_runtime_vars_in_roundtrip(self): + """`expr.Var` nodes should be fully roundtripped.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + d = expr.Var.new("d", types.Uint(8)) + qc = QuantumCircuit(inputs=[a, c]) + qc.add_var(b, False) + qc.add_var(d, 255) + qc.store(a, expr.logic_or(a, b)) + with qc.if_test(expr.logic_and(a, expr.equal(c, d))): + pass + with qc.while_loop(a): + qc.store(a, expr.logic_or(a, b)) + with qc.switch(d) as case: + with case(0): + qc.store(c, d) + with case(case.DEFAULT): + qc.store(a, False) + + roundtrip = dag_to_circuit(circuit_to_dag(qc)) + self.assertEqual(qc, roundtrip) + + self.assertIsInstance(qc.data[-1].operation, SwitchCaseOp) + # This is guaranteed to be topologically last, even after the DAG roundtrip. + self.assertIsInstance(roundtrip.data[-1].operation, SwitchCaseOp) + self.assertEqual(qc.data[-1].operation.blocks, roundtrip.data[-1].operation.blocks) + + blocks = roundtrip.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_captured_vars()), {c, d}) + self.assertEqual(set(blocks[1].iter_captured_vars()), {a}) + def test_wire_order(self): """Test that the `qubit_order` and `clbit_order` parameters are respected.""" permutation = [2, 3, 1, 4, 0, 5] # Arbitrary. diff --git a/test/python/converters/test_circuit_to_gate.py b/test/python/converters/test_circuit_to_gate.py index de3ad079e56..8e71a7f595a 100644 --- a/test/python/converters/test_circuit_to_gate.py +++ b/test/python/converters/test_circuit_to_gate.py @@ -18,6 +18,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import Gate, Qubit +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import Operator from qiskit.exceptions import QiskitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -122,3 +123,16 @@ def test_zero_operands(self): compound = QuantumCircuit(1) compound.append(gate, [], []) np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16) + + def test_realtime_vars_rejected(self): + """Gates can't have realtime variables.""" + qc = QuantumCircuit(1, inputs=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() + qc = QuantumCircuit(1, captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() + qc = QuantumCircuit(1) + qc.add_var("a", False) + with self.assertRaisesRegex(QiskitError, "circuits with realtime classical variables"): + qc.to_gate() diff --git a/test/python/converters/test_circuit_to_instruction.py b/test/python/converters/test_circuit_to_instruction.py index 56a227dbad9..e3239d4b5ff 100644 --- a/test/python/converters/test_circuit_to_instruction.py +++ b/test/python/converters/test_circuit_to_instruction.py @@ -21,6 +21,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import Qubit, Clbit, Instruction from qiskit.circuit import Parameter +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import Operator from qiskit.exceptions import QiskitError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -218,6 +219,38 @@ def test_zero_operands(self): compound.append(instruction, [], []) np.testing.assert_allclose(-np.eye(2), Operator(compound), atol=1e-16) + def test_forbids_captured_vars(self): + """Instructions (here an analogue of functions) cannot close over outer scopes.""" + qc = QuantumCircuit(captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "Circuits that capture variables cannot"): + qc.to_instruction() + + def test_forbids_input_vars(self): + """This test can be relaxed when we have proper support for the behavior. + + This actually has a natural meaning; the input variables could become typed parameters. + We don't have a formal structure for managing that yet, though, so it's forbidden until the + library is ready for that.""" + qc = QuantumCircuit(inputs=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex(QiskitError, "Circuits with 'input' variables cannot"): + qc.to_instruction() + + def test_forbids_declared_vars(self): + """This test can be relaxed when we have proper support for the behavior. + + This has a very natural representation, which needs basically zero special handling, since + the variables are necessarily entirely internal to the subroutine. The reason it is + starting off as forbidden is because we don't have a good way to support variable renaming + during unrolling in transpilation, and we want the error to indicate an alternative at the + point the conversion happens.""" + qc = QuantumCircuit() + qc.add_var("a", False) + with self.assertRaisesRegex( + QiskitError, + "Circuits with internal variables.*You may be able to use `QuantumCircuit.compose`", + ): + qc.to_instruction() + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/test/python/dagcircuit/test_collect_blocks.py b/test/python/dagcircuit/test_collect_blocks.py index 2fe3e4bad7b..b2715078d7f 100644 --- a/test/python/dagcircuit/test_collect_blocks.py +++ b/test/python/dagcircuit/test_collect_blocks.py @@ -163,7 +163,7 @@ def test_collect_and_split_gates_from_dagcircuit(self): self.assertEqual(len(split_blocks), 3) def test_collect_and_split_gates_from_dagdependency(self): - """Test collecting and splitting blocks from DAGDependecy.""" + """Test collecting and splitting blocks from DAGDependency.""" qc = QuantumCircuit(6) qc.cx(0, 1) qc.cx(3, 5) diff --git a/test/python/dagcircuit/test_compose.py b/test/python/dagcircuit/test_compose.py index c2862eb200f..ff5014eacef 100644 --- a/test/python/dagcircuit/test_compose.py +++ b/test/python/dagcircuit/test_compose.py @@ -22,9 +22,10 @@ WhileLoopOp, SwitchCaseOp, CASE_DEFAULT, + Store, ) from qiskit.circuit.classical import expr, types -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGCircuitError from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.pulse import Schedule from qiskit.circuit.gate import Gate @@ -540,6 +541,91 @@ def test_compose_expr_target(self): self.assertEqual(dest, circuit_to_dag(expected)) + def test_join_unrelated_dags(self): + """This isn't expected to be common, but should work anyway.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_declared_var(b) + source.add_input_var(c) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_declared_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_join_unrelated_dags_captures(self): + """This isn't expected to be common, but should work anyway.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Uint(8)) + + dest = DAGCircuit() + dest.add_captured_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_declared_var(b) + source.add_captured_var(c) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source, inline_captures=False) + + expected = DAGCircuit() + expected.add_captured_var(a) + expected.add_declared_var(b) + expected.add_captured_var(c) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_inline_capture_var(self): + """Should be able to append uses onto another DAG.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.add_input_var(b) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_captured_var(b) + source.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dest.compose(source, inline_captures=True) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(Store(a, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dest, expected) + + def test_reject_inline_to_nonexistent_var(self): + """Should not be able to inline a variable that doesn't exist.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dest = DAGCircuit() + dest.add_input_var(a) + dest.apply_operation_back(Store(a, expr.lift(False)), (), ()) + source = DAGCircuit() + source.add_captured_var(b) + with self.assertRaisesRegex( + DAGCircuitError, "Variable '.*' to be inlined is not in the base DAG" + ): + dest.compose(source, inline_captures=True) + def test_compose_calibrations(self): """Test that compose carries over the calibrations.""" dag_cal = QuantumCircuit(1) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 3fcf5ff7a27..3e4d4bf4e68 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -38,8 +38,10 @@ SwitchCaseOp, IfElseOp, WhileLoopOp, + CASE_DEFAULT, + Store, ) -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import IGate, HGate, CXGate, CZGate, XGate, YGate, U1Gate, RXGate from qiskit.converters import circuit_to_dag from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -81,8 +83,8 @@ def raise_if_dagcircuit_invalid(dag): ] if edges_outside_wires: raise DAGCircuitError( - "multi_graph contains one or more edges ({}) " - "not found in DAGCircuit.wires ({}).".format(edges_outside_wires, dag.wires) + f"multi_graph contains one or more edges ({edges_outside_wires}) " + f"not found in DAGCircuit.wires ({dag.wires})." ) # Every wire should have exactly one input node and one output node. @@ -132,9 +134,7 @@ def raise_if_dagcircuit_invalid(dag): all_bits = node_qubits | node_clbits | node_cond_bits assert in_wires == all_bits, f"In-edge wires {in_wires} != node bits {all_bits}" - assert out_wires == all_bits, "Out-edge wires {} != node bits {}".format( - out_wires, all_bits - ) + assert out_wires == all_bits, f"Out-edge wires {out_wires} != node bits {all_bits}" class TestDagRegisters(QiskitTestCase): @@ -421,6 +421,67 @@ def test_copy_empty_like(self): self.assertEqual(self.dag.duration, result_dag.duration) self.assertEqual(self.dag.unit, result_dag.unit) + def test_copy_empty_like_vars(self): + """Variables should be part of the empty copy.""" + dag = DAGCircuit() + dag.add_input_var(expr.Var.new("a", types.Bool())) + dag.add_input_var(expr.Var.new("b", types.Uint(8))) + dag.add_declared_var(expr.Var.new("c", types.Bool())) + dag.add_declared_var(expr.Var.new("d", types.Uint(8))) + self.assertEqual(dag, dag.copy_empty_like()) + + dag = DAGCircuit() + dag.add_captured_var(expr.Var.new("a", types.Bool())) + dag.add_captured_var(expr.Var.new("b", types.Uint(8))) + dag.add_declared_var(expr.Var.new("c", types.Bool())) + dag.add_declared_var(expr.Var.new("d", types.Uint(8))) + self.assertEqual(dag, dag.copy_empty_like()) + + def test_copy_empty_like_vars_captures(self): + """Variables can be converted to captures as part of the empty copy.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + all_captures = DAGCircuit() + for var in [a, b, c, d]: + all_captures.add_captured_var(var) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(all_captures, dag.copy_empty_like(vars_mode="captures")) + + dag = DAGCircuit() + dag.add_captured_var(a) + dag.add_captured_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(all_captures, dag.copy_empty_like(vars_mode="captures")) + + def test_copy_empty_like_vars_drop(self): + """Variables can be dropped as part of the empty copy.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(DAGCircuit(), dag.copy_empty_like(vars_mode="drop")) + + dag = DAGCircuit() + dag.add_captured_var(a) + dag.add_captured_var(b) + dag.add_declared_var(c) + dag.add_declared_var(d) + self.assertEqual(DAGCircuit(), dag.copy_empty_like(vars_mode="drop")) + def test_remove_busy_clbit(self): """Classical bit removal of busy classical bits raises.""" self.dag.apply_operation_back(Measure(), [self.qreg[0]], [self.individual_clbit]) @@ -792,7 +853,7 @@ def test_quantum_successors(self): self.assertIsInstance(cnot_node.op, CXGate) successor_cnot = self.dag.quantum_successors(cnot_node) - # Ordering between Reset and out[q1] is indeterminant. + # Ordering between Reset and out[q1] is indeterminate. successor1 = next(successor_cnot) successor2 = next(successor_cnot) @@ -841,7 +902,7 @@ def test_quantum_predecessors(self): self.assertIsInstance(cnot_node.op, CXGate) predecessor_cnot = self.dag.quantum_predecessors(cnot_node) - # Ordering between Reset and in[q1] is indeterminant. + # Ordering between Reset and in[q1] is indeterminate. predecessor1 = next(predecessor_cnot) predecessor2 = next(predecessor_cnot) @@ -1550,7 +1611,7 @@ def setUp(self): qc.h(0) qc.measure(0, 0) # The depth of an if-else is the path through the longest block (regardless of the - # condition). The size is the sum of both blocks (mostly for optimisation-target purposes). + # condition). The size is the sum of both blocks (mostly for optimization-target purposes). with qc.if_test((qc.clbits[0], True)) as else_: qc.x(1) qc.cx(2, 3) @@ -1822,6 +1883,231 @@ def test_semantic_expr(self): qc2.switch(expr.bit_and(cr, 5), [(1, body)], [0], []) self.assertNotEqual(circuit_to_dag(qc1), circuit_to_dag(qc2)) + def test_present_vars(self): + """The vars should be compared whether or not they're used.""" + a_bool = expr.Var.new("a", types.Bool()) + a_u8 = expr.Var.new("a", types.Uint(8)) + a_u8_other = expr.Var.new("a", types.Uint(8)) + b_bool = expr.Var.new("b", types.Bool()) + + left = DAGCircuit() + left.add_input_var(a_bool) + left.add_input_var(b_bool) + self.assertEqual(left.num_input_vars, 2) + self.assertEqual(left.num_captured_vars, 0) + self.assertEqual(left.num_declared_vars, 0) + self.assertEqual(left.num_vars, 2) + + right = DAGCircuit() + right.add_input_var(a_bool) + right.add_input_var(b_bool) + self.assertEqual(right.num_input_vars, 2) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(left.num_vars, 2) + self.assertEqual(left, right) + + right = DAGCircuit() + right.add_input_var(a_u8) + right.add_input_var(b_bool) + self.assertEqual(right.num_input_vars, 2) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + right = DAGCircuit() + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 0) + self.assertNotEqual(left, right) + + right = DAGCircuit() + right.add_captured_var(a_bool) + right.add_captured_var(b_bool) + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 2) + self.assertEqual(right.num_declared_vars, 0) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + right = DAGCircuit() + right.add_declared_var(a_bool) + right.add_declared_var(b_bool) + self.assertEqual(right.num_input_vars, 0) + self.assertEqual(right.num_captured_vars, 0) + self.assertEqual(right.num_declared_vars, 2) + self.assertEqual(right.num_vars, 2) + self.assertNotEqual(left, right) + + left = DAGCircuit() + left.add_captured_var(a_u8) + + right = DAGCircuit() + right.add_captured_var(a_u8) + self.assertEqual(left, right) + + right = DAGCircuit() + right.add_captured_var(a_u8_other) + self.assertNotEqual(left, right) + + def test_wires_added_for_simple_classical_vars(self): + """Var uses should be represented in the wire structure.""" + a = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_input_var(a) + self.assertEqual(list(dag.iter_vars()), [a]) + self.assertEqual(list(dag.iter_input_vars()), [a]) + self.assertEqual(list(dag.iter_captured_vars()), []) + self.assertEqual(list(dag.iter_declared_vars()), []) + + expected_nodes = [dag.input_map[a], dag.output_map[a]] + self.assertEqual(list(dag.topological_nodes()), expected_nodes) + self.assertTrue(dag.is_successor(dag.input_map[a], dag.output_map[a])) + + op_mid = dag.apply_operation_back(Store(a, expr.lift(True)), (), ()) + self.assertTrue(dag.is_successor(dag.input_map[a], op_mid)) + self.assertTrue(dag.is_successor(op_mid, dag.output_map[a])) + self.assertFalse(dag.is_successor(dag.input_map[a], dag.output_map[a])) + + op_front = dag.apply_operation_front(Store(a, expr.logic_not(a)), (), ()) + self.assertTrue(dag.is_successor(dag.input_map[a], op_front)) + self.assertTrue(dag.is_successor(op_front, op_mid)) + self.assertFalse(dag.is_successor(dag.input_map[a], op_mid)) + + op_back = dag.apply_operation_back(Store(a, expr.logic_not(a)), (), ()) + self.assertTrue(dag.is_successor(op_mid, op_back)) + self.assertTrue(dag.is_successor(op_back, dag.output_map[a])) + self.assertFalse(dag.is_successor(op_mid, dag.output_map[a])) + + def test_wires_added_for_var_control_flow_condition(self): + """Vars used in if/else or while conditionals should be added to the wire structure.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + dag = DAGCircuit() + dag.add_declared_var(a) + dag.add_input_var(b) + + op_store = dag.apply_operation_back(Store(a, expr.lift(False)), (), ()) + op_if = dag.apply_operation_back(IfElseOp(a, QuantumCircuit()), (), ()) + op_while = dag.apply_operation_back( + WhileLoopOp(expr.logic_or(a, b), QuantumCircuit()), (), () + ) + + expected_edges = { + (dag.input_map[a], op_store, a), + (op_store, op_if, a), + (op_if, op_while, a), + (op_while, dag.output_map[a], a), + (dag.input_map[b], op_while, b), + (op_while, dag.output_map[b], b), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_wires_added_for_var_control_flow_target(self): + """Vars used in switch targets should be added to the wire structure.""" + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(8)) + dag = DAGCircuit() + dag.add_declared_var(a) + dag.add_input_var(b) + + op_store = dag.apply_operation_back(Store(a, expr.lift(3, a.type)), (), ()) + op_switch = dag.apply_operation_back( + SwitchCaseOp(expr.bit_xor(a, b), [(CASE_DEFAULT, QuantumCircuit())]), (), () + ) + + expected_edges = { + (dag.input_map[a], op_store, a), + (op_store, op_switch, a), + (op_switch, dag.output_map[a], a), + (dag.input_map[b], op_switch, b), + (op_switch, dag.output_map[b], b), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_wires_added_for_control_flow_captures(self): + """Vars captured in control-flow blocks should be in the wire structure.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_declared_var(c) + dag.add_input_var(d) + op_store = dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + op_if = dag.apply_operation_back(IfElseOp(a, QuantumCircuit(captures=[b])), (), ()) + op_switch = dag.apply_operation_back( + SwitchCaseOp( + d, + [ + (0, QuantumCircuit(captures=[b])), + (CASE_DEFAULT, QuantumCircuit(captures=[c])), + ], + ), + (), + (), + ) + + expected_edges = { + # a + (dag.input_map[a], op_if, a), + (op_if, dag.output_map[a], a), + # b + (dag.input_map[b], op_if, b), + (op_if, op_switch, b), + (op_switch, dag.output_map[b], b), + # c + (dag.input_map[c], op_store, c), + (op_store, op_switch, c), + (op_switch, dag.output_map[c], c), + # d + (dag.input_map[d], op_switch, d), + (op_switch, dag.output_map[d], d), + } + self.assertEqual(set(dag.edges()), expected_edges) + + def test_forbid_mixing_captures_inputs(self): + """Test that a DAG can't have both captures and inputs.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + with self.assertRaisesRegex( + DAGCircuitError, "cannot add captures to a circuit with inputs" + ): + dag.add_captured_var(b) + + dag = DAGCircuit() + dag.add_captured_var(a) + with self.assertRaisesRegex( + DAGCircuitError, "cannot add inputs to a circuit with captures" + ): + dag.add_input_var(b) + + def test_forbid_adding_nonstandalone_var(self): + """Temporary "wrapping" vars aren't standalone and can't be tracked separately.""" + dag = DAGCircuit() + with self.assertRaisesRegex(DAGCircuitError, "cannot add variables that wrap"): + dag.add_input_var(expr.lift(ClassicalRegister(4, "c"))) + with self.assertRaisesRegex(DAGCircuitError, "cannot add variables that wrap"): + dag.add_declared_var(expr.lift(Clbit())) + + def test_forbid_adding_conflicting_vars(self): + """Can't re-add a variable that exists, nor a shadowing variable in the same scope.""" + a1 = expr.Var.new("a", types.Bool()) + a2 = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_declared_var(a1) + with self.assertRaisesRegex(DAGCircuitError, "already present in the circuit"): + dag.add_declared_var(a1) + with self.assertRaisesRegex(DAGCircuitError, "cannot add .* as its name shadows"): + dag.add_declared_var(a2) + class TestDagSubstitute(QiskitTestCase): """Test substituting a dag node with a sub-dag""" @@ -2012,14 +2298,133 @@ def test_substitute_dag_switch_expr(self): self.assertEqual(src, expected) + def test_substitute_dag_vars(self): + """Should be possible to replace a node with a DAG acting on the same wires.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_input_var(c) + dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + node = dag.apply_operation_back(Store(a, expr.logic_or(expr.logic_or(a, b), c)), (), ()) + dag.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + replace = DAGCircuit() + replace.add_captured_var(a) + replace.add_captured_var(b) + replace.add_captured_var(c) + replace.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()) + replace.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(c, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + dag.substitute_node_with_dag(node, replace, wires={}) + + self.assertEqual(dag, expected) + + def test_substitute_dag_if_else_expr_var(self): + """Test that substitution works with if/else ops with standalone Vars.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + body_rep = QuantumCircuit(1) + body_rep.z(0) + + q_rep = QuantumRegister(1) + c_rep = ClassicalRegister(2) + replacement = DAGCircuit() + replacement.add_qreg(q_rep) + replacement.add_creg(c_rep) + replacement.add_captured_var(b) + replacement.apply_operation_back(XGate(), [q_rep[0]], []) + replacement.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c_rep, 1)), body_rep, None), [q_rep[0]], [] + ) + + true_src = QuantumCircuit(1) + true_src.x(0) + true_src.z(0) + false_src = QuantumCircuit(1) + false_src.x(0) + q_src = QuantumRegister(4) + c1_src = ClassicalRegister(2) + c2_src = ClassicalRegister(2) + src = DAGCircuit() + src.add_qreg(q_src) + src.add_creg(c1_src) + src.add_creg(c2_src) + src.add_input_var(a) + src.add_input_var(b) + node = src.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c1_src, 1)), true_src, false_src), [q_src[2]], [] + ) + + wires = {q_rep[0]: q_src[2], c_rep[0]: c1_src[0], c_rep[1]: c1_src[1]} + src.substitute_node_with_dag(node, replacement, wires=wires) + + expected = DAGCircuit() + expected.add_qreg(q_src) + expected.add_creg(c1_src) + expected.add_creg(c2_src) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(XGate(), [q_src[2]], []) + expected.apply_operation_back( + IfElseOp(expr.logic_and(b, expr.equal(c1_src, 1)), body_rep, None), [q_src[2]], [] + ) + + self.assertEqual(src, expected) + + def test_contract_var_use_to_nothing(self): + """The replacement DAG can drop wires.""" + a = expr.Var.new("a", types.Bool()) + + src = DAGCircuit() + src.add_input_var(a) + node = src.apply_operation_back(Store(a, a), (), ()) + replace = DAGCircuit() + src.substitute_node_with_dag(node, replace, {}) + + expected = DAGCircuit() + expected.add_input_var(a) + + self.assertEqual(src, expected) + + def test_raise_if_var_mismatch(self): + """The DAG can't add more wires.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + src = DAGCircuit() + src.add_input_var(a) + node = src.apply_operation_back(Store(a, a), (), ()) + + replace = DAGCircuit() + replace.add_input_var(a) + replace.add_input_var(b) + replace.apply_operation_back(Store(a, b), (), ()) + + with self.assertRaisesRegex(DAGCircuitError, "Cannot replace a node with a DAG with more"): + src.substitute_node_with_dag(node, replace, wires={}) + def test_raise_if_substituting_dag_modifies_its_conditional(self): """Verify that we raise if the input dag modifies any of the bits in node.op.condition.""" - # The `propagate_condition=True` argument (and behaviour of `substitute_node_with_dag` + # The `propagate_condition=True` argument (and behavior of `substitute_node_with_dag` # before the parameter was added) treats the replacement DAG as implementing only the # un-controlled operation. The original contract considers it an error to replace a node # with an operation that may modify one of the condition bits in case this affects - # subsequent operations, so when `propagate_condition=True`, this error behaviour is + # subsequent operations, so when `propagate_condition=True`, this error behavior is # maintained. instr = Instruction("opaque", 1, 1, []) @@ -2402,6 +2807,55 @@ def test_reject_replace_switch_with_other_resources(self, inplace): node, SwitchCaseOp(expr.lift(cr2), [((1, 3), case.copy())]), inplace=inplace ) + @data(True, False) + def test_replace_switch_case_standalone_var(self, inplace): + """Replace a standalone-Var switch/case with another.""" + a = expr.Var.new("a", types.Uint(8)) + b = expr.Var.new("b", types.Uint(8)) + + case = QuantumCircuit(1) + case.x(0) + + qr = QuantumRegister(1) + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_input_var(a) + dag.add_input_var(b) + node = dag.apply_operation_back(SwitchCaseOp(a, [((1, 3), case.copy())]), qr, []) + dag.substitute_node( + node, SwitchCaseOp(expr.bit_and(a, 1), [(1, case.copy())]), inplace=inplace + ) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(SwitchCaseOp(expr.bit_and(a, 1), [(1, case.copy())]), qr, []) + + self.assertEqual(dag, expected) + + @data(True, False) + def test_replace_store_standalone_var(self, inplace): + """Replace a standalone-Var Store with another.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + qr = QuantumRegister(1) + dag = DAGCircuit() + dag.add_qreg(qr) + dag.add_input_var(a) + dag.add_input_var(b) + node = dag.apply_operation_back(Store(a, a), (), ()) + dag.substitute_node(node, Store(a, expr.logic_not(a)), inplace=inplace) + + expected = DAGCircuit() + expected.add_qreg(qr) + expected.add_input_var(a) + expected.add_input_var(b) + expected.apply_operation_back(Store(a, expr.logic_not(a)), (), ()) + + self.assertEqual(dag, expected) + class TestReplaceBlock(QiskitTestCase): """Test replacing a block of nodes in a DAG.""" @@ -2486,6 +2940,34 @@ def test_replace_control_flow_block(self): self.assertEqual(dag, expected) + def test_contract_stores(self): + """Test that contraction over nodes with `Var` wires works.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + + dag = DAGCircuit() + dag.add_input_var(a) + dag.add_input_var(b) + dag.add_input_var(c) + dag.apply_operation_back(Store(c, expr.lift(False)), (), ()) + nodes = [ + dag.apply_operation_back(Store(a, expr.logic_or(a, b)), (), ()), + dag.apply_operation_back(Store(a, expr.logic_or(a, c)), (), ()), + ] + dag.apply_operation_back(Store(b, expr.lift(True)), (), ()) + dag.replace_block_with_op(nodes, Store(a, expr.logic_or(expr.logic_or(a, b), c)), {}) + + expected = DAGCircuit() + expected.add_input_var(a) + expected.add_input_var(b) + expected.add_input_var(c) + expected.apply_operation_back(Store(c, expr.lift(False)), (), ()) + expected.apply_operation_back(Store(a, expr.logic_or(expr.logic_or(a, b), c)), (), ()) + expected.apply_operation_back(Store(b, expr.lift(True)), (), ()) + + self.assertEqual(dag, expected) + class TestDagProperties(QiskitTestCase): """Test the DAG properties.""" @@ -2993,6 +3475,103 @@ def test_causal_cone_barriers(self): self.assertEqual(result, expected) + def test_causal_cone_more_barriers(self): + """Test causal cone for circuit with barriers. This example shows + why barriers may need to be examined multiple times.""" + + # q0_0: ──■────────░──────────────────────── + # ┌─┴─┐ ░ + # q0_1: ┤ X ├──────░───■──────────────────── + # ├───┤ ░ ┌─┴─┐┌───┐┌───┐┌───┐ + # q0_2: ┤ H ├──────░─┤ X ├┤ H ├┤ H ├┤ H ├─X─ + # ├───┤┌───┐ ░ └───┘└───┘└───┘└───┘ │ + # q0_3: ┤ H ├┤ X ├─░──────────────────────X─ + # ├───┤└─┬─┘ ░ + # q0_4: ┤ X ├──■───░──────────────────────── + # └─┬─┘ ░ + # q0_5: ──■────────░──────────────────────── + + qreg = QuantumRegister(6) + qc = QuantumCircuit(qreg) + qc.cx(0, 1) + qc.h(2) + qc.cx(5, 4) + qc.h(3) + qc.cx(4, 3) + qc.barrier() + qc.cx(1, 2) + + qc.h(2) + qc.h(2) + qc.h(2) + qc.swap(2, 3) + + dag = circuit_to_dag(qc) + + result = dag.quantum_causal_cone(qreg[2]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3], qreg[4], qreg[5]} + + self.assertEqual(result, expected) + + def test_causal_cone_measure(self): + """Test causal cone with measures.""" + + # ┌───┐ ░ ┌─┐ + # q_0: ┤ H ├─░─┤M├──────────── + # ├───┤ ░ └╥┘┌─┐ + # q_1: ┤ H ├─░──╫─┤M├───────── + # ├───┤ ░ ║ └╥┘┌─┐ + # q_2: ┤ H ├─░──╫──╫─┤M├────── + # ├───┤ ░ ║ ║ └╥┘┌─┐ + # q_3: ┤ H ├─░──╫──╫──╫─┤M├─── + # ├───┤ ░ ║ ║ ║ └╥┘┌─┐ + # q_4: ┤ H ├─░──╫──╫──╫──╫─┤M├ + # └───┘ ░ ║ ║ ║ ║ └╥┘ + # c: 5/═════════╬══╬══╬══╬══╬═ + # ║ ║ ║ ║ ║ + # meas: 5/═════════╩══╩══╩══╩══╩═ + # 0 1 2 3 4 + + qreg = QuantumRegister(5) + creg = ClassicalRegister(5) + circuit = QuantumCircuit(qreg, creg) + for i in range(5): + circuit.h(i) + circuit.measure_all() + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[1]} + self.assertEqual(result, expected) + + def test_reconvergent_paths(self): + """Test circuit with reconvergent paths.""" + + # q0_0: ──■─────────■─────────■─────────■─────────■─────────■─────── + # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ + # q0_1: ┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■── + # └───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐ + # q0_2: ──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├ + # ┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘ + # q0_3: ┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├───── + # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + # q0_4: ──────────────────────────────────────────────────────────── + + qreg = QuantumRegister(5) + circuit = QuantumCircuit(qreg) + + for _ in range(6): + circuit.cx(0, 1) + circuit.cx(2, 3) + circuit.cx(1, 2) + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3]} + self.assertEqual(result, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/containers/test_bit_array.py b/test/python/primitives/containers/test_bit_array.py index a85118e27ea..69f02fd46da 100644 --- a/test/python/primitives/containers/test_bit_array.py +++ b/test/python/primitives/containers/test_bit_array.py @@ -13,13 +13,14 @@ """Unit tests for BitArray.""" from itertools import product +from test import QiskitTestCase import ddt import numpy as np from qiskit.primitives.containers import BitArray +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.result import Counts -from test import QiskitTestCase # pylint: disable=wrong-import-order def u_8(arr): @@ -282,3 +283,415 @@ def test_reshape(self): self.assertEqual(ba.reshape(360 * 2, 16).shape, (720,)) self.assertEqual(ba.reshape(360 * 2, 16).num_shots, 16) self.assertEqual(ba.reshape(360 * 2, 16).num_bits, 15) + + def test_transpose(self): + """Test the transpose method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + # Since the input dtype is uint16, bit array requires at least two u8. + # Thus, 9 is the minimum number of qubits, i.e., 8 + 1. + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("default arg"): + ba2 = ba.transpose() + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("tuple 1"): + ba2 = ba.transpose((2, 1, 0)) + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("tuple 2"): + ba2 = ba.transpose((0, 1, 2)) + self.assertEqual(ba2.shape, (1, 2, 3)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("tuple 3"): + ba2 = ba.transpose((0, 2, 1)) + self.assertEqual(ba2.shape, (1, 3, 2)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, k, j))) + + with self.subTest("tuple, negative indices"): + ba2 = ba.transpose((0, -1, -2)) + self.assertEqual(ba2.shape, (1, 3, 2)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, k, j))) + + with self.subTest("ints"): + ba2 = ba.transpose(2, 1, 0) + self.assertEqual(ba2.shape, (3, 2, 1)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((k, j, i))) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "axes don't match bit array"): + _ = ba.transpose((0, 1)) + with self.assertRaisesRegex(ValueError, "axes don't match bit array"): + _ = ba.transpose((0, 1, 2, 3)) + with self.assertRaisesRegex(ValueError, "axis [0-9]+ is out of bounds for bit array"): + _ = ba.transpose((0, 1, 4)) + with self.assertRaisesRegex(ValueError, "axis -[0-9]+ is out of bounds for bit array"): + _ = ba.transpose((0, 1, -4)) + with self.assertRaisesRegex(ValueError, "repeated axis in transpose"): + _ = ba.transpose((0, 1, 1)) + + def test_concatenate(self): + """Test the concatenate function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate = BitArray.concatenate + + with self.subTest("2 arrays, default"): + ba2 = concatenate([ba, ba]) + self.assertEqual(ba2.shape, (2, 2, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((1, j, k))) + + with self.subTest("2 arrays, axis"): + ba2 = concatenate([ba, ba], axis=1) + self.assertEqual(ba2.shape, (1, 4, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((0, j + 2, k))) + + with self.subTest("3 arrays"): + ba2 = concatenate([ba, ba, ba]) + self.assertEqual(ba2.shape, (3, 2, 3)) + for j, k in product(range(2), range(3)): + self.assertEqual(ba2.get_counts((0, j, k)), ba2.get_counts((1, j, k))) + self.assertEqual(ba2.get_counts((1, j, k)), ba2.get_counts((2, j, k))) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to concatenate"): + _ = concatenate([]) + with self.assertRaisesRegex(ValueError, "axis -1 is out of bounds"): + _ = concatenate([ba, ba], -1) + with self.assertRaisesRegex(ValueError, "axis 100 is out of bounds"): + _ = concatenate([ba, ba], 100) + + ba2 = BitArray(data, 10) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same number of bits"): + _ = concatenate([ba, ba2]) + + data2 = np.frombuffer(np.arange(30, dtype=np.uint16).tobytes(), dtype=np.uint8) + data2 = data2.reshape(1, 2, 3, 5, 2)[..., ::-1] + ba2 = BitArray(data2, 9) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of shots" + ): + _ = concatenate([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of dimensions" + ): + _ = concatenate([ba, ba2]) + + def test_concatenate_shots(self): + """Test the concatenate_shots function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate_shots = BitArray.concatenate_shots + + with self.subTest("2 arrays"): + ba2 = concatenate_shots([ba, ba]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 9) + self.assertEqual(ba2.num_shots, 2 * ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + expected = {key: val * 2 for key, val in ba.get_counts((i, j, k)).items()} + counts2 = ba2.get_counts((i, j, k)) + self.assertEqual(counts2, expected) + + with self.subTest("3 arrays"): + ba2 = concatenate_shots([ba, ba, ba]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 9) + self.assertEqual(ba2.num_shots, 3 * ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + expected = {key: val * 3 for key, val in ba.get_counts((i, j, k)).items()} + counts2 = ba2.get_counts((i, j, k)) + self.assertEqual(counts2, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to stack"): + _ = concatenate_shots([]) + + ba2 = BitArray(data, 10) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same number of bits"): + _ = concatenate_shots([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same shape"): + _ = concatenate_shots([ba, ba2]) + + def test_concatenate_bits(self): + """Test the concatenate_bits function.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + concatenate_bits = BitArray.concatenate_bits + + with self.subTest("2 arrays"): + ba_01 = ba.slice_bits([0, 1]) + ba2 = concatenate_bits([ba, ba_01]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 11) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + bs = ba.get_bitstrings((i, j, k)) + bs_01 = ba_01.get_bitstrings((i, j, k)) + expected = [s1 + s2 for s1, s2 in zip(bs_01, bs)] + bs2 = ba2.get_bitstrings((i, j, k)) + self.assertEqual(bs2, expected) + + with self.subTest("3 arrays"): + ba_01 = ba.slice_bits([0, 1]) + ba2 = concatenate_bits([ba, ba_01, ba_01]) + self.assertEqual(ba2.shape, (1, 2, 3)) + self.assertEqual(ba2.num_bits, 13) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + bs = ba.get_bitstrings((i, j, k)) + bs_01 = ba_01.get_bitstrings((i, j, k)) + expected = [s1 + s1 + s2 for s1, s2 in zip(bs_01, bs)] + bs2 = ba2.get_bitstrings((i, j, k)) + self.assertEqual(bs2, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "Need at least one bit array to stack"): + _ = concatenate_bits([]) + + data2 = np.frombuffer(np.arange(30, dtype=np.uint16).tobytes(), dtype=np.uint8) + data2 = data2.reshape(1, 2, 3, 5, 2)[..., ::-1] + ba2 = BitArray(data2, 9) + with self.assertRaisesRegex( + ValueError, "All bit arrays must have same number of shots" + ): + _ = concatenate_bits([ba, ba2]) + + ba2 = ba.reshape(2, 3) + with self.assertRaisesRegex(ValueError, "All bit arrays must have same shape"): + _ = concatenate_bits([ba, ba2]) + + def test_getitem(self): + """Test the __getitem__ method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba[:] + self.assertEqual(ba2.shape, (1, 2, 3)) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("no slice"): + ba2 = ba[0, 1, 2] + self.assertEqual(ba2.shape, ()) + self.assertEqual(ba.get_counts((0, 1, 2)), ba2.get_counts()) + + with self.subTest("slice"): + ba2 = ba[0, :, 2] + self.assertEqual(ba2.shape, (2,)) + for j in range(2): + self.assertEqual(ba.get_counts((0, j, 2)), ba2.get_counts(j)) + + def test_slice_bits(self): + """Test the slice_bits method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba.slice_bits(range(ba.num_bits)) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, ba.num_bits) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("1 bit, int"): + ba2 = ba.slice_bits(0) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_counts((i, j, k)), {"0": 5, "1": 5}) + + with self.subTest("1 bit, list"): + ba2 = ba.slice_bits([0]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_counts((i, j, k)), {"0": 5, "1": 5}) + + with self.subTest("2 bits"): + ba2 = ba.slice_bits([0, 1]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_shots, ba.num_shots) + self.assertEqual(ba2.num_bits, 2) + even = {"00": 3, "01": 3, "10": 2, "11": 2} + odd = {"10": 3, "11": 3, "00": 2, "01": 2} + for count, (i, j, k) in enumerate(product(range(1), range(2), range(3))): + expect = even if count % 2 == 0 else odd + self.assertEqual(ba2.get_counts((i, j, k)), expect) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + _ = ba.slice_bits(-1) + with self.assertRaisesRegex(ValueError, "index 9 is out of bounds"): + _ = ba.slice_bits(9) + + def test_slice_shots(self): + """Test the slice_shots method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + + with self.subTest("all"): + ba2 = ba.slice_shots(range(ba.num_shots)) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, ba.num_shots) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba.get_counts((i, j, k)), ba2.get_counts((i, j, k))) + + with self.subTest("1 shot, int"): + ba2 = ba.slice_shots(0) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_bitstrings((i, j, k)), [ba.get_bitstrings((i, j, k))[0]]) + + with self.subTest("1 shot, list"): + ba2 = ba.slice_shots([0]) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, 1) + for i, j, k in product(range(1), range(2), range(3)): + self.assertEqual(ba2.get_bitstrings((i, j, k)), [ba.get_bitstrings((i, j, k))[0]]) + + with self.subTest("multiple shots"): + indices = [1, 2, 3, 5, 8] + ba2 = ba.slice_shots(indices) + self.assertEqual(ba2.shape, ba.shape) + self.assertEqual(ba2.num_bits, ba.num_bits) + self.assertEqual(ba2.num_shots, len(indices)) + for i, j, k in product(range(1), range(2), range(3)): + expected = [ + bs for ind, bs in enumerate(ba.get_bitstrings((i, j, k))) if ind in indices + ] + self.assertEqual(ba2.get_bitstrings((i, j, k)), expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "index -1 is out of bounds"): + _ = ba.slice_shots(-1) + with self.assertRaisesRegex(ValueError, "index 10 is out of bounds"): + _ = ba.slice_shots(10) + + def test_expectation_values(self): + """Test the expectation_values method.""" + # this creates incrementing bitstrings from 0 to 59 + data = np.frombuffer(np.arange(60, dtype=np.uint16).tobytes(), dtype=np.uint8) + data = data.reshape(1, 2, 3, 10, 2)[..., ::-1] + ba = BitArray(data, 9) + self.assertEqual(ba.shape, (1, 2, 3)) + op = "I" * 8 + "Z" + op2 = "I" * 8 + "0" + op3 = "I" * 8 + "1" + pauli = Pauli(op) + sp_op = SparsePauliOp(op) + sp_op2 = SparsePauliOp.from_sparse_list([("Z", [6], 1)], num_qubits=9) + + with self.subTest("str"): + expval = ba.expectation_values(op) + # both 0 and 1 appear 5 times + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + expval = ba.expectation_values(op2) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.full((ba.shape), 0.5)) + + expval = ba.expectation_values(op3) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.full((ba.shape), 0.5)) + + ba2 = ba.slice_bits(6) + # 6th bit are all 0 + expval = ba2.expectation_values("Z") + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.ones(ba.shape)) + + ba3 = ba.slice_bits(5) + # 5th bit distributes as follows. + # (0, 0, 0) {'0': 10} + # (0, 0, 1) {'0': 10} + # (0, 0, 2) {'0': 10} + # (0, 1, 0) {'0': 2, '1': 8} + # (0, 1, 1) {'1': 10} + # (0, 1, 2) {'1': 10} + expval = ba3.expectation_values("0") + expected = np.array([[[1, 1, 1], [0.2, 0, 0]]]) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, expected) + + with self.subTest("Pauli"): + expval = ba.expectation_values(pauli) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + with self.subTest("SparsePauliOp"): + expval = ba.expectation_values(sp_op) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.zeros((ba.shape))) + + expval = ba.expectation_values(sp_op2) + # 6th bit are all 0 + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, np.ones((ba.shape))) + + with self.subTest("ObservableArray"): + obs = ["Z", "0", "1"] + ba2 = ba.slice_bits(5) + expval = ba2.expectation_values(obs) + expected = np.array([[[1, 1, 0], [-0.6, 0, 1]]]) + self.assertEqual(expval.shape, ba.shape) + np.testing.assert_allclose(expval, expected) + + ba4 = BitArray.from_counts([{0: 1}, {1: 1}]).reshape(2, 1) + expval = ba4.expectation_values(obs) + expected = np.array([[1, 1, 0], [-1, 0, 1]]) + self.assertEqual(expval.shape, (2, 3)) + np.testing.assert_allclose(expval, expected) + + with self.subTest("errors"): + with self.assertRaisesRegex(ValueError, "shape mismatch"): + _ = ba.expectation_values([op, op2]) + with self.assertRaisesRegex(ValueError, "One or more operators not same length"): + _ = ba.expectation_values("Z") + with self.assertRaisesRegex(ValueError, "is not diagonal"): + _ = ba.expectation_values("X" * ba.num_bits) diff --git a/test/python/primitives/containers/test_data_bin.py b/test/python/primitives/containers/test_data_bin.py index b750174b7b6..a8f802ebaeb 100644 --- a/test/python/primitives/containers/test_data_bin.py +++ b/test/python/primitives/containers/test_data_bin.py @@ -15,65 +15,61 @@ import numpy as np -import numpy.typing as npt -from qiskit.primitives.containers import make_data_bin -from qiskit.primitives.containers.data_bin import DataBin, DataBinMeta +from qiskit.primitives.containers.data_bin import DataBin from test import QiskitTestCase # pylint: disable=wrong-import-order class DataBinTestCase(QiskitTestCase): """Test the DataBin class.""" - def test_make_databin(self): - """Test the make_databin() function.""" - data_bin_cls = make_data_bin( - [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) - ) - - self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) - self.assertTrue(issubclass(data_bin_cls, DataBin)) - self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) - self.assertEqual(data_bin_cls._FIELD_TYPES, (npt.NDArray[np.uint16], np.ndarray)) + def test_make_databin_no_fields(self): + """Test DataBin when no fields are given.""" + data_bin = DataBin() + self.assertEqual(len(data_bin), 0) + self.assertEqual(data_bin.shape, ()) + def test_data_bin_basic(self): + """Test DataBin function basic access.""" alpha = np.empty((10, 20), dtype=np.uint16) beta = np.empty((10, 20), dtype=int) - my_bin = data_bin_cls(alpha, beta) + my_bin = DataBin(alpha=alpha, beta=beta) + self.assertEqual(len(my_bin), 2) self.assertTrue(np.all(my_bin.alpha == alpha)) self.assertTrue(np.all(my_bin.beta == beta)) self.assertTrue("alpha=" in str(my_bin)) - self.assertTrue(str(my_bin).startswith("DataBin<10,20>")) + self.assertTrue(str(my_bin).startswith("DataBin")) + self.assertEqual(my_bin._FIELDS, ("alpha", "beta")) + self.assertEqual(my_bin._FIELD_TYPES, (np.ndarray, np.ndarray)) - my_bin = data_bin_cls(beta=beta, alpha=alpha) + my_bin = DataBin(beta=beta, alpha=alpha) self.assertTrue(np.all(my_bin.alpha == alpha)) self.assertTrue(np.all(my_bin.beta == beta)) - def test_make_databin_no_shape(self): - """Test the make_databin() function with no shape.""" - data_bin_cls = make_data_bin([("alpha", dict), ("beta", int)]) + def test_constructor_failures(self): + """Test that the constructor fails when expected.""" - self.assertTrue(issubclass(type(data_bin_cls), DataBinMeta)) - self.assertTrue(issubclass(data_bin_cls, DataBin)) - self.assertEqual(data_bin_cls._FIELDS, ("alpha", "beta")) - self.assertEqual(data_bin_cls._FIELD_TYPES, (dict, int)) + with self.assertRaisesRegex(ValueError, "Cannot assign with these field names"): + DataBin(values=6) - my_bin = data_bin_cls({1: 2}, 5) - self.assertEqual(my_bin.alpha, {1: 2}) - self.assertEqual(my_bin.beta, 5) - self.assertTrue("alpha=" in str(my_bin)) - self.assertTrue(">" not in str(my_bin)) + with self.assertRaisesRegex(ValueError, "does not lead with the shape"): + DataBin(x=np.empty((5,)), shape=(1,)) - def test_make_databin_no_fields(self): - """Test the make_data_bin() function when no fields are given.""" - data_bin_cls = make_data_bin([]) - data_bin = data_bin_cls() - self.assertEqual(len(data_bin), 0) + with self.assertRaisesRegex(ValueError, "does not lead with the shape"): + DataBin(x=np.empty((5, 2, 3)), shape=(5, 2, 3, 4)) + + def test_shape(self): + """Test shape setting and attributes.""" + databin = DataBin(x=6, y=np.empty((2, 3))) + self.assertEqual(databin.shape, ()) + + databin = DataBin(x=np.empty((5, 2)), y=np.empty((5, 2, 6)), shape=(5, 2)) + self.assertEqual(databin.shape, (5, 2)) def test_make_databin_mapping(self): - """Test the make_data_bin() function with mapping features.""" - data_bin_cls = make_data_bin([("alpha", int), ("beta", dict)]) - data_bin = data_bin_cls(10, {1: 2}) + """Test DataBin with mapping features.""" + data_bin = DataBin(alpha=10, beta={1: 2}) self.assertEqual(len(data_bin), 2) with self.subTest("iterator"): @@ -86,14 +82,14 @@ def test_make_databin_mapping(self): _ = next(iterator) with self.subTest("keys"): - lst = data_bin.keys() + lst = list(data_bin.keys()) key = lst[0] self.assertEqual(key, "alpha") key = lst[1] self.assertEqual(key, "beta") with self.subTest("values"): - lst = data_bin.values() + lst = list(data_bin.values()) val = lst[0] self.assertIsInstance(val, int) self.assertEqual(val, 10) @@ -102,7 +98,7 @@ def test_make_databin_mapping(self): self.assertEqual(val, {1: 2}) with self.subTest("items"): - lst = data_bin.items() + lst = list(data_bin.items()) key, val = lst[0] self.assertEqual(key, "alpha") self.assertIsInstance(val, int) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index 5a8513a5ed9..ea51718aebe 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -112,7 +112,7 @@ def test_coerce_observable_zero_sparse_pauli_op(self): self.assertEqual(obs["Z"], 1) def test_coerce_observable_duplicate_sparse_pauli_op(self): - """Test coerce_observable for SparsePauliOp wiht duplicate paulis""" + """Test coerce_observable for SparsePauliOp with duplicate paulis""" op = qi.SparsePauliOp(["XX", "-XX", "XX", "-XX"], [2, 1, 3, 2]) obs = ObservablesArray.coerce_observable(op) self.assertIsInstance(obs, dict) diff --git a/test/python/primitives/containers/test_primitive_result.py b/test/python/primitives/containers/test_primitive_result.py index 93e563379c2..fc8c774a164 100644 --- a/test/python/primitives/containers/test_primitive_result.py +++ b/test/python/primitives/containers/test_primitive_result.py @@ -14,9 +14,8 @@ """Unit tests for PrimitiveResult.""" import numpy as np -import numpy.typing as npt -from qiskit.primitives.containers import PrimitiveResult, PubResult, make_data_bin +from qiskit.primitives.containers import DataBin, PrimitiveResult, PubResult from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -25,16 +24,12 @@ class PrimitiveResultCase(QiskitTestCase): def test_primitive_result(self): """Test the PrimitiveResult class.""" - data_bin_cls = make_data_bin( - [("alpha", npt.NDArray[np.uint16]), ("beta", np.ndarray)], shape=(10, 20) - ) - alpha = np.empty((10, 20), dtype=np.uint16) beta = np.empty((10, 20), dtype=int) pub_results = [ - PubResult(data_bin_cls(alpha, beta)), - PubResult(data_bin_cls(alpha, beta)), + PubResult(DataBin(alpha=alpha, beta=beta, shape=(10, 20))), + PubResult(DataBin(alpha=alpha, beta=beta, shape=(10, 20))), ] result = PrimitiveResult(pub_results, {"x": 2}) diff --git a/test/python/primitives/containers/test_pub_result.py b/test/python/primitives/containers/test_pub_result.py index 1849b77c475..011110df3bd 100644 --- a/test/python/primitives/containers/test_pub_result.py +++ b/test/python/primitives/containers/test_pub_result.py @@ -13,7 +13,7 @@ """Unit tests for PubResult.""" -from qiskit.primitives.containers import PubResult, make_data_bin +from qiskit.primitives.containers import DataBin, PubResult from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -22,13 +22,12 @@ class PubResultCase(QiskitTestCase): def test_construction(self): """Test that the constructor works.""" - data_bin = make_data_bin((("a", float), ("b", int))) - pub_result = PubResult(data_bin(a=1.0, b=2)) + pub_result = PubResult(DataBin(a=1.0, b=2)) self.assertEqual(pub_result.data.a, 1.0) self.assertEqual(pub_result.data.b, 2) self.assertEqual(pub_result.metadata, {}) - pub_result = PubResult(data_bin(a=1.0, b=2), {"x": 1}) + pub_result = PubResult(DataBin(a=1.0, b=2), {"x": 1}) self.assertEqual(pub_result.data.a, 1.0) self.assertEqual(pub_result.data.b, 2) self.assertEqual(pub_result.metadata, {"x": 1}) @@ -38,6 +37,5 @@ def test_repr(self): # we are primarily interested in making sure some future change doesn't cause the repr to # raise an error. it is more sensible for humans to detect a deficiency in the formatting # itself, should one be uncovered - data_bin = make_data_bin((("a", float), ("b", int))) - self.assertTrue(repr(PubResult(data_bin(a=1.0, b=2))).startswith("PubResult")) - self.assertTrue(repr(PubResult(data_bin(a=1.0, b=2), {"x": 1})).startswith("PubResult")) + self.assertTrue(repr(PubResult(DataBin(a=1.0, b=2))).startswith("PubResult")) + self.assertTrue(repr(PubResult(DataBin(a=1.0, b=2), {"x": 1})).startswith("PubResult")) diff --git a/test/python/primitives/containers/test_sampler_pub_result.py b/test/python/primitives/containers/test_sampler_pub_result.py new file mode 100644 index 00000000000..fe7144a0741 --- /dev/null +++ b/test/python/primitives/containers/test_sampler_pub_result.py @@ -0,0 +1,104 @@ +# 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. + + +"""Unit tests for SamplerPubResult.""" + +from test import QiskitTestCase + +import numpy as np + +from qiskit.primitives.containers import BitArray, DataBin, SamplerPubResult + + +class SamplerPubResultCase(QiskitTestCase): + """Test the SamplerPubResult class.""" + + def test_construction(self): + """Test that the constructor works.""" + ba = BitArray.from_samples(["00", "11"], 2) + counts = {"00": 1, "11": 1} + data_bin = DataBin(a=ba, b=ba) + pub_result = SamplerPubResult(data_bin) + self.assertEqual(pub_result.data.a.get_counts(), counts) + self.assertEqual(pub_result.data.b.get_counts(), counts) + self.assertEqual(pub_result.metadata, {}) + + pub_result = SamplerPubResult(data_bin, {"x": 1}) + self.assertEqual(pub_result.data.a.get_counts(), counts) + self.assertEqual(pub_result.data.b.get_counts(), counts) + self.assertEqual(pub_result.metadata, {"x": 1}) + + def test_repr(self): + """Test that the repr doesn't fail""" + # we are primarily interested in making sure some future change doesn't cause the repr to + # raise an error. it is more sensible for humans to detect a deficiency in the formatting + # itself, should one be uncovered + ba = BitArray.from_samples(["00", "11"], 2) + data_bin = DataBin(a=ba, b=ba) + self.assertTrue(repr(SamplerPubResult(data_bin)).startswith("SamplerPubResult")) + self.assertTrue(repr(SamplerPubResult(data_bin, {"x": 1})).startswith("SamplerPubResult")) + + def test_join_data_failures(self): + """Test the join_data() failure mechanisms work.""" + + result = SamplerPubResult(DataBin()) + with self.assertRaisesRegex(ValueError, "No entry exists in the data bin"): + result.join_data() + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(ValueError, "An empty name list is given"): + result.join_data([]) + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(ValueError, "Name 'foo' does not exist"): + result.join_data(["alpha", "foo"]) + + alpha = BitArray.from_samples(["00", "11"], 2) + beta = np.empty((2,)) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + alpha = np.empty((2,)) + beta = BitArray.from_samples(["00", "11"], 2) + result = SamplerPubResult(DataBin(alpha=alpha, beta=beta)) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + result = SamplerPubResult(DataBin(alpha=1, beta={})) + with self.assertRaisesRegex(TypeError, "Data comes from incompatible types"): + result.join_data() + + def test_join_data_bit_array_default(self): + """Test the join_data() method with no arguments and bit arrays.""" + alpha = BitArray.from_samples(["00", "11"], 2) + beta = BitArray.from_samples(["010", "101"], 3) + data_bin = DataBin(alpha=alpha, beta=beta) + result = SamplerPubResult(data_bin) + + gamma = result.join_data() + self.assertEqual(list(gamma.get_bitstrings()), ["01000", "10111"]) + + def test_join_data_ndarray_default(self): + """Test the join_data() method with no arguments and ndarrays.""" + alpha = np.linspace(0, 1, 30).reshape((2, 3, 5)) + beta = np.linspace(0, 1, 12).reshape((2, 3, 2)) + data_bin = DataBin(alpha=alpha, beta=beta, shape=(2, 3)) + result = SamplerPubResult(data_bin) + + gamma = result.join_data() + np.testing.assert_allclose(gamma, np.concatenate([alpha, beta], axis=2)) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 2af6b15b877..6728d57e3fd 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -461,6 +461,18 @@ def test_job_size_limit_backend_v1(self): estimator.run([(qc, op, param_list)] * k).result() self.assertEqual(run_mock.call_count, 10) + def test_iter_pub(self): + """test for an iterable of pubs""" + backend = BasicSimulator() + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + circuit = pm.run(circuit) + estimator = BackendEstimatorV2(backend=backend, options=self._options) + observable = self.observable.apply_layout(circuit.layout) + result = estimator.run(iter([(circuit, observable), (circuit, observable)])).result() + np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol) + np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733], rtol=self._rtol) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 64a26471a33..b03818846c8 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -15,7 +15,6 @@ from __future__ import annotations import unittest -from dataclasses import astuple from test import QiskitTestCase, combine import numpy as np @@ -604,7 +603,7 @@ def test_circuit_with_multiple_cregs(self, backend): result = sampler.run([qc], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + self.assertEqual(len(data), 3) for creg in qc.cregs: self.assertTrue(hasattr(data, creg.name)) self._assert_allclose(getattr(data, creg.name), np.array(target[creg.name])) @@ -640,10 +639,10 @@ def test_circuit_with_aliased_cregs(self, backend): result = sampler.run([qc2], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) - for creg_name in target: + self.assertEqual(len(data), 3) + for creg_name, creg in target.items(): self.assertTrue(hasattr(data, creg_name)) - self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) + self._assert_allclose(getattr(data, creg_name), np.array(creg)) @combine(backend=BACKENDS) def test_no_cregs(self, backend): @@ -733,6 +732,23 @@ def test_job_size_limit_backend_v1(self): self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + def test_iter_pub(self): + """Test of an iterable of pubs""" + backend = BasicSimulator() + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + sampler = BackendSamplerV2(backend=backend) + result = sampler.run(iter([qc, qc2]), shots=self._shots).result() + self.assertIsInstance(result, PrimitiveResult) + self.assertEqual(len(result), 2) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[1], PubResult) + self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) + self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index 80045dee0d6..535841cc90f 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -346,9 +346,9 @@ class TestObservableValidation(QiskitTestCase): ), ) @unpack - def test_validate_observables(self, obsevables, expected): - """Test obsevables standardization.""" - self.assertEqual(validation._validate_observables(obsevables), expected) + def test_validate_observables(self, observables, expected): + """Test observables standardization.""" + self.assertEqual(validation._validate_observables(observables), expected) @data(None, "ERROR") def test_qiskit_error(self, observables): @@ -358,7 +358,7 @@ def test_qiskit_error(self, observables): @data((), []) def test_value_error(self, observables): - """Test value error if no obsevables are provided.""" + """Test value error if no observables are provided.""" with self.assertRaises(ValueError): validation._validate_observables(observables) diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 15c022f770c..1ed2d42e0e3 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -276,11 +276,20 @@ def test_precision_seed(self): result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.901141473854881]) np.testing.assert_allclose(result[1].data.evs, [1.901141473854881]) - # precision=0 impliese the exact expectation value + # precision=0 implies the exact expectation value job = estimator.run([(psi1, hamiltonian1, [theta1])], precision=0) result = job.result() np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956]) + def test_iter_pub(self): + """test for an iterable of pubs""" + estimator = StatevectorEstimator() + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + observable = self.observable.apply_layout(circuit.layout) + result = estimator.run(iter([(circuit, observable), (circuit, observable)])).result() + np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733]) + np.testing.assert_allclose(result[1].data.evs, [-1.284366511861733]) + if __name__ == "__main__": unittest.main() diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index cd0622b18de..c065871025d 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -15,7 +15,6 @@ from __future__ import annotations import unittest -from dataclasses import astuple import numpy as np from numpy.typing import NDArray @@ -573,7 +572,7 @@ def test_circuit_with_multiple_cregs(self): result = sampler.run([qc], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) + self.assertEqual(len(data), 3) for creg in qc.cregs: self.assertTrue(hasattr(data, creg.name)) self._assert_allclose(getattr(data, creg.name), np.array(target[creg.name])) @@ -606,10 +605,10 @@ def test_circuit_with_aliased_cregs(self): result = sampler.run([qc2], shots=self._shots).result() self.assertEqual(len(result), 1) data = result[0].data - self.assertEqual(len(astuple(data)), 3) - for creg_name in target: + self.assertEqual(len(data), 3) + for creg_name, creg in target.items(): self.assertTrue(hasattr(data, creg_name)) - self._assert_allclose(getattr(data, creg_name), np.array(target[creg_name])) + self._assert_allclose(getattr(data, creg_name), np.array(creg)) def test_no_cregs(self): """Test that the sampler works when there are no classical register in the circuit.""" @@ -621,6 +620,22 @@ def test_no_cregs(self): self.assertEqual(len(result), 1) self.assertEqual(len(result[0].data), 0) + def test_iter_pub(self): + """Test of an iterable of pubs""" + qc = QuantumCircuit(1) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.x(0) + qc2.measure_all() + sampler = StatevectorSampler() + result = sampler.run(iter([qc, qc2]), shots=self._shots).result() + self.assertIsInstance(result, PrimitiveResult) + self.assertEqual(len(result), 2) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[1], PubResult) + self._assert_allclose(result[0].data.meas, np.array({0: self._shots})) + self._assert_allclose(result[1].data.meas, np.array({1: self._shots})) + if __name__ == "__main__": unittest.main() diff --git a/test/python/providers/basic_provider/test_standard_library.py b/test/python/providers/basic_provider/test_standard_library.py new file mode 100644 index 00000000000..3d6b5c83ccc --- /dev/null +++ b/test/python/providers/basic_provider/test_standard_library.py @@ -0,0 +1,531 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +# pylint: disable=missing-function-docstring, missing-module-docstring + +import unittest + +from qiskit import QuantumCircuit +from qiskit.providers.basic_provider import BasicSimulator +import qiskit.circuit.library.standard_gates as lib +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestStandardGates(QiskitTestCase): + """Standard gates support in BasicSimulator, up to 3 qubits""" + + def setUp(self): + super().setUp() + self.seed = 43 + self.shots = 1 + self.circuit = QuantumCircuit(4) + + def test_barrier(self): + self.circuit.barrier(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_barrier_none(self): + self.circuit.barrier() + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_unitary(self): + matrix = [[0, 0, 0, 1], [0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0]] + self.circuit.unitary(matrix, [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u(self): + self.circuit.u(0.5, 1.5, 1.5, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u1(self): + self.circuit.append(lib.U1Gate(0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u2(self): + self.circuit.append(lib.U2Gate(0.5, 0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_u3(self): + self.circuit.append(lib.U3Gate(0.5, 0.5, 0.5), [1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ccx(self): + self.circuit.ccx(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ccz(self): + self.circuit.ccz(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ch(self): + self.circuit.ch(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cp(self): + self.circuit.cp(0, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_crx(self): + self.circuit.crx(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cry(self): + self.circuit.cry(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_crz(self): + self.circuit.crz(1, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cswap(self): + self.circuit.cswap(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu1(self): + self.circuit.append(lib.CU1Gate(1), [1, 2]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu3(self): + self.circuit.append(lib.CU3Gate(1, 2, 3), [1, 2]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cx(self): + self.circuit.cx(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ecr(self): + self.circuit.ecr(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cy(self): + self.circuit.cy(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cz(self): + self.circuit.cz(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_h(self): + self.circuit.h(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_id(self): + self.circuit.id(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rx(self): + self.circuit.rx(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ry(self): + self.circuit.ry(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rz(self): + self.circuit.rz(1, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rxx(self): + self.circuit.rxx(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rzx(self): + self.circuit.rzx(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_ryy(self): + self.circuit.ryy(1, 1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rzz(self): + self.circuit.rzz(1, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_s(self): + self.circuit.s(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sdg(self): + self.circuit.sdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sx(self): + self.circuit.sx(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_sxdg(self): + self.circuit.sxdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_swap(self): + self.circuit.swap(1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_iswap(self): + self.circuit.iswap(1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_p(self): + self.circuit.p(1, 0) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_r(self): + self.circuit.r(0.5, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_t(self): + self.circuit.t(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_tdg(self): + self.circuit.tdg(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_x(self): + self.circuit.x(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_y(self): + self.circuit.y(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_z(self): + self.circuit.z(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cs(self): + self.circuit.cs(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_csdg(self): + self.circuit.csdg(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_csx(self): + self.circuit.csx(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_cu(self): + self.circuit.cu(0.5, 0.5, 0.5, 0.5, 0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_dcx(self): + self.circuit.dcx(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_delay(self): + self.circuit.delay(0, 1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_reset(self): + self.circuit.reset(1) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_rcx(self): + self.circuit.rccx(0, 1, 2) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_global_phase(self): + qc = self.circuit + qc.append(lib.GlobalPhaseGate(0.1), []) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_xx_minus_yy(self): + self.circuit.append(lib.XXMinusYYGate(0.1, 0.2), [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + def test_xx_plus_yy(self): + self.circuit.append(lib.XXPlusYYGate(0.1, 0.2), [0, 1]) + self.circuit.measure_all() + result = ( + BasicSimulator().run(self.circuit, shots=self.shots, seed_simulator=self.seed).result() + ) + self.assertEqual(result.success, True) + + +class TestStandardGatesTarget(QiskitTestCase): + """Standard gates, up to 3 qubits, as a target""" + + def test_target(self): + target = BasicSimulator().target + expected = { + "cz", + "u3", + "p", + "cswap", + "z", + "cu1", + "ecr", + "reset", + "ch", + "cy", + "dcx", + "crx", + "sx", + "unitary", + "csdg", + "rzz", + "measure", + "swap", + "csx", + "y", + "s", + "xx_plus_yy", + "cs", + "h", + "t", + "u", + "rxx", + "cu", + "rzx", + "ry", + "rx", + "cu3", + "tdg", + "u2", + "xx_minus_yy", + "global_phase", + "u1", + "id", + "cx", + "cp", + "rz", + "sxdg", + "x", + "ryy", + "sdg", + "ccz", + "delay", + "crz", + "iswap", + "ccx", + "cry", + "rccx", + "r", + } + self.assertEqual(set(target.operation_names), expected) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index b4fbe944c33..cd7c611b221 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -35,6 +35,16 @@ def test_supported_basis_gates(self): with self.assertRaises(QiskitError): GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"]) + def test_cx_1Q(self): + """Test failing with a backend with single qubit but with a two-qubit basis gate""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=1, basis_gates=["cx", "id"]) + + def test_ccx_2Q(self): + """Test failing with a backend with two qubits but with a three-qubit basis gate""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"]) + def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 101e35acc8e..d5c5507b3b8 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -105,7 +105,7 @@ def setUpClass(cls): ) def test_circuit_on_fake_backend_v2(self, backend, optimization_level): if not optionals.HAS_AER and backend.num_qubits > 20: - self.skipTest("Unable to run fake_backend %s without qiskit-aer" % backend.name) + self.skipTest(f"Unable to run fake_backend {backend.name} without qiskit-aer") job = backend.run( transpile( self.circuit, backend, seed_transpiler=42, optimization_level=optimization_level @@ -126,8 +126,7 @@ def test_circuit_on_fake_backend_v2(self, backend, optimization_level): def test_circuit_on_fake_backend(self, backend, optimization_level): if not optionals.HAS_AER and backend.configuration().num_qubits > 20: self.skipTest( - "Unable to run fake_backend %s without qiskit-aer" - % backend.configuration().backend_name + f"Unable to run fake_backend {backend.configuration().backend_name} without qiskit-aer" ) job = backend.run( transpile( @@ -202,7 +201,7 @@ def test_defaults_to_dict(self, backend): self.assertGreater(i, 1e6) self.assertGreater(i, 1e6) else: - self.skipTest("Backend %s does not have defaults" % backend) + self.skipTest(f"Backend {backend} does not have defaults") def test_delay_circuit(self): backend = Fake27QPulseV1() diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index bf56f980a72..67628ba845a 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -342,7 +342,7 @@ def test_sequenced_parameterized_schedule(self): self.assertEqual(sched.instructions[2][-1].phase, 3) def test_schedule_generator(self): - """Test schedule generator functionalty.""" + """Test schedule generator functionality.""" dur_val = 10 amp = 1.0 @@ -364,7 +364,7 @@ def test_func(dur: int): self.assertEqual(inst_map.get_parameters("f", (0,)), ("dur",)) def test_schedule_generator_supports_parameter_expressions(self): - """Test expression-based schedule generator functionalty.""" + """Test expression-based schedule generator functionality.""" t_param = Parameter("t") amp = 1.0 diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index 54268af1457..0b91aaeaab4 100644 --- a/test/python/pulse/test_parameter_manager.py +++ b/test/python/pulse/test_parameter_manager.py @@ -515,6 +515,44 @@ def test_parametric_pulses_with_parameter_vector(self): self.assertEqual(sched2.instructions[0][1].pulse.sigma, 4.0) self.assertEqual(sched2.instructions[1][1].phase, 0.1) + def test_pulse_assignment_with_parameter_names(self): + """Test pulse assignment with parameter names.""" + sigma = Parameter("sigma") + amp = Parameter("amp") + param_vec = ParameterVector("param_vec", 2) + + waveform = pulse.library.Gaussian(duration=128, sigma=sigma, amp=amp) + waveform2 = pulse.library.Gaussian(duration=128, sigma=40, amp=amp) + block = pulse.ScheduleBlock() + block += pulse.Play(waveform, pulse.DriveChannel(10)) + block += pulse.Play(waveform2, pulse.DriveChannel(10)) + block += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) + block += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) + block1 = block.assign_parameters( + {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False + ) + + self.assertEqual(block1.blocks[0].pulse.amp, 0.2) + self.assertEqual(block1.blocks[0].pulse.sigma, 4.0) + self.assertEqual(block1.blocks[1].pulse.amp, 0.2) + self.assertEqual(block1.blocks[2].phase, 3.14) + self.assertEqual(block1.blocks[3].phase, 1.57) + + sched = pulse.Schedule() + sched += pulse.Play(waveform, pulse.DriveChannel(10)) + sched += pulse.Play(waveform2, pulse.DriveChannel(10)) + sched += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) + sched += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) + sched1 = sched.assign_parameters( + {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False + ) + + self.assertEqual(sched1.instructions[0][1].pulse.amp, 0.2) + self.assertEqual(sched1.instructions[0][1].pulse.sigma, 4.0) + self.assertEqual(sched1.instructions[1][1].pulse.amp, 0.2) + self.assertEqual(sched1.instructions[2][1].phase, 3.14) + self.assertEqual(sched1.instructions[3][1].phase, 1.57) + class TestScheduleTimeslots(QiskitTestCase): """Test for edge cases of timing overlap on parametrized channels. diff --git a/test/python/pulse/test_reference.py b/test/python/pulse/test_reference.py index c33d7588e92..3d760346175 100644 --- a/test/python/pulse/test_reference.py +++ b/test/python/pulse/test_reference.py @@ -87,7 +87,7 @@ def test_refer_schedule_parameter_scope(self): self.assertEqual(sched_z1.parameters, sched_y1.parameters) def test_refer_schedule_parameter_assignment(self): - """Test assigning to parametr in referenced schedule""" + """Test assigning to parameter in referenced schedule""" param = circuit.Parameter("name") with pulse.build() as sched_x1: @@ -197,7 +197,7 @@ def test_calling_similar_schedule(self): """Test calling schedules with the same representation. sched_x1 and sched_y1 are the different subroutines, but same representation. - Two references shoud be created. + Two references should be created. """ param1 = circuit.Parameter("param") param2 = circuit.Parameter("param") @@ -539,7 +539,7 @@ def test_lazy_ecr(self): def test_cnot(self): """Integration test with CNOT schedule construction.""" - # echeod cross resonance + # echoed cross resonance with pulse.build(name="ecr", default_alignment="sequential") as ecr_sched: pulse.call(self.cr_sched, name="cr") pulse.call(self.xp_sched, name="xp") diff --git a/test/python/qasm2/test_export.py b/test/python/qasm2/test_export.py index a0a3ade6ce8..85172ec3ce8 100644 --- a/test/python/qasm2/test_export.py +++ b/test/python/qasm2/test_export.py @@ -387,13 +387,14 @@ def test_mcx_gate(self): # qasm output doesn't support parameterized gate yet. # param0 for "gate mcuq(param0) is not used inside the definition - expected_qasm = """\ -OPENQASM 2.0; + pattern = r"""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; } -qreg q[4]; -mcx q[0],q[1],q[2],q[3];""" - self.assertEqual(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; } +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) def test_mcx_gate_variants(self): n = 5 diff --git a/test/python/qasm2/test_expression.py b/test/python/qasm2/test_expression.py index 98aead7f3b4..2ef35abde9e 100644 --- a/test/python/qasm2/test_expression.py +++ b/test/python/qasm2/test_expression.py @@ -123,6 +123,18 @@ def test_function_symbolic(self, function_str, function_py): actual = [float(x) for x in abstract_op.definition.data[0].operation.params] self.assertEqual(list(actual), expected) + def test_bigint(self): + """Test that an expression can be evaluated even if it contains an integer that will + overflow the integer handling.""" + bigint = 1 << 200 + # Sanity check that the number we're trying for is represented at full precision in floating + # point (which it should be - it's a power of two with fewer than 11 bits of exponent). + self.assertEqual(int(float(bigint)), bigint) + program = f"qreg q[1]; U({bigint}, -{bigint}, {bigint} * 2.0) q[0];" + parsed = qiskit.qasm2.loads(program) + parameters = list(parsed.data[0].operation.params) + self.assertEqual([bigint, -bigint, 2 * bigint], parameters) + class TestPrecedenceAssociativity(QiskitTestCase): def test_precedence(self): diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index 141d3c0f8b0..22eff30b38f 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -22,6 +22,7 @@ import tempfile import ddt +import numpy as np import qiskit.qasm2 from qiskit import qpy @@ -34,6 +35,7 @@ Qubit, library as lib, ) +from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order from . import gate_builder @@ -906,6 +908,30 @@ def test_conditioned_broadcast_against_empty_register(self): ) self.assertEqual(parsed, qc) + def test_has_to_matrix(self): + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg qr[1]; + gate my_gate(a) q { + rz(a) q; + rx(pi / 2) q; + rz(-a) q; + } + my_gate(1.0) qr[0]; + """ + parsed = qiskit.qasm2.loads(program) + expected = ( + lib.RZGate(-1.0).to_matrix() + @ lib.RXGate(math.pi / 2).to_matrix() + @ lib.RZGate(1.0).to_matrix() + ) + defined_gate = parsed.data[0].operation + self.assertEqual(defined_gate.name, "my_gate") + np.testing.assert_allclose(defined_gate.to_matrix(), expected, atol=1e-14, rtol=0) + # Also test that the standard `Operator` method on the whole circuit still works. + np.testing.assert_allclose(Operator(parsed), expected, atol=1e-14, rtol=0) + class TestReset(QiskitTestCase): def test_single(self): diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 3bb1667992a..6df04142088 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -24,7 +24,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile from qiskit.circuit import Parameter, Qubit, Clbit, Instruction, Gate, Delay, Barrier -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import CASE_DEFAULT from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError, ExperimentalFeatures from qiskit.qasm3.exporter import QASM3Builder @@ -495,7 +495,7 @@ def test_unbound_circuit(self): self.assertEqual(Exporter().dumps(qc), expected_qasm) def test_unknown_parameterized_gate_called_multiple_times(self): - """Test that a parameterised gate is called correctly if the first instance of it is + """Test that a parameterized gate is called correctly if the first instance of it is generic.""" x, y = Parameter("x"), Parameter("y") qc = QuantumCircuit(2) @@ -948,7 +948,7 @@ def test_old_alias_classical_registers_option(self): def test_simple_for_loop(self): """Test that a simple for loop outputs the expected result.""" - parameter = Parameter("x") + parameter = Parameter("my_x") loop_body = QuantumCircuit(1) loop_body.rx(parameter, 0) loop_body.break_loop() @@ -978,8 +978,8 @@ def test_simple_for_loop(self): def test_nested_for_loop(self): """Test that a for loop nested inside another outputs the expected result.""" - inner_parameter = Parameter("x") - outer_parameter = Parameter("y") + inner_parameter = Parameter("my_x") + outer_parameter = Parameter("my_y") inner_body = QuantumCircuit(2) inner_body.rz(inner_parameter, 0) @@ -1024,9 +1024,9 @@ def test_nested_for_loop(self): def test_regular_parameter_in_nested_for_loop(self): """Test that a for loop nested inside another outputs the expected result, including defining parameters that are used in nested loop scopes.""" - inner_parameter = Parameter("x") - outer_parameter = Parameter("y") - regular_parameter = Parameter("t") + inner_parameter = Parameter("my_x") + outer_parameter = Parameter("my_y") + regular_parameter = Parameter("my_t") inner_body = QuantumCircuit(2) inner_body.h(0) @@ -1310,7 +1310,7 @@ def test_chain_else_if(self): "", ] ) - # This is not the default behaviour, and it's pretty buried how you'd access it. + # This is not the default behavior, and it's pretty buried how you'd access it. builder = QASM3Builder( qc, includeslist=("stdgates.inc",), @@ -1370,7 +1370,7 @@ def test_chain_else_if_does_not_chain_if_extra_instructions(self): "", ] ) - # This is not the default behaviour, and it's pretty buried how you'd access it. + # This is not the default behavior, and it's pretty buried how you'd access it. builder = QASM3Builder( qc, includeslist=("stdgates.inc",), @@ -1471,6 +1471,17 @@ def test_parameters_and_registers_cannot_have_naming_clashes(self): self.assertIn("clash", parameter_name["name"]) self.assertNotEqual(register_name["name"], parameter_name["name"]) + def test_parameters_and_gates_cannot_have_naming_clashes(self): + """Test that parameters are renamed to avoid collisions with gate names.""" + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.rz(Parameter("rz"), 0) + + out_qasm = dumps(qc) + parameter_name = self.scalar_parameter_regex.search(out_qasm) + self.assertTrue(parameter_name) + self.assertIn("rz", parameter_name["name"]) + self.assertNotEqual(parameter_name["name"], "rz") + # Not necessarily all the reserved keywords, just a sensibly-sized subset. @data("bit", "const", "def", "defcal", "float", "gate", "include", "int", "let", "measure") def test_reserved_keywords_as_names_are_escaped(self, keyword): @@ -1574,11 +1585,20 @@ def test_expr_associativity_left(self): qc.if_test(expr.equal(expr.bit_and(expr.bit_and(cr1, cr2), cr3), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_or(expr.bit_or(cr1, cr2), cr3), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_xor(expr.bit_xor(cr1, cr2), cr3), 7), body.copy(), [], []) + qc.if_test( + expr.equal(expr.shift_left(expr.shift_left(cr1, cr2), cr3), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_right(expr.shift_right(cr1, cr2), cr3), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_left(expr.shift_right(cr1, cr2), cr3), 7), body.copy(), [], [] + ) qc.if_test(expr.logic_and(expr.logic_and(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) qc.if_test(expr.logic_or(expr.logic_or(cr1[0], cr1[1]), cr1[2]), body.copy(), [], []) - # Note that bitwise operations have lower priority than `==` so there's extra parentheses. - # All these operators are left-associative in OQ3. + # Note that bitwise operations except shift have lower priority than `==` so there's extra + # parentheses. All these operators are left-associative in OQ3. expected = """\ OPENQASM 3.0; include "stdgates.inc"; @@ -1591,6 +1611,12 @@ def test_expr_associativity_left(self): } if ((cr1 ^ cr2 ^ cr3) == 7) { } +if (cr1 << cr2 << cr3 == 7) { +} +if (cr1 >> cr2 >> cr3 == 7) { +} +if (cr1 >> cr2 << cr3 == 7) { +} if (cr1[0] && cr1[1] && cr1[2]) { } if (cr1[0] || cr1[1] || cr1[2]) { @@ -1610,6 +1636,15 @@ def test_expr_associativity_right(self): qc.if_test(expr.equal(expr.bit_and(cr1, expr.bit_and(cr2, cr3)), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_or(cr1, expr.bit_or(cr2, cr3)), 7), body.copy(), [], []) qc.if_test(expr.equal(expr.bit_xor(cr1, expr.bit_xor(cr2, cr3)), 7), body.copy(), [], []) + qc.if_test( + expr.equal(expr.shift_left(cr1, expr.shift_left(cr2, cr3)), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_right(cr1, expr.shift_right(cr2, cr3)), 7), body.copy(), [], [] + ) + qc.if_test( + expr.equal(expr.shift_left(cr1, expr.shift_right(cr2, cr3)), 7), body.copy(), [], [] + ) qc.if_test(expr.logic_and(cr1[0], expr.logic_and(cr1[1], cr1[2])), body.copy(), [], []) qc.if_test(expr.logic_or(cr1[0], expr.logic_or(cr1[1], cr1[2])), body.copy(), [], []) @@ -1629,6 +1664,12 @@ def test_expr_associativity_right(self): } if ((cr1 ^ (cr2 ^ cr3)) == 7) { } +if (cr1 << (cr2 << cr3) == 7) { +} +if (cr1 >> (cr2 >> cr3) == 7) { +} +if (cr1 << (cr2 >> cr3) == 7) { +} if (cr1[0] && (cr1[1] && cr1[2])) { } if (cr1[0] || (cr1[1] || cr1[2])) { @@ -1698,10 +1739,21 @@ def test_expr_precedence(self): ), ) + # An extra test of the bitshifting rules, since we have to pick one or the other of + # bitshifts vs comparisons due to the typing. The first operand is inside out, the second + bitshifts = expr.equal( + expr.shift_left(expr.bit_and(expr.bit_xor(cr, cr), cr), expr.bit_or(cr, cr)), + expr.bit_or( + expr.bit_xor(expr.shift_right(cr, 3), expr.shift_left(cr, 4)), + expr.shift_left(cr, 1), + ), + ) + qc = QuantumCircuit(cr) qc.if_test(inside_out, body.copy(), [], []) qc.if_test(outside_in, body.copy(), [], []) qc.if_test(logics, body.copy(), [], []) + qc.if_test(bitshifts, body.copy(), [], []) expected = """\ OPENQASM 3.0; @@ -1715,6 +1767,8 @@ def test_expr_precedence(self): } if ((!cr[0] || !cr[0]) && !(cr[0] && cr[0]) || !(cr[0] && cr[0]) && (!cr[0] || !cr[0])) { } +if (((cr ^ cr) & cr) << (cr | cr) == (cr >> 3 ^ cr << 4 | cr << 1)) { +} """ self.assertEqual(dumps(qc), expected) @@ -1736,13 +1790,152 @@ def test_no_unnecessary_cast(self): bit[8] cr; if (cr == 1) { } +""" + self.assertEqual(dumps(qc), expected) + + def test_var_use(self): + """Test that input and declared vars work in simple local scopes and can be set.""" + qc = QuantumCircuit() + a = qc.add_input("a", types.Bool()) + b = qc.add_input("b", types.Uint(8)) + qc.store(a, expr.logic_not(a)) + qc.store(b, expr.bit_and(b, 8)) + qc.add_var("c", expr.bit_not(b)) + # All inputs should come first, regardless of declaration order. + qc.add_input("d", types.Bool()) + + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool a; +input uint[8] b; +input bool d; +uint[8] c; +a = !a; +b = b & 8; +c = ~b; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_use_in_scopes(self): + """Test that usage of `Var` nodes works in capturing scopes.""" + qc = QuantumCircuit(2, 2) + a = qc.add_input("a", types.Bool()) + b_outer = qc.add_var("b", expr.lift(5, types.Uint(16))) + with qc.if_test(expr.logic_not(a)) as else_: + qc.store(b_outer, expr.bit_not(b_outer)) + qc.h(0) + with else_: + # Shadow of the same type. + qc.add_var("b", expr.lift(7, b_outer.type)) + with qc.while_loop(a): + # Shadow of a different type. + qc.add_var("b", a) + with qc.switch(b_outer) as case: + with case(0): + qc.store(b_outer, expr.lift(3, b_outer.type)) + with case(case.DEFAULT): + qc.add_var("b", expr.logic_not(a)) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool a; +bit[2] c; +int switch_dummy; +qubit[2] q; +uint[16] b; +b = 5; +if (!a) { + b = ~b; + h q[0]; +} else { + uint[16] b; + b = 7; +} +while (a) { + bool b; + b = a; +} +switch_dummy = b; +switch (switch_dummy) { + case 0 { + b = 3; + } + default { + bool b; + b = !a; + cx q[0], q[1]; + } +} +c[0] = measure q[0]; +c[1] = measure q[1]; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_parameter(self): + """We should support a `Var` clashing in name with a `Parameter` if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(1) + qc.add_var("a", False) + qc.rx(Parameter("a"), 0) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input float[64] a; +qubit[1] q; +bool a__generated0; +a__generated0 = false; +rx(a) q[0]; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_register(self): + """We should support a `Var` clashing in name with a `Register` if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c")) + qc.add_input("c", types.Bool()) + qc.add_var("q", False) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool c__generated0; +bit[2] c; +qubit[2] q; +bool q__generated1; +q__generated1 = false; +""" + self.assertEqual(dumps(qc), expected) + + def test_var_naming_clash_gate(self): + """We should support a `Var` clashing in name with some gate if `QuantumCircuit` allows + it.""" + qc = QuantumCircuit(2) + qc.add_input("cx", types.Bool()) + qc.add_input("U", types.Bool()) + qc.add_var("rx", expr.lift(5, types.Uint(8))) + + qc.cx(0, 1) + qc.u(0.5, 0.125, 0.25, 0) + # We don't actually use `rx`, but it's still in the `stdgates` include. + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input bool cx__generated0; +input bool U__generated1; +qubit[2] q; +uint[8] rx__generated2; +rx__generated2 = 5; +cx q[0], q[1]; +U(0.5, 0.125, 0.25) q[0]; """ self.assertEqual(dumps(qc), expected) class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCase): """Test functionality that is not what we _want_, but is what we need to do while the definition - of custom gates with parameterisation does not work correctly. + of custom gates with parameterization does not work correctly. These tests are modified versions of those marked with the `requires_fixed_parameterisation` decorator, and this whole class can be deleted once those are fixed. See gh-7335. @@ -1753,7 +1946,7 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) - first_h = qc.h(1)[0].operation + qc.h(1) qc.cx(1, 2) qc.barrier() qc.cx(0, 1) @@ -1764,52 +1957,51 @@ def test_basis_gates(self): first_x = qc.x(2).c_if(qc.clbits[1], 1)[0].operation qc.z(2).c_if(qc.clbits[0], 1) - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - u3_2 = first_x.definition.data[0].operation - - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - "bit[2] c;", - "qubit[3] q;", - "h q[1];", - "cx q[1], q[2];", - "barrier q[0], q[1], q[2];", - "cx q[0], q[1];", - "h q[0];", - "barrier q[0], q[1], q[2];", - "c[0] = measure q[0];", - "c[1] = measure q[1];", - "barrier q[0], q[1], q[2];", - "if (c[1]) {", - " x q[2];", - "}", - "if (c[0]) {", - " z q[2];", - "}", - "", - ] - ) - self.assertEqual( - Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc), - expected_qasm, - ) + id_len = len(str(id(first_x))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "qubit[3] q;", + "h q[1];", + "cx q[1], q[2];", + "barrier q[0], q[1], q[2];", + "cx q[0], q[1];", + "h q[0];", + "barrier q[0], q[1], q[2];", + "c[0] = measure q[0];", + "c[1] = measure q[1];", + "barrier q[0], q[1], q[2];", + "if (c[1]) {", + " x q[2];", + "}", + "if (c[0]) {", + " z q[2];", + "}", + "", + ] + res = Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_teleportation(self): """Teleportation with physical qubits""" @@ -1826,65 +2018,63 @@ def test_teleportation(self): qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) - first_h = transpiled.data[0].operation - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - first_x = transpiled.data[-2].operation - u3_2 = first_x.definition.data[0].operation - first_z = transpiled.data[-1].operation - u1 = first_z.definition.data[0].operation - u3_3 = u1.definition.data[0].operation - - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi) _gate_q_0;", - "}", - f"gate u1_{id(u1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_3)}(0, 0, pi) _gate_q_0;", - "}", - "gate z _gate_q_0 {", - f" u1_{id(u1)}(pi) _gate_q_0;", - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) + id_len = len(str(id(transpiled.data[0].operation))) + + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate z _gate_q_0 {", + re.compile(r" u1_\d{%s}\(pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "h $1;", + "cx $1, $2;", + "barrier $0, $1, $2;", + "cx $0, $1;", + "h $0;", + "barrier $0, $1, $2;", + "c[0] = measure $0;", + "c[1] = measure $1;", + "barrier $0, $1, $2;", + "if (c[1]) {", + " x $2;", + "}", + "if (c[0]) {", + " z $2;", + "}", + "", + ] + res = Exporter(includes=[]).dumps(transpiled).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" @@ -1927,62 +2117,58 @@ def test_no_include(self): circuit.sx(0) circuit.cx(0, 1) - rz = circuit.data[0].operation - u1_1 = rz.definition.data[0].operation - u3_1 = u1_1.definition.data[0].operation - sx = circuit.data[1].operation - sdg = sx.definition.data[0].operation - u1_2 = sdg.definition.data[0].operation - u3_2 = u1_2.definition.data[0].operation - h_ = sx.definition.data[1].operation - u2_1 = h_.definition.data[0].operation - u3_3 = u2_1.definition.data[0].operation - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_1)}(0, 0, pi/2) _gate_q_0;", - "}", - f"gate rz_{id(rz)}(_gate_p_0) _gate_q_0 {{", - f" u1_{id(u1_1)}(pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, -pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_2)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_2)}(0, 0, -pi/2) _gate_q_0;", - "}", - "gate sdg _gate_q_0 {", - f" u1_{id(u1_2)}(-pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2_1)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_3)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2_1)}(0, pi) _gate_q_0;", - "}", - "gate sx _gate_q_0 {", - " sdg _gate_q_0;", - " h _gate_q_0;", - " sdg _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "qubit[2] q;", - f"rz_{id(rz)}(pi/2) q[0];", - "sx q[0];", - "cx q[0], q[1];", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) + id_len = len(str(id(circuit.data[0].operation))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate rz_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u1_\d{%s}\(pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, -pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, -pi/2\) _gate_q_0;" % id_len), + "}", + "gate sdg _gate_q_0 {", + re.compile(r" u1_\d{%s}\(-pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate sx _gate_q_0 {", + " sdg _gate_q_0;", + " h _gate_q_0;", + " sdg _gate_q_0;", + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + "qubit[2] q;", + re.compile(r"rz_\d{%s}\(pi/2\) q\[0\];" % id_len), + "sx q[0];", + "cx q[0], q[1];", + "", + ] + res = Exporter(includes=[]).dumps(circuit).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_unusual_conditions(self): """Test that special QASM constructs such as ``measure`` are correctly handled when the @@ -2654,3 +2840,11 @@ def test_disallow_opaque_instruction(self): QASM3ExporterError, "Exporting opaque instructions .* is not yet supported" ): exporter.dumps(qc) + + def test_disallow_export_of_inner_scope(self): + """A circuit with captures can't be a top-level OQ3 program.""" + qc = QuantumCircuit(captures=[expr.Var.new("a", types.Bool())]) + with self.assertRaisesRegex( + QASM3ExporterError, "cannot export an inner scope.*as a top-level program" + ): + dumps(qc) diff --git a/test/python/qasm3/test_import.py b/test/python/qasm3/test_import.py index 85da3f41933..522f68c8956 100644 --- a/test/python/qasm3/test_import.py +++ b/test/python/qasm3/test_import.py @@ -13,7 +13,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring # Since the import is nearly entirely delegated to an external package, most of the testing is done -# there. Here we need to test our wrapping behaviour for base functionality and exceptions. We +# there. Here we need to test our wrapping behavior for base functionality and exceptions. We # don't want to get into a situation where updates to `qiskit_qasm3_import` breaks Terra's test # suite due to too specific tests on the Terra side. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index f23c0155bc6..36d716d2d85 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2023. +# (C) Copyright IBM 2017, 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 @@ -17,7 +17,9 @@ import numpy as np from ddt import ddt -from qiskit.circuit import Gate, QuantumCircuit, QuantumRegister +from qiskit.circuit import Gate, QuantumCircuit +from qiskit.circuit.random import random_clifford_circuit + from qiskit.circuit.library import ( CPhaseGate, CRXGate, @@ -26,7 +28,6 @@ CXGate, CYGate, CZGate, - DCXGate, ECRGate, HGate, IGate, @@ -37,10 +38,7 @@ RYYGate, RZZGate, RZXGate, - SdgGate, SGate, - SXGate, - SXdgGate, SwapGate, XGate, XXMinusYYGate, @@ -57,98 +55,11 @@ from qiskit.quantum_info.operators import Clifford, Operator from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info.operators.symplectic.clifford_circuits import _append_operation -from qiskit.synthesis.clifford import ( - synth_clifford_full, - synth_clifford_ag, - synth_clifford_bm, - synth_clifford_greedy, -) from qiskit.synthesis.linear import random_invertible_binary_matrix from test import QiskitTestCase # pylint: disable=wrong-import-order from test import combine # pylint: disable=wrong-import-order -class VGate(Gate): - """V Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new V Gate.""" - super().__init__("v", 1, []) - - def _define(self): - """V Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.sdg(0) - qc.h(0) - self.definition = qc - - -class WGate(Gate): - """W Gate used in Clifford synthesis.""" - - def __init__(self): - """Create new W Gate.""" - super().__init__("w", 1, []) - - def _define(self): - """W Gate definition.""" - q = QuantumRegister(1, "q") - qc = QuantumCircuit(q) - qc.append(VGate(), [q[0]], []) - qc.append(VGate(), [q[0]], []) - self.definition = qc - - -def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): - """Generate a pseudo random Clifford circuit.""" - - qubits_1_gates = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg", "v", "w"] - qubits_2_gates = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] - if gates == "all": - if num_qubits == 1: - gates = qubits_1_gates - else: - gates = qubits_1_gates + qubits_2_gates - - instructions = { - "i": (IGate(), 1), - "x": (XGate(), 1), - "y": (YGate(), 1), - "z": (ZGate(), 1), - "h": (HGate(), 1), - "s": (SGate(), 1), - "sdg": (SdgGate(), 1), - "sx": (SXGate(), 1), - "sxdg": (SXdgGate(), 1), - "v": (VGate(), 1), - "w": (WGate(), 1), - "cx": (CXGate(), 2), - "cy": (CYGate(), 2), - "cz": (CZGate(), 2), - "swap": (SwapGate(), 2), - "iswap": (iSwapGate(), 2), - "ecr": (ECRGate(), 2), - "dcx": (DCXGate(), 2), - } - - if isinstance(seed, np.random.Generator): - rng = seed - else: - rng = np.random.default_rng(seed) - - samples = rng.choice(gates, num_gates) - - circ = QuantumCircuit(num_qubits) - - for name in samples: - gate, nqargs = instructions[name] - qargs = rng.choice(range(num_qubits), nqargs, replace=False).tolist() - circ.append(gate, qargs) - - return circ - - @ddt class TestCliffordGates(QiskitTestCase): """Tests for clifford append gate functions.""" @@ -239,7 +150,7 @@ def test_append_1_qubit_gate(self): "sx", "sxdg", ): - with self.subTest(msg="append gate %s" % gate_name): + with self.subTest(msg=f"append gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff = _append_operation(cliff, gate_name, [0]) value_table = cliff.tableau[:, :-1] @@ -259,7 +170,7 @@ def test_1_qubit_identity_relations(self): """Tests identity relations for 1-qubit gates""" for gate_name in ("x", "y", "z", "h"): - with self.subTest(msg="identity for gate %s" % gate_name): + with self.subTest(msg=f"identity for gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() cliff = _append_operation(cliff, gate_name, [0]) @@ -270,7 +181,7 @@ def test_1_qubit_identity_relations(self): inv_gates = ["sdg", "sinv", "w"] for gate_name, inv_gate in zip(gates, inv_gates): - with self.subTest(msg="identity for gate %s" % gate_name): + with self.subTest(msg=f"identity for gate {gate_name}"): cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() cliff = _append_operation(cliff, gate_name, [0]) @@ -292,7 +203,7 @@ def test_1_qubit_mult_relations(self): ] for rel in rels: - with self.subTest(msg="relation %s" % rel): + with self.subTest(msg=f"relation {rel}"): split_rel = rel.split() cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() @@ -316,7 +227,7 @@ def test_1_qubit_conj_relations(self): ] for rel in rels: - with self.subTest(msg="relation %s" % rel): + with self.subTest(msg=f"relation {rel}"): split_rel = rel.split() cliff = Clifford([[1, 0], [0, 1]]) cliff1 = cliff.copy() @@ -513,9 +424,9 @@ def test_from_linear_function(self, num_qubits): """Test initialization from linear function.""" rng = np.random.default_rng(1234) samples = 50 - - for _ in range(samples): - mat = random_invertible_binary_matrix(num_qubits, seed=rng) + seeds = rng.integers(100000, size=samples, dtype=np.uint64) + for seed in seeds: + mat = random_invertible_binary_matrix(num_qubits, seed=seed) lin = LinearFunction(mat) cliff = Clifford(lin) self.assertTrue(Operator(cliff).equiv(Operator(lin))) @@ -588,92 +499,6 @@ def test_from_circuit_with_all_types(self): self.assertEqual(combined_clifford, expected_clifford) -@ddt -class TestCliffordSynthesis(QiskitTestCase): - """Test Clifford synthesis methods.""" - - @staticmethod - def _cliffords_1q(): - clifford_dicts = [ - {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, - {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, - {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, - {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, - {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, - ] - return [Clifford.from_dict(i) for i in clifford_dicts] - - def test_decompose_1q(self): - """Test synthesis for all 1-qubit Cliffords""" - for cliff in self._cliffords_1q(): - with self.subTest(msg=f"Test circuit {cliff}"): - target = cliff - value = Clifford(cliff.to_circuit()) - self.assertEqual(target, value) - - @combine(num_qubits=[2, 3]) - def test_synth_bm(self, num_qubits): - """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_bm(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[2, 3, 4, 5]) - def test_synth_ag(self, num_qubits): - """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_ag(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_greedy(self, num_qubits): - """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_greedy(target)) - self.assertEqual(value, target) - - @combine(num_qubits=[1, 2, 3, 4, 5]) - def test_synth_full(self, num_qubits): - """Test synthesis for set of {num_qubits}-qubit Cliffords""" - rng = np.random.default_rng(1234) - samples = 50 - for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) - value = Clifford(synth_clifford_full(target)) - self.assertEqual(value, target) - - @ddt class TestCliffordDecomposition(QiskitTestCase): """Test Clifford decompositions.""" @@ -683,11 +508,9 @@ class TestCliffordDecomposition(QiskitTestCase): ["h", "s"], ["h", "s", "i", "x", "y", "z"], ["h", "s", "sdg"], - ["h", "s", "v"], - ["h", "s", "w"], ["h", "sx", "sxdg"], ["s", "sx", "sxdg"], - ["h", "s", "sdg", "i", "x", "y", "z", "v", "w", "sx", "sxdg"], + ["h", "s", "sdg", "i", "x", "y", "z", "sx", "sxdg"], ] ) def test_to_operator_1qubit_gates(self, gates): diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py index 875dd923781..35acd46a4d0 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli.py @@ -14,42 +14,41 @@ """Tests for Pauli operator class.""" +import itertools as it import re import unittest -import itertools as it from functools import lru_cache +from test import QiskitTestCase, combine + import numpy as np -from ddt import ddt, data, unpack +from ddt import data, ddt, unpack from qiskit import QuantumCircuit from qiskit.circuit import Qubit -from qiskit.exceptions import QiskitError from qiskit.circuit.library import ( - IGate, - XGate, - YGate, - ZGate, - HGate, - SGate, - SdgGate, CXGate, - CZGate, CYGate, - SwapGate, + CZGate, ECRGate, EfficientSU2, + HGate, + IGate, + SdgGate, + SGate, + SwapGate, + XGate, + YGate, + ZGate, ) from qiskit.circuit.library.generalized_gates import PauliGate from qiskit.compiler.transpiler import transpile -from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.exceptions import QiskitError from qiskit.primitives import BackendEstimator +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.quantum_info.operators import Operator, Pauli, SparsePauliOp from qiskit.quantum_info.random import random_clifford, random_pauli -from qiskit.quantum_info.operators import Pauli, Operator, SparsePauliOp from qiskit.utils import optionals -from test import QiskitTestCase # pylint: disable=wrong-import-order - - LABEL_REGEX = re.compile(r"(?P[+-]?1?[ij]?)(?P[IXYZ]*)") PHASE_MAP = {"": 0, "-i": 1, "-": 2, "i": 3} @@ -606,6 +605,25 @@ def test_apply_layout_null_layout_invalid_num_qubits(self): with self.assertRaises(QiskitError): op.apply_layout(layout=None, num_qubits=1) + def test_apply_layout_negative_indices(self): + """Test apply_layout with negative indices""" + op = Pauli("IZ") + with self.assertRaises(QiskitError): + op.apply_layout(layout=[-1, 0], num_qubits=3) + + def test_apply_layout_duplicate_indices(self): + """Test apply_layout with duplicate indices""" + op = Pauli("IZ") + with self.assertRaises(QiskitError): + op.apply_layout(layout=[0, 0], num_qubits=3) + + @combine(phase=["", "-i", "-", "i"], layout=[None, []]) + def test_apply_layout_zero_qubit(self, phase, layout): + """Test apply_layout with a zero-qubit operator""" + op = Pauli(phase) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(Pauli(phase + "IIIII"), res) + if __name__ == "__main__": unittest.main() diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py index 0ef7079f461..8c96f63c4dd 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -2119,7 +2119,7 @@ def qubitwise_commutes(left: Pauli, right: Pauli) -> bool: pauli_list = PauliList(input_labels) groups = pauli_list.group_qubit_wise_commuting() - # checking that every input Pauli in pauli_list is in a group in the ouput + # checking that every input Pauli in pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group] self.assertListEqual(sorted(output_labels), sorted(input_labels)) @@ -2153,7 +2153,7 @@ def commutes(left: Pauli, right: Pauli) -> bool: # if qubit_wise=True, equivalent to test_group_qubit_wise_commuting groups = pauli_list.group_commuting(qubit_wise=False) - # checking that every input Pauli in pauli_list is in a group in the ouput + # checking that every input Pauli in pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group] self.assertListEqual(sorted(output_labels), sorted(input_labels)) # Within each group, every operator commutes with every other operator. 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 330fd53bc35..c4f09ec2d79 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 @@ -14,23 +14,22 @@ import itertools as it import unittest +from test import QiskitTestCase, combine + import numpy as np -import scipy.sparse import rustworkx as rx +import scipy.sparse from ddt import ddt - from qiskit import QiskitError -from qiskit.circuit import ParameterExpression, Parameter, ParameterVector -from qiskit.circuit.parametertable import ParameterView -from qiskit.quantum_info.operators import Operator, Pauli, PauliList, SparsePauliOp +from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit.library import EfficientSU2 +from qiskit.circuit.parametertable import ParameterView +from qiskit.compiler.transpiler import transpile from qiskit.primitives import BackendEstimator from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.compiler.transpiler import transpile +from qiskit.quantum_info.operators import Operator, Pauli, PauliList, SparsePauliOp from qiskit.utils import optionals -from test import QiskitTestCase # pylint: disable=wrong-import-order -from test import combine # pylint: disable=wrong-import-order def pauli_mat(label): @@ -182,7 +181,7 @@ def test_from_index_list(self): self.assertEqual(spp_op.paulis, PauliList(expected_labels)) def test_from_index_list_parameters(self): - """Test from_list method specifying the Paulis via indices with paramteres.""" + """Test from_list method specifying the Paulis via indices with parameters.""" expected_labels = ["XXZ", "IXI", "YIZ", "III"] paulis = ["XXZ", "X", "YZ", ""] indices = [[2, 1, 0], [1], [2, 0], []] @@ -1029,7 +1028,7 @@ def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool: coeffs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j sparse_pauli_list = SparsePauliOp(input_labels, coeffs) groups = sparse_pauli_list.group_commuting(qubit_wise) - # checking that every input Pauli in sparse_pauli_list is in a group in the ouput + # checking that every input Pauli in sparse_pauli_list is in a group in the output output_labels = [pauli.to_label() for group in groups for pauli in group.paulis] self.assertListEqual(sorted(output_labels), sorted(input_labels)) # checking that every coeffs are grouped according to sparse_pauli_list group @@ -1057,7 +1056,7 @@ def commutes(left: Pauli, right: Pauli, qubit_wise: bool) -> bool: ) def test_dot_real(self): - """Test dot for real coefficiets.""" + """Test dot for real coefficients.""" x = SparsePauliOp("X", np.array([1])) y = SparsePauliOp("Y", np.array([1])) iz = SparsePauliOp("Z", 1j) @@ -1179,6 +1178,34 @@ def test_apply_layout_null_layout_invalid_num_qubits(self): with self.assertRaises(QiskitError): op.apply_layout(layout=None, num_qubits=1) + def test_apply_layout_negative_indices(self): + """Test apply_layout with negative indices""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + with self.assertRaises(QiskitError): + op.apply_layout(layout=[-1, 0], num_qubits=3) + + def test_apply_layout_duplicate_indices(self): + """Test apply_layout with duplicate indices""" + op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + with self.assertRaises(QiskitError): + op.apply_layout(layout=[0, 0], num_qubits=3) + + @combine(layout=[None, []]) + def test_apply_layout_zero_qubit(self, layout): + """Test apply_layout with a zero-qubit operator""" + with self.subTest("default"): + op = SparsePauliOp("") + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp("IIIII"), res) + with self.subTest("coeff"): + op = SparsePauliOp("", 2) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp("IIIII", 2), res) + with self.subTest("multiple ops"): + op = SparsePauliOp.from_list([("", 1), ("", 2)]) + res = op.apply_layout(layout=layout, num_qubits=5) + self.assertEqual(SparsePauliOp.from_list([("IIIII", 1), ("IIIII", 2)]), res) + if __name__ == "__main__": unittest.main() diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index fc824643a0b..d653d618201 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -17,6 +17,7 @@ import unittest import logging import copy + from test import combine import numpy as np from ddt import ddt @@ -26,6 +27,7 @@ from qiskit import QiskitError from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit.library import HGate, CHGate, CXGate, QFT +from qiskit.transpiler import CouplingMap from qiskit.transpiler.layout import Layout, TranspileLayout from qiskit.quantum_info.operators import Operator, ScalarOp from qiskit.quantum_info.operators.predicates import matrix_equal @@ -735,6 +737,28 @@ def test_from_circuit_constructor_no_layout(self): global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True) self.assertTrue(global_phase_equivalent) + def test_from_circuit_initial_layout_final_layout(self): + """Test initialization from a circuit with a non-trivial initial_layout and final_layout as given + by a transpiled circuit.""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(2, 1) + qc.cx(1, 2) + qc.cx(1, 0) + qc.cx(1, 3) + qc.cx(1, 4) + qc.h(2) + + qc_transpiled = transpile( + qc, + coupling_map=CouplingMap.from_line(5), + initial_layout=[2, 3, 4, 0, 1], + optimization_level=1, + seed_transpiler=17, + ) + + self.assertTrue(Operator.from_circuit(qc_transpiled).equiv(qc)) + def test_from_circuit_constructor_reverse_embedded_layout(self): """Test initialization from a circuit with an embedded reverse layout.""" # Test tensor product of 1-qubit gates @@ -817,7 +841,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_final_layout(self) circuit._layout = TranspileLayout( Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}), {qubit: index for index, qubit in enumerate(circuit.qubits)}, - Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0}), + Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1}), ) circuit.swap(0, 1) circuit.swap(1, 2) @@ -839,7 +863,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_manual_final_layou Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}), {qubit: index for index, qubit in enumerate(circuit.qubits)}, ) - final_layout = Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0}) + final_layout = Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1}) circuit.swap(0, 1) circuit.swap(1, 2) op = Operator.from_circuit(circuit, final_layout=final_layout) @@ -966,7 +990,7 @@ def test_from_circuit_constructor_empty_layout(self): circuit.h(0) circuit.cx(0, 1) layout = Layout() - with self.assertRaises(IndexError): + with self.assertRaises(KeyError): Operator.from_circuit(circuit, layout=layout) def test_compose_scalar(self): @@ -1078,6 +1102,27 @@ def test_from_circuit_mixed_reg_loose_bits_transpiled(self): result = Operator.from_circuit(tqc) self.assertTrue(Operator(circuit).equiv(result)) + def test_from_circuit_into_larger_map(self): + """Test from_circuit method when the number of physical + qubits is larger than the number of original virtual qubits.""" + + # original circuit on 3 qubits + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + + # transpile into 5-qubits + tqc = transpile(qc, coupling_map=CouplingMap.from_line(5), initial_layout=[0, 2, 4]) + + # qc expanded with ancilla qubits + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.cx(1, 2) + + self.assertEqual(Operator.from_circuit(tqc), Operator(expected)) + def test_apply_permutation_back(self): """Test applying permutation to the operator, where the operator is applied first and the permutation second.""" diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index 4f5c728604b..cf6ad3c3509 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -398,7 +398,7 @@ def test_to_dict(self): target = {} for i in range(2): for j in range(3): - key = "{1}{0}|{1}{0}".format(i, j) + key = f"{j}{i}|{j}{i}" target[key] = 2 * j + i + 1 self.assertDictAlmostEqual(target, rho.to_dict()) @@ -407,7 +407,7 @@ def test_to_dict(self): target = {} for i in range(2): for j in range(11): - key = "{1},{0}|{1},{0}".format(i, j) + key = f"{j},{i}|{j},{i}" target[key] = 2 * j + i + 1 self.assertDictAlmostEqual(target, vec.to_dict()) diff --git a/test/python/quantum_info/states/test_stabilizerstate.py b/test/python/quantum_info/states/test_stabilizerstate.py index 56fecafbe58..4e1659ff699 100644 --- a/test/python/quantum_info/states/test_stabilizerstate.py +++ b/test/python/quantum_info/states/test_stabilizerstate.py @@ -13,6 +13,7 @@ """Tests for Stabilizerstate quantum state class.""" +from itertools import product import unittest import logging from ddt import ddt, data, unpack @@ -32,6 +33,61 @@ logger = logging.getLogger(__name__) +class StabilizerStateTestingTools: + """Test tools for verifying test cases in StabilizerState""" + + @staticmethod + def _bitstring_product_dict(bitstring_length: int, skip_entries: dict = None) -> dict: + """Retrieves a dict of every possible product of '0', '1' for length bitstring_length + pass in a dict to use the keys as entries to skip adding to the dict + + Args: + bitstring_length (int): length of the bitstring product + skip_entries (dict[str, float], optional): dict entries to skip adding to the dict based + on existing keys in the dict passed in. Defaults to {}. + + Returns: + dict[str, float]: dict with entries, all set to 0 + """ + if skip_entries is None: + skip_entries = {} + return { + result: 0 + for result in ["".join(x) for x in product(["0", "1"], repeat=bitstring_length)] + if result not in skip_entries + } + + @staticmethod + def _verify_individual_bitstrings( + testcase: QiskitTestCase, + target_dict: dict, + stab: StabilizerState, + qargs: list = None, + decimals: int = None, + dict_almost_equal: bool = False, + ) -> None: + """Helper that iterates through the target_dict and checks all probabilities by + running the value through the probabilities_dict_from_bitstring method for + retrieving a single measurement + + Args: + target_dict (dict[str, float]): dict to check probabilities for + stab (StabilizerState): stabilizerstate object to run probabilities_dict_from_bitstring on + qargs (None or list): subsystems to return probabilities for, + if None return for all subsystems (Default: None). + decimals (None or int): the number of decimal places to round + values. If None no rounding is done (Default: None) + dict_almost_equal (bool): utilize assertDictAlmostEqual when true, assertDictEqual when false + """ + for outcome_bitstring in target_dict: + (testcase.assertDictAlmostEqual if (dict_almost_equal) else testcase.assertDictEqual)( + stab.probabilities_dict_from_bitstring( + outcome_bitstring=outcome_bitstring, qargs=qargs, decimals=decimals + ), + {outcome_bitstring: target_dict[outcome_bitstring]}, + ) + + @ddt class TestStabilizerState(QiskitTestCase): """Tests for StabilizerState class.""" @@ -315,6 +371,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 1} self.assertEqual(value, target) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -326,6 +384,8 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"1": 1} self.assertEqual(value, target) + target.update({"0": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0, 1]) self.assertTrue(np.allclose(probs, target)) @@ -338,6 +398,7 @@ def test_probabilities_dict_single_qubit(self): value = stab.probabilities_dict() target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -355,43 +416,56 @@ def test_probabilities_dict_two_qubits(self): value = stab.probabilities_dict() target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0, 1] for _ in range(self.samples): with self.subTest(msg="P([0, 1])"): - value = stab.probabilities_dict([0, 1]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "01": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0, 1]) + target.update({"10": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5, 0, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1, 0] for _ in range(self.samples): with self.subTest(msg="P([1, 0])"): - value = stab.probabilities_dict([1, 0]) + value = stab.probabilities_dict(qargs) target = {"00": 0.5, "10": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([1, 0]) + target.update({"01": 0.0, "11": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0.5, 0]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [0] for _ in range(self.samples): with self.subTest(msg="P[0]"): - value = stab.probabilities_dict([0]) + value = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertEqual(value, target) - probs = stab.probabilities([0]) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) + qargs: list = [1] for _ in range(self.samples): with self.subTest(msg="P([1])"): - value = stab.probabilities_dict([1]) + value = stab.probabilities_dict(qargs) target = {"0": 1.0} self.assertEqual(value, target) - probs = stab.probabilities([1]) + target.update({"1": 0.0}) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) + probs = stab.probabilities(qargs) target = np.array([1, 0]) self.assertTrue(np.allclose(probs, target)) @@ -405,9 +479,10 @@ def test_probabilities_dict_qubits(self): qc.h(2) stab = StabilizerState(qc) + decimals: int = 1 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=1"): - value = stab.probabilities_dict(decimals=1) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.1, "001": 0.1, @@ -419,13 +494,17 @@ def test_probabilities_dict_qubits(self): "111": 0.1, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=1) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 2 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=2"): - value = stab.probabilities_dict(decimals=2) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.12, "001": 0.12, @@ -437,13 +516,17 @@ def test_probabilities_dict_qubits(self): "111": 0.12, } self.assertEqual(value, target) - probs = stab.probabilities(decimals=2) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) + probs = stab.probabilities(decimals=decimals) target = np.array([0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]) self.assertTrue(np.allclose(probs, target)) + decimals: int = 3 for _ in range(self.samples): with self.subTest(msg="P(None), decimals=3"): - value = stab.probabilities_dict(decimals=3) + value = stab.probabilities_dict(decimals=decimals) target = { "000": 0.125, "001": 0.125, @@ -455,10 +538,72 @@ def test_probabilities_dict_qubits(self): "111": 0.125, } self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, decimals=decimals + ) probs = stab.probabilities(decimals=3) target = np.array([0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]) self.assertTrue(np.allclose(probs, target)) + @combine(num_qubits=[5, 6, 7, 8, 9]) + def test_probabilities_dict_from_bitstring(self, num_qubits): + """Test probabilities_dict_from_bitstring methods with medium number of qubits that are still + reasonable to calculate the full dict with probabilities_dict of all possible outcomes""" + + qc: QuantumCircuit = QuantumCircuit(num_qubits) + for qubit_num in range(0, num_qubits): + qc.h(qubit_num) + stab = StabilizerState(qc) + + expected_result: float = float(1 / (2**num_qubits)) + target_dict: dict = StabilizerStateTestingTools._bitstring_product_dict(num_qubits) + target_dict.update((k, expected_result) for k in target_dict) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + value = stab.probabilities_dict() + self.assertDictEqual(value, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target_dict, stab) + probs = stab.probabilities() + target = np.array(([expected_result] * (2**num_qubits))) + self.assertTrue(np.allclose(probs, target)) + + # H gate at qubit 0, Every gate after is an X gate + # will result in 2 outcomes with 0.5 + qc = QuantumCircuit(num_qubits) + qc.h(0) + for qubit_num in range(1, num_qubits): + qc.x(qubit_num) + stab = StabilizerState(qc) + + # Build the 2 expected outcome bitstrings for + # 0.5 probability based on h and x gates + target_1: str = "".join(["1" * (num_qubits - 1)] + ["0"]) + target_2: str = "".join(["1" * num_qubits]) + target: dict = {target_1: 0.5, target_2: 0.5} + target_all_bitstrings: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, target + ) + target_all_bitstrings.update(target_all_bitstrings) + + # Numpy Array to verify stab.probabilities() + target_np_dict: dict = StabilizerStateTestingTools._bitstring_product_dict( + num_qubits, [target_1, target_2] + ) + target_np_dict.update(target) + target_np_array: np.ndarray = np.array(list(target_np_dict.values())) + + for _ in range(self.samples): + with self.subTest(msg="P(None)"): + stab = StabilizerState(qc) + value = stab.probabilities_dict() + self.assertEqual(value, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_all_bitstrings, stab + ) + probs = stab.probabilities() + self.assertTrue(np.allclose(probs, target_np_array)) + def test_probabilities_dict_ghz(self): """Test probabilities and probabilities_dict method of a subsystem of qubits""" @@ -473,6 +618,8 @@ def test_probabilities_dict_ghz(self): value = stab.probabilities_dict() target = {"000": 0.5, "111": 0.5} self.assertEqual(value, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target)) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab) probs = stab.probabilities() target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -483,6 +630,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"000": 0.5, "111": 0.5} self.assertDictAlmostEqual(probs, target) + target.update( + StabilizerStateTestingTools._bitstring_product_dict(num_qubits, target) + ) + StabilizerStateTestingTools._verify_individual_bitstrings(self, target, stab, qargs) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0, 0, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -493,6 +644,10 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"00": 0.5, "11": 0.5} self.assertDictAlmostEqual(probs, target) + target.update(StabilizerStateTestingTools._bitstring_product_dict(2, target)) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0, 0, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -503,6 +658,9 @@ def test_probabilities_dict_ghz(self): probs = stab.probabilities_dict(qargs) target = {"0": 0.5, "1": 0.5} self.assertDictAlmostEqual(probs, target) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target, stab, qargs, dict_almost_equal=True + ) probs = stab.probabilities(qargs) target = np.array([0.5, 0.5]) self.assertTrue(np.allclose(probs, target)) @@ -520,10 +678,17 @@ def test_probs_random_subsystem(self, num_qubits): stab = StabilizerState(cliff) probs = stab.probabilities(qargs) probs_dict = stab.probabilities_dict(qargs) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, probs_dict, stab, qargs + ) target = Statevector(qc).probabilities(qargs) target_dict = Statevector(qc).probabilities_dict(qargs) + Statevector(qc).probabilities_dict() self.assertTrue(np.allclose(probs, target)) self.assertDictAlmostEqual(probs_dict, target_dict) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, target_dict, stab, qargs, dict_almost_equal=True + ) @combine(num_qubits=[2, 3, 4, 5]) def test_expval_from_random_clifford(self, num_qubits): @@ -972,10 +1137,22 @@ def test_stabilizer_bell_equiv(self): # [XX, -ZZ] and [XX, YY] both generate the stabilizer group {II, XX, YY, -ZZ} self.assertTrue(cliff1.equiv(cliff2)) self.assertEqual(cliff1.probabilities_dict(), cliff2.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff1.probabilities_dict(), cliff2 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff2.probabilities_dict(), cliff1 + ) # [XX, ZZ] and [XX, -YY] both generate the stabilizer group {II, XX, -YY, ZZ} self.assertTrue(cliff3.equiv(cliff4)) self.assertEqual(cliff3.probabilities_dict(), cliff4.probabilities_dict()) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff3.probabilities_dict(), cliff4 + ) + StabilizerStateTestingTools._verify_individual_bitstrings( + self, cliff4.probabilities_dict(), cliff3 + ) self.assertFalse(cliff1.equiv(cliff3)) self.assertFalse(cliff2.equiv(cliff4)) diff --git a/test/python/quantum_info/states/test_utils.py b/test/python/quantum_info/states/test_utils.py index 9a9015944e7..1382963ed55 100644 --- a/test/python/quantum_info/states/test_utils.py +++ b/test/python/quantum_info/states/test_utils.py @@ -113,14 +113,14 @@ def test_schmidt_decomposition_3_level_system(self): # check decomposition elements self.assertAlmostEqual(schmidt_comps[0][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[0][1], Statevector(np.array([1, 0, 0]), dims=(3))) - self.assertEqual(schmidt_comps[0][2], Statevector(np.array([1, 0, 0]), dims=(3))) + self.assertEqual(schmidt_comps[0][1], Statevector(np.array([1, 0, 0]), dims=3)) + self.assertEqual(schmidt_comps[0][2], Statevector(np.array([1, 0, 0]), dims=3)) self.assertAlmostEqual(schmidt_comps[1][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[1][1], Statevector(np.array([0, 1, 0]), dims=(3))) - self.assertEqual(schmidt_comps[1][2], Statevector(np.array([0, 1, 0]), dims=(3))) + self.assertEqual(schmidt_comps[1][1], Statevector(np.array([0, 1, 0]), dims=3)) + self.assertEqual(schmidt_comps[1][2], Statevector(np.array([0, 1, 0]), dims=3)) self.assertAlmostEqual(schmidt_comps[2][0], 1 / np.sqrt(3)) - self.assertEqual(schmidt_comps[2][1], Statevector(np.array([0, 0, 1]), dims=(3))) - self.assertEqual(schmidt_comps[2][2], Statevector(np.array([0, 0, 1]), dims=(3))) + self.assertEqual(schmidt_comps[2][1], Statevector(np.array([0, 0, 1]), dims=3)) + self.assertEqual(schmidt_comps[2][2], Statevector(np.array([0, 0, 1]), dims=3)) # check that state can be properly reconstructed state = Statevector( diff --git a/test/python/quantum_info/test_quaternions.py b/test/python/quantum_info/test_quaternions.py index 48e2ead8b89..d838ee2d1b5 100644 --- a/test/python/quantum_info/test_quaternions.py +++ b/test/python/quantum_info/test_quaternions.py @@ -92,12 +92,14 @@ def test_mul_by_quat(self): def test_mul_by_array(self): """Quaternions cannot be multiplied with an array.""" other_array = np.array([0.1, 0.2, 0.3, 0.4]) - self.assertRaises(Exception, self.quat_unnormalized.__mul__, other_array) + with self.assertRaises(TypeError): + _ = self.quat_unnormalized * other_array def test_mul_by_scalar(self): """Quaternions cannot be multiplied with a scalar.""" other_scalar = 0.123456789 - self.assertRaises(Exception, self.quat_unnormalized.__mul__, other_scalar) + with self.assertRaises(TypeError): + _ = self.quat_unnormalized * other_scalar def test_rotation(self): """Multiplication by -1 should give the same rotation.""" diff --git a/test/python/result/test_mitigators.py b/test/python/result/test_mitigators.py index d290fc8ed48..3b3e83bce00 100644 --- a/test/python/result/test_mitigators.py +++ b/test/python/result/test_mitigators.py @@ -140,22 +140,16 @@ def test_mitigation_improvement(self): self.assertLess( mitigated_error, unmitigated_error * 0.8, - "Mitigator {} did not improve circuit {} measurements".format( - mitigator, circuit_name - ), + f"Mitigator {mitigator} did not improve circuit {circuit_name} measurements", ) mitigated_stddev_upper_bound = mitigated_quasi_probs._stddev_upper_bound max_unmitigated_stddev = max(unmitigated_stddev.values()) self.assertGreaterEqual( mitigated_stddev_upper_bound, max_unmitigated_stddev, - "Mitigator {} on circuit {} gave stddev upper bound {} " - "while unmitigated stddev maximum is {}".format( - mitigator, - circuit_name, - mitigated_stddev_upper_bound, - max_unmitigated_stddev, - ), + f"Mitigator {mitigator} on circuit {circuit_name} gave stddev upper bound " + f"{mitigated_stddev_upper_bound} while unmitigated stddev maximum is " + f"{max_unmitigated_stddev}", ) def test_expectation_improvement(self): @@ -190,22 +184,15 @@ def test_expectation_improvement(self): self.assertLess( mitigated_error, unmitigated_error, - "Mitigator {} did not improve circuit {} expectation computation for diagonal {} " - "ideal: {}, unmitigated: {} mitigated: {}".format( - mitigator, - circuit_name, - diagonal, - ideal_expectation, - unmitigated_expectation, - mitigated_expectation, - ), + f"Mitigator {mitigator} did not improve circuit {circuit_name} expectation " + f"computation for diagonal {diagonal} ideal: {ideal_expectation}, unmitigated:" + f" {unmitigated_expectation} mitigated: {mitigated_expectation}", ) self.assertGreaterEqual( mitigated_stddev, unmitigated_stddev, - "Mitigator {} did not increase circuit {} the standard deviation".format( - mitigator, circuit_name - ), + f"Mitigator {mitigator} did not increase circuit {circuit_name} the" + f" standard deviation", ) def test_clbits_parameter(self): @@ -228,7 +215,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly marganalize for qubits 1,2".format(mitigator), + f"Mitigator {mitigator} did not correctly marginalize for qubits 1,2", ) mitigated_probs_02 = ( @@ -240,7 +227,7 @@ def test_clbits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly marganalize for qubits 0,2".format(mitigator), + f"Mitigator {mitigator} did not correctly marginalize for qubits 0,2", ) def test_qubits_parameter(self): @@ -264,7 +251,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 0, 1, 2".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 0, 1, 2", ) mitigated_probs_210 = ( @@ -276,7 +263,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 2, 1, 0".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 2, 1, 0", ) mitigated_probs_102 = ( @@ -288,7 +275,7 @@ def test_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 1, 0, 2".format(mitigator), + "Mitigator {mitigator} did not correctly handle qubit order 1, 0, 2", ) def test_repeated_qubits_parameter(self): @@ -311,7 +298,7 @@ def test_repeated_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 2,1,0".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit order 2,1,0", ) # checking qubit order 2,1,0 should not "overwrite" the default 0,1,2 @@ -324,9 +311,8 @@ def test_repeated_qubits_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit order 0,1,2 (the expected default)".format( - mitigator - ), + f"Mitigator {mitigator} did not correctly handle qubit order 0,1,2 " + f"(the expected default)", ) def test_qubits_subset_parameter(self): @@ -350,7 +336,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit subset".format(mitigator), + "Mitigator {mitigator} did not correctly handle qubit subset", ) mitigated_probs_6 = ( @@ -362,7 +348,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.001, - "Mitigator {} did not correctly handle qubit subset".format(mitigator), + f"Mitigator {mitigator} did not correctly handle qubit subset", ) diagonal = str2diag("ZZ") ideal_expectation = 0 @@ -373,7 +359,7 @@ def test_qubits_subset_parameter(self): self.assertLess( mitigated_error, 0.1, - "Mitigator {} did not improve circuit expectation".format(mitigator), + f"Mitigator {mitigator} did not improve circuit expectation", ) def test_from_backend(self): diff --git a/test/python/result/test_result.py b/test/python/result/test_result.py index 7d73ab2ebcf..ff1f4cbf29a 100644 --- a/test/python/result/test_result.py +++ b/test/python/result/test_result.py @@ -105,7 +105,7 @@ def test_counts_duplicate_name(self): result.get_counts("foo") def test_result_repr(self): - """Test that repr is contstructed correctly for a results object.""" + """Test that repr is constructed correctly for a results object.""" raw_counts = {"0x0": 4, "0x2": 10} data = models.ExperimentResultData(counts=raw_counts) exp_result_header = QobjExperimentHeader( diff --git a/test/python/synthesis/aqc/fast_gradient/test_layer1q.py b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py index 43b164c4225..d2c5108391f 100644 --- a/test/python/synthesis/aqc/fast_gradient/test_layer1q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer1q.py @@ -62,7 +62,7 @@ def test_layer1q_matrix(self): # T == P^t @ G @ P. err = tut.relative_error(t_mat, iden[perm].T @ g_mat @ iden[perm]) - self.assertLess(err, eps, "err = {:0.16f}".format(err)) + self.assertLess(err, eps, f"err = {err:0.16f}") max_rel_err = max(max_rel_err, err) # Multiplication by permutation matrix of the left can be @@ -79,8 +79,7 @@ def test_layer1q_matrix(self): self.assertTrue( err1 < eps and err2 < eps and err3 < eps and err4 < eps, - "err1 = {:f}, err2 = {:f}, " - "err3 = {:f}, err4 = {:f}".format(err1, err2, err3, err4), + f"err1 = {err1:f}, err2 = {err2:f}, " f"err3 = {err3:f}, err4 = {err4:f}", ) max_rel_err = max(max_rel_err, err1, err2, err3, err4) @@ -128,12 +127,12 @@ def test_pmatrix_class(self): alt_ttmtt = pmat.finalize(temp_mat=tmp1) err1 = tut.relative_error(alt_ttmtt, ttmtt) - self.assertLess(err1, _eps, "relative error: {:f}".format(err1)) + self.assertLess(err1, _eps, f"relative error: {err1:f}") prod = np.complex128(np.trace(ttmtt @ t4)) alt_prod = pmat.product_q1(layer=c4, tmp1=tmp1, tmp2=tmp2) err2 = abs(alt_prod - prod) / abs(prod) - self.assertLess(err2, _eps, "relative error: {:f}".format(err2)) + self.assertLess(err2, _eps, f"relative error: {err2:f}") max_rel_err = max(max_rel_err, err1, err2) diff --git a/test/python/synthesis/aqc/fast_gradient/test_layer2q.py b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py index 9de1e13df2d..8f5655d6057 100644 --- a/test/python/synthesis/aqc/fast_gradient/test_layer2q.py +++ b/test/python/synthesis/aqc/fast_gradient/test_layer2q.py @@ -65,7 +65,7 @@ def test_layer2q_matrix(self): # T == P^t @ G @ P. err = tut.relative_error(t_mat, iden[perm].T @ g_mat @ iden[perm]) - self.assertLess(err, _eps, "err = {:0.16f}".format(err)) + self.assertLess(err, _eps, f"err = {err:0.16f}") max_rel_err = max(max_rel_err, err) # Multiplication by permutation matrix of the left can be @@ -82,8 +82,8 @@ def test_layer2q_matrix(self): self.assertTrue( err1 < _eps and err2 < _eps and err3 < _eps and err4 < _eps, - "err1 = {:f}, err2 = {:f}, " - "err3 = {:f}, err4 = {:f}".format(err1, err2, err3, err4), + f"err1 = {err1:f}, err2 = {err2:f}, " + f"err3 = {err3:f}, err4 = {err4:f}", ) max_rel_err = max(max_rel_err, err1, err2, err3, err4) @@ -136,12 +136,12 @@ def test_pmatrix_class(self): alt_ttmtt = pmat.finalize(temp_mat=tmp1) err1 = tut.relative_error(alt_ttmtt, ttmtt) - self.assertLess(err1, _eps, "relative error: {:f}".format(err1)) + self.assertLess(err1, _eps, f"relative error: {err1:f}") prod = np.complex128(np.trace(ttmtt @ t4)) alt_prod = pmat.product_q2(layer=c4, tmp1=tmp1, tmp2=tmp2) err2 = abs(alt_prod - prod) / abs(prod) - self.assertLess(err2, _eps, "relative error: {:f}".format(err2)) + self.assertLess(err2, _eps, f"relative error: {err2:f}") max_rel_err = max(max_rel_err, err1, err2) diff --git a/test/python/synthesis/test_clifford_decompose_layers.py b/test/python/synthesis/test_clifford_decompose_layers.py index 19183ce1730..1db810621e3 100644 --- a/test/python/synthesis/test_clifford_decompose_layers.py +++ b/test/python/synthesis/test_clifford_decompose_layers.py @@ -53,7 +53,7 @@ def test_decompose_clifford(self, num_qubits): @combine(num_qubits=[4, 5, 6, 7]) def test_decompose_lnn_depth(self, num_qubits): - """Test layered decomposition for linear-nearest-neighbour (LNN) connectivity.""" + """Test layered decomposition for linear-nearest-neighbor (LNN) connectivity.""" rng = np.random.default_rng(1234) samples = 10 for _ in range(samples): @@ -64,7 +64,7 @@ def test_decompose_lnn_depth(self, num_qubits): filter_function=lambda x: x.operation.num_qubits == 2 ) self.assertTrue(depth2q <= 7 * num_qubits + 2) - # Check that the Clifford circuit has linear nearest neighbour connectivity + # Check that the Clifford circuit has linear nearest neighbor connectivity self.assertTrue(check_lnn_connectivity(circ.decompose())) cliff_target = Clifford(circ) self.assertEqual(cliff, cliff_target) diff --git a/test/python/synthesis/test_clifford_sythesis.py b/test/python/synthesis/test_clifford_sythesis.py new file mode 100644 index 00000000000..887f1af5ad9 --- /dev/null +++ b/test/python/synthesis/test_clifford_sythesis.py @@ -0,0 +1,118 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +# pylint: disable=invalid-name +"""Tests for Clifford synthesis functions.""" + +import numpy as np +from ddt import ddt +from qiskit.circuit.random import random_clifford_circuit +from qiskit.quantum_info.operators import Clifford +from qiskit.synthesis.clifford import ( + synth_clifford_full, + synth_clifford_ag, + synth_clifford_bm, + synth_clifford_greedy, +) + +from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import combine # pylint: disable=wrong-import-order + + +@ddt +class TestCliffordSynthesis(QiskitTestCase): + """Tests for clifford synthesis functions.""" + + @staticmethod + def _cliffords_1q(): + clifford_dicts = [ + {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, + ] + return [Clifford.from_dict(i) for i in clifford_dicts] + + def test_decompose_1q(self): + """Test synthesis for all 1-qubit Cliffords""" + for cliff in self._cliffords_1q(): + with self.subTest(msg=f"Test circuit {cliff}"): + target = cliff + value = Clifford(cliff.to_circuit()) + self.assertEqual(target, value) + + @combine(num_qubits=[2, 3]) + def test_synth_bm(self, num_qubits): + """Test B&M synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_bm(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[2, 3, 4, 5]) + def test_synth_ag(self, num_qubits): + """Test A&G synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 1 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_ag(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_greedy(self, num_qubits): + """Test greedy synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_greedy(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) + + @combine(num_qubits=[1, 2, 3, 4, 5]) + def test_synth_full(self, num_qubits): + """Test synthesis for set of {num_qubits}-qubit Cliffords""" + rng = np.random.default_rng(1234) + samples = 50 + for _ in range(samples): + circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) + target = Clifford(circ) + synth_circ = synth_clifford_full(target) + value = Clifford(synth_circ) + self.assertEqual(value, target) diff --git a/test/python/synthesis/test_cx_cz_synthesis.py b/test/python/synthesis/test_cx_cz_synthesis.py index ef7eeb38b8f..28a26df181a 100644 --- a/test/python/synthesis/test_cx_cz_synthesis.py +++ b/test/python/synthesis/test_cx_cz_synthesis.py @@ -34,13 +34,14 @@ class TestCXCZSynth(QiskitTestCase): @combine(num_qubits=[3, 4, 5, 6, 7, 8, 9, 10]) def test_cx_cz_synth_lnn(self, num_qubits): - """Test the CXCZ synthesis code for linear nearest neighbour connectivity.""" + """Test the CXCZ synthesis code for linear nearest neighbor connectivity.""" seed = 1234 rng = np.random.default_rng(seed) num_gates = 10 num_trials = 8 + seeds = rng.integers(100000, size=num_trials, dtype=np.uint64) - for _ in range(num_trials): + for seed in seeds: # Generate a random CZ circuit mat_z = np.zeros((num_qubits, num_qubits)) cir_z = QuantumCircuit(num_qubits) @@ -55,7 +56,7 @@ def test_cx_cz_synth_lnn(self, num_qubits): mat_z[j][i] = (mat_z[j][i] + 1) % 2 # Generate a random CX circuit - mat_x = random_invertible_binary_matrix(num_qubits, seed=rng) + mat_x = random_invertible_binary_matrix(num_qubits, seed=seed) mat_x = np.array(mat_x, dtype=bool) cir_x = synth_cnot_depth_line_kms(mat_x) diff --git a/test/python/synthesis/test_cz_synthesis.py b/test/python/synthesis/test_cz_synthesis.py index 7284039e56c..af663a4f0d3 100644 --- a/test/python/synthesis/test_cz_synthesis.py +++ b/test/python/synthesis/test_cz_synthesis.py @@ -31,7 +31,7 @@ class TestCZSynth(QiskitTestCase): @combine(num_qubits=[3, 4, 5, 6, 7]) def test_cz_synth_lnn(self, num_qubits): - """Test the CZ synthesis code for linear nearest neighbour connectivity.""" + """Test the CZ synthesis code for linear nearest neighbor connectivity.""" seed = 1234 rng = np.random.default_rng(seed) num_gates = 10 diff --git a/test/python/synthesis/test_linear_synthesis.py b/test/python/synthesis/test_linear_synthesis.py index 98a49b6642f..bbfab20a30f 100644 --- a/test/python/synthesis/test_linear_synthesis.py +++ b/test/python/synthesis/test_linear_synthesis.py @@ -24,6 +24,7 @@ random_invertible_binary_matrix, check_invertible_binary_matrix, calc_inverse_matrix, + binary_matmul, ) from qiskit.synthesis.linear.linear_circuits_utils import transpose_cx_circ, optimize_cx_4_options from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -107,8 +108,9 @@ def test_invertible_matrix(self, n): """Test the functions for generating a random invertible matrix and inverting it.""" mat = random_invertible_binary_matrix(n, seed=1234) out = check_invertible_binary_matrix(mat) + mat = mat.astype(bool) mat_inv = calc_inverse_matrix(mat, verify=True) - mat_out = np.dot(mat, mat_inv) % 2 + mat_out = binary_matmul(mat, mat_inv) self.assertTrue(np.array_equal(mat_out, np.eye(n))) self.assertTrue(out) @@ -117,8 +119,9 @@ def test_synth_lnn_kms(self, num_qubits): """Test that synth_cnot_depth_line_kms produces the correct synthesis.""" rng = np.random.default_rng(1234) num_trials = 10 - for _ in range(num_trials): - mat = random_invertible_binary_matrix(num_qubits, seed=rng) + seeds = rng.integers(100000, size=num_trials, dtype=np.uint64) + for seed in seeds: + mat = random_invertible_binary_matrix(num_qubits, seed=seed) mat = np.array(mat, dtype=bool) qc = synth_cnot_depth_line_kms(mat) mat1 = LinearFunction(qc).linear diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 7fc6f5e24ab..b6a1ca9e185 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -19,9 +19,16 @@ from qiskit.quantum_info.operators import Operator from qiskit.circuit.library import LinearFunction, PermutationGate -from qiskit.synthesis import synth_permutation_acg -from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms, synth_permutation_basic -from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap +from qiskit.synthesis.permutation import ( + synth_permutation_acg, + synth_permutation_depth_lnn_kms, + synth_permutation_basic, + synth_permutation_reverse_lnn_kms, +) +from qiskit.synthesis.permutation.permutation_utils import ( + _inverse_pattern, + _validate_permutation, +) from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -30,17 +37,40 @@ class TestPermutationSynthesis(QiskitTestCase): """Test the permutation synthesis functions.""" @data(4, 5, 10, 15, 20) - def test_get_ordered_swap(self, width): - """Test get_ordered_swap function produces correct swap list.""" + def test_inverse_pattern(self, width): + """Test _inverse_pattern function produces correct index map.""" np.random.seed(1) for _ in range(5): pattern = np.random.permutation(width) - swap_list = _get_ordered_swap(pattern) - output = list(range(width)) - for i, j in swap_list: - output[i], output[j] = output[j], output[i] - self.assertTrue(np.array_equal(pattern, output)) - self.assertLess(len(swap_list), width) + inverse = _inverse_pattern(pattern) + for ii, jj in enumerate(pattern): + self.assertTrue(inverse[jj] == ii) + + @data(10, 20) + def test_invalid_permutations(self, width): + """Check that _validate_permutation raises exceptions when the + input is not a permutation.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + + pattern_out_of_range = np.copy(pattern) + pattern_out_of_range[0] = -1 + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_out_of_range) + self.assertIn("input contains a negative number", str(exc.exception)) + + pattern_out_of_range = np.copy(pattern) + pattern_out_of_range[0] = width + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_out_of_range) + self.assertIn(f"input has length {width} and contains {width}", str(exc.exception)) + + pattern_duplicate = np.copy(pattern) + pattern_duplicate[-1] = pattern[0] + with self.assertRaises(ValueError) as exc: + _validate_permutation(pattern_duplicate) + self.assertIn(f"input contains {pattern[0]} more than once", str(exc.exception)) @data(4, 5, 10, 15, 20) def test_synth_permutation_basic(self, width): @@ -108,6 +138,26 @@ def test_synth_permutation_depth_lnn_kms(self, width): synthesized_pattern = LinearFunction(qc).permutation_pattern() self.assertTrue(np.array_equal(synthesized_pattern, pattern)) + @data(1, 2, 3, 4, 5, 10, 15, 20) + def test_synth_permutation_reverse_lnn_kms(self, num_qubits): + """Test synth_permutation_reverse_lnn_kms function produces the correct + circuit.""" + pattern = list(reversed(range(num_qubits))) + qc = synth_permutation_reverse_lnn_kms(num_qubits) + self.assertListEqual((LinearFunction(qc).permutation_pattern()).tolist(), pattern) + + # Check that the CX depth of the circuit is at 2*n+2 + self.assertTrue(qc.depth() <= 2 * num_qubits + 2) + + # Check that the synthesized circuit consists of CX gates only, + # and that these CXs adhere to the LNN connectivity. + for instruction in qc.data: + self.assertEqual(instruction.operation.name, "cx") + q0 = qc.find_bit(instruction.qubits[0]).index + q1 = qc.find_bit(instruction.qubits[1]).index + dist = abs(q0 - q1) + self.assertEqual(dist, 1) + @data(4, 5, 6, 7) def test_permutation_matrix(self, width): """Test that the unitary matrix constructed from permutation pattern diff --git a/test/python/synthesis/test_stabilizer_synthesis.py b/test/python/synthesis/test_stabilizer_synthesis.py index e195c9cf270..958faa204c1 100644 --- a/test/python/synthesis/test_stabilizer_synthesis.py +++ b/test/python/synthesis/test_stabilizer_synthesis.py @@ -54,7 +54,7 @@ def test_decompose_stab(self, num_qubits): @combine(num_qubits=[4, 5, 6, 7]) def test_decompose_lnn_depth(self, num_qubits): - """Test stabilizer state decomposition for linear-nearest-neighbour (LNN) connectivity.""" + """Test stabilizer state decomposition for linear-nearest-neighbor (LNN) connectivity.""" rng = np.random.default_rng(1234) samples = 10 for _ in range(samples): @@ -66,7 +66,7 @@ def test_decompose_lnn_depth(self, num_qubits): filter_function=lambda x: x.operation.num_qubits == 2 ) self.assertTrue(depth2q == 2 * num_qubits + 2) - # Check that the stabilizer state circuit has linear nearest neighbour connectivity + # Check that the stabilizer state circuit has linear nearest neighbor connectivity self.assertTrue(check_lnn_connectivity(circ.decompose())) stab_target = StabilizerState(circ) # Verify that the two stabilizers generate the same state diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index cb918b29146..025b9accf22 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -138,13 +138,13 @@ def assertDebugOnly(self): # FIXME: when at python 3.10+ replace with assertNoL """Context manager, asserts log is emitted at level DEBUG but no higher""" with self.assertLogs("qiskit.synthesis", "DEBUG") as ctx: yield - for i in range(len(ctx.records)): + for i, record in enumerate(ctx.records): self.assertLessEqual( - ctx.records[i].levelno, + record.levelno, logging.DEBUG, msg=f"Unexpected logging entry: {ctx.output[i]}", ) - self.assertIn("Requested fidelity:", ctx.records[i].getMessage()) + self.assertIn("Requested fidelity:", record.getMessage()) def assertRoundTrip(self, weyl1: TwoQubitWeylDecomposition): """Fail if eval(repr(weyl1)) not equal to weyl1""" @@ -219,7 +219,7 @@ def check_two_qubit_weyl_specialization( ): """Check that the two qubit Weyl decomposition gets specialized as expected""" - # Loop to check both for implicit and explicity specialization + # Loop to check both for implicit and explicitly specialization for decomposer in (TwoQubitWeylDecomposition, expected_specialization): if isinstance(decomposer, TwoQubitWeylDecomposition): with self.assertDebugOnly(): diff --git a/test/python/test_user_config.py b/test/python/test_user_config.py index e3e01213463..5b63462963c 100644 --- a/test/python/test_user_config.py +++ b/test/python/test_user_config.py @@ -25,7 +25,7 @@ class TestUserConfig(QiskitTestCase): def setUp(self): super().setUp() - self.file_path = "test_%s.conf" % uuid4() + self.file_path = f"test_{uuid4()}.conf" def test_empty_file_read(self): config = user_config.UserConfig(self.file_path) @@ -94,6 +94,31 @@ def test_circuit_reverse_bits_valid(self): config.read_config_file() self.assertEqual({"circuit_reverse_bits": False}, config.settings) + def test_invalid_circuit_idle_wires(self): + test_config = """ + [default] + circuit_idle_wires = Neither + """ + self.addCleanup(os.remove, self.file_path) + with open(self.file_path, "w") as file: + file.write(test_config) + file.flush() + config = user_config.UserConfig(self.file_path) + self.assertRaises(exceptions.QiskitUserConfigError, config.read_config_file) + + def test_circuit_idle_wires_valid(self): + test_config = """ + [default] + circuit_idle_wires = true + """ + self.addCleanup(os.remove, self.file_path) + with open(self.file_path, "w") as file: + file.write(test_config) + file.flush() + config = user_config.UserConfig(self.file_path) + config.read_config_file() + self.assertEqual({"circuit_idle_wires": True}, config.settings) + def test_optimization_level_valid(self): test_config = """ [default] @@ -152,6 +177,7 @@ def test_all_options_valid(self): circuit_mpl_style = default circuit_mpl_style_path = ~:~/.qiskit circuit_reverse_bits = false + circuit_idle_wires = true transpile_optimization_level = 3 suppress_packaging_warnings = true parallel = false @@ -170,6 +196,7 @@ def test_all_options_valid(self): "circuit_mpl_style": "default", "circuit_mpl_style_path": ["~", "~/.qiskit"], "circuit_reverse_bits": False, + "circuit_idle_wires": True, "transpile_optimization_level": 3, "num_processes": 15, "parallel_enabled": False, @@ -184,6 +211,7 @@ def test_set_config_all_options_valid(self): user_config.set_config("circuit_mpl_style", "default", file_path=self.file_path) user_config.set_config("circuit_mpl_style_path", "~:~/.qiskit", file_path=self.file_path) user_config.set_config("circuit_reverse_bits", "false", file_path=self.file_path) + user_config.set_config("circuit_idle_wires", "true", file_path=self.file_path) user_config.set_config("transpile_optimization_level", "3", file_path=self.file_path) user_config.set_config("parallel", "false", file_path=self.file_path) user_config.set_config("num_processes", "15", file_path=self.file_path) @@ -198,6 +226,7 @@ def test_set_config_all_options_valid(self): "circuit_mpl_style": "default", "circuit_mpl_style_path": ["~", "~/.qiskit"], "circuit_reverse_bits": False, + "circuit_idle_wires": True, "transpile_optimization_level": 3, "num_processes": 15, "parallel_enabled": False, diff --git a/test/python/test_util.py b/test/python/test_util.py index f807f0d4e25..d403ed004bc 100644 --- a/test/python/test_util.py +++ b/test/python/test_util.py @@ -43,7 +43,7 @@ def test_local_hardware_no_cpu_count(self): self.assertEqual(1, result["cpus"]) def test_local_hardware_no_sched_five_count(self): - """Test cpu cound if sched affinity method is missing and cpu count is 5.""" + """Test cpu could if sched affinity method is missing and cpu count is 5.""" with mock.patch.object(multiprocessing, "os", spec=[]): multiprocessing.os.cpu_count = mock.MagicMock(return_value=5) del multiprocessing.os.sched_getaffinity @@ -51,7 +51,7 @@ def test_local_hardware_no_sched_five_count(self): self.assertEqual(2, result["cpus"]) def test_local_hardware_no_sched_sixty_four_count(self): - """Test cpu cound if sched affinity method is missing and cpu count is 64.""" + """Test cpu could if sched affinity method is missing and cpu count is 64.""" with mock.patch.object(multiprocessing, "os", spec=[]): multiprocessing.os.cpu_count = mock.MagicMock(return_value=64) del multiprocessing.os.sched_getaffinity diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index c1df8adbad2..c95d65422d4 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -406,7 +406,7 @@ def test_valid_pulse_duration(self): self.pulse_gate_validation_pass(circuit) def test_no_calibration(self): - """No error raises if no calibration is addedd.""" + """No error raises if no calibration is added.""" circuit = QuantumCircuit(1) circuit.x(0) diff --git a/test/python/transpiler/test_apply_layout.py b/test/python/transpiler/test_apply_layout.py index bd119c010f0..b92cc710095 100644 --- a/test/python/transpiler/test_apply_layout.py +++ b/test/python/transpiler/test_apply_layout.py @@ -15,6 +15,7 @@ import unittest from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes import ApplyLayout, SetLayout @@ -167,6 +168,31 @@ def test_final_layout_is_updated(self): ), ) + def test_works_with_var_nodes(self): + """Test that standalone var nodes work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + expected = QuantumCircuit(QuantumRegister(2, "q"), *qc.cregs, inputs=[a]) + expected.add_var(b, 12) + expected.h(1) + expected.cx(1, 0) + expected.measure([1, 0], [0, 1]) + expected.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + pass_ = ApplyLayout() + pass_.property_set["layout"] = Layout(dict(enumerate(reversed(qc.qubits)))) + after = pass_(qc) + + self.assertEqual(after, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 218cd8162d5..fc933cd8f66 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -19,8 +19,10 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit import transpile -from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit +from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit, Measure +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( + HGate, U1Gate, U2Gate, U3Gate, @@ -889,6 +891,50 @@ def test_unrolling_parameterized_composite_gates(self): self.assertEqual(circuit_to_dag(expected), out_dag) + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with no target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the basis set. + out = BasisTranslator(eq_lib, ["my_h", "my_cx"])(qc) + self.assertEqual(out, expected) + class TestBasisExamples(QiskitTestCase): """Test example circuits targeting example bases over the StandardEquivalenceLibrary.""" @@ -1104,15 +1150,16 @@ def setUp(self): self.target.add_instruction(CXGate(), cx_props) def test_2q_with_non_global_1q(self): - """Test translation works with a 2q gate on an non-global 1q basis.""" + """Test translation works with a 2q gate on a non-global 1q basis.""" qc = QuantumCircuit(2) qc.cz(0, 1) bt_pass = BasisTranslator(std_eqlib, target_basis=None, target=self.target) output = bt_pass(qc) - # We need a second run of BasisTranslator to correct gates outside of - # the target basis. This is a known isssue, see: - # https://docs.quantum.ibm.com/api/qiskit/release-notes/0.33#known-issues + # We need a second run of BasisTranslator to correct gates outside + # the target basis. This is a known issue, see: + # https://github.com/Qiskit/qiskit/issues/11339 + # TODO: remove the second bt_pass call once fixed. output = bt_pass(output) expected = QuantumCircuit(2) expected.rz(pi, 1) @@ -1127,3 +1174,52 @@ def test_2q_with_non_global_1q(self): expected.sx(1) expected.rz(3 * pi, 1) self.assertEqual(output, expected) + + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with a target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the target. + target = Target() + target.add_instruction(MyHGate(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(Measure(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(MyCXGate(), {(0, 1): None, (1, 0): None}) + + out = BasisTranslator(eq_lib, {"my_h", "my_cx"}, target)(qc) + self.assertEqual(out, expected) diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 206a652299c..ff8be63ffbc 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -119,7 +119,7 @@ def test_can_combine_cliffords(self): cliff2 = self.create_cliff2() cliff3 = self.create_cliff3() - # Create a circuit with two consective cliffords + # Create a circuit with two consecutive cliffords qc1 = QuantumCircuit(4) qc1.append(cliff1, [3, 1, 2]) qc1.append(cliff2, [3, 1, 2]) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 1030b83ae2a..71bab61708c 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -198,7 +198,7 @@ def test_control_bit_of_cnot(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot1(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[Z]------.-- qr0:---[Z]--- | | @@ -219,7 +219,7 @@ def test_control_bit_of_cnot1(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot2(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[T]------.-- qr0:---[T]--- | | @@ -240,7 +240,7 @@ def test_control_bit_of_cnot2(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot3(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[Rz]------.-- qr0:---[Rz]--- | | @@ -261,7 +261,7 @@ def test_control_bit_of_cnot3(self): self.assertEqual(expected, new_circuit) def test_control_bit_of_cnot4(self): - """A simple circuit where the two cnots shoule be cancelled. + """A simple circuit where the two cnots should be cancelled. qr0:----.------[T]------.-- qr0:---[T]--- | | @@ -662,7 +662,7 @@ def test_basis_global_phase_02(self): self.assertEqual(Operator(circ), Operator(ccirc)) def test_basis_global_phase_03(self): - """Test global phase preservation if cummulative z-rotation is 0""" + """Test global phase preservation if cumulative z-rotation is 0""" circ = QuantumCircuit(1) circ.rz(np.pi / 2, 0) circ.p(np.pi / 2, 0) diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py index 9b34d095b3b..8a11af2bd68 100644 --- a/test/python/transpiler/test_consolidate_blocks.py +++ b/test/python/transpiler/test_consolidate_blocks.py @@ -517,7 +517,7 @@ def test_inverted_order(self): # The first two 'if' blocks here represent exactly the same operation as each other on the # outer bits, because in the second, the bit-order of the block is reversed, but so is the # order of the bits in the outer circuit that they're bound to, which makes them the same. - # The second two 'if' blocks also represnt the same operation as each other, but the 'first + # The second two 'if' blocks also represent the same operation as each other, but the 'first # two' and 'second two' pairs represent qubit-flipped operations. qc.if_test((0, False), body.copy(), qc.qubits, qc.clbits) qc.if_test((0, False), body.reverse_bits(), reversed(qc.qubits), qc.clbits) diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 91ebede9fa8..7b364f3ac10 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -216,7 +216,7 @@ def test_decompose_only_given_label(self): def test_decompose_only_given_name(self): """Test decomposition parameters so that only given name is decomposed.""" - decom_circ = self.complex_circuit.decompose(["mcx"]) + decom_circ = self.complex_circuit.decompose(["mcx"], reps=2) dag = circuit_to_dag(decom_circ) self.assertEqual(len(dag.op_nodes()), 13) @@ -236,7 +236,7 @@ def test_decompose_only_given_name(self): def test_decompose_mixture_of_names_and_labels(self): """Test decomposition parameters so that mixture of names and labels is decomposed""" - decom_circ = self.complex_circuit.decompose(["mcx", "gate2"]) + decom_circ = self.complex_circuit.decompose(["mcx", "gate2"], reps=2) dag = circuit_to_dag(decom_circ) self.assertEqual(len(dag.op_nodes()), 15) diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py new file mode 100644 index 00000000000..c96b5ca32d8 --- /dev/null +++ b/test/python/transpiler/test_elide_permutations.py @@ -0,0 +1,459 @@ +# 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. + +"""Test ElidePermutations pass""" + +import unittest + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.library.generalized_gates import PermutationGate +from qiskit.transpiler.passes.optimization.elide_permutations import ElidePermutations +from qiskit.transpiler.passes.routing import StarPreRouting +from qiskit.circuit.controlflow import IfElseOp +from qiskit.quantum_info import Operator +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestElidePermutations(QiskitTestCase): + """Test elide permutations logical optimization pass.""" + + def setUp(self): + super().setUp() + self.swap_pass = ElidePermutations() + + def test_no_swap(self): + """Test no swap means no transform.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_swap_in_middle(self): + """Test swap in middle of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(0, 1) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(0, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_at_beginning(self): + """Test swap in beginning of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.swap(0, 1) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(1) + expected.cx(0, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_at_end(self): + """Test swap at the end of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + qc.swap(0, 1) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_multiple_swaps(self): + """Test quantum circuit with multiple swaps.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + expected = QuantumCircuit(3) + expected.h(0) + expected.cx(2, 1) + expected.h(2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_before_measure(self): + """Test swap before measure is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.swap(0, 1) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(0, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_if_else_block(self): + """Test swap elision only happens outside control flow.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + with qc.if_test((0, 0)): + qc.swap(0, 1) + qc.cx(0, 1) + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_swap_if_else_block_with_outside_swap(self): + """Test swap elision only happens outside control flow.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(2, 0) + body = QuantumCircuit(2) + body.swap(0, 1) + if_else_op = IfElseOp((qc.clbits[0], 0), body) + + qc.append(if_else_op, [0, 1]) + qc.cx(0, 1) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.append(IfElseOp((expected.clbits[0], 0), body), [2, 1]) + expected.cx(2, 1) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_condition(self): + """Test swap elision doesn't touch conditioned swap.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.swap(0, 1).c_if(qc.clbits[0], 0) + qc.cx(0, 1) + res = self.swap_pass(qc) + self.assertEqual(res, qc) + + def test_permutation_in_middle(self): + """Test permutation in middle of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 0) + expected.barrier(0, 1, 2) + expected.measure(2, 0) + expected.measure(1, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_at_beginning(self): + """Test permutation in beginning of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(2) + expected.cx(1, 0) + expected.barrier(0, 1, 2) + expected.measure(2, 0) + expected.measure(1, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_at_end(self): + """Test permutation at end of bell is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_swap_and_permutation(self): + """Test a combination of swap and permutation gates.""" + qc = QuantumCircuit(3, 3) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + qc.swap(0, 2) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(0, 0) + expected.measure(1, 1) + expected.measure(2, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + def test_permutation_before_measure(self): + """Test permutation before measure is elided.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(1, 2) + qc.barrier(0, 1, 2) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2]) + qc.measure(0, 0) + qc.measure(1, 1) + qc.measure(2, 2) + + expected = QuantumCircuit(3, 3) + expected.h(0) + expected.cx(1, 2) + expected.barrier(0, 1, 2) + expected.measure(1, 0) + expected.measure(2, 1) + expected.measure(0, 2) + + res = self.swap_pass(qc) + self.assertEqual(res, expected) + + +class TestElidePermutationsInTranspileFlow(QiskitTestCase): + """ + Test elide permutations in the full transpile pipeline, especially that + "layout" and "final_layout" attributes are updated correctly + as to preserve unitary equivalence. + """ + + def test_not_run_after_layout(self): + """Test ElidePermutations doesn't do anything after layout.""" + + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + spm = generate_preset_pass_manager( + optimization_level=1, initial_layout=list(range(2, -1, -1)), seed_transpiler=42 + ) + spm.layout += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + self.assertIn("swap", res.count_ops()) + self.assertTrue(res.layout.final_index_layout(), [0, 1, 2]) + + def test_unitary_equivalence(self): + """Test unitary equivalence of the original and transpiled circuits.""" + qc = QuantumCircuit(3) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.h(1) + + with self.subTest("no coupling map"): + spm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, seed_transpiler=42, coupling_map=CouplingMap.from_line(3) + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + def test_unitary_equivalence_routing_and_basis_translation(self): + """Test on a larger example that includes routing and basis translation.""" + + qc = QuantumCircuit(5) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.append(PermutationGate([0, 2, 1]), [0, 1, 2]) + qc.h(1) + + with self.subTest("no coupling map"): + spm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + coupling_map=CouplingMap.from_line(5), + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("no coupling map but with initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + initial_layout=[4, 2, 1, 3, 0], + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("coupling map and initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + initial_layout=[4, 2, 1, 3, 0], + basis_gates=["u", "cz"], + coupling_map=CouplingMap.from_line(5), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + with self.subTest("larger coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=42, + coupling_map=CouplingMap.from_line(8), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + + qc_with_ancillas = QuantumCircuit(8) + qc_with_ancillas.append(qc, [0, 1, 2, 3, 4]) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc_with_ancillas))) + + with self.subTest("larger coupling map and initial layout"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=42, + initial_layout=[4, 2, 7, 3, 6], + coupling_map=CouplingMap.from_line(8), + ) + spm.init += ElidePermutations() + res = spm.run(qc) + + qc_with_ancillas = QuantumCircuit(8) + qc_with_ancillas.append(qc, [0, 1, 2, 3, 4]) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc_with_ancillas))) + + def test_unitary_equivalence_virtual_permutation_layout_composition(self): + """Test on a larger example that includes routing and basis translation.""" + + qc = QuantumCircuit(5) + qc.h(0) + qc.swap(0, 2) + qc.cx(0, 1) + qc.swap(1, 0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.append(PermutationGate([0, 2, 1]), [0, 1, 2]) + qc.h(1) + + with self.subTest("with coupling map"): + spm = generate_preset_pass_manager( + optimization_level=3, + seed_transpiler=1234, + coupling_map=CouplingMap.from_line(5), + basis_gates=["u", "cz"], + ) + spm.init += ElidePermutations() + spm.init += StarPreRouting() + res = spm.run(qc) + self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc))) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/transpiler/test_full_ancilla_allocation.py b/test/python/transpiler/test_full_ancilla_allocation.py index 73d9708d0ba..452d9d93965 100644 --- a/test/python/transpiler/test_full_ancilla_allocation.py +++ b/test/python/transpiler/test_full_ancilla_allocation.py @@ -194,7 +194,7 @@ def test_name_collision(self): ) def test_bad_layout(self): - """Layout referes to a register that do not exist in the circuit""" + """Layout refers to a register that do not exist in the circuit""" qr = QuantumRegister(3, "q") circ = QuantumCircuit(qr) dag = circuit_to_dag(circ) diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py index 1e0f19b1a33..569a210f8a9 100644 --- a/test/python/transpiler/test_gate_direction.py +++ b/test/python/transpiler/test_gate_direction.py @@ -342,7 +342,7 @@ def test_symmetric_gates(self, gate): self.assertEqual(pass_(circuit), expected) def test_target_parameter_any(self): - """Test that a parametrised 2q gate is replaced correctly both if available and not + """Test that a parametrized 2q gate is replaced correctly both if available and not available.""" circuit = QuantumCircuit(2) circuit.rzx(1.5, 0, 1) @@ -356,7 +356,7 @@ def test_target_parameter_any(self): self.assertNotEqual(GateDirection(None, target=swapped)(circuit), circuit) def test_target_parameter_exact(self): - """Test that a parametrised 2q gate is detected correctly both if available and not + """Test that a parametrized 2q gate is detected correctly both if available and not available.""" circuit = QuantumCircuit(2) circuit.rzx(1.5, 0, 1) diff --git a/test/python/transpiler/test_gates_in_basis_pass.py b/test/python/transpiler/test_gates_in_basis_pass.py index 2138070ed9d..06ce5e0f670 100644 --- a/test/python/transpiler/test_gates_in_basis_pass.py +++ b/test/python/transpiler/test_gates_in_basis_pass.py @@ -13,6 +13,7 @@ """Test GatesInBasis pass.""" from qiskit.circuit import QuantumCircuit, ForLoopOp, IfElseOp, SwitchCaseOp, Clbit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import HGate, CXGate, UGate, XGate, ZGate from qiskit.circuit.measure import Measure from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary @@ -269,3 +270,44 @@ def test_basis_gates_target(self): pass_ = GatesInBasis(target=complete) pass_(circuit) self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_basis_gates(self): + """Test that `Store` is treated as an automatic built-in when given basis gates.""" + pass_ = GatesInBasis(basis_gates=["h", "cx"]) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_target(self): + """Test that `Store` is treated as an automatic built-in when given a target.""" + target = Target() + target.add_instruction(HGate(), {(0,): None, (1,): None}) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + pass_ = GatesInBasis(target=target) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 5ab78af8f58..a76ab08d90e 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -28,6 +28,7 @@ Operation, EquivalenceLibrary, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( SwapGate, CXGate, @@ -36,6 +37,7 @@ U3Gate, U2Gate, U1Gate, + UGate, CU3Gate, CU1Gate, ) @@ -65,6 +67,7 @@ ) from test import QiskitTestCase # pylint: disable=wrong-import-order + # In what follows, we create two simple operations OpA and OpB, that potentially mimic # higher-level objects written by a user. # For OpA we define two synthesis methods: @@ -123,7 +126,7 @@ class OpARepeatSynthesisPlugin(HighLevelSynthesisPlugin): """The repeat synthesis for opA""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): - if "n" not in options.keys(): + if "n" not in options: return None qc = QuantumCircuit(1) @@ -203,7 +206,7 @@ def __init__(self): def method_names(self, op_name): """Returns plugin methods for op_name.""" - if op_name in self.plugins_by_op.keys(): + if op_name in self.plugins_by_op: return self.plugins_by_op[op_name] else: return [] @@ -531,22 +534,22 @@ def test_section_size(self): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 1})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 22) - self.assertEqual(qct.depth(), 20) + self.assertEqual(qct.size(), 30) + self.assertEqual(qct.depth(), 27) with self.subTest("section_size_2"): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 2})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 23) - self.assertEqual(qct.depth(), 19) + self.assertEqual(qct.size(), 27) + self.assertEqual(qct.depth(), 23) with self.subTest("section_size_3"): hls_config = HLSConfig(linear_function=[("pmh", {"section_size": 3})]) qct = HighLevelSynthesis(hls_config=hls_config)(qc) self.assertEqual(LinearFunction(qct), LinearFunction(qc)) - self.assertEqual(qct.size(), 23) - self.assertEqual(qct.depth(), 17) + self.assertEqual(qct.size(), 29) + self.assertEqual(qct.depth(), 23) def test_invert_and_transpose(self): """Test that the plugin takes the use_inverted and use_transposed arguments into account.""" @@ -586,6 +589,78 @@ def test_invert_and_transpose(self): self.assertEqual(qct.size(), 6) self.assertEqual(qct.depth(), 6) + def test_plugin_selection_all(self): + """Test setting plugin_selection to all.""" + + linear_function = LinearFunction(self.construct_linear_circuit(7)) + qc = QuantumCircuit(7) + qc.append(linear_function, [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("sequential"): + # In the default "run sequential" mode, we stop as soon as a plugin + # in the list returns a circuit. + # For this specific example the default options lead to a suboptimal circuit. + hls_config = HLSConfig(linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})]) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 12) + self.assertEqual(qct.depth(), 8) + + with self.subTest("all"): + # In the non-default "run all" mode, we examine all plugins in the list. + # For this specific example we get the better result for the second plugin in the list. + hls_config = HLSConfig( + linear_function=[("pmh", {}), ("pmh", {"use_inverted": True})], + plugin_selection="all", + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 6) + self.assertEqual(qct.depth(), 6) + + def test_plugin_selection_all_with_metrix(self): + """Test setting plugin_selection to all and specifying different evaluation functions.""" + + # The seed is chosen so that we get different best circuits depending on whether we + # want to minimize size or depth. + mat = random_invertible_binary_matrix(7, seed=38) + qc = QuantumCircuit(7) + qc.append(LinearFunction(mat), [0, 1, 2, 3, 4, 5, 6]) + + with self.subTest("size_fn"): + # We want to minimize the "size" (aka the number of gates) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.size(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 23) + self.assertEqual(qct.depth(), 19) + + with self.subTest("depth_fn"): + # We want to minimize the "depth" (aka the number of layers) in the circuit + hls_config = HLSConfig( + linear_function=[ + ("pmh", {}), + ("pmh", {"use_inverted": True}), + ("pmh", {"use_transposed": True}), + ("pmh", {"use_inverted": True, "use_transposed": True}), + ], + plugin_selection="all", + plugin_evaluation_fn=lambda qc: qc.depth(), + ) + qct = HighLevelSynthesis(hls_config=hls_config)(qc) + self.assertEqual(LinearFunction(qct), LinearFunction(qc)) + self.assertEqual(qct.size(), 24) + self.assertEqual(qct.depth(), 13) + class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the KMSSynthesisLinearFunction plugin for synthesizing linear functions.""" @@ -1969,6 +2044,59 @@ def test_unroll_empty_definition_with_phase(self): expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + def test_leave_store_alone_basis(self): + """Don't attempt to synthesize `Store` instructions with basis gates.""" + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, basis_gates=["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to synthesize `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index bd14891bb8c..1431449779b 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -98,7 +98,7 @@ def test_valid_pulse_duration(self): pm.run(circuit) def test_no_calibration(self): - """No error raises if no calibration is addedd.""" + """No error raises if no calibration is added.""" circuit = QuantumCircuit(1) circuit.x(0) diff --git a/test/python/transpiler/test_optimize_1q_gates.py b/test/python/transpiler/test_optimize_1q_gates.py index 9253130bedb..e5483dd4749 100644 --- a/test/python/transpiler/test_optimize_1q_gates.py +++ b/test/python/transpiler/test_optimize_1q_gates.py @@ -19,7 +19,7 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator from qiskit.converters import circuit_to_dag -from qiskit.circuit import Parameter +from qiskit.circuit import Parameter, Gate from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, UGate, PhaseGate from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target @@ -323,9 +323,24 @@ def test_parameterized_expressions_in_circuits(self): def test_global_phase_u3_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate definition.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 @@ -337,9 +352,24 @@ def test_global_phase_u3_on_left(self): def test_global_phase_u_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 diff --git a/test/python/transpiler/test_optimize_annotated.py b/test/python/transpiler/test_optimize_annotated.py index 6a506516d19..0b15b79e2cf 100644 --- a/test/python/transpiler/test_optimize_annotated.py +++ b/test/python/transpiler/test_optimize_annotated.py @@ -13,7 +13,8 @@ """Test OptimizeAnnotated pass""" from qiskit.circuit import QuantumCircuit, Gate -from qiskit.circuit.library import SwapGate, CXGate +from qiskit.circuit.classical import expr, types +from qiskit.circuit.library import SwapGate, CXGate, HGate from qiskit.circuit.annotated_operation import ( AnnotatedOperation, ControlModifier, @@ -21,6 +22,7 @@ PowerModifier, ) from qiskit.transpiler.passes import OptimizeAnnotated +from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -101,7 +103,9 @@ def test_optimize_definitions(self): qc.h(0) qc.append(gate, [0, 1, 3]) - qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + # Add "swap" to the basis gates to prevent conjugate reduction from replacing + # control-[SWAP] by CX(0,1) -- CCX(1, 0) -- CX(0, 1) + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u", "swap"])(qc) self.assertEqual(qc_optimized[1].operation.definition, expected_qc_def_optimized) def test_do_not_optimize_definitions_without_basis_gates(self): @@ -193,3 +197,245 @@ def test_if_else(self): ) self.assertEqual(qc_optimized, expected_qc) + + def test_conjugate_reduction(self): + """Test conjugate reduction optimization.""" + + # Create a control-annotated operation. + # The definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.z(0) # P + qc_def.s(0) # P + qc_def.cx(0, 4) # P + qc_def.cx(4, 3) # P + qc_def.y(3) # Q + qc_def.cx(3, 0) # Q + qc_def.cx(4, 3) # R + qc_def.cx(0, 4) # R + qc_def.sdg(0) # R + qc_def.z(0) # R + qc_def.cx(0, 1) # R + qc_def.z(5) # P + qc_def.z(5) # R + qc_def.x(2) # Q + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation + qc = QuantumCircuit(8) + qc.cx(0, 2) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + qc.h(0) + qc.z(4) + + qc_keys = qc.count_ops().keys() + self.assertIn("annotated", qc_keys) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # The pass should simplify the gate + qc_optimized_keys = qc_optimized.count_ops().keys() + self.assertIn("optimized", qc_optimized_keys) + self.assertNotIn("annotated", qc_optimized_keys) + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + def test_conjugate_reduction_collection(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (using annotated gate from the previous example). + """ + + # Create a control-annotated operation. + # The definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.z(0) # P + qc_def.s(0) # P + qc_def.cx(0, 4) # P + qc_def.cx(4, 3) # P + qc_def.y(3) # Q + qc_def.cx(3, 0) # Q + qc_def.cx(4, 3) # R + qc_def.cx(0, 4) # R + qc_def.sdg(0) # R + qc_def.z(0) # R + qc_def.cx(0, 1) # R + qc_def.z(5) # P + qc_def.z(5) # R + qc_def.x(2) # Q + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "s": 1, "sdg": 1, "z": 4, "cx": 6}) + + def test_conjugate_reduction_consecutive_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (multiple consecutive gates on the same pair of qubits). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(0, 1) # P + qc_def.cz(1, 2) # Q + qc_def.swap(0, 1) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "swap": 2}) + + def test_conjugate_reduction_chain_of_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (chain of gates). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.cx(1, 2) # P + qc_def.cx(2, 3) # P + qc_def.h(3) # Q + qc_def.cx(2, 3) # R + qc_def.cx(1, 2) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 6}) + + def test_conjugate_reduction_empty_middle(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (with no gates in the middle circuit). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(0, 1) # P + qc_def.cz(1, 2) # P + qc_def.cz(1, 2) # R + qc_def.swap(0, 1) # R + qc_def.cx(0, 1) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "cz": 2, "swap": 2}) + + def test_conjugate_reduction_parallel_gates(self): + """Test conjugate reduction optimization including an assertion on which gates + are collected (multiple gates in front and back layers). + """ + + # Create a control-annotated operation. + # the definition of the base operation has conjugate decomposition P -- Q -- R with R = P^{-1} + qc_def = QuantumCircuit(6) + qc_def.cx(0, 1) # P + qc_def.swap(2, 3) # P + qc_def.cz(4, 5) # P + qc_def.h(0) # Q + qc_def.h(1) # Q + qc_def.cx(0, 1) # R + qc_def.swap(2, 3) # R + qc_def.cz(4, 5) # R + custom = qc_def.to_gate().control(annotated=True) + + # Create a quantum circuit with an annotated operation. + qc = QuantumCircuit(8) + qc.append(custom, [0, 1, 3, 4, 5, 7, 6]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Check that the optimization finds correct pairs of inverse gates + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2, "cz": 2, "swap": 2}) + + def test_conjugate_reduction_cswap(self): + """Test conjugate reduction optimization for control-SWAP.""" + + # Create a circuit with a control-annotated swap + qc = QuantumCircuit(3) + qc.append(SwapGate().control(annotated=True), [0, 1, 2]) + + # Run optimization pass + qc_optimized = OptimizeAnnotated(basis_gates=["cx", "u"])(qc) + + # Check that the optimization is correct + self.assertEqual(Operator(qc), Operator(qc_optimized)) + + # Swap(0, 1) gets translated to CX(0, 1), CX(1, 0), CX(0, 1). + # The first and the last of the CXs should be detected as inverse of each other. + new_def_ops = dict(qc_optimized[0].operation.definition.count_ops()) + self.assertEqual(new_def_ops, {"annotated": 1, "cx": 2}) + + def test_standalone_var(self): + """Test that standalone vars work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(3, 3, inputs=[a]) + qc.add_var(b, 12) + qc.append(AnnotatedOperation(HGate(), [ControlModifier(1), ControlModifier(1)]), [0, 1, 2]) + qc.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc.measure([0, 1, 2], [0, 1, 2]) + qc.store(a, expr.logic_and(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(HGate().control(2, annotated=True), [0, 1, 2]) + expected.cx(0, 1) + expected.measure([0, 1, 2], [0, 1, 2]) + expected.store(a, expr.logic_and(expected.clbits[0], expected.clbits[1])) + + self.assertEqual(OptimizeAnnotated()(qc), expected) diff --git a/test/python/transpiler/test_pass_scheduler.py b/test/python/transpiler/test_pass_scheduler.py index 6d6026d6148..12ba81c78a6 100644 --- a/test/python/transpiler/test_pass_scheduler.py +++ b/test/python/transpiler/test_pass_scheduler.py @@ -703,7 +703,7 @@ def assertPassLog(self, passmanager, list_of_passes): output_lines = self.output.readlines() pass_log_lines = [x for x in output_lines if x.startswith("Pass:")] for index, pass_name in enumerate(list_of_passes): - self.assertTrue(pass_log_lines[index].startswith("Pass: %s -" % pass_name)) + self.assertTrue(pass_log_lines[index].startswith(f"Pass: {pass_name} -")) def test_passes(self): """Dump passes in different FlowControllerLinear""" diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index fe209e3571a..01ec7ebf133 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -93,39 +93,77 @@ def test_str(self): pm_config.inst_map = None str_out = str(pm_config) expected = """Pass Manager Config: - initial_layout: None - basis_gates: ['h', 'u', 'p', 'u1', 'u2', 'u3', 'rz', 'sx', 'x', 'cx', 'id', 'unitary', 'measure', 'delay', 'reset'] - inst_map: None - coupling_map: None - layout_method: None - routing_method: None - translation_method: None - scheduling_method: None - instruction_durations: - backend_properties: None - approximation_degree: None - seed_transpiler: None - timing_constraints: None - unitary_synthesis_method: default - unitary_synthesis_plugin_config: None - target: Target: Basic Target - Number of qubits: None - Instructions: - h - u - p - u1 - u2 - u3 - rz - sx - x - cx - id - unitary - measure - delay - reset - +\tinitial_layout: None +\tbasis_gates: ['ccx', 'ccz', 'ch', 'cp', 'crx', 'cry', 'crz', 'cs', 'csdg', 'cswap', 'csx', 'cu', 'cu1', 'cu3', 'cx', 'cy', 'cz', 'dcx', 'delay', 'ecr', 'global_phase', 'h', 'id', 'iswap', 'measure', 'p', 'r', 'rccx', 'reset', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzx', 'rzz', 's', 'sdg', 'swap', 'sx', 'sxdg', 't', 'tdg', 'u', 'u1', 'u2', 'u3', 'unitary', 'x', 'xx_minus_yy', 'xx_plus_yy', 'y', 'z'] +\tinst_map: None +\tcoupling_map: None +\tlayout_method: None +\trouting_method: None +\ttranslation_method: None +\tscheduling_method: None +\tinstruction_durations:\u0020 +\tbackend_properties: None +\tapproximation_degree: None +\tseed_transpiler: None +\ttiming_constraints: None +\tunitary_synthesis_method: default +\tunitary_synthesis_plugin_config: None +\ttarget: Target: Basic Target +\tNumber of qubits: None +\tInstructions: +\t\tccx +\t\tccz +\t\tch +\t\tcp +\t\tcrx +\t\tcry +\t\tcrz +\t\tcs +\t\tcsdg +\t\tcswap +\t\tcsx +\t\tcu +\t\tcu1 +\t\tcu3 +\t\tcx +\t\tcy +\t\tcz +\t\tdcx +\t\tdelay +\t\tecr +\t\tglobal_phase +\t\th +\t\tid +\t\tiswap +\t\tmeasure +\t\tp +\t\tr +\t\trccx +\t\treset +\t\trx +\t\trxx +\t\try +\t\tryy +\t\trz +\t\trzx +\t\trzz +\t\ts +\t\tsdg +\t\tswap +\t\tsx +\t\tsxdg +\t\tt +\t\ttdg +\t\tu +\t\tu1 +\t\tu2 +\t\tu3 +\t\tunitary +\t\tx +\t\txx_minus_yy +\t\txx_plus_yy +\t\ty +\t\tz +\t """ self.assertEqual(str_out, expected) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 247aa82ec03..ee85dc34ffd 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -71,7 +71,7 @@ def mock_get_passmanager_stage( elif stage_name == "layout": return PassManager([]) else: - raise Exception("Failure, unexpected stage plugin combo for test") + raise RuntimeError("Failure, unexpected stage plugin combo for test") def emptycircuit(): @@ -1110,7 +1110,7 @@ def test_1(self, circuit, level): self.assertIn("swap", resulting_basis) # Skipping optimization level 3 because the swap gates get absorbed into - # a unitary block as part of the KAK decompostion optimization passes and + # a unitary block as part of the KAK decomposition optimization passes and # optimized away. @combine( level=[0, 1, 2], @@ -1487,7 +1487,7 @@ def _define(self): optimization_level=optimization_level, seed_transpiler=2022_10_04, ) - # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # Tests of the complete validity of a circuit are mostly done at the individual pass level; # here we're just checking that various passes do appear to have run. self.assertIsInstance(transpiled, QuantumCircuit) # Assert layout ran. diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7640149e039..0a7b977162a 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -17,9 +17,10 @@ import math from qiskit import QuantumRegister, QuantumCircuit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import EfficientSU2 from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager -from qiskit.transpiler.passes import SabreLayout, DenseLayout +from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.compiler.transpiler import transpile @@ -257,6 +258,46 @@ def test_layout_many_search_trials(self): [layout[q] for q in qc.qubits], [22, 7, 2, 12, 1, 5, 14, 4, 11, 0, 16, 15, 3, 10] ) + def test_support_var_with_rust_fastpath(self): + """Test that the joint layout/embed/routing logic for the Rust-space fast-path works in the + presence of standalone `Var` nodes.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 0) + + out = SabreLayout(CouplingMap.from_line(8), seed=0, swap_trials=2, layout_trials=2)(qc) + + self.assertIsInstance(out, QuantumCircuit) + self.assertEqual(out.layout.initial_index_layout(), [4, 5, 6, 3, 2, 0, 1, 7]) + + def test_support_var_with_explicit_routing_pass(self): + """Test that the logic works if an explicit routing pass is given.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + 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) + layout = pass_.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5]) + class DensePartialSabreTrial(AnalysisPass): """Pass to run dense layout as a sabre trial.""" @@ -317,7 +358,7 @@ def test_dual_ghz_with_wide_barrier(self): self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) def test_dual_ghz_with_intermediate_barriers(self): - """Test dual ghz circuit with intermediate barriers local to each componennt.""" + """Test dual ghz circuit with intermediate barriers local to each component.""" qc = QuantumCircuit(8, name="double dhz") qc.h(0) qc.cz(0, 1) diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index a9fec85be66..b1effdae7d8 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -241,7 +241,7 @@ def test_do_not_reorder_measurements(self): self.assertIsInstance(second_measure.operation, Measure) # Assert that the first measure is on the same qubit that the HGate was applied to, and the # second measurement is on a different qubit (though we don't care which exactly - that - # depends a little on the randomisation of the pass). + # depends a little on the randomization of the pass). self.assertEqual(last_h.qubits, first_measure.qubits) self.assertNotEqual(last_h.qubits, second_measure.qubits) @@ -1329,9 +1329,9 @@ def setUpClass(cls): super().setUpClass() cls.backend = Fake27QPulseV1() cls.backend.configuration().coupling_map = MUMBAI_CMAP + cls.backend.configuration().basis_gates += ["for_loop", "while_loop", "if_else"] cls.coupling_edge_set = {tuple(x) for x in cls.backend.configuration().coupling_map} cls.basis_gates = set(cls.backend.configuration().basis_gates) - cls.basis_gates.update(["for_loop", "while_loop", "if_else"]) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" @@ -1346,7 +1346,7 @@ def _visit_block(circuit, qubit_mapping=None): qargs = tuple(qubit_mapping[x] for x in instruction.qubits) if not isinstance(instruction.operation, ControlFlowOp): if len(qargs) > 2 or len(qargs) < 0: - raise Exception("Invalid number of qargs for instruction") + raise RuntimeError("Invalid number of qargs for instruction") if len(qargs) == 2: self.assertIn(qargs, self.coupling_edge_set) else: diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index e15a080f6f0..62b811c8e3b 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -12,8 +12,10 @@ """Test the Solovay Kitaev transpilation pass.""" +import os import unittest import math +import tempfile import numpy as np import scipy @@ -230,6 +232,35 @@ def test_u_gates_work(self): included_gates = set(discretized.count_ops().keys()) self.assertEqual(set(basis_gates), included_gates) + def test_load_from_file(self): + """Test loading basic approximations from a file works. + + Regression test of Qiskit/qiskit#12576. + """ + filename = "approximations.npy" + + with tempfile.TemporaryDirectory() as tmp_dir: + fullpath = os.path.join(tmp_dir, filename) + + # dump approximations to file + generate_basic_approximations(basis_gates=["h", "s", "sdg"], depth=3, filename=fullpath) + + # circuit to decompose and reference decomp + circuit = QuantumCircuit(1) + circuit.rx(0.8, 0) + + reference = QuantumCircuit(1, global_phase=3 * np.pi / 4) + reference.h(0) + reference.s(0) + reference.h(0) + + # load the decomp and compare to reference + skd = SolovayKitaev(basic_approximations=fullpath) + # skd = SolovayKitaev(basic_approximations=filename) + discretized = skd(circuit) + + self.assertEqual(discretized, reference) + @ddt class TestGateSequence(QiskitTestCase): diff --git a/test/python/transpiler/test_star_prerouting.py b/test/python/transpiler/test_star_prerouting.py new file mode 100644 index 00000000000..ddc8096eefd --- /dev/null +++ b/test/python/transpiler/test_star_prerouting.py @@ -0,0 +1,484 @@ +# 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-function-docstring + +"""Test the StarPreRouting pass""" + +import unittest +from test import QiskitTestCase +import ddt + +from qiskit.circuit.library import QFT +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.converters import ( + circuit_to_dag, + dag_to_circuit, +) +from qiskit.quantum_info import Operator +from qiskit.transpiler.passes import VF2Layout, ApplyLayout, SabreSwap, SabreLayout +from qiskit.transpiler.passes.routing.star_prerouting import StarPreRouting +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.utils.optionals import HAS_AER + + +@ddt.ddt +class TestStarPreRouting(QiskitTestCase): + """Tests the StarPreRouting pass""" + + def test_simple_ghz_dagcircuit(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, range(1, 5)) + dag = circuit_to_dag(qc) + new_dag = StarPreRouting().run(dag) + new_qc = dag_to_circuit(new_dag) + + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.cx(0, 2) + expected.swap(0, 2) + expected.cx(2, 3) + expected.swap(2, 3) + expected.cx(3, 4) + # expected.swap(3,4) + + self.assertTrue(Operator(expected).equiv(Operator(new_qc))) + + def test_simple_ghz_dagdependency(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, range(1, 5)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + def test_double_ghz_dagcircuit(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + new_qc = pm.run(qc) + + self.assertTrue(Operator.from_circuit(new_qc).equiv(Operator(qc))) + + def test_double_ghz_dagdependency(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + new_qc = pm.run(qc) + + self.assertTrue(Operator(qc).equiv(Operator.from_circuit(new_qc))) + + def test_mixed_double_ghz_dagdependency(self): + """Shows off the power of using commutation analysis.""" + qc = QuantumCircuit(4) + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(3, 1) + qc.cx(3, 2) + # qc.measure_all() + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + def test_double_ghz(self): + qc = QuantumCircuit(10) + qc.h(0) + qc.cx(0, range(1, 5)) + qc.h(9) + qc.cx(9, range(8, 4, -1)) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + result = pm.run(qc) + + self.assertEqual(Operator.from_circuit(result), Operator(qc)) + + def test_linear_ghz_no_change(self): + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 5) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + + self.assertEqual(Operator.from_circuit(result), Operator(qc)) + + def test_no_star(self): + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(3, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(1, 4) + qc.cx(2, 1) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + result = pm.run(qc) + + self.assertTrue(Operator.from_circuit(result).equiv(qc)) + + def test_10q_bv(self): + num_qubits = 10 + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.barrier() + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + result = StarPreRouting()(qc) + + expected = QuantumCircuit(num_qubits, num_qubits - 1) + expected.h(0) + expected.h(1) + expected.h(2) + expected.h(3) + expected.h(4) + expected.h(5) + expected.h(6) + expected.h(7) + expected.h(8) + expected.x(9) + expected.h(9) + expected.cx(0, 9) + expected.cx(1, 9) + expected.swap(1, 9) + expected.cx(2, 1) + expected.swap(2, 1) + expected.cx(3, 2) + expected.swap(3, 2) + expected.cx(4, 3) + expected.swap(4, 3) + expected.cx(5, 4) + expected.swap(5, 4) + expected.cx(6, 5) + expected.swap(6, 5) + expected.cx(7, 6) + expected.swap(7, 6) + expected.cx(8, 7) + expected.barrier() + expected.h(0) + expected.h(1) + expected.h(2) + expected.h(3) + expected.h(4) + expected.h(5) + expected.h(6) + expected.h(8) + expected.h(9) + expected.measure(0, 0) + expected.measure(9, 1) + expected.measure(1, 2) + expected.measure(2, 3) + expected.measure(3, 4) + expected.measure(4, 5) + expected.measure(5, 6) + expected.measure(6, 7) + expected.measure(8, 8) + self.assertEqual(result, expected) + + # Skip level 3 because of unitary synth introducing non-clifford gates + @unittest.skipUnless(HAS_AER, "Aer required for clifford simulation") + @ddt.data(0, 1) + def test_100q_grid_full_path(self, opt_level): + from qiskit_aer import AerSimulator + + num_qubits = 100 + coupling_map = CouplingMap.from_grid(10, 10) + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.barrier() + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + pm = generate_preset_pass_manager( + opt_level, basis_gates=["h", "cx", "x"], coupling_map=coupling_map + ) + pm.init += StarPreRouting() + result = pm.run(qc) + counts_before = AerSimulator().run(qc).result().get_counts() + counts_after = AerSimulator().run(result).result().get_counts() + self.assertEqual(counts_before, counts_after) + + def test_10q_bv_no_barrier(self): + num_qubits = 6 + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.h(qc.qubits[:-1]) + + pm = generate_preset_pass_manager(optimization_level=3, seed_transpiler=42) + pm.init += StarPreRouting() + + result = pm.run(qc) + self.assertTrue(Operator.from_circuit(result).equiv(Operator(qc))) + + # Skip level 3 because of unitary synth introducing non-clifford gates + @unittest.skipUnless(HAS_AER, "Aer required for clifford simulation") + @ddt.data(0, 1) + def test_100q_grid_full_path_no_barrier(self, opt_level): + from qiskit_aer import AerSimulator + + num_qubits = 100 + coupling_map = CouplingMap.from_grid(10, 10) + qc = QuantumCircuit(num_qubits, num_qubits - 1) + qc.x(num_qubits - 1) + qc.h(qc.qubits) + for i in range(num_qubits - 1): + qc.cx(i, num_qubits - 1) + qc.h(qc.qubits[:-1]) + for i in range(num_qubits - 1): + qc.measure(i, i) + pm = generate_preset_pass_manager( + opt_level, basis_gates=["h", "cx", "x"], coupling_map=coupling_map + ) + pm.init += StarPreRouting() + result = pm.run(qc) + counts_before = AerSimulator().run(qc).result().get_counts() + counts_after = AerSimulator().run(result).result().get_counts() + self.assertEqual(counts_before, counts_after) + + def test_hadamard_ordering(self): + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.h(0) + qc.cx(0, 2) + qc.h(0) + qc.cx(0, 3) + qc.h(0) + qc.cx(0, 4) + result = StarPreRouting()(qc) + expected = QuantumCircuit(5) + expected.h(0) + expected.cx(0, 1) + expected.h(0) + expected.cx(0, 2) + expected.swap(0, 2) + expected.h(2) + expected.cx(2, 3) + expected.swap(2, 3) + expected.h(3) + expected.cx(3, 4) + # expected.swap(3, 4) + self.assertEqual(expected, result) + + def test_count_1_stars_starting_center(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + spr = StarPreRouting() + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 1) + self.assertEqual(len(star_blocks[0].nodes), 5) + + def test_count_1_stars_starting_branch(self): + qc = QuantumCircuit(6) + qc.cx(1, 0) + qc.cx(2, 0) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + spr = StarPreRouting() + _ = spr(qc) + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 1) + self.assertEqual(len(star_blocks[0].nodes), 5) + + def test_count_2_stars(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + + qc.cx(1, 2) + qc.cx(1, 3) + qc.cx(1, 4) + qc.cx(1, 5) + spr = StarPreRouting() + _ = spr(qc) + + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 2) + self.assertEqual(len(star_blocks[0].nodes), 5) + self.assertEqual(len(star_blocks[1].nodes), 4) + + def test_count_3_stars(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + + qc.cx(1, 2) + qc.cx(1, 3) + qc.cx(1, 4) + qc.cx(1, 5) + + qc.cx(2, 3) + qc.cx(2, 4) + qc.cx(2, 5) + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + + self.assertEqual(len(star_blocks), 3) + self.assertEqual(len(star_blocks[0].nodes), 5) + self.assertEqual(len(star_blocks[1].nodes), 4) + self.assertEqual(len(star_blocks[2].nodes), 3) + + def test_count_70_qft_stars(self): + qft_module = QFT(10, do_swaps=False).decompose() + qftqc = QuantumCircuit(100) + for i in range(10): + qftqc.compose(qft_module, qubits=range(i * 10, (i + 1) * 10), inplace=True) + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing( + circuit_to_dag(qftqc), min_block_size=2 + ) + + self.assertEqual(len(star_blocks), 80) + star_len_list = [len([n for n in b.nodes if len(n.qargs) > 1]) for b in star_blocks] + expected_star_size = {2, 3, 4, 5, 6, 7, 8, 9} + self.assertEqual(set(star_len_list), expected_star_size) + for i in expected_star_size: + self.assertEqual(star_len_list.count(i), 10) + + def test_count_50_qft_stars(self): + qft_module = QFT(10, do_swaps=False).decompose() + qftqc = QuantumCircuit(10) + for _ in range(10): + qftqc.compose(qft_module, qubits=range(10), inplace=True) + spr = StarPreRouting() + _ = spr(qftqc) + + star_blocks, _ = spr.determine_star_blocks_processing( + circuit_to_dag(qftqc), min_block_size=2 + ) + self.assertEqual(len(star_blocks), 50) + star_len_list = [len([n for n in b.nodes if len(n.qargs) > 1]) for b in star_blocks] + expected_star_size = {9} + self.assertEqual(set(star_len_list), expected_star_size) + + def test_two_star_routing(self): + qc = QuantumCircuit(4) + qc.cx(0, 1) + qc.cx(0, 2) + + qc.cx(2, 3) + qc.cx(2, 1) + + spr = StarPreRouting() + res = spr(qc) + + self.assertTrue(Operator.from_circuit(res).equiv(qc)) + + def test_detect_two_opposite_stars_barrier(self): + qc = QuantumCircuit(6) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.barrier() + qc.cx(5, 1) + qc.cx(5, 2) + qc.cx(5, 3) + qc.cx(5, 4) + + spr = StarPreRouting() + star_blocks, _ = spr.determine_star_blocks_processing(circuit_to_dag(qc), min_block_size=2) + self.assertEqual(len(star_blocks), 2) + self.assertEqual(len(star_blocks[0].nodes), 4) + self.assertEqual(len(star_blocks[1].nodes), 4) + + def test_routing_after_star_prerouting(self): + nq = 6 + qc = QFT(nq, do_swaps=False, insert_barriers=True).decompose() + cm = CouplingMap.from_line(nq) + + pm_preroute = PassManager() + pm_preroute.append(StarPreRouting()) + pm_preroute.append(VF2Layout(coupling_map=cm, seed=17)) + pm_preroute.append(ApplyLayout()) + pm_preroute.append(SabreSwap(coupling_map=cm, seed=17)) + + pm_sabre = PassManager() + pm_sabre.append(SabreLayout(coupling_map=cm, seed=17)) + + res_sabre = pm_sabre.run(qc) + res_star = pm_sabre.run(qc) + + self.assertTrue(Operator.from_circuit(res_sabre), qc) + self.assertTrue(Operator.from_circuit(res_star), qc) + self.assertTrue(Operator.from_circuit(res_star), Operator.from_circuit(res_sabre)) diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index fb27076d03d..8c96150ae8f 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -27,7 +27,7 @@ from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 from qiskit.compiler.transpiler import transpile from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order from test.utils._canonical import canonicalize_control_flow # pylint: disable=wrong-import-order @@ -897,6 +897,48 @@ def test_if_else_expr(self): check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_standalone_vars(self): + """Test that the routing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(3, 2) + qc.cx(3, 0) + qc.cx(4, 2) + qc.cx(4, 0) + qc.cx(1, 4) + qc.cx(3, 4) + with qc.if_test(a): + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with qc.if_test(a) as else_: + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with else_: + qc.cx(1, 2) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cx(1, 3) + qc.store(a, False) + with qc.switch(b) as case: + with case(0): + qc.add_var(c, 12) + qc.cx(3, 1) + with case(case.DEFAULT): + qc.cx(3, 1) + + cm = CouplingMap.from_line(5) + 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): """test controlflow with no layout change needed""" num_qubits = 5 @@ -1447,9 +1489,9 @@ class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase): def setUpClass(cls): super().setUpClass() cls.backend = Fake27QPulseV1() + cls.backend.configuration().basis_gates += ["for_loop", "while_loop", "if_else"] cls.coupling_edge_set = {tuple(x) for x in cls.backend.configuration().coupling_map} cls.basis_gates = set(cls.backend.configuration().basis_gates) - cls.basis_gates.update(["for_loop", "while_loop", "if_else"]) def assert_valid_circuit(self, transpiled): """Assert circuit complies with constraints of backend.""" @@ -1464,7 +1506,7 @@ def _visit_block(circuit, qubit_mapping=None): qargs = tuple(qubit_mapping[x] for x in instruction.qubits) if not isinstance(instruction.operation, ControlFlowOp): if len(qargs) > 2 or len(qargs) < 0: - raise Exception("Invalid number of qargs for instruction") + raise RuntimeError("Invalid number of qargs for instruction") if len(qargs) == 2: self.assertIn(qargs, self.coupling_edge_set) else: diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index 4a46efd57b2..d6ca1bde53d 100644 --- a/test/python/transpiler/test_swap_strategy_router.py +++ b/test/python/transpiler/test_swap_strategy_router.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 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 @@ -15,12 +15,14 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, Qubit, QuantumRegister +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.transpiler import PassManager, CouplingMap, Layout, TranspilerError from qiskit.circuit.library import PauliEvolutionGate, CXGate from qiskit.circuit.library.n_local import QAOAAnsatz from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.passes import FullAncillaAllocation from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import ApplyLayout @@ -562,9 +564,47 @@ def test_edge_coloring(self, edge_coloring): self.assertEqual(pm_.run(circ), expected) + def test_permutation_tracking(self): + """Test that circuit layout permutations are properly tracked in the pass property + set and returned with the output circuit.""" + + # We use the same scenario as the QAOA test above + mixer = QuantumCircuit(4) + for idx in range(4): + mixer.ry(-idx, idx) + + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + circ = QAOAAnsatz(op, reps=2, mixer_operator=mixer) + + expected_swap_permutation = [3, 1, 2, 0] + expected_full_permutation = [1, 3, 2, 0] + + cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) + swap_strat = SwapStrategy(cmap, swap_layers=[[(0, 1), (2, 3)], [(1, 2)]]) + + # test standalone + swap_pm = PassManager( + [ + FindCommutingPauliEvolutions(), + Commuting2qGateRouter(swap_strat), + ] + ) + swapped = swap_pm.run(circ.decompose()) + + # test as pre-routing step + backend = GenericBackendV2(num_qubits=4, coupling_map=[[0, 1], [0, 2], [0, 3]], seed=42) + pm = generate_preset_pass_manager( + optimization_level=3, target=backend.target, seed_transpiler=40 + ) + pm.pre_routing = swap_pm + full = pm.run(circ.decompose()) + + self.assertEqual(swapped.layout.routing_permutation(), expected_swap_permutation) + self.assertEqual(full.layout.routing_permutation(), expected_full_permutation) + class TestSwapRouterExceptions(QiskitTestCase): - """Test that exceptions are properly raises.""" + """Test that exceptions are properly raised.""" def setUp(self): """Setup useful variables.""" diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 646b29dd383..f63ed5061cc 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1366,6 +1366,31 @@ def test_get_empty_target_calibration(self): self.assertIsNone(target["x"][(0,)].calibration) + def test_has_calibration(self): + target = Target() + properties = { + (0,): InstructionProperties(duration=100, error=0.1), + (1,): None, + } + target.add_instruction(XGate(), properties) + + # Test false for properties with no calibration + self.assertFalse(target.has_calibration("x", (0,))) + # Test false for no properties + self.assertFalse(target.has_calibration("x", (1,))) + + properties = { + (0,): InstructionProperties( + duration=self.custom_sx_q0.duration, + error=None, + calibration=self.custom_sx_q0, + ) + } + target.add_instruction(SXGate(), properties) + + # Test true for properties with calibration + self.assertTrue(target.has_calibration("sx", (0,))) + def test_loading_legacy_ugate_instmap(self): # This is typical IBM backend situation. # IBM provider used to have u1, u2, u3 in the basis gates and diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index 1e4da01cb42..d7c4baa18fd 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -43,7 +43,7 @@ def _ry_to_rz_template_pass(parameter: Parameter = None, extra_costs=None): - """Create a simple pass manager that runs a template optimisation with a single transformation. + """Create a simple pass manager that runs a template optimization with a single transformation. It turns ``RX(pi/2).RY(parameter).RX(-pi/2)`` into the equivalent virtual ``RZ`` rotation, where if ``parameter`` is given, it will be the instance used in the template.""" if parameter is None: @@ -409,7 +409,7 @@ def test_optimizer_does_not_replace_unbound_partial_match(self): circuit_out = PassManager(pass_).run(circuit_in) - # The template optimisation should not have replaced anything, because + # The template optimization should not have replaced anything, because # that would require it to leave dummy parameters in place without # binding them. self.assertEqual(circuit_in, circuit_out) diff --git a/test/python/transpiler/test_token_swapper.py b/test/python/transpiler/test_token_swapper.py index 9ded634eba7..8a3a8c72ee2 100644 --- a/test/python/transpiler/test_token_swapper.py +++ b/test/python/transpiler/test_token_swapper.py @@ -67,7 +67,7 @@ def test_small(self) -> None: self.assertEqual({i: i for i in range(8)}, permutation) def test_bug1(self) -> None: - """Tests for a bug that occured in happy swap chains of length >2.""" + """Tests for a bug that occurred in happy swap chains of length >2.""" graph = rx.PyGraph() graph.extend_from_edge_list( [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (3, 6)] diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index ceca591ce08..f6790e8ed14 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -71,7 +71,7 @@ class ControllableSynthesis(UnitarySynthesisPlugin): """A dummy synthesis plugin, which can have its ``supports_`` properties changed to test different parts of the synthesis plugin interface. By default, it accepts all keyword arguments and accepts all number of qubits, but if its run method is called, it just returns ``None`` to - indicate that the gate should not be synthesised.""" + indicate that the gate should not be synthesized.""" min_qubits = None max_qubits = None @@ -153,7 +153,7 @@ def mock_default_run_method(self): # We need to mock out DefaultUnitarySynthesis.run, except it will actually get called as an # instance method, so we can't just wrap the method defined on the class, but instead we # need to wrap a method that has been bound to a particular instance. This is slightly - # frgaile, because we're likely wrapping a _different_ instance, but since there are no + # fragile, because we're likely wrapping a _different_ instance, but since there are no # arguments to __init__, and no internal state, it should be ok. It doesn't matter if we # dodged the patching of the manager class that happens elsewhere in this test suite, # because we're always accessing something that the patch would delegate to the inner diff --git a/test/python/transpiler/test_unroll_custom_definitions.py b/test/python/transpiler/test_unroll_custom_definitions.py index cfed023795d..5bd16f027e4 100644 --- a/test/python/transpiler/test_unroll_custom_definitions.py +++ b/test/python/transpiler/test_unroll_custom_definitions.py @@ -16,10 +16,11 @@ from qiskit.circuit import EquivalenceLibrary, Gate, Qubit, Clbit, Parameter from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.transpiler import Target -from qiskit.circuit.library import CXGate, U3Gate +from qiskit.circuit.library import CXGate, U3Gate, UGate from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -317,3 +318,56 @@ def test_unroll_empty_definition_with_phase(self): pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u"]) expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone(self): + """Don't attempt to unroll `Store` instructions.""" + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to unroll `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index b0957c82468..716e49d3500 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -570,6 +570,26 @@ def test_3_q_gate(self): pass_1.property_set["VF2Layout_stop_reason"], VF2LayoutStopReason.MORE_THAN_2Q ) + def test_target_without_coupling_map(self): + """When a target has no coupling_map but it is provided as argument. + See: https://github.com/Qiskit/qiskit/pull/11585""" + + circuit = QuantumCircuit(3) + circuit.cx(0, 1) + dag = circuit_to_dag(circuit) + + target = Target(num_qubits=3) + target.add_instruction(CXGate()) + + vf2_pass = VF2Layout( + coupling_map=CouplingMap([[0, 2], [1, 2]]), target=target, seed=42, max_trials=1 + ) + vf2_pass.run(dag) + + self.assertEqual( + vf2_pass.property_set["VF2Layout_stop_reason"], VF2LayoutStopReason.SOLUTION_FOUND + ) + class TestMultipleTrials(QiskitTestCase): """Test the passes behavior with >1 trial.""" diff --git a/test/python/utils/test_lazy_loaders.py b/test/python/utils/test_lazy_loaders.py index bd63d7ff04a..11b37ccb9d1 100644 --- a/test/python/utils/test_lazy_loaders.py +++ b/test/python/utils/test_lazy_loaders.py @@ -423,7 +423,7 @@ def exec_module(self, module): def test_import_allows_attributes_failure(self): """Check that the import tester can accept a dictionary mapping module names to attributes, - and that these are recognised when they are missing.""" + and that these are recognized when they are missing.""" # We can just use existing modules for this. name_map = { "sys": ("executable", "path"), diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index f02f1ad1143..e6b430c4ee8 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -55,6 +55,7 @@ def test_default_output(self): @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "Skipped because matplotlib is not available") def test_mpl_config_with_path(self): + # pylint: disable=possibly-used-before-assignment # It's too easy to get too nested in a test with many context managers. tempdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with self.addCleanup(tempdir.cleanup) @@ -128,6 +129,7 @@ def test_latex_unsupported_image_format_error_message(self): @_latex_drawer_condition def test_latex_output_file_correct_format(self): + # pylint: disable=possibly-used-before-assignment with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "latex"}): circuit = QuantumCircuit() filename = "file.gif" diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 9b34257f567..e7d28aac8a9 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -12,6 +12,9 @@ """circuit_drawer with output="text" draws a circuit in ascii art""" +# Sometimes we want to test long-lined output. +# pylint: disable=line-too-long + import pathlib import os import tempfile @@ -37,7 +40,7 @@ from qiskit.visualization import circuit_drawer from qiskit.visualization.circuit import text as elements from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( HGate, U2Gate, @@ -182,6 +185,7 @@ def test_text_no_pager(self): class TestTextDrawerGatesInCircuit(QiskitTestCase): + # pylint: disable=possibly-used-before-assignment """Gate by gate checks in different settings.""" def test_text_measure_cregbundle(self): @@ -492,6 +496,58 @@ def test_text_reverse_bits_read_from_config(self): test_reverse = str(circuit_drawer(circuit, output="text")) self.assertEqual(test_reverse, expected_reverse) + def test_text_idle_wires_read_from_config(self): + """Swap drawing with idle_wires set in the configuration file.""" + expected_with = "\n".join( + [ + " ┌───┐", + "q1_0: ┤ H ├", + " └───┘", + "q1_1: ─────", + " ┌───┐", + "q2_0: ┤ H ├", + " └───┘", + "q2_1: ─────", + " ", + ] + ) + expected_without = "\n".join( + [ + " ┌───┐", + "q1_0: ┤ H ├", + " ├───┤", + "q2_0: ┤ H ├", + " └───┘", + ] + ) + qr1 = QuantumRegister(2, "q1") + qr2 = QuantumRegister(2, "q2") + circuit = QuantumCircuit(qr1, qr2) + circuit.h(qr1[0]) + circuit.h(qr2[0]) + + self.assertEqual( + str( + circuit_drawer( + circuit, + output="text", + ) + ), + expected_with, + ) + + config_content = """ + [default] + circuit_idle_wires = false + """ + with tempfile.TemporaryDirectory() as dir_path: + file_path = pathlib.Path(dir_path) / "qiskit.conf" + with open(file_path, "w") as fptr: + fptr.write(config_content) + with unittest.mock.patch.dict(os.environ, {"QISKIT_SETTINGS": str(file_path)}): + test_without = str(circuit_drawer(circuit, output="text")) + self.assertEqual(test_without, expected_without) + def test_text_cswap(self): """CSwap drawing.""" expected = "\n".join( @@ -511,6 +567,7 @@ def test_text_cswap(self): circuit.cswap(qr[0], qr[1], qr[2]) circuit.cswap(qr[1], qr[0], qr[2]) circuit.cswap(qr[2], qr[1], qr[0]) + self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) def test_text_cswap_reverse_bits(self): @@ -4220,7 +4277,6 @@ def test_text_4q_2c(self): cr6 = ClassicalRegister(6, "c") circuit = QuantumCircuit(qr6, cr6) circuit.append(inst, qr6[1:5], cr6[1:3]) - self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) def test_text_2q_1c(self): @@ -5570,7 +5626,7 @@ def test_draw_hamiltonian_single(self): self.assertEqual(circuit.draw(output="text").single_string(), expected) def test_draw_hamiltonian_multi(self): - """Text Hamiltonian gate with mutiple qubits.""" + """Text Hamiltonian gate with multiple qubits.""" expected = "\n".join( [ " ┌──────────────┐", @@ -5591,7 +5647,7 @@ def test_draw_hamiltonian_multi(self): class TestTextPhase(QiskitTestCase): - """Testing the draweing a circuit with phase""" + """Testing the drawing a circuit with phase""" def test_bell(self): """Text Bell state with phase.""" @@ -5665,7 +5721,6 @@ def test_registerless_one_bit(self): qry = QuantumRegister(1, "qry") crx = ClassicalRegister(2, "crx") circuit = QuantumCircuit(qrx, [Qubit(), Qubit()], qry, [Clbit(), Clbit()], crx) - self.assertEqual(circuit.draw(output="text", cregbundle=True).single_string(), expected) @@ -6316,6 +6371,80 @@ def test_switch_with_expression(self): expected, ) + def test_nested_if_else_op_var(self): + """Test if/else with standalone Var.""" + expected = "\n".join( + [ + " ┌───────── ┌──────────────── ───────┐ ┌──────────────────── ┌───┐ ───────┐ ───────┐ ", + "q_0: ┤ ┤ ──■── ├─┤ If-1 c && a == 128 ┤ H ├ End-1 ├─ ├─", + " │ If-0 !b │ If-1 b == c[0] ┌─┴─┐ End-1 │ └──────────────────── └───┘ ───────┘ End-0 │ ", + "q_1: ┤ ┤ ┤ X ├ ├────────────────────────────────────── ├─", + " └───────── └───────╥──────── └───┘ ───────┘ ───────┘ ", + " ┌───╨────┐ ", + "c: 2/═══════════════╡ [expr] ╞══════════════════════════════════════════════════════════════════", + " └────────┘ ", + ] + ) + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", False) + qc.store(a, 128) + with qc.if_test(expr.logic_not(b)): + # Mix old-style and new-style. + with qc.if_test(expr.equal(b, qc.clbits[0])): + qc.cx(0, 1) + c = qc.add_var("c", b) + with qc.if_test(expr.logic_and(c, expr.equal(a, 128))): + qc.h(0) + + actual = str(qc.draw("text", fold=-1, initial_state=False)) + self.assertEqual(actual, expected) + + def test_nested_switch_op_var(self): + """Test switch with standalone Var.""" + expected = "\n".join( + [ + " ┌───────────── ┌──────────── ┌──────────── ┌──────────── »", + "q_0: ┤ ┤ ┤ ┤ ──■──»", + " │ Switch-0 ~a │ Case-0 (0) │ Switch-1 b │ Case-1 (2) ┌─┴─┐»", + "q_1: ┤ ┤ ┤ ┤ ┤ X ├»", + " └───────────── └──────────── └──────────── └──────────── └───┘»", + "c: 2/══════════════════════════════════════════════════════════════»", + " »", + "« ┌──────────────── ┌───┐ ───────┐ ┌──────────────── ┌──────── ┌───┐»", + "«q_0: ┤ ┤ X ├ ├─┤ ┤ If-1 c ┤ H ├»", + "« │ Case-1 default └─┬─┘ End-1 │ │ Case-0 default └──────── └───┘»", + "«q_1: ┤ ──■── ├─┤ ───────────────»", + "« └──────────────── ───────┘ └──────────────── »", + "«c: 2/══════════════════════════════════════════════════════════════════»", + "« »", + "« ───────┐ ───────┐ ", + "«q_0: End-1 ├─ ├─", + "« ───────┘ End-0 │ ", + "«q_1: ────────── ├─", + "« ───────┘ ", + "«c: 2/════════════════════", + "« ", + ] + ) + + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", expr.lift(5, a.type)) + with qc.switch(expr.bit_not(a)) as case: + with case(0): + with qc.switch(b) as case2: + with case2(2): + qc.cx(0, 1) + with case2(case2.DEFAULT): + qc.cx(1, 0) + with case(case.DEFAULT): + c = qc.add_var("c", expr.equal(a, b)) + with qc.if_test(c): + qc.h(0) + actual = str(qc.draw("text", fold=80, initial_state=False)) + self.assertEqual(actual, expected) + class TestCircuitAnnotatedOperations(QiskitVisualizationTestCase): """Test AnnotatedOperations and other non-Instructions.""" diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index dd3a479ba65..bf9b1ca80d7 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -45,6 +45,7 @@ @unittest.skipUnless(optionals.HAS_PIL, "PIL not available") @unittest.skipUnless(optionals.HAS_SEABORN, "seaborn not available") class TestGateMap(QiskitVisualizationTestCase): + # pylint: disable=possibly-used-before-assignment """visual tests for plot_gate_map""" backends = [Fake5QV1(), Fake20QV1(), Fake7QPulseV1()] diff --git a/test/python/visualization/test_plot_histogram.py b/test/python/visualization/test_plot_histogram.py index 7c530851326..2668f3ff679 100644 --- a/test/python/visualization/test_plot_histogram.py +++ b/test/python/visualization/test_plot_histogram.py @@ -28,6 +28,7 @@ @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") class TestPlotHistogram(QiskitVisualizationTestCase): + # pylint: disable=possibly-used-before-assignment """Qiskit plot_histogram tests.""" def test_different_counts_lengths(self): diff --git a/test/python/visualization/timeline/test_generators.py b/test/python/visualization/timeline/test_generators.py index 66afe3556b3..5554248089e 100644 --- a/test/python/visualization/timeline/test_generators.py +++ b/test/python/visualization/timeline/test_generators.py @@ -109,9 +109,7 @@ def test_gen_full_gate_name_with_finite_duration(self): self.assertListEqual(list(drawing_obj.yvals), [0.0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u3(0.00, 0.00, 0.00)[20]") - ref_latex = "{name}(0.00, 0.00, 0.00)[20]".format( - name=self.formatter["latex_symbol.gates"]["u3"] - ) + ref_latex = f"{self.formatter['latex_symbol.gates']['u3']}(0.00, 0.00, 0.00)[20]" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -132,7 +130,7 @@ def test_gen_full_gate_name_with_zero_duration(self): self.assertListEqual(list(drawing_obj.yvals), [self.formatter["label_offset.frame_change"]]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u1(0.00)") - ref_latex = "{name}(0.00)".format(name=self.formatter["latex_symbol.gates"]["u1"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u1']}(0.00)" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -159,7 +157,7 @@ def test_gen_short_gate_name_with_finite_duration(self): self.assertListEqual(list(drawing_obj.yvals), [0.0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u3") - ref_latex = "{name}".format(name=self.formatter["latex_symbol.gates"]["u3"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u3']}" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -180,7 +178,7 @@ def test_gen_short_gate_name_with_zero_duration(self): self.assertListEqual(list(drawing_obj.yvals), [self.formatter["label_offset.frame_change"]]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "u1") - ref_latex = "{name}".format(name=self.formatter["latex_symbol.gates"]["u1"]) + ref_latex = f"{self.formatter['latex_symbol.gates']['u1']}" self.assertEqual(drawing_obj.latex, ref_latex) ref_styles = { @@ -250,6 +248,7 @@ def test_gen_bit_name(self): self.assertListEqual(list(drawing_obj.yvals), [0]) self.assertListEqual(drawing_obj.bits, [self.qubit]) self.assertEqual(drawing_obj.text, "bar") + # pylint: disable-next=consider-using-f-string ref_latex = r"{{\rm {register}}}_{{{index}}}".format(register="q", index="0") self.assertEqual(drawing_obj.latex, ref_latex) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 345d9dc0a44..cc70cccf7d4 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -661,7 +661,7 @@ def generate_annotated_circuits(): CXGate(), [InverseModifier(), ControlModifier(1), PowerModifier(1.4), InverseModifier()] ) op2 = AnnotatedOperation(XGate(), InverseModifier()) - qc = QuantumCircuit(6, 1) + qc = QuantumCircuit(6, 1, name="Annotated circuits") qc.cx(0, 1) qc.append(op1, [0, 1, 2]) qc.h(4) @@ -754,6 +754,72 @@ def generate_control_flow_expr(): return [qc1, qc2, qc3, qc4] +def generate_standalone_var(): + """Circuits that use standalone variables.""" + import uuid + from qiskit.circuit.classical import expr, types + + # This is the low-level, non-preferred way to construct variables, but we need the UUIDs to be + # deterministic between separate invocations of the script. + uuids = [ + uuid.UUID(bytes=b"hello, qpy world", version=4), + uuid.UUID(bytes=b"not a good uuid4", version=4), + uuid.UUID(bytes=b"but it's ok here", version=4), + uuid.UUID(bytes=b"any old 16 bytes", version=4), + uuid.UUID(bytes=b"and another load", version=4), + ] + a = expr.Var(uuids[0], types.Bool(), name="a") + b = expr.Var(uuids[1], types.Bool(), name="θψφ") + b_other = expr.Var(uuids[2], types.Bool(), name=b.name) + c = expr.Var(uuids[3], types.Uint(8), name="🐍🐍🐍") + d = expr.Var(uuids[4], types.Uint(8), name="d") + + qc = QuantumCircuit(1, 1, inputs=[a], name="standalone_var") + qc.add_var(b, expr.logic_not(a)) + + qc.add_var(c, expr.lift(0, c.type)) + with qc.if_test(b) as else_: + qc.store(c, expr.lift(3, c.type)) + with qc.while_loop(b): + qc.add_var(c, expr.lift(7, c.type)) + with else_: + qc.add_var(d, expr.lift(7, d.type)) + + qc.measure(0, 0) + with qc.switch(c) as case: + with case(0): + qc.store(b, True) + with case(1): + qc.store(qc.clbits[0], False) + with case(2): + # Explicit shadowing. + qc.add_var(b_other, True) + with case(3): + qc.store(a, False) + with case(case.DEFAULT): + pass + + return [qc] + + +def generate_v12_expr(): + """Circuits that contain the `Index` and bitshift operators new in QPY v12.""" + import uuid + from qiskit.circuit.classical import expr, types + + a = expr.Var(uuid.UUID(bytes=b"hello, qpy world", version=4), types.Uint(8), name="a") + cr = ClassicalRegister(4, "cr") + + index = QuantumCircuit(cr, inputs=[a], name="index_expr") + index.store(expr.index(cr, 0), expr.index(a, a)) + + shift = QuantumCircuit(cr, inputs=[a], name="shift_expr") + with shift.if_test(expr.equal(expr.shift_right(expr.shift_left(a, 1), 1), a)): + pass + + return [index, shift] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -802,6 +868,9 @@ def generate_circuits(version_parts): output_circuits["clifford.qpy"] = generate_clifford_circuits() if version_parts >= (1, 0, 0): output_circuits["annotated.qpy"] = generate_annotated_circuits() + if version_parts >= (1, 1, 0): + output_circuits["standalone_vars.qpy"] = generate_standalone_var() + output_circuits["v12_expr.qpy"] = generate_v12_expr() return output_circuits @@ -906,7 +975,7 @@ def load_qpy(qpy_files, version_parts): def _main(): - parser = argparse.ArgumentParser(description="Test QPY backwards compatibilty") + parser = argparse.ArgumentParser(description="Test QPY backwards compatibility") parser.add_argument("command", choices=["generate", "load"]) parser.add_argument( "--version", diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index 2dde71a5d39..3bd09d89348 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -258,9 +258,9 @@ def add_c_if_last_gate(self, carg, data): last_gate = self.qc.data[-1] # Conditional instructions are not supported - assume(isinstance(last_gate[0], Gate)) + assume(isinstance(last_gate.operation, Gate)) - last_gate[0].c_if(creg, val) + last_gate.operation.c_if(creg, val) # Properties to check @@ -269,7 +269,7 @@ def qasm(self): """After each circuit operation, it should be possible to build QASM.""" qasm2.dumps(self.qc) - @precondition(lambda self: any(isinstance(d[0], Measure) for d in self.qc.data)) + @precondition(lambda self: any(isinstance(d.operation, Measure) for d in self.qc.data)) @rule(kwargs=transpiler_conf()) def equivalent_transpile(self, kwargs): """Simulate, transpile and simulate the present circuit. Verify that the @@ -306,10 +306,9 @@ def equivalent_transpile(self, kwargs): count_differences = dicts_almost_equal(aer_counts, xpiled_aer_counts, 0.05 * shots) - assert ( - count_differences == "" - ), "Counts not equivalent: {}\nFailing QASM Input:\n{}\n\nFailing QASM Output:\n{}".format( - count_differences, qasm2.dumps(self.qc), qasm2.dumps(xpiled_qc) + assert count_differences == "", ( + f"Counts not equivalent: {count_differences}\nFailing QASM Input:\n" + f"{qasm2.dumps(self.qc)}\n\nFailing QASM Output:\n{qasm2.dumps(xpiled_qc)}" ) diff --git a/test/utils/_canonical.py b/test/utils/_canonical.py index 367281f512c..b05254b3e7b 100644 --- a/test/utils/_canonical.py +++ b/test/utils/_canonical.py @@ -52,7 +52,7 @@ def canonicalize_control_flow(circuit: QuantumCircuit) -> QuantumCircuit: """Canonicalize all control-flow operations in a circuit. This is not an efficient operation, and does not affect any properties of the circuit. Its - intent is to normalise parts of circuits that have a non-deterministic construction. These are + intent is to normalize parts of circuits that have a non-deterministic construction. These are the ordering of bit arguments in control-flow blocks output by the builder interface, and automatically generated ``for``-loop variables. diff --git a/test/utils/base.py b/test/utils/base.py index 747be7f66b5..ce9509709ba 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -81,10 +81,10 @@ def setUp(self): self.addTypeEqualityFunc(QuantumCircuit, self.assertQuantumCircuitEqual) if self.__setup_called: raise ValueError( - "In File: %s\n" + f"In File: {(sys.modules[self.__class__.__module__].__file__,)}\n" "TestCase.setUp was already called. Do not explicitly call " "setUp from your tests. In your own setUp, use super to call " - "the base setUp." % (sys.modules[self.__class__.__module__].__file__,) + "the base setUp." ) self.__setup_called = True @@ -92,10 +92,10 @@ def tearDown(self): super().tearDown() if self.__teardown_called: raise ValueError( - "In File: %s\n" + f"In File: {(sys.modules[self.__class__.__module__].__file__,)}\n" "TestCase.tearDown was already called. Do not explicitly call " "tearDown from your tests. In your own tearDown, use super to " - "call the base tearDown." % (sys.modules[self.__class__.__module__].__file__,) + "call the base tearDown." ) self.__teardown_called = True @@ -204,6 +204,20 @@ def setUpClass(cls): warnings.filterwarnings("error", category=DeprecationWarning) warnings.filterwarnings("error", category=QiskitWarning) + # Numpy 2 made a few new modules private, and have warnings that trigger if you try to + # access attributes that _would_ have existed. Unfortunately, Python's `warnings` module + # adds a field called `__warningregistry__` to any module that triggers a warning, and + # `unittest.TestCase.assertWarns` then queries said fields on all existing modules. On + # macOS ARM, we see some (we think harmless) warnings come out of `numpy.linalg._linalg` (a + # now-private module) during transpilation, which means that subsequent `assertWarns` calls + # can spuriously trick Numpy into sending out a nonsense `DeprecationWarning`. + # Tracking issue: https://github.com/Qiskit/qiskit/issues/12679 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*numpy\.(\w+\.)*__warningregistry__", + ) + # We only use pandas transitively through seaborn, so it's their responsibility to mark if # their use of pandas would be a problem. warnings.filterwarnings( @@ -215,6 +229,15 @@ def setUpClass(cls): module=r"seaborn(\..*)?", ) + # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version + # of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + message="Treating CircuitInstruction as an iterable is deprecated", + module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", + ) + allow_DeprecationWarning_modules = [ "test.python.pulse.test_builder", "test.python.pulse.test_block", @@ -305,10 +328,10 @@ def valid_comparison(value): if places is not None: if delta is not None: raise TypeError("specify delta or places not both") - msg_suffix = " within %s places" % places + msg_suffix = f" within {places} places" else: delta = delta or 1e-8 - msg_suffix = " within %s delta" % delta + msg_suffix = f" within {delta} delta" # Compare all keys in both dicts, populating error_msg. error_msg = "" diff --git a/test/visual/mpl/circuit/references/if_else_standalone_var.png b/test/visual/mpl/circuit/references/if_else_standalone_var.png new file mode 100644 index 00000000000..6266a0caeb0 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_standalone_var.png differ diff --git a/test/visual/mpl/circuit/references/switch_standalone_var.png b/test/visual/mpl/circuit/references/switch_standalone_var.png new file mode 100644 index 00000000000..8b8c7882891 Binary files /dev/null and b/test/visual/mpl/circuit/references/switch_standalone_var.png differ diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index e99cb3f628d..9e3dd5cc48e 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -47,7 +47,7 @@ ) from qiskit.circuit import Parameter, Qubit, Clbit, IfElseOp, SwitchCaseOp from qiskit.circuit.library import IQP -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from qiskit.quantum_info import random_clifford from qiskit.quantum_info.random import random_unitary from qiskit.utils import optionals @@ -2300,6 +2300,59 @@ def test_no_qreg_names_after_layout(self): ) self.assertGreaterEqual(ratio, self.threshold) + def test_if_else_standalone_var(self): + """Test if/else with standalone Var.""" + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", False) + qc.store(a, 128) + with qc.if_test(expr.logic_not(b)): + # Mix old-style and new-style. + with qc.if_test(expr.equal(b, qc.clbits[0])): + qc.cx(0, 1) + c = qc.add_var("c", b) + with qc.if_test(expr.logic_and(c, expr.equal(a, 128))): + qc.h(0) + fname = "if_else_standalone_var.png" + self.circuit_drawer(qc, output="mpl", filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, self.threshold) + + def test_switch_standalone_var(self): + """Test switch with standalone Var.""" + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(2, 2, inputs=[a]) + b = qc.add_var("b", expr.lift(5, a.type)) + with qc.switch(expr.bit_not(a)) as case: + with case(0): + with qc.switch(b) as case2: + with case2(2): + qc.cx(0, 1) + with case2(case2.DEFAULT): + qc.cx(1, 0) + with case(case.DEFAULT): + c = qc.add_var("c", expr.equal(a, b)) + with qc.if_test(c): + qc.h(0) + fname = "switch_standalone_var.png" + self.circuit_drawer(qc, output="mpl", filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, self.threshold) + if __name__ == "__main__": unittest.main(verbosity=1) diff --git a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py index ae69f212f89..20fae107d30 100644 --- a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py +++ b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py @@ -389,7 +389,7 @@ def test_plot_1_qubit_gate_map(self): """Test plot_gate_map using 1 qubit backend""" # getting the mock backend from FakeProvider - backend = GenericBackendV2(num_qubits=1) + backend = GenericBackendV2(num_qubits=1, basis_gates=["id", "rz", "sx", "x"]) fname = "1_qubit_gate_map.png" self.graph_plot_gate_map(backend=backend, filename=fname) diff --git a/test/visual/results.py b/test/visual/results.py index efaf09bbbbf..76fde794b39 100644 --- a/test/visual/results.py +++ b/test/visual/results.py @@ -83,30 +83,30 @@ def _new_gray(size, color): @staticmethod def passed_result_html(result, reference, diff, title): """Creates the html for passing tests""" - ret = '
%s ' % title + ret = f'
{title} ' ret += "" - ret += '
' % result - ret += '' % reference - ret += '' % diff + ret += f'
' + ret += f'' + ret += f'' ret += "
" return ret @staticmethod def failed_result_html(result, reference, diff, title): """Creates the html for failing tests""" - ret = '
%s ' % title + ret = f'
{title} ' ret += "" - ret += '
' % result - ret += '' % reference - ret += '' % diff + ret += f'
' + ret += f'' + ret += f'' ret += "
" return ret @staticmethod def no_reference_html(result, title): """Creates the html for missing-reference tests""" - ret = '
%s ' % title - ret += '" % (name, fullpath_name, fullpath_reference) + f'Download this image' + f" to {fullpath_reference}" + " and add/push to the repo" ) ret += Results.no_reference_html(fullpath_name, title) ret += "" diff --git a/tools/build_pgo.sh b/tools/build_pgo.sh index d0e88bf6f74..8553691bdfe 100755 --- a/tools/build_pgo.sh +++ b/tools/build_pgo.sh @@ -17,6 +17,12 @@ else source build_pgo/bin/activate fi +arch=`uname -m` +# Handle macOS calling the architecture arm64 and rust calling it aarch64 +if [[ $arch == "arm64" ]]; then + arch="aarch64" +fi + # Build with instrumentation pip install -U -c constraints.txt setuptools-rust wheel setuptools RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" pip install --prefer-binary -c constraints.txt -r requirements-dev.txt -e . @@ -29,4 +35,4 @@ python tools/pgo_scripts/test_utility_scale.py deactivate -${HOME}/.rustup/toolchains/*x86_64*/lib/rustlib/x86_64*/bin/llvm-profdata merge -o $merged_path /tmp/pgo-data +${HOME}/.rustup/toolchains/*$arch*/lib/rustlib/$arch*/bin/llvm-profdata merge -o $merged_path /tmp/pgo-data diff --git a/tools/build_standard_commutations.py b/tools/build_standard_commutations.py index 72798f0eb4b..0e1fcdf1797 100644 --- a/tools/build_standard_commutations.py +++ b/tools/build_standard_commutations.py @@ -102,12 +102,12 @@ def _generate_commutation_dict(considered_gates: List[Gate] = None) -> dict: commutation_relation = cc.commute( op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits=4 ) + + gate_pair_commutation[relative_placement] = commutation_relation else: pass # TODO - gate_pair_commutation[relative_placement] = commutation_relation - commutations[gate0.name, gate1.name] = gate_pair_commutation return commutations @@ -143,12 +143,14 @@ def _dump_commuting_dict_as_python( dir_str = "standard_gates_commutations = {\n" for k, v in commutations.items(): if not isinstance(v, dict): + # pylint: disable-next=consider-using-f-string dir_str += ' ("{}", "{}"): {},\n'.format(*k, v) else: + # pylint: disable-next=consider-using-f-string dir_str += ' ("{}", "{}"): {{\n'.format(*k) for entry_key, entry_val in v.items(): - dir_str += " {}: {},\n".format(entry_key, entry_val) + dir_str += f" {entry_key}: {entry_val},\n" dir_str += " },\n" dir_str += "}\n" diff --git a/tools/find_stray_release_notes.py b/tools/find_stray_release_notes.py index 7e04f5ecc32..d694e0d89b8 100755 --- a/tools/find_stray_release_notes.py +++ b/tools/find_stray_release_notes.py @@ -49,7 +49,7 @@ def _main(): failed_files = [x for x in res if x is not None] if len(failed_files) > 0: for failed_file in failed_files: - sys.stderr.write("%s is not in the correct location.\n" % failed_file) + sys.stderr.write(f"{failed_file} is not in the correct location.\n") sys.exit(1) sys.exit(0) diff --git a/tools/verify_headers.py b/tools/verify_headers.py index 7bd7d2bad4e..552372b7725 100755 --- a/tools/verify_headers.py +++ b/tools/verify_headers.py @@ -88,18 +88,18 @@ def validate_header(file_path): break if file_path.endswith(".rs"): if "".join(lines[start : start + 2]) != header_rs: - return (file_path, False, "Header up to copyright line does not match: %s" % header) + return (file_path, False, f"Header up to copyright line does not match: {header}") if not copyright_line.search(lines[start + 2]): return (file_path, False, "Header copyright line not found") if "".join(lines[start + 3 : start + 11]) != apache_text_rs: - return (file_path, False, "Header apache text string doesn't match:\n %s" % apache_text) + return (file_path, False, f"Header apache text string doesn't match:\n {apache_text}") else: if "".join(lines[start : start + 2]) != header: - return (file_path, False, "Header up to copyright line does not match: %s" % header) + return (file_path, False, f"Header up to copyright line does not match: {header}") if not copyright_line.search(lines[start + 2]): return (file_path, False, "Header copyright line not found") if "".join(lines[start + 3 : start + 11]) != apache_text: - return (file_path, False, "Header apache text string doesn't match:\n %s" % apache_text) + return (file_path, False, f"Header apache text string doesn't match:\n {apache_text}") return (file_path, True, None) @@ -122,8 +122,8 @@ def _main(): failed_files = [x for x in res if x[1] is False] if len(failed_files) > 0: for failed_file in failed_files: - sys.stderr.write("%s failed header check because:\n" % failed_file[0]) - sys.stderr.write("%s\n\n" % failed_file[2]) + sys.stderr.write(f"{failed_file[0]} failed header check because:\n") + sys.stderr.write(f"{failed_file[2]}\n\n") sys.exit(1) sys.exit(0)
' % result + ret = f'
{title} ' + ret += f'
' ret += "
" return ret @@ -119,11 +119,7 @@ def diff_images(self): if os.path.exists(os.path.join(SWD, fullpath_reference)): ratio, diff_name = Results._similarity_ratio(fullpath_name, fullpath_reference) - title = "{} | {} | ratio: {}".format( - name, - self.data[name]["testname"], - ratio, - ) + title = f"{name} | {self.data[name]['testname']} | ratio: {ratio}" if ratio == 1: self.exact_match.append(fullpath_name) else: @@ -158,8 +154,9 @@ def _repr_html_(self): ) else: title = ( - 'Download this image to %s' - " and add/push to the repo