diff --git a/.azure/lint_docs_qpy-linux.yml b/.azure/lint_docs-linux.yml similarity index 61% rename from .azure/lint_docs_qpy-linux.yml rename to .azure/lint_docs-linux.yml index c08ea702ede5..b9e0c37892df 100644 --- a/.azure/lint_docs_qpy-linux.yml +++ b/.azure/lint_docs-linux.yml @@ -4,8 +4,8 @@ parameters: displayName: "Version of Python to use" jobs: - - job: 'Lint_Docs_QPY' - displayName: 'Lint, documentation and QPY' + - job: 'Lint_Docs' + displayName: 'Lint and documentation' pool: {vmImage: 'ubuntu-latest'} variables: @@ -43,24 +43,3 @@ jobs: artifactName: 'html_docs' Parallel: true ParallelCount: 8 - - - task: Cache@2 - inputs: - key: 'qpy | test/qpy_compat/test_qpy.py | "$(Build.BuildNumber)"' - restoreKeys: | - qpy | test/qpy_compat/test_qpy.py - path: qpy_files - displayName: Cache old QPY files - - - bash: | - set -e - # Reuse the docs environment to avoid needing to rebuild another - # version of Qiskit. - source .tox/docs/bin/activate - mv qpy_files/* test/qpy_compat || : - pushd test/qpy_compat - ./run_tests.sh - popd - mkdir -p qpy_files - mv test/qpy_compat/qpy_* qpy_files/. - displayName: 'Run QPY backwards compat tests' diff --git a/.github/workflows/qpy.yml b/.github/workflows/qpy.yml new file mode 100644 index 000000000000..2c127add907a --- /dev/null +++ b/.github/workflows/qpy.yml @@ -0,0 +1,39 @@ +name: QPY + +on: + push: + branches: + - 'main' + - 'stable/*' + pull_request: + merge_group: +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + backward_compat: + if: github.repository_owner == 'Qiskit' + name: Backwards compatibility + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/cache@v4 + with: + path: test/qpy_compat/qpy_cache + # The hashing is this key can be too eager to invalidate the cache, + # but since we risk the QPY tests failing to update if they're not in + # sync, it's better safe than sorry. + key: qpy-${{ hashFiles('test/qpy_compat/**') }} + + - name: Run QPY backwards compatibility tests + working-directory: test/qpy_compat + run: ./run_tests.sh diff --git a/.mergify.yml b/.mergify.yml index 50a638989a6c..3b5ad2c9ed31 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,4 +6,4 @@ pull_request_rules: actions: backport: branches: - - stable/1.2 + - stable/1.3 diff --git a/Cargo.lock b/Cargo.lock index 0638429aaf4b..b1bbde626b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,15 +41,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "always-assert" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4436e0292ab1bb631b42973c61205e704475fe8126af845c8d923c0996328127" -dependencies = [ - "log", -] - [[package]] name = "approx" version = "0.4.0" @@ -120,9 +111,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" dependencies = [ "bytemuck_derive", ] @@ -197,15 +188,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -647,12 +629,6 @@ dependencies = [ "either", ] -[[package]] -name = "jod-thread" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" - [[package]] name = "lazy_static" version = "1.5.0" @@ -671,12 +647,6 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - [[package]] name = "matrixcompare" version = "0.3.0" @@ -718,15 +688,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "miow" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ffbca2f655e33c08be35d87278e5b18b89550a37dbd598c20db92f6a471123" -dependencies = [ - "windows-sys 0.42.0", -] - [[package]] name = "nano-gemm" version = "0.1.2" @@ -878,9 +839,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf314fca279e6e6ac2126a4ff98f26d88aa4ad06bc68fb6ae5cf4bd706758311" +checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e" dependencies = [ "libc", "ndarray", @@ -899,9 +860,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oq3_lexer" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2f0f9d48042c12f82b2550808378718627e108fc3f6adf63e02e5293541a3" +checksum = "a27bbc91e3e9d6193a44aac8f5d62c1507c41669af71a4e7e0ef66fd6470e960" dependencies = [ "unicode-properties", "unicode-xid", @@ -909,9 +870,9 @@ dependencies = [ [[package]] name = "oq3_parser" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69b215426a4a2a023fd62cca4436c633ba0ab39ee260aca875ac60321b3704b" +checksum = "9a72022fcb414e8a0912920a1cf46417b6aa95f19d4b38778df7450f8a3c17fa" dependencies = [ "drop_bomb", "oq3_lexer", @@ -920,9 +881,9 @@ dependencies = [ [[package]] name = "oq3_semantics" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e15e9cee54e92fb1b3aaa42556b0bd76c8c1c10912a7d6798f43dfc3afdcb0d" +checksum = "b72dffd869f3548190c705828d030fbb7fca94e519dcfa6a489227e5c3ffd777" dependencies = [ "boolenum", "hashbrown 0.12.3", @@ -933,9 +894,9 @@ dependencies = [ [[package]] name = "oq3_source_file" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f65243cc4807c600c544a21db6c17544c53aa2bc034b3eccf388251cc6530e7" +checksum = "ff8c03f1f92c7a8f0b5249664b526169ceb8f925cb314ff93d3b27d8a4afb78c" dependencies = [ "ariadne", "oq3_syntax", @@ -943,9 +904,9 @@ dependencies = [ [[package]] name = "oq3_syntax" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" +checksum = "42c754ce1d9da28d6c0334c212d64b521288fe8c7cf16e9727d45dcf661ff084" dependencies = [ "cov-mark", "either", @@ -954,7 +915,6 @@ dependencies = [ "once_cell", "oq3_lexer", "oq3_parser", - "ra_ap_stdx", "rowan", "rustc-hash", "rustversion", @@ -1122,9 +1082,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" dependencies = [ "cfg-if", "hashbrown 0.14.5", @@ -1145,9 +1105,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" dependencies = [ "once_cell", "target-lexicon", @@ -1155,9 +1115,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" dependencies = [ "libc", "pyo3-build-config", @@ -1165,9 +1125,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1177,9 +1137,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" dependencies = [ "heck", "proc-macro2", @@ -1190,7 +1150,7 @@ dependencies = [ [[package]] name = "qiskit-accelerate" -version = "1.3.0" +version = "2.0.0" dependencies = [ "ahash 0.8.11", "approx 0.5.1", @@ -1214,6 +1174,7 @@ dependencies = [ "rand_distr", "rand_pcg", "rayon", + "rustiq-core", "rustworkx-core", "smallvec", "thiserror", @@ -1221,7 +1182,7 @@ dependencies = [ [[package]] name = "qiskit-circuit" -version = "1.3.0" +version = "2.0.0" dependencies = [ "ahash 0.8.11", "approx 0.5.1", @@ -1241,7 +1202,7 @@ dependencies = [ [[package]] name = "qiskit-pyext" -version = "1.3.0" +version = "2.0.0" dependencies = [ "pyo3", "qiskit-accelerate", @@ -1252,7 +1213,7 @@ dependencies = [ [[package]] name = "qiskit-qasm2" -version = "1.3.0" +version = "2.0.0" dependencies = [ "hashbrown 0.14.5", "num-bigint", @@ -1262,7 +1223,7 @@ dependencies = [ [[package]] name = "qiskit-qasm3" -version = "1.3.0" +version = "2.0.0" dependencies = [ "ahash 0.8.11", "hashbrown 0.14.5", @@ -1286,20 +1247,6 @@ version = "0.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d33758724f997689f84146e5401e28d875a061804f861f113696f44f5232aa" -[[package]] -name = "ra_ap_stdx" -version = "0.0.188" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80fb2ff88b31fa35cde89ae13ea7c9ada97c7a2c778dcafef530a267658000" -dependencies = [ - "always-assert", - "crossbeam-channel", - "jod-thread", - "libc", - "miow", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -1449,6 +1396,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustiq-core" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b580cb45b60a39f5a17b284bbe8343cfcd67929931729b4afee19ec94d308" +dependencies = [ + "itertools 0.10.5", + "petgraph", + "rand", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1674,22 +1632,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.9" @@ -1699,27 +1641,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 512ee095732e..52a9471497b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "1.3.0" +version = "2.0.0" edition = "2021" rust-version = "1.70" # Keep in sync with README.md and rust-toolchain.toml. license = "Apache-2.0" @@ -14,13 +14,13 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] -bytemuck = "1.19" +bytemuck = "1.20" indexmap.version = "2.6.0" hashbrown.version = "0.14.5" num-bigint = "0.4" num-complex = "0.4" ndarray = "0.15" -numpy = "0.22.0" +numpy = "0.22.1" smallvec = "1.13" thiserror = "1.0" rustworkx-core = "0.15" @@ -34,7 +34,7 @@ rayon = "1.10" # distributions). We only activate that feature when building the C extension module; we still need # it disabled for Rust-only tests to avoid linker errors with it not being loaded. See # https://pyo3.rs/main/features#extension-module for more. -pyo3 = { version = "0.22.5", features = ["abi3-py39"] } +pyo3 = { version = "0.22.6", features = ["abi3-py39"] } # These are our own crates. qiskit-accelerate = { path = "crates/accelerate" } diff --git a/README.md b/README.md index 9238bf88fa41..0546e76cf63e 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,13 @@ we use `measure_all(inplace=False)` to get a copy of the circuit in which all th qc_measured = qc_example.measure_all(inplace=False) # 3. Execute using the Sampler primitive -from qiskit.primitives.sampler import Sampler -sampler = Sampler() -job = sampler.run(qc_measured, shots=1000) +from qiskit.primitives import StatevectorSampler +sampler = StatevectorSampler() +job = sampler.run([qc_measured], shots=1000) result = job.result() -print(f" > Quasi probability distribution: {result.quasi_dists}") +print(f" > Counts: {result[0].meas.get_counts()}") ``` -Running this will give an outcome similar to `{0: 0.497, 7: 0.503}` which is `000` 50% of the time and `111` 50% of the time up to statistical fluctuations. +Running this will give an outcome similar to `{'000': 497, '111': 503}` which is `000` 50% of the time and `111` 50% of the time up to statistical fluctuations. To illustrate the power of Estimator, we now use the quantum information toolbox to create the operator $XXY+XYX+YXX-YYY$ and pass it to the `run()` function, along with our quantum circuit. Note the Estimator requires a circuit _**without**_ measurement, so we use the `qc_example` circuit we created earlier. ```python @@ -81,17 +81,17 @@ from qiskit.quantum_info import SparsePauliOp operator = SparsePauliOp.from_list([("XXY", 1), ("XYX", 1), ("YXX", 1), ("YYY", -1)]) # 3. Execute using the Estimator primitive -from qiskit.primitives import Estimator -estimator = Estimator() -job = estimator.run(qc_example, operator, shots=1000) +from qiskit.primitives import StatevectorEstimator +estimator = StatevectorEstimator() +job = estimator.run([(qc_example, operator)], precision=1e-3) result = job.result() -print(f" > Expectation values: {result.values}") +print(f" > Expectation values: {result[0].data.evs}") ``` Running this will give the outcome `4`. For fun, try to assign a value of +/- 1 to each single-qubit operator X and Y and see if you can achieve this outcome. (Spoiler alert: this is not possible!) -Using the Qiskit-provided `qiskit.primitives.Sampler` and `qiskit.primitives.Estimator` will not take you very far. +Using the Qiskit-provided `qiskit.primitives.StatevectorSampler` and `qiskit.primitives.StatevectorEstimator` will not take you very far. The power of quantum computing cannot be simulated on classical computers and you need to use real quantum hardware to scale to larger quantum circuits. However, running a quantum circuit on hardware requires rewriting to the basis gates and connectivity of the quantum hardware. The tool that does this is the [transpiler](https://docs.quantum.ibm.com/api/qiskit/transpiler), and Qiskit includes transpiler passes for synthesis, optimization, mapping, and scheduling. @@ -106,7 +106,7 @@ qc_transpiled = transpile(qc_example, basis_gates = ['cz', 'sx', 'rz'], coupling ### Executing your code on real quantum hardware Qiskit provides an abstraction layer that lets users run quantum circuits on hardware from any vendor that provides a compatible interface. -The best way to use Qiskit is with a runtime environment that provides optimized implementations of `sampler` and `estimator` for a given hardware platform. This runtime may involve using pre- and post-processing, such as optimized transpiler passes with error suppression, error mitigation, and, eventually, error correction built in. A runtime implements `qiskit.primitives.BaseSampler` and `qiskit.primitives.BaseEstimator` interfaces. For example, +The best way to use Qiskit is with a runtime environment that provides optimized implementations of `sampler` and `estimator` for a given hardware platform. This runtime may involve using pre- and post-processing, such as optimized transpiler passes with error suppression, error mitigation, and, eventually, error correction built in. A runtime implements `qiskit.primitives.BaseSamplerV2` and `qiskit.primitives.BaseEstimatorV2` interfaces. For example, some packages that provide implementations of a runtime primitive implementation are: * https://github.com/Qiskit/qiskit-ibm-runtime @@ -165,4 +165,4 @@ We acknowledge partial support for Qiskit development from the DOE Office of Sci ## License -[Apache License 2.0](LICENSE.txt) \ No newline at end of file +[Apache License 2.0](LICENSE.txt) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d182ba32f1b3..94ed7d1fad7f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -144,7 +144,7 @@ stages: - stage: "Lint_Docs_Prelim_Tests" displayName: "Preliminary tests" jobs: - - template: ".azure/lint_docs_qpy-linux.yml" + - template: ".azure/lint_docs-linux.yml" parameters: pythonVersion: ${{ parameters.minimumPythonVersion }} @@ -208,7 +208,7 @@ stages: - stage: "Merge_Queue" displayName: "Merge queue" jobs: - - template: ".azure/lint_docs_qpy-linux.yml" + - template: ".azure/lint_docs-linux.yml" parameters: pythonVersion: ${{ parameters.minimumPythonVersion }} diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 9f57288a64f6..cc624e7750d6 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -26,6 +26,7 @@ qiskit-circuit.workspace = true thiserror.workspace = true ndarray_einsum_beta = "0.7" once_cell = "1.20.2" +rustiq-core = "0.0.10" bytemuck.workspace = true [dependencies.smallvec] diff --git a/crates/accelerate/src/basis/basis_translator/basis_search.rs b/crates/accelerate/src/basis/basis_translator/basis_search.rs index 2810765db741..4686ba9c4c3f 100644 --- a/crates/accelerate/src/basis/basis_translator/basis_search.rs +++ b/crates/accelerate/src/basis/basis_translator/basis_search.rs @@ -13,7 +13,6 @@ use std::cell::RefCell; use hashbrown::{HashMap, HashSet}; -use pyo3::prelude::*; use crate::equivalence::{EdgeData, Equivalence, EquivalenceLibrary, Key, NodeData}; use qiskit_circuit::operations::Operation; @@ -23,28 +22,6 @@ use rustworkx_core::traversal::{dijkstra_search, DijkstraEvent}; use super::compose_transforms::{BasisTransformIn, GateIdentifier}; -/// Search for a set of transformations from source_basis to target_basis. -/// Args: -/// equiv_lib (EquivalenceLibrary): Source of valid translations -/// source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis. -/// target_basis (Set[gate_name: str]): Target basis. -/// -/// Returns: -/// Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate, -/// equiv_params, equiv_circuit) tuples tuples which, if applied in order -/// will map from source_basis to target_basis. Returns None if no path -/// was found. -#[pyfunction] -#[pyo3(name = "basis_search")] -pub(crate) fn py_basis_search( - py: Python, - equiv_lib: &mut EquivalenceLibrary, - source_basis: HashSet, - target_basis: HashSet, -) -> PyObject { - basis_search(equiv_lib, &source_basis, &target_basis).into_py(py) -} - type BasisTransforms = Vec<(GateIdentifier, BasisTransformIn)>; /// Search for a set of transformations from source_basis to target_basis. /// diff --git a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs index e4498af0f8a5..b1366c30bf2f 100644 --- a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs +++ b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs @@ -30,16 +30,6 @@ pub type GateIdentifier = (String, u32); pub type BasisTransformIn = (SmallVec<[Param; 3]>, CircuitFromPython); pub type BasisTransformOut = (SmallVec<[Param; 3]>, DAGCircuit); -#[pyfunction(name = "compose_transforms")] -pub(super) fn py_compose_transforms( - py: Python, - basis_transforms: Vec<(GateIdentifier, BasisTransformIn)>, - source_basis: HashSet, - source_dag: &DAGCircuit, -) -> PyResult> { - compose_transforms(py, &basis_transforms, &source_basis, source_dag) -} - pub(super) fn compose_transforms<'a>( py: Python, basis_transforms: &'a [(GateIdentifier, BasisTransformIn)], diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 18970065267c..c900f80beff4 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -10,14 +10,803 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use compose_transforms::BasisTransformIn; +use compose_transforms::BasisTransformOut; +use compose_transforms::GateIdentifier; + +use basis_search::basis_search; +use compose_transforms::compose_transforms; +use hashbrown::{HashMap, HashSet}; +use itertools::Itertools; +use pyo3::intern; use pyo3::prelude::*; -pub mod basis_search; +mod basis_search; mod compose_transforms; +use pyo3::types::{IntoPyDict, PyComplex, PyDict, PyTuple}; +use pyo3::PyTypeInfo; +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::converters::circuit_to_dag; +use qiskit_circuit::imports::DAG_TO_CIRCUIT; +use qiskit_circuit::imports::PARAMETER_EXPRESSION; +use qiskit_circuit::operations::Param; +use qiskit_circuit::packed_instruction::PackedInstruction; +use qiskit_circuit::{ + circuit_data::CircuitData, + dag_circuit::DAGCircuit, + operations::{Operation, OperationRef}, +}; +use qiskit_circuit::{Clbit, Qubit}; +use smallvec::SmallVec; + +use crate::equivalence::EquivalenceLibrary; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::exceptions::TranspilerError; +use crate::target_transpiler::{Qargs, Target}; + +type InstMap = HashMap; +type ExtraInstructionMap<'a> = HashMap<&'a Option, InstMap>; + +#[allow(clippy::too_many_arguments)] +#[pyfunction(name = "base_run", signature = (dag, equiv_lib, qargs_with_non_global_operation, min_qubits, target_basis=None, target=None, non_global_operations=None))] +fn run( + py: Python<'_>, + dag: DAGCircuit, + equiv_lib: &mut EquivalenceLibrary, + qargs_with_non_global_operation: HashMap, HashSet>, + min_qubits: usize, + target_basis: Option>, + target: Option<&Target>, + non_global_operations: Option>, +) -> PyResult { + if target_basis.is_none() && target.is_none() { + return Ok(dag); + } + + let basic_instrs: HashSet; + let mut source_basis: HashSet = HashSet::default(); + let mut new_target_basis: HashSet; + let mut qargs_local_source_basis: HashMap, HashSet> = + HashMap::default(); + if let Some(target) = target.as_ref() { + basic_instrs = ["barrier", "snapshot", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + let non_global_str: HashSet<&str> = if let Some(operations) = non_global_operations.as_ref() + { + operations.iter().map(|x| x.as_str()).collect() + } else { + HashSet::default() + }; + let target_keys = target.keys().collect::>(); + new_target_basis = target_keys + .difference(&non_global_str) + .map(|x| x.to_string()) + .collect(); + extract_basis_target( + py, + &dag, + &mut source_basis, + &mut qargs_local_source_basis, + min_qubits, + &qargs_with_non_global_operation, + )?; + } else { + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + source_basis = extract_basis(py, &dag, min_qubits)?; + new_target_basis = target_basis.unwrap(); + } + new_target_basis = new_target_basis + .union(&basic_instrs) + .map(|x| x.to_string()) + .collect(); + // If the source basis is a subset of the target basis and we have no circuit + // instructions on qargs that have non-global operations there is nothing to + // translate and we can exit early. + let source_basis_names: HashSet = source_basis.iter().map(|x| x.0.clone()).collect(); + if source_basis_names.is_subset(&new_target_basis) && qargs_local_source_basis.is_empty() { + return Ok(dag); + } + let basis_transforms = basis_search(equiv_lib, &source_basis, &new_target_basis); + let mut qarg_local_basis_transforms: HashMap< + Option, + Vec<(GateIdentifier, BasisTransformIn)>, + > = HashMap::default(); + for (qarg, local_source_basis) in qargs_local_source_basis.iter() { + // For any multiqubit operation that contains a subset of qubits that + // has a non-local operation, include that non-local operation in the + // search. This matches with the check we did above to include those + // subset non-local operations in the check here. + let mut expanded_target = new_target_basis.clone(); + if qarg.as_ref().is_some_and(|qarg| qarg.len() > 1) { + let qarg_as_set: HashSet = + HashSet::from_iter(qarg.as_ref().unwrap().iter().copied()); + for (non_local_qarg, local_basis) in qargs_with_non_global_operation.iter() { + if let Some(non_local_qarg) = non_local_qarg { + let non_local_qarg_as_set = HashSet::from_iter(non_local_qarg.iter().copied()); + if qarg_as_set.is_superset(&non_local_qarg_as_set) { + expanded_target = expanded_target.union(local_basis).cloned().collect(); + } + } + } + } else { + expanded_target = expanded_target + .union(&qargs_with_non_global_operation[qarg]) + .cloned() + .collect(); + } + let local_basis_transforms = basis_search(equiv_lib, local_source_basis, &expanded_target); + if let Some(local_basis_transforms) = local_basis_transforms { + qarg_local_basis_transforms.insert(qarg.clone(), local_basis_transforms); + } else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) 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/qiskit.transpiler.passes.\ + BasisTranslator#translation-errors", + local_source_basis + .iter() + .map(|x| x.0.as_str()) + .collect_vec(), + &expanded_target + ))); + } + } + + let Some(basis_transforms) = basis_transforms else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) 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/qiskit.transpiler.passes. \ + BasisTranslator#translation-errors", + source_basis.iter().map(|x| x.0.as_str()).collect_vec(), + &new_target_basis + ))); + }; + + let instr_map: InstMap = compose_transforms(py, &basis_transforms, &source_basis, &dag)?; + let extra_inst_map: ExtraInstructionMap = qarg_local_basis_transforms + .iter() + .map(|(qarg, transform)| -> PyResult<_> { + Ok(( + qarg, + compose_transforms(py, transform, &qargs_local_source_basis[qarg], &dag)?, + )) + }) + .collect::>()?; + + let (out_dag, _) = apply_translation( + py, + &dag, + &new_target_basis, + &instr_map, + &extra_inst_map, + min_qubits, + &qargs_with_non_global_operation, + )?; + Ok(out_dag) +} + +/// Method that extracts all non-calibrated gate instances identifiers from a DAGCircuit. +fn extract_basis( + py: Python, + circuit: &DAGCircuit, + min_qubits: usize, +) -> PyResult> { + let mut basis = HashSet::default(); + // Recurse for DAGCircuit + fn recurse_dag( + py: Python, + circuit: &DAGCircuit, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + for node in circuit.op_nodes(true) { + let operation: &PackedInstruction = circuit.dag()[node].unwrap_operation(); + if !circuit.has_calibration_for_index(py, node)? + && circuit.get_qargs(operation.qubits).len() >= min_qubits + { + basis.insert((operation.op.name().to_string(), operation.op.num_qubits())); + } + if operation.op.control_flow() { + let OperationRef::Instruction(inst) = operation.op.view() else { + unreachable!("Control flow operation is not an instance of PyInstruction.") + }; + let inst_bound = inst.instruction.bind(py); + for block in inst_bound.getattr("blocks")?.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + // Recurse for QuantumCircuit + fn recurse_circuit( + py: Python, + circuit: Bound, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + let circuit_data: PyRef = circuit + .getattr(intern!(py, "_data"))? + .downcast_into()? + .borrow(); + for (index, inst) in circuit_data.iter().enumerate() { + let instruction_object = circuit.get_item(index)?; + let has_calibration = circuit + .call_method1(intern!(py, "_has_calibration_for"), (&instruction_object,))?; + if !has_calibration.is_truthy()? + && circuit_data.get_qargs(inst.qubits).len() >= min_qubits + { + basis.insert((inst.op.name().to_string(), inst.op.num_qubits())); + } + if inst.op.control_flow() { + let operation_ob = instruction_object.getattr(intern!(py, "operation"))?; + let blocks = operation_ob.getattr("blocks")?; + for block in blocks.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + recurse_dag(py, circuit, &mut basis, min_qubits)?; + Ok(basis) +} + +/// Method that extracts a mapping of all the qargs in the local_source basis +/// obtained from the [Target], to all non-calibrated gate instances identifiers from a DAGCircuit. +/// When dealing with `ControlFlowOp` instances the function will perform a recursion call +/// to a variant design to handle instances of `QuantumCircuit`. +fn extract_basis_target( + py: Python, + dag: &DAGCircuit, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + min_qubits: usize, + qargs_with_non_global_operation: &HashMap, HashSet>, +) -> PyResult<()> { + for node in dag.op_nodes(true) { + let node_obj: &PackedInstruction = dag.dag()[node].unwrap_operation(); + let qargs: &[Qubit] = dag.get_qargs(node_obj.qubits); + if dag.has_calibration_for_index(py, node)? || qargs.len() < min_qubits { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if qargs_with_non_global_operation.contains_key(&Some(physical_qargs)) + || qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!("Control flow op is not a control flow op. But control_flow is `true`") + }; + let bound_inst = op.instruction.bind(py); + // Use python side extraction instead of the Rust method `op.blocks` due to + // required usage of a python-space method `QuantumCircuit.has_calibration_for`. + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + min_qubits, + qargs_with_non_global_operation, + )?; + } + } + } + Ok(()) +} + +/// Variant of extract_basis_target that takes an instance of QuantumCircuit. +/// This needs to use a Python instance of `QuantumCircuit` due to it needing +/// to access `has_calibration_for()` which is unavailable through rust. However, +/// this API will be removed with the deprecation of `Pulse`. +fn extract_basis_target_circ( + circuit: &Bound, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + min_qubits: usize, + qargs_with_non_global_operation: &HashMap, HashSet>, +) -> PyResult<()> { + let py = circuit.py(); + let circ_data_bound = circuit.getattr("_data")?.downcast_into::()?; + let circ_data = circ_data_bound.borrow(); + for (index, node_obj) in circ_data.iter().enumerate() { + let qargs = circ_data.get_qargs(node_obj.qubits); + if circuit + .call_method1("_has_calibration_for", (circuit.get_item(index)?,))? + .is_truthy()? + || qargs.len() < min_qubits + { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if qargs_with_non_global_operation.contains_key(&Some(physical_qargs)) + || qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!("Control flow op is not a control flow op. But control_flow is `true`") + }; + let bound_inst = op.instruction.bind(py); + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + min_qubits, + qargs_with_non_global_operation, + )?; + } + } + } + Ok(()) +} + +fn apply_translation( + py: Python, + dag: &DAGCircuit, + target_basis: &HashSet, + instr_map: &InstMap, + extra_inst_map: &ExtraInstructionMap, + min_qubits: usize, + qargs_with_non_global_operation: &HashMap, HashSet>, +) -> PyResult<(DAGCircuit, bool)> { + let mut is_updated = false; + let mut out_dag = dag.copy_empty_like(py, "alike")?; + for node in dag.topological_op_nodes()? { + let node_obj = dag.dag()[node].unwrap_operation(); + let node_qarg = dag.get_qargs(node_obj.qubits); + let node_carg = dag.get_cargs(node_obj.clbits); + let qubit_set: HashSet = HashSet::from_iter(node_qarg.iter().copied()); + let mut new_op: Option = None; + if target_basis.contains(node_obj.op.name()) || node_qarg.len() < min_qubits { + if node_obj.op.control_flow() { + let OperationRef::Instruction(control_op) = node_obj.op.view() else { + unreachable!("This instruction {} says it is of control flow type, but is not an Instruction instance", node_obj.op.name()) + }; + let mut flow_blocks = vec![]; + let bound_obj = control_op.instruction.bind(py); + let blocks = bound_obj.getattr("blocks")?; + for block in blocks.iter()? { + let block = block?; + let dag_block: DAGCircuit = + circuit_to_dag(py, block.extract()?, true, None, None)?; + let updated_dag: DAGCircuit; + (updated_dag, is_updated) = apply_translation( + py, + &dag_block, + target_basis, + instr_map, + extra_inst_map, + min_qubits, + qargs_with_non_global_operation, + )?; + let flow_circ_block = if is_updated { + DAG_TO_CIRCUIT + .get_bound(py) + .call1((updated_dag,))? + .extract()? + } else { + block + }; + flow_blocks.push(flow_circ_block); + } + let replaced_blocks = bound_obj.call_method1("replace_blocks", (flow_blocks,))?; + new_op = Some(replaced_blocks.extract()?); + } + if let Some(new_op) = new_op { + out_dag.apply_operation_back( + py, + new_op.operation, + node_qarg, + node_carg, + if new_op.params.is_empty() { + None + } else { + Some(new_op.params) + }, + new_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + } else { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + continue; + } + let node_qarg_as_physical: Option = + Some(node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect()); + if qargs_with_non_global_operation.contains_key(&node_qarg_as_physical) + && qargs_with_non_global_operation[&node_qarg_as_physical].contains(node_obj.op.name()) + { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + + if dag.has_calibration_for_index(py, node)? { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + let unique_qargs: Option = if qubit_set.is_empty() { + None + } else { + Some(qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect()) + }; + if extra_inst_map.contains_key(&unique_qargs) { + replace_node( + py, + &mut out_dag, + node_obj.clone(), + &extra_inst_map[&unique_qargs], + )?; + } else if instr_map + .contains_key(&(node_obj.op.name().to_string(), node_obj.op.num_qubits())) + { + replace_node(py, &mut out_dag, node_obj.clone(), instr_map)?; + } else { + return Err(TranspilerError::new_err(format!( + "BasisTranslator did not map {}", + node_obj.op.name() + ))); + } + is_updated = true; + } + + Ok((out_dag, is_updated)) +} + +fn replace_node( + py: Python, + dag: &mut DAGCircuit, + node: PackedInstruction, + instr_map: &HashMap, DAGCircuit)>, +) -> PyResult<()> { + let (target_params, target_dag) = + &instr_map[&(node.op.name().to_string(), node.op.num_qubits())]; + if node.params_view().len() != target_params.len() { + return Err(TranspilerError::new_err(format!( + "Translation num_params not equal to op num_params. \ + Op: {:?} {} Translation: {:?}\n{:?}", + node.params_view(), + node.op.name(), + &target_params, + &target_dag + ))); + } + if node.params_view().is_empty() { + for inner_index in target_dag.topological_op_nodes()? { + let inner_node = &target_dag.dag()[inner_index].unwrap_operation(); + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + if node.condition().is_some() { + match new_op.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr(py, "condition", node.condition())? + } + OperationRef::Instruction(inst) => { + inst.instruction + .setattr(py, "condition", node.condition())? + } + OperationRef::Operation(oper) => { + oper.operation.setattr(py, "condition", node.condition())? + } + _ => (), + } + } + let new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + let new_extra_props = node.extra_attrs.clone(); + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + new_extra_props, + #[cfg(feature = "cache_pygates")] + None, + )?; + } + dag.add_global_phase(py, target_dag.global_phase())?; + } else { + let parameter_map = target_params + .iter() + .zip(node.params_view()) + .into_py_dict_bound(py); + for inner_index in target_dag.topological_op_nodes()? { + let inner_node = &target_dag.dag()[inner_index].unwrap_operation(); + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + let mut new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + if inner_node + .params_view() + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))) + { + new_params = SmallVec::new(); + for param in inner_node.params_view() { + if let Param::ParameterExpression(param_obj) = param { + let bound_param = param_obj.bind(py); + let exp_params = param.iter_parameters(py)?; + let bind_dict = PyDict::new_bound(py); + for key in exp_params { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_value: Bound; + let comparison = bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }); + if comparison { + new_value = bound_param.clone(); + for items in bind_dict.items() { + new_value = new_value.call_method1( + intern!(py, "assign"), + items.downcast::()?, + )?; + } + } else { + new_value = + bound_param.call_method1(intern!(py, "bind"), (&bind_dict,))?; + } + let eval = new_value.getattr(intern!(py, "parameters"))?; + if eval.is_empty()? { + new_value = new_value.call_method0(intern!(py, "numeric"))?; + } + new_params.push(new_value.extract()?); + } else { + new_params.push(param.clone_ref(py)); + } + } + if new_op.try_standard_gate().is_none() { + match new_op.view() { + OperationRef::Instruction(inst) => inst + .instruction + .bind(py) + .setattr("params", new_params.clone())?, + OperationRef::Gate(gate) => { + gate.gate.bind(py).setattr("params", new_params.clone())? + } + OperationRef::Operation(oper) => oper + .operation + .bind(py) + .setattr("params", new_params.clone())?, + _ => (), + } + } + } + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + inner_node.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + + if let Param::ParameterExpression(old_phase) = target_dag.global_phase() { + let bound_old_phase = old_phase.bind(py); + let bind_dict = PyDict::new_bound(py); + for key in target_dag.global_phase().iter_parameters(py)? { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_phase: Bound; + if bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }) { + new_phase = bound_old_phase.clone(); + for key_val in bind_dict.items() { + new_phase = + new_phase.call_method1(intern!(py, "assign"), key_val.downcast()?)?; + } + } else { + new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?; + } + if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? { + new_phase = new_phase.call_method0(intern!(py, "numeric"))?; + if new_phase.is_instance(&PyComplex::type_object_bound(py))? { + return Err(TranspilerError::new_err(format!( + "Global phase must be real, but got {}", + new_phase.repr()? + ))); + } + } + let new_phase: Param = new_phase.extract()?; + dag.add_global_phase(py, &new_phase)?; + } + } + + Ok(()) +} + #[pymodule] pub fn basis_translator(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(basis_search::py_basis_search))?; - m.add_wrapped(wrap_pyfunction!(compose_transforms::py_compose_transforms))?; + m.add_wrapped(wrap_pyfunction!(run))?; Ok(()) } diff --git a/crates/accelerate/src/circuit_library/blocks.rs b/crates/accelerate/src/circuit_library/blocks.rs new file mode 100644 index 000000000000..80add611abb1 --- /dev/null +++ b/crates/accelerate/src/circuit_library/blocks.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::*, + types::{PyList, PyTuple}, +}; +use qiskit_circuit::{ + circuit_instruction::OperationFromPython, + operations::{Operation, Param, StandardGate}, + packed_instruction::PackedOperation, +}; +use smallvec::SmallVec; + +use crate::{circuit_library::entanglement::get_entanglement, QiskitError}; + +#[derive(Debug, Clone)] +pub enum BlockOperation { + Standard { gate: StandardGate }, + PyCustom { builder: Py }, +} + +impl BlockOperation { + pub fn assign_parameters( + &self, + py: Python, + params: &[&Param], + ) -> PyResult<(PackedOperation, SmallVec<[Param; 3]>)> { + match self { + Self::Standard { gate } => Ok(( + (*gate).into(), + SmallVec::from_iter(params.iter().map(|&p| p.clone())), + )), + Self::PyCustom { builder } => { + // the builder returns a Python operation plus the bound parameters + let py_params = + PyList::new_bound(py, params.iter().map(|&p| p.clone().into_py(py))).into_any(); + + let job = builder.call1(py, (py_params,))?; + let result = job.downcast_bound::(py)?; + + let operation: OperationFromPython = result.get_item(0)?.extract()?; + let bound_params = result + .get_item(1)? + .iter()? + .map(|ob| Param::extract_no_coerce(&ob?)) + .collect::>>()?; + + Ok((operation.operation, bound_params)) + } + } + } +} + +#[derive(Debug, Clone)] +#[pyclass] +pub struct Block { + pub operation: BlockOperation, + pub num_qubits: u32, + pub num_parameters: usize, +} + +#[pymethods] +impl Block { + #[staticmethod] + #[pyo3(signature = (gate,))] + pub fn from_standard_gate(gate: StandardGate) -> Self { + Block { + operation: BlockOperation::Standard { gate }, + num_qubits: gate.num_qubits(), + num_parameters: gate.num_params() as usize, + } + } + + #[staticmethod] + #[pyo3(signature = (num_qubits, num_parameters, builder,))] + pub fn from_callable( + py: Python, + num_qubits: i64, + num_parameters: i64, + builder: &Bound, + ) -> PyResult { + if !builder.is_callable() { + return Err(QiskitError::new_err( + "builder must be a callable: parameters->(bound gate, bound gate params)", + )); + } + let block = Block { + operation: BlockOperation::PyCustom { + builder: builder.to_object(py), + }, + num_qubits: num_qubits as u32, + num_parameters: num_parameters as usize, + }; + + Ok(block) + } +} + +// We introduce typedefs to make the types more legible. We can understand the hierarchy +// as follows: +// Connection: Vec -- indices that the multi-qubit gate acts on +// BlockEntanglement: Vec -- entanglement for single block +// LayerEntanglement: Vec -- entanglements for all blocks in the layer +// Entanglement: Vec -- entanglement for every layer +type BlockEntanglement = Vec>; +pub(super) type LayerEntanglement = Vec; + +/// Represent the entanglement in an n-local circuit. +pub struct Entanglement { + // Possible optimization in future: This eagerly expands the full entanglement for every layer. + // This could be done more efficiently, e.g., by creating entanglement objects that store + // their underlying representation (e.g. a string or a list of connections) and returning + // these when given a layer-index. + entanglement_vec: Vec, +} + +impl Entanglement { + /// Create an entanglement from the input of an n_local circuit. + pub fn from_py( + num_qubits: u32, + reps: usize, + entanglement: &Bound, + entanglement_blocks: &[&Block], + ) -> PyResult { + let entanglement_vec = (0..reps) + .map(|layer| -> PyResult { + if entanglement.is_callable() { + let as_any = entanglement.call1((layer,))?; + let as_list = as_any.downcast::()?; + unpack_entanglement(num_qubits, layer, as_list, entanglement_blocks) + } else { + let as_list = entanglement.downcast::()?; + unpack_entanglement(num_qubits, layer, as_list, entanglement_blocks) + } + }) + .collect::>()?; + + Ok(Self { entanglement_vec }) + } + + pub fn get_layer(&self, layer: usize) -> &LayerEntanglement { + &self.entanglement_vec[layer] + } + + pub fn iter(&self) -> impl Iterator { + self.entanglement_vec.iter() + } +} + +fn unpack_entanglement( + num_qubits: u32, + layer: usize, + entanglement: &Bound, + entanglement_blocks: &[&Block], +) -> PyResult { + entanglement_blocks + .iter() + .zip(entanglement.iter()) + .map(|(block, ent)| -> PyResult>> { + get_entanglement(num_qubits, block.num_qubits, &ent, layer)?.collect() + }) + .collect() +} diff --git a/crates/accelerate/src/circuit_library/entanglement.rs b/crates/accelerate/src/circuit_library/entanglement.rs index fbfb5c0193f1..2168414cc4b0 100644 --- a/crates/accelerate/src/circuit_library/entanglement.rs +++ b/crates/accelerate/src/circuit_library/entanglement.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use pyo3::prelude::*; use pyo3::types::PyDict; use pyo3::{ - types::{PyAnyMethods, PyInt, PyList, PyListMethods, PyString, PyTuple}, + types::{PyAnyMethods, PyList, PyListMethods, PyString, PyTuple}, Bound, PyAny, PyResult, }; @@ -194,12 +194,7 @@ fn _check_entanglement_list<'a>( block_size: u32, ) -> PyResult>> + 'a>> { let entanglement_iter = list.iter().map(move |el| { - let connections = el - .downcast::()? - // .expect("Entanglement must be list of tuples") // clearer error message than `?` - .iter() - .map(|index| index.downcast::()?.extract()) - .collect::, _>>()?; + let connections: Vec = el.extract()?; if connections.len() != block_size as usize { return Err(QiskitError::new_err(format!( diff --git a/crates/accelerate/src/circuit_library/iqp.rs b/crates/accelerate/src/circuit_library/iqp.rs new file mode 100644 index 000000000000..4cb931f8c228 --- /dev/null +++ b/crates/accelerate/src/circuit_library/iqp.rs @@ -0,0 +1,165 @@ +// 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 ndarray::{Array2, ArrayView2}; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use qiskit_circuit::{ + circuit_data::CircuitData, + operations::{Param, StandardGate}, + Qubit, +}; +use rand::{Rng, SeedableRng}; +use rand_pcg::Pcg64Mcg; +use smallvec::{smallvec, SmallVec}; + +use crate::CircuitError; + +const PI2: f64 = PI / 2.0; +const PI8: f64 = PI / 8.0; + +fn iqp( + interactions: ArrayView2, +) -> impl Iterator, SmallVec<[Qubit; 2]>)> + '_ { + let num_qubits = interactions.ncols(); + + // The initial and final Hadamard layer. + let h_layer = + (0..num_qubits).map(|i| (StandardGate::HGate, smallvec![], smallvec![Qubit(i as u32)])); + + // The circuit interactions are powers of the CSGate, which is implemented by calling + // the CPhaseGate with angles of Pi/2 times the power. The gate powers are given by the + // upper triangular part of the symmetric ``interactions`` matrix. + let connections = (0..num_qubits).flat_map(move |i| { + (i + 1..num_qubits) + .map(move |j| (j, interactions[(i, j)])) + .filter(move |(_, value)| value % 4 != 0) + .map(move |(j, value)| { + ( + StandardGate::CPhaseGate, + smallvec![Param::Float(PI2 * value as f64)], + smallvec![Qubit(i as u32), Qubit(j as u32)], + ) + }) + }); + + // The layer of T gates. Again we use the PhaseGate, now with powers of Pi/8. The powers + // are given by the diagonal of the ``interactions`` matrix. + let shifts = (0..num_qubits) + .map(move |i| interactions[(i, i)]) + .enumerate() + .filter(|(_, value)| value % 8 != 0) + .map(|(i, value)| { + ( + StandardGate::PhaseGate, + smallvec![Param::Float(PI8 * value as f64)], + smallvec![Qubit(i as u32)], + ) + }); + + h_layer + .clone() + .chain(connections) + .chain(shifts) + .chain(h_layer) +} + +/// This generates a random symmetric integer matrix with values in [0,7]. +fn generate_random_interactions(num_qubits: u32, seed: Option) -> Array2 { + let num_qubits = num_qubits as usize; + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + + let mut mat = Array2::zeros((num_qubits, num_qubits)); + for i in 0..num_qubits { + mat[[i, i]] = rng.gen_range(0..8) as i64; + for j in 0..i { + mat[[i, j]] = rng.gen_range(0..8) as i64; + mat[[j, i]] = mat[[i, j]]; + } + } + mat +} + +/// Returns true if the input matrix is symmetric, otherwise false. +fn check_symmetric(matrix: &ArrayView2) -> bool { + let nrows = matrix.nrows(); + + if matrix.ncols() != nrows { + return false; + } + + for i in 0..nrows { + for j in i + 1..nrows { + if matrix[(i, j)] != matrix[(j, i)] { + return false; + } + } + } + + true +} + +/// Implement an Instantaneous Quantum Polynomial time (IQP) circuit. +/// +/// This class of circuits is conjectured to be classically hard to simulate, +/// forming a generalization of the Boson sampling problem. See Ref. [1] for +/// more details. +/// +/// Args: +/// interactions: If provided, this is a symmetric square matrix of width ``num_qubits``, +/// determining the operations in the IQP circuit. The diagonal represents the power +/// of single-qubit T gates and the upper triangular part the power of CS gates +/// in between qubit pairs. If None, a random interactions matrix will be sampled. +/// +/// Returns: +/// The IQP circuit. +/// +/// References: +/// +/// [1] M. J. Bremner et al. Average-case complexity versus approximate simulation of +/// commuting quantum computations, Phys. Rev. Lett. 117, 080501 (2016). +/// `arXiv:1504.07999 `_ +#[pyfunction] +#[pyo3(signature = (interactions))] +pub fn py_iqp(py: Python, interactions: PyReadonlyArray2) -> PyResult { + let array = interactions.as_array(); + let view = array.view(); + if !check_symmetric(&view) { + return Err(CircuitError::new_err("IQP matrix must be symmetric.")); + } + + let num_qubits = view.ncols() as u32; + let instructions = iqp(view); + CircuitData::from_standard_gates(py, num_qubits, instructions, Param::Float(0.0)) +} + +/// Generate a random Instantaneous Quantum Polynomial time (IQP) circuit. +/// +/// Args: +/// num_qubits: The number of qubits. +/// seed: A random seed for generating the interactions matrix. +/// +/// Returns: +/// A random IQP circuit. +#[pyfunction] +#[pyo3(signature = (num_qubits, seed=None))] +pub fn py_random_iqp(py: Python, num_qubits: u32, seed: Option) -> PyResult { + let interactions = generate_random_interactions(num_qubits, seed); + let view = interactions.view(); + let instructions = iqp(view); + CircuitData::from_standard_gates(py, num_qubits, instructions, Param::Float(0.0)) +} diff --git a/crates/accelerate/src/circuit_library/mod.rs b/crates/accelerate/src/circuit_library/mod.rs index bc960bab2aab..2e087b442a9f 100644 --- a/crates/accelerate/src/circuit_library/mod.rs +++ b/crates/accelerate/src/circuit_library/mod.rs @@ -12,13 +12,24 @@ use pyo3::prelude::*; +mod blocks; mod entanglement; +mod iqp; +mod multi_local; +mod parameter_ledger; +mod pauli_evolution; mod pauli_feature_map; mod quantum_volume; pub fn circuit_library(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(pauli_evolution::py_pauli_evolution))?; m.add_wrapped(wrap_pyfunction!(pauli_feature_map::pauli_feature_map))?; m.add_wrapped(wrap_pyfunction!(entanglement::get_entangler_map))?; + m.add_wrapped(wrap_pyfunction!(iqp::py_iqp))?; + m.add_wrapped(wrap_pyfunction!(iqp::py_random_iqp))?; m.add_wrapped(wrap_pyfunction!(quantum_volume::quantum_volume))?; + m.add_wrapped(wrap_pyfunction!(multi_local::py_n_local))?; + m.add_class::()?; + Ok(()) } diff --git a/crates/accelerate/src/circuit_library/multi_local.rs b/crates/accelerate/src/circuit_library/multi_local.rs new file mode 100644 index 000000000000..dd75ebbcc5e8 --- /dev/null +++ b/crates/accelerate/src/circuit_library/multi_local.rs @@ -0,0 +1,317 @@ +// 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::Deref; + +use hashbrown::HashSet; +use pyo3::prelude::*; +use pyo3::types::PyString; +use qiskit_circuit::packed_instruction::PackedOperation; +use smallvec::{smallvec, SmallVec}; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, PyInstruction}; +use qiskit_circuit::{imports, Clbit, Qubit}; + +use itertools::izip; + +use super::blocks::{Block, Entanglement, LayerEntanglement}; +use super::parameter_ledger::{LayerParameters, LayerType, ParameterLedger}; + +type Instruction = ( + PackedOperation, + SmallVec<[Param; 3]>, + Vec, + Vec, +); + +/// Construct a rotation layer. +/// +/// # Arguments +/// +/// - `num_qubits`: The number of qubits in the circuit. +/// - `rotation_blocks`: A reference to a vector containing the instructions to insert. +/// This is a vector (sind we can have multiple rotations operations per layer), with +/// 3-tuple elements containing (packed_operation, num_qubits, num_params). +/// - `parameters`: The set of parameter objects to use for the operations. This is a 3x nested +/// vector, organized as operation -> block -> param. That means that for operation `i` +/// and block `j`, the parameters are given by `parameters[i][j]`. +/// - skipped_qubits: A hash-set containing which qubits are skipped in the rotation layer. +/// +/// # Returns +/// +/// An iterator for the rotation instructions. +fn rotation_layer<'a>( + py: Python<'a>, + num_qubits: u32, + rotation_blocks: &'a [&'a Block], + parameters: Vec>>, + skipped_qubits: &'a HashSet, +) -> impl Iterator> + 'a { + rotation_blocks + .iter() + .zip(parameters) + .flat_map(move |(block, block_params)| { + (0..num_qubits) + .step_by(block.num_qubits as usize) + .filter(move |start_idx| { + skipped_qubits.is_disjoint(&HashSet::from_iter( + *start_idx..(*start_idx + block.num_qubits), + )) + }) + .zip(block_params) + .map(move |(start_idx, params)| { + let (bound_op, bound_params) = block + .operation + .assign_parameters(py, ¶ms) + .expect("Failed to rebind"); + Ok(( + bound_op, + bound_params, + (0..block.num_qubits) + .map(|i| Qubit(start_idx + i)) + .collect(), + vec![] as Vec, + )) + }) + }) +} + +/// Construct an entanglement layer. +/// +/// # Arguments +/// +/// - `entanglement`: The entanglement structure in this layer. Given as 3x nested vector, which +/// for each entanglement block contains a vector of connections, where each connection +/// is a vector of indices. +/// - `entanglement_blocks`: A reference to a vector containing the instructions to insert. +/// This is a vector (sind we can have multiple entanglement operations per layer), with +/// 3-tuple elements containing (packed_operation, num_qubits, num_params). +/// - `parameters`: The set of parameter objects to use for the operations. This is a 3x nested +/// vector, organized as operation -> block -> param. That means that for operation `i` +/// and block `j`, the parameters are given by `parameters[i][j]`. +/// +/// # Returns +/// +/// An iterator for the entanglement instructions. +fn entanglement_layer<'a>( + py: Python<'a>, + entanglement: &'a LayerEntanglement, + entanglement_blocks: &'a [&'a Block], + parameters: LayerParameters<'a>, +) -> impl Iterator> + 'a { + let zipped = izip!(entanglement_blocks, parameters, entanglement); + zipped.flat_map(move |(block, block_params, block_entanglement)| { + block_entanglement + .iter() + .zip(block_params) + .map(move |(indices, params)| { + let (bound_op, bound_params) = block + .operation + .assign_parameters(py, ¶ms) + .expect("Failed to rebind"); + Ok(( + bound_op, + bound_params, + indices.iter().map(|i| Qubit(*i)).collect(), + vec![] as Vec, + )) + }) + }) +} + +/// # Arguments +/// +/// - `num_qubits`: The number of qubits of the circuit. +/// - `rotation_blocks`: The blocks used in the rotation layers. If multiple are passed, +/// these will be applied one after another (like new sub-layers). +/// - `entanglement_blocks`: The blocks used in the entanglement layers. If multiple are passed, +/// these will be applied one after another. +/// - `entanglement`: The indices specifying on which qubits the input blocks act. This is +/// specified by string describing an entanglement strategy (see the additional info) +/// or a list of qubit connections. +/// If a list of entanglement blocks is passed, different entanglement for each block can +/// be specified by passing a list of entanglements. To specify varying entanglement for +/// each repetition, pass a callable that takes as input the layer and returns the +/// entanglement for that layer. +/// Defaults to ``"full"``, meaning an all-to-all entanglement structure. +/// - `reps`: Specifies how often the rotation blocks and entanglement blocks are repeated. +/// - `insert_barriers`: If ``True``, barriers are inserted in between each layer. If ``False``, +/// no barriers are inserted. +/// - `parameter_prefix`: The prefix used if default parameters are generated. +/// - `skip_final_rotation_layer`: Whether a final rotation layer is added to the circuit. +/// - `skip_unentangled_qubits`: If ``True``, the rotation gates act only on qubits that +/// are entangled. If ``False``, the rotation gates act on all qubits. +/// +/// # Returns +/// +/// An N-local circuit. +#[allow(clippy::too_many_arguments)] +pub fn n_local( + py: Python, + num_qubits: u32, + rotation_blocks: &[&Block], + entanglement_blocks: &[&Block], + entanglement: &Entanglement, + reps: usize, + insert_barriers: bool, + parameter_prefix: &String, + skip_final_rotation_layer: bool, + skip_unentangled_qubits: bool, +) -> PyResult { + // Construct the parameter ledger, which will define all free parameters and provide + // access to them, given an index for a layer and the current gate to implement. + let ledger = ParameterLedger::from_nlocal( + py, + num_qubits, + reps, + entanglement, + rotation_blocks, + entanglement_blocks, + skip_final_rotation_layer, + parameter_prefix, + )?; + + // Compute the qubits that are skipped in the rotation layer. If this is set, + // we skip qubits that do not appear in any of the entanglement layers. + let skipped_qubits = if skip_unentangled_qubits { + let active: HashSet<&u32> = + HashSet::from_iter(entanglement.iter().flatten().flatten().flatten()); + HashSet::from_iter((0..num_qubits).filter(|i| !active.contains(i))) + } else { + HashSet::new() + }; + + // This struct can be used to yield barrier if insert_barriers is true, otherwise + // it returns an empty iterator. For conveniently injecting barriers in-between operations. + let maybe_barrier = MaybeBarrier::new(py, num_qubits, insert_barriers)?; + + let packed_insts = (0..reps).flat_map(|layer| { + rotation_layer( + py, + num_qubits, + rotation_blocks, + ledger.get_parameters(LayerType::Rotation, layer), + &skipped_qubits, + ) + .chain(maybe_barrier.get()) + .chain(entanglement_layer( + py, + entanglement.get_layer(layer), + entanglement_blocks, + ledger.get_parameters(LayerType::Entangle, layer), + )) + .chain(maybe_barrier.get()) + }); + if !skip_final_rotation_layer { + let packed_insts = packed_insts.chain(rotation_layer( + py, + num_qubits, + rotation_blocks, + ledger.get_parameters(LayerType::Rotation, reps), + &skipped_qubits, + )); + CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) + } else { + CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) + } +} + +#[pyfunction] +#[pyo3(signature = (num_qubits, rotation_blocks, entanglement_blocks, entanglement, reps, insert_barriers, parameter_prefix, skip_final_rotation_layer, skip_unentangled_qubits))] +#[allow(clippy::too_many_arguments)] +pub fn py_n_local( + py: Python, + num_qubits: u32, + rotation_blocks: Vec>, + entanglement_blocks: Vec>, + entanglement: &Bound, + reps: usize, + insert_barriers: bool, + parameter_prefix: &Bound, + skip_final_rotation_layer: bool, + skip_unentangled_qubits: bool, +) -> PyResult { + // Normalize the Python data. + let parameter_prefix = parameter_prefix.to_string(); + let rotation_blocks: Vec<&Block> = rotation_blocks + .iter() + .map(|py_block| py_block.deref()) + .collect(); + let entanglement_blocks: Vec<&Block> = entanglement_blocks + .iter() + .map(|py_block| py_block.deref()) + .collect(); + + // Expand the entanglement. This will (currently) eagerly expand the entanglement for each + // circuit layer. + let entanglement = Entanglement::from_py(num_qubits, reps, entanglement, &entanglement_blocks)?; + + n_local( + py, + num_qubits, + &rotation_blocks, + &entanglement_blocks, + &entanglement, + reps, + insert_barriers, + ¶meter_prefix, + skip_final_rotation_layer, + skip_unentangled_qubits, + ) +} + +/// A convenient struct to optionally yield barriers to inject in-between circuit layers. +/// +/// If constructed with ``insert_barriers=false``, then the method ``.get`` yields empty iterators, +/// otherwise it will yield a barrier. This is a struct such that the call to Python that +/// creates the Barrier object can be done a single time, but barriers can be yielded multiple times. +struct MaybeBarrier { + barrier: Option, +} + +impl MaybeBarrier { + fn new(py: Python, num_qubits: u32, insert_barriers: bool) -> PyResult { + if !insert_barriers { + Ok(Self { barrier: None }) + } else { + let barrier_cls = imports::BARRIER.get_bound(py); + let py_barrier = barrier_cls.call1((num_qubits,))?; + let py_inst = PyInstruction { + qubits: num_qubits, + clbits: 0, + params: 0, + op_name: "barrier".to_string(), + control_flow: false, + instruction: py_barrier.into(), + }; + + let inst = ( + py_inst.into(), + smallvec![], + (0..num_qubits).map(Qubit).collect(), + vec![] as Vec, + ); + + Ok(Self { + barrier: Some(inst), + }) + } + } + + fn get(&self) -> Box>> { + match &self.barrier { + None => Box::new(std::iter::empty()), + Some(inst) => Box::new(std::iter::once(Ok(inst.clone()))), + } + } +} diff --git a/crates/accelerate/src/circuit_library/parameter_ledger.rs b/crates/accelerate/src/circuit_library/parameter_ledger.rs new file mode 100644 index 000000000000..457034850196 --- /dev/null +++ b/crates/accelerate/src/circuit_library/parameter_ledger.rs @@ -0,0 +1,174 @@ +// 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 qiskit_circuit::{imports, operations::Param}; + +use super::blocks::{Block, Entanglement}; + +/// Enum to determine the type of circuit layer. +pub(super) enum LayerType { + Rotation, + Entangle, +} + +type BlockParameters<'a> = Vec>; // parameters for each gate in the block +pub(super) type LayerParameters<'a> = Vec>; // parameter in a layer + +/// The ParameterLedger stores the parameter objects contained in the n-local circuit. +/// +/// Internally, the parameters are stored in a 1-D vector and the ledger keeps track of +/// which indices belong to which layer. For example, a 2-qubit circuit where both the +/// rotation and entanglement layer have 1 block with 2 parameters each, we would store +/// +/// [x0 x1 x2 x3 x4 x5 x6 x7 ....] +/// ----- ----- ----- ----- +/// rep0 rep0 rep1 rep2 +/// rot ent rot ent +/// +/// This allows accessing the parameters by index of the rotation or entanglement layer by means +/// of the ``get_parameters`` method, e.g. as +/// +/// let layer: usize = 4; +/// let params_in_that_layer: LayerParameters = +/// ledger.get_parameter(LayerType::Rotation, layer); +/// +pub(super) struct ParameterLedger { + parameter_vector: Vec, // all parameters + rotation_indices: Vec, // indices where rotation blocks start + entangle_indices: Vec, + rotations: Vec<(u32, usize)>, // (#blocks per layer, #params per block) for each block + entanglements: Vec>, // this might additionally change per layer +} + +impl ParameterLedger { + /// Initialize the ledger n-local input data. This will call Python to create a new + /// ``ParameterVector`` of adequate size and compute all required indices to access + /// parameter of a specific layer. + #[allow(clippy::too_many_arguments)] + pub(super) fn from_nlocal( + py: Python, + num_qubits: u32, + reps: usize, + entanglement: &Entanglement, + rotation_blocks: &[&Block], + entanglement_blocks: &[&Block], + skip_final_rotation_layer: bool, + parameter_prefix: &String, + ) -> PyResult { + // if we keep the final layer (i.e. skip=false), add parameters on the final layer + let final_layer_rep = match skip_final_rotation_layer { + true => 0, + false => 1, + }; + + // compute the number of parameters used for the rotation layers + let mut num_rotation_params_per_layer: usize = 0; + let mut rotations: Vec<(u32, usize)> = Vec::new(); + + for block in rotation_blocks { + let num_blocks = num_qubits / block.num_qubits; + rotations.push((num_blocks, block.num_parameters)); + num_rotation_params_per_layer += (num_blocks as usize) * block.num_parameters; + } + + // compute the number of parameters used for the entanglement layers + let mut num_entangle_params_per_layer: Vec = Vec::with_capacity(reps); + let mut entanglements: Vec> = Vec::with_capacity(reps); + for this_entanglement in entanglement.iter() { + let mut this_entanglements: Vec<(u32, usize)> = Vec::new(); + let mut this_num_params: usize = 0; + for (block, block_entanglement) in entanglement_blocks.iter().zip(this_entanglement) { + let num_blocks = block_entanglement.len(); + this_num_params += num_blocks * block.num_parameters; + this_entanglements.push((num_blocks as u32, block.num_parameters)); + } + num_entangle_params_per_layer.push(this_num_params); + entanglements.push(this_entanglements); + } + + let num_rotation_params: usize = (reps + final_layer_rep) * num_rotation_params_per_layer; + let num_entangle_params: usize = num_entangle_params_per_layer.iter().sum(); + + // generate a ParameterVector Python-side, containing all parameters, and then + // map it onto Rust-space parameters + let num_parameters = num_rotation_params + num_entangle_params; + let parameter_vector: Vec = imports::PARAMETER_VECTOR + .get_bound(py) + .call1((parameter_prefix, num_parameters))? // get the Python ParameterVector + .iter()? // iterate over the elements and cast them to Rust Params + .map(|ob| Param::extract_no_coerce(&ob?)) + .collect::>()?; + + // finally, distribute the parameters onto the repetitions and blocks for each + // rotation layer and entanglement layer + let mut rotation_indices: Vec = Vec::with_capacity(reps + final_layer_rep); + let mut entangle_indices: Vec = Vec::with_capacity(reps); + let mut index: usize = 0; + for num_entangle in num_entangle_params_per_layer { + rotation_indices.push(index); + index += num_rotation_params_per_layer; + entangle_indices.push(index); + index += num_entangle; + } + if !skip_final_rotation_layer { + rotation_indices.push(index); + } + + Ok(ParameterLedger { + parameter_vector, + rotation_indices, + entangle_indices, + rotations, + entanglements, + }) + } + + /// Get the parameters in the rotation or entanglement layer. + pub(super) fn get_parameters(&self, kind: LayerType, layer: usize) -> LayerParameters { + let (mut index, blocks) = match kind { + LayerType::Rotation => ( + *self + .rotation_indices + .get(layer) + .expect("Out of bounds in rotation_indices."), + &self.rotations, + ), + LayerType::Entangle => ( + *self + .entangle_indices + .get(layer) + .expect("Out of bounds in entangle_indices."), + &self.entanglements[layer], + ), + }; + + let mut parameters: LayerParameters = Vec::new(); + for (num_blocks, num_params) in blocks { + let mut per_block: BlockParameters = Vec::new(); + for _ in 0..*num_blocks { + let gate_params: Vec<&Param> = (index..index + num_params) + .map(|i| { + self.parameter_vector + .get(i) + .expect("Ran out of parameters!") + }) + .collect(); + index += num_params; + per_block.push(gate_params); + } + parameters.push(per_block); + } + + parameters + } +} diff --git a/crates/accelerate/src/circuit_library/pauli_evolution.rs b/crates/accelerate/src/circuit_library/pauli_evolution.rs new file mode 100644 index 000000000000..3c5164314c08 --- /dev/null +++ b/crates/accelerate/src/circuit_library/pauli_evolution.rs @@ -0,0 +1,351 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::prelude::*; +use pyo3::types::{PyList, PyString, PyTuple}; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{multiply_param, radd_param, Param, PyInstruction, StandardGate}; +use qiskit_circuit::packed_instruction::PackedOperation; +use qiskit_circuit::{imports, Clbit, Qubit}; +use smallvec::{smallvec, SmallVec}; + +// custom types for a more readable code +type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>); +type Instruction = ( + PackedOperation, + SmallVec<[Param; 3]>, + Vec, + Vec, +); + +/// Return instructions (using only StandardGate operations) to implement a Pauli evolution +/// of a given Pauli string over a given time (as Param). +/// +/// Args: +/// pauli: The Pauli string, e.g. "IXYZ". +/// indices: The qubit indices the Pauli acts on, e.g. if given as [0, 1, 2, 3] with the +/// Pauli "IXYZ", then the correspondence is I_0 X_1 Y_2 Z_3. +/// time: The rotation angle. Note that this will directly be used as input of the +/// rotation gate and not be multiplied by a factor of 2 (that should be done before so +/// that this function can remain Rust-only). +/// phase_gate: If ``true``, use the ``PhaseGate`` instead of ``RZGate`` as single-qubit rotation. +/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each +/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between +/// neighboring qubits are used. +/// +/// Returns: +/// A pointer to an iterator over standard instructions. +pub fn pauli_evolution<'a>( + pauli: &'a str, + indices: Vec, + time: Param, + phase_gate: bool, + do_fountain: bool, +) -> Box + 'a> { + // ensure the Pauli has no identity terms + let binding = pauli.to_lowercase(); // lowercase for convenience + let active = binding + .as_str() + .chars() + .zip(indices) + .filter(|(pauli, _)| *pauli != 'i'); + let (paulis, indices): (Vec, Vec) = active.unzip(); + + match (phase_gate, indices.len()) { + (_, 0) => Box::new(std::iter::empty()), + (false, 1) => Box::new(single_qubit_evolution(paulis[0], indices[0], time)), + (false, 2) => two_qubit_evolution(paulis, indices, time), + _ => Box::new(multi_qubit_evolution( + paulis, + indices, + time, + phase_gate, + do_fountain, + )), + } +} + +/// Implement a single-qubit Pauli evolution of a Pauli given as char, on a given index and +/// for given time. Note that the time here equals the angle of the rotation and is not +/// multiplied by a factor of 2. +fn single_qubit_evolution( + pauli: char, + index: u32, + time: Param, +) -> impl Iterator { + let qubit: SmallVec<[Qubit; 2]> = smallvec![Qubit(index)]; + let param: SmallVec<[Param; 3]> = smallvec![time]; + + std::iter::once(match pauli { + 'x' => (StandardGate::RXGate, param, qubit), + 'y' => (StandardGate::RYGate, param, qubit), + 'z' => (StandardGate::RZGate, param, qubit), + _ => unreachable!("Unsupported Pauli, at this point we expected one of x, y, z."), + }) +} + +/// Implement a 2-qubit Pauli evolution of a Pauli string, on a given indices and +/// for given time. Note that the time here equals the angle of the rotation and is not +/// multiplied by a factor of 2. +/// +/// If possible, Qiskit's native 2-qubit Pauli rotations are used. Otherwise, the general +/// multi-qubit evolution is called. +fn two_qubit_evolution<'a>( + pauli: Vec, + indices: Vec, + time: Param, +) -> Box + 'a> { + let qubits: SmallVec<[Qubit; 2]> = smallvec![Qubit(indices[0]), Qubit(indices[1])]; + let param: SmallVec<[Param; 3]> = smallvec![time.clone()]; + let paulistring: String = pauli.iter().collect(); + + match paulistring.as_str() { + "xx" => Box::new(std::iter::once((StandardGate::RXXGate, param, qubits))), + "zx" => Box::new(std::iter::once((StandardGate::RZXGate, param, qubits))), + "yy" => Box::new(std::iter::once((StandardGate::RYYGate, param, qubits))), + "zz" => Box::new(std::iter::once((StandardGate::RZZGate, param, qubits))), + // Note: the CX modes (do_fountain=true/false) give the same circuit for a 2-qubit + // Pauli, so we just set it to false here + _ => Box::new(multi_qubit_evolution(pauli, indices, time, false, false)), + } +} + +/// Implement a multi-qubit Pauli evolution. See ``pauli_evolution`` detailed docs. +fn multi_qubit_evolution( + pauli: Vec, + indices: Vec, + time: Param, + phase_gate: bool, + do_fountain: bool, +) -> impl Iterator { + let active_paulis: Vec<(char, Qubit)> = pauli + .into_iter() + .zip(indices.into_iter().map(Qubit)) + .collect(); + + // get the basis change: x -> HGate, y -> SXdgGate, z -> nothing + let basis_change: Vec = active_paulis + .iter() + .filter(|(p, _)| *p != 'z') + .map(|(p, q)| match p { + 'x' => (StandardGate::HGate, smallvec![], smallvec![*q]), + 'y' => (StandardGate::SXGate, smallvec![], smallvec![*q]), + _ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out + }) + .collect(); + + // get the inverse basis change + let inverse_basis_change: Vec = basis_change + .iter() + .map(|(gate, _, qubit)| match gate { + StandardGate::HGate => (StandardGate::HGate, smallvec![], qubit.clone()), + StandardGate::SXGate => (StandardGate::SXdgGate, smallvec![], qubit.clone()), + _ => unreachable!("Invalid basis-changing Clifford."), + }) + .collect(); + + // get the CX propagation up to the first qubit, and down + let (chain_up, chain_down) = match do_fountain { + true => ( + cx_fountain(active_paulis.clone()), + cx_fountain(active_paulis.clone()).rev(), + ), + false => ( + cx_chain(active_paulis.clone()), + cx_chain(active_paulis.clone()).rev(), + ), + }; + + // get the RZ gate on the first qubit + let first_qubit = active_paulis.first().unwrap().1; + let z_rotation = std::iter::once(( + if phase_gate { + StandardGate::PhaseGate + } else { + StandardGate::RZGate + }, + smallvec![time], + smallvec![first_qubit], + )); + + // and finally chain everything together + basis_change + .into_iter() + .chain(chain_down) + .chain(z_rotation) + .chain(chain_up) + .chain(inverse_basis_change) +} + +/// Implement a Pauli evolution circuit. +/// +/// The Pauli evolution is implemented as a basis transformation to the Pauli-Z basis, +/// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain +/// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the +/// Pauli string XIYZ we have the circuit +/// ┌───┐┌───────┐┌───┐ +/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├─────────── +/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐ +/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├ +/// └──────┘└─┬─┘ └─┬─┘└────┘ +/// 2: ──────────┼───────────────────────┼──────── +/// ┌───┐ │ │ ┌───┐ +/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─ +/// └───┘ └───┘ +/// +/// Args: +/// num_qubits: The number of qubits in the Hamiltonian. +/// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements +/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form +/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as +/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4. +/// insert_barriers: If ``true``, insert a barrier in between the evolution of individual +/// Pauli terms. +/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each +/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between +/// neighboring qubits are used. +/// +/// Returns: +/// Circuit data for to implement the evolution. +#[pyfunction] +#[pyo3(name = "pauli_evolution", signature = (num_qubits, sparse_paulis, insert_barriers=false, do_fountain=false))] +pub fn py_pauli_evolution( + num_qubits: i64, + sparse_paulis: &Bound, + insert_barriers: bool, + do_fountain: bool, +) -> PyResult { + let py = sparse_paulis.py(); + let num_paulis = sparse_paulis.len(); + let mut paulis: Vec = Vec::with_capacity(num_paulis); + let mut indices: Vec> = Vec::with_capacity(num_paulis); + let mut times: Vec = Vec::with_capacity(num_paulis); + let mut global_phase = Param::Float(0.0); + let mut modified_phase = false; // keep track of whether we modified the phase + + for el in sparse_paulis.iter() { + let tuple = el.downcast::()?; + let pauli = tuple.get_item(0)?.downcast::()?.to_string(); + let time = Param::extract_no_coerce(&tuple.get_item(2)?)?; + + if pauli.as_str().chars().all(|p| p == 'i') { + global_phase = radd_param(global_phase, time, py); + modified_phase = true; + continue; + } + + paulis.push(pauli); + times.push(time); // note we do not multiply by 2 here, this is done Python side! + indices.push(tuple.get_item(1)?.extract::>()?) + } + + let barrier = get_barrier(py, num_qubits as u32); + + let evos = paulis.iter().enumerate().zip(indices).zip(times).flat_map( + |(((i, pauli), qubits), time)| { + let as_packed = pauli_evolution(pauli, qubits, time, false, do_fountain).map( + |(gate, params, qubits)| -> PyResult { + Ok((gate.into(), params, Vec::from_iter(qubits), Vec::new())) + }, + ); + + // this creates an iterator containing a barrier only if required, otherwise it is empty + let maybe_barrier = (insert_barriers && i < (num_paulis - 1)) + .then_some(Ok(barrier.clone())) + .into_iter(); + as_packed.chain(maybe_barrier) + }, + ); + + // When handling all-identity Paulis above, we added the time as global phase. + // However, the all-identity Paulis should add a negative phase, as they implement + // exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication, + // instead of doing it every time we find an all-identity Pauli. + if modified_phase { + global_phase = multiply_param(&global_phase, -1.0, py); + } + + CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase) +} + +/// Build a CX chain over the active qubits. E.g. with q_1 inactive, this would return +/// +/// ┌───┐ +/// q_0: ──────────┤ X ├ +/// └─┬─┘ +/// q_1: ────────────┼── +/// ┌───┐ │ +/// q_2: ─────┤ X ├──■── +/// ┌───┐└─┬─┘ +/// q_3: ┤ X ├──■─────── +/// └─┬─┘ +/// q_4: ──■──────────── +/// +fn cx_chain( + active_paulis: Vec<(char, Qubit)>, +) -> Box> { + let num_terms = active_paulis.len(); + Box::new( + (0..num_terms - 1) + .map(move |i| (active_paulis[i].1, active_paulis[i + 1].1)) + .map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])), + ) +} + +/// Build a CX fountain over the active qubits. E.g. with q_1 inactive, this would return +/// +/// ┌───┐┌───┐┌───┐ +/// q_0: ┤ X ├┤ X ├┤ X ├ +/// └─┬─┘└─┬─┘└─┬─┘ +/// q_1: ──┼────┼────┼── +/// │ │ │ +/// q_2: ──■────┼────┼── +/// │ │ +/// q_3: ───────■────┼── +/// │ +/// q_4: ────────────■── +/// +fn cx_fountain( + active_paulis: Vec<(char, Qubit)>, +) -> Box> { + let num_terms = active_paulis.len(); + let first_qubit = active_paulis[0].1; + Box::new((1..num_terms).rev().map(move |i| { + let ctrl = active_paulis[i].1; + ( + StandardGate::CXGate, + smallvec![], + smallvec![ctrl, first_qubit], + ) + })) +} + +fn get_barrier(py: Python, num_qubits: u32) -> Instruction { + let barrier_cls = imports::BARRIER.get_bound(py); + let barrier = barrier_cls + .call1((num_qubits,)) + .expect("Could not create Barrier Python-side"); + let barrier_inst = PyInstruction { + qubits: num_qubits, + clbits: 0, + params: 0, + op_name: "barrier".to_string(), + control_flow: false, + instruction: barrier.into(), + }; + ( + barrier_inst.into(), + smallvec![], + (0..num_qubits).map(Qubit).collect(), + vec![], + ) +} diff --git a/crates/accelerate/src/circuit_library/pauli_feature_map.rs b/crates/accelerate/src/circuit_library/pauli_feature_map.rs index 6fa88d187182..bb9f8c25eb24 100644 --- a/crates/accelerate/src/circuit_library/pauli_feature_map.rs +++ b/crates/accelerate/src/circuit_library/pauli_feature_map.rs @@ -10,7 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use itertools::Itertools; use pyo3::prelude::*; use pyo3::types::PySequence; use pyo3::types::PyString; @@ -24,106 +23,15 @@ use smallvec::{smallvec, SmallVec}; use std::f64::consts::PI; use crate::circuit_library::entanglement; +use crate::circuit_library::pauli_evolution; use crate::QiskitError; -// custom math and types for a more readable code -const PI2: f64 = PI / 2.; type Instruction = ( PackedOperation, SmallVec<[Param; 3]>, Vec, Vec, ); -type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>); - -/// Return instructions (using only StandardGate operations) to implement a Pauli evolution -/// of a given Pauli string over a given time (as Param). -/// -/// The Pauli evolution is implemented as a basis transformation to the Pauli-Z basis, -/// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain -/// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the -/// Pauli string XIYZ we have the circuit -/// ┌───┐┌───────┐┌───┐ -/// 0: ─────────────────┤ X ├┤ Rz(2) ├┤ X ├────────────────── -/// ┌──────────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌───────────┐ -/// 1: ┤ Rx(pi/2) ├┤ X ├──■─────────────■──┤ X ├┤ Rx(-pi/2) ├ -/// └──────────┘└─┬─┘ └─┬─┘└───────────┘ -/// 2: ──────────────┼───────────────────────┼─────────────── -/// ┌───┐ │ │ ┌───┐ -/// 3: ─┤ H ├────────■───────────────────────■──┤ H ├──────── -/// └───┘ └───┘ -fn pauli_evolution( - pauli: &str, - indices: Vec, - time: Param, -) -> impl Iterator + '_ { - // Get pairs of (pauli, qubit) that are active, i.e. that are not the identity. Note that - // the rest of the code also works if there are only identities, in which case we will - // effectively return an empty iterator. - let qubits = indices.iter().map(|i| Qubit(*i)).collect_vec(); - let binding = pauli.to_lowercase(); // lowercase for convenience - let active_paulis = binding - .as_str() - .chars() - .rev() // reverse due to Qiskit's bit ordering convention - .zip(qubits) - .filter(|(p, _)| *p != 'i') - .collect_vec(); - - // get the basis change: x -> HGate, y -> RXGate(pi/2), z -> nothing - let basis_change = active_paulis - .clone() - .into_iter() - .filter(|(p, _)| *p != 'z') - .map(|(p, q)| match p { - 'x' => (StandardGate::HGate, smallvec![], smallvec![q]), - 'y' => ( - StandardGate::RXGate, - smallvec![Param::Float(PI2)], - smallvec![q], - ), - _ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out - }); - - // get the inverse basis change - let inverse_basis_change = basis_change.clone().map(|(gate, _, qubit)| match gate { - StandardGate::HGate => (gate, smallvec![], qubit), - StandardGate::RXGate => (gate, smallvec![Param::Float(-PI2)], qubit), - _ => unreachable!(), - }); - - // get the CX chain down to the target rotation qubit - let chain_down = active_paulis - .clone() - .into_iter() - .map(|(_, q)| q) - .tuple_windows() // iterates over (q[i], q[i+1]) windows - .map(|(ctrl, target)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); - - // get the CX chain up (cannot use chain_down.rev since tuple_windows is not double ended) - let chain_up = active_paulis - .clone() - .into_iter() - .rev() - .map(|(_, q)| q) - .tuple_windows() - .map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); - - // get the RZ gate on the last qubit - let last_qubit = active_paulis.last().unwrap().1; - let z_rotation = std::iter::once(( - StandardGate::PhaseGate, - smallvec![time], - smallvec![last_qubit], - )); - - // and finally chain everything together - basis_change - .chain(chain_down) - .chain(z_rotation) - .chain(chain_up) - .chain(inverse_basis_change) -} /// Build a Pauli feature map circuit. /// @@ -263,11 +171,17 @@ fn _get_evolution_layer<'a>( // to call CircuitData::from_packed_operations. This is needed since we might // have to interject barriers, which are not a standard gate and prevents us // from using CircuitData::from_standard_gates. - let evo = pauli_evolution(pauli, indices.clone(), multiply_param(&angle, alpha, py)) - .map(|(gate, params, qargs)| { - (gate.into(), params, qargs.to_vec(), vec![] as Vec) - }) - .collect::>(); + let evo = pauli_evolution::pauli_evolution( + pauli, + indices.into_iter().rev().collect(), + multiply_param(&angle, alpha, py), + true, + false, + ) + .map(|(gate, params, qargs)| { + (gate.into(), params, qargs.to_vec(), vec![] as Vec) + }) + .collect::>(); insts.extend(evo); } } diff --git a/crates/accelerate/src/circuit_library/quantum_volume.rs b/crates/accelerate/src/circuit_library/quantum_volume.rs index e17357e7cec2..a6c5a3839d90 100644 --- a/crates/accelerate/src/circuit_library/quantum_volume.rs +++ b/crates/accelerate/src/circuit_library/quantum_volume.rs @@ -27,7 +27,7 @@ use rayon::prelude::*; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::imports::UNITARY_GATE; use qiskit_circuit::operations::Param; -use qiskit_circuit::operations::PyInstruction; +use qiskit_circuit::operations::PyGate; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::{Clbit, Qubit}; use smallvec::{smallvec, SmallVec}; @@ -127,17 +127,16 @@ pub fn quantum_volume( let unitary_gate = UNITARY_GATE .get_bound(py) .call((unitary.clone(), py.None(), false), Some(&kwargs))?; - let instruction = PyInstruction { + let instruction = PyGate { qubits: 2, clbits: 0, params: 1, op_name: "unitary".to_string(), - control_flow: false, - instruction: unitary_gate.unbind(), + gate: unitary_gate.unbind(), }; let qubit = layer_index * 2; Ok(( - PackedOperation::from_instruction(Box::new(instruction)), + PackedOperation::from_gate(Box::new(instruction)), smallvec![Param::Obj(unitary.unbind().into())], vec![permutation[qubit], permutation[qubit + 1]], vec![], diff --git a/crates/accelerate/src/commutation_analysis.rs b/crates/accelerate/src/commutation_analysis.rs index a29c648a5f81..07266191fe45 100644 --- a/crates/accelerate/src/commutation_analysis.rs +++ b/crates/accelerate/src/commutation_analysis.rs @@ -61,7 +61,7 @@ pub(crate) fn analyze_commutations_inner( for qubit in 0..dag.num_qubits() { let wire = Wire::Qubit(Qubit(qubit as u32)); - for current_gate_idx in dag.nodes_on_wire(py, &wire, false) { + for current_gate_idx in dag.nodes_on_wire(&wire, false) { // get the commutation set associated with the current wire, or create a new // index set containing the current gate let commutation_entry = commutation_set diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs index 005e37ecc375..fe242c73422f 100644 --- a/crates/accelerate/src/commutation_checker.rs +++ b/crates/accelerate/src/commutation_checker.rs @@ -28,7 +28,7 @@ use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationF use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::imports::QI_OPERATOR; use qiskit_circuit::operations::OperationRef::{Gate as PyGateType, Operation as PyOperationType}; -use qiskit_circuit::operations::{Operation, OperationRef, Param}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; use qiskit_circuit::{BitType, Clbit, Qubit}; use crate::unitary_compose; @@ -38,8 +38,30 @@ static SKIPPED_NAMES: [&str; 4] = ["measure", "reset", "delay", "initialize"]; static NO_CACHE_NAMES: [&str; 2] = ["annotated", "linear_function"]; static SUPPORTED_OP: Lazy> = Lazy::new(|| { HashSet::from([ - "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx", "cy", "cz", "swap", - "iswap", "ecr", "ccx", "cswap", + "rxx", "ryy", "rzz", "rzx", "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx", + "cy", "cz", "swap", "iswap", "ecr", "ccx", "cswap", + ]) +}); + +const TWOPI: f64 = 2.0 * std::f64::consts::PI; + +// map rotation gates to their generators, or to ``None`` if we cannot currently efficiently +// represent the generator in Rust and store the commutation relation in the commutation dictionary +static SUPPORTED_ROTATIONS: Lazy>> = Lazy::new(|| { + HashMap::from([ + ("rx", Some(OperationRef::Standard(StandardGate::XGate))), + ("ry", Some(OperationRef::Standard(StandardGate::YGate))), + ("rz", Some(OperationRef::Standard(StandardGate::ZGate))), + ("p", Some(OperationRef::Standard(StandardGate::ZGate))), + ("u1", Some(OperationRef::Standard(StandardGate::ZGate))), + ("crx", Some(OperationRef::Standard(StandardGate::CXGate))), + ("cry", Some(OperationRef::Standard(StandardGate::CYGate))), + ("crz", Some(OperationRef::Standard(StandardGate::CZGate))), + ("cp", Some(OperationRef::Standard(StandardGate::CZGate))), + ("rxx", None), // None means the gate is in the commutation dictionary + ("ryy", None), + ("rzx", None), + ("rzz", None), ]) }); @@ -89,6 +111,7 @@ impl CommutationChecker { ) -> Self { // Initialize sets before they are used in the commutation checker Lazy::force(&SUPPORTED_OP); + Lazy::force(&SUPPORTED_ROTATIONS); CommutationChecker { library: CommutationLibrary::new(standard_gate_commutations), cache: HashMap::new(), @@ -242,6 +265,23 @@ impl CommutationChecker { cargs2: &[Clbit], max_num_qubits: u32, ) -> PyResult { + // relative and absolute tolerance used to (1) check whether rotation gates commute + // trivially (i.e. the rotation angle is so small we assume it commutes) and (2) define + // comparison for the matrix-based commutation checks + let rtol = 1e-5; + let atol = 1e-8; + + // if we have rotation gates, we attempt to map them to their generators, for example + // RX -> X or CPhase -> CZ + let (op1, params1, trivial1) = map_rotation(op1, params1, rtol); + if trivial1 { + return Ok(true); + } + let (op2, params2, trivial2) = map_rotation(op2, params2, rtol); + if trivial2 { + return Ok(true); + } + if let Some(gates) = &self.gates { if !gates.is_empty() && (!gates.contains(op1.name()) || !gates.contains(op2.name())) { return Ok(false); @@ -286,7 +326,9 @@ impl CommutationChecker { NO_CACHE_NAMES.contains(&second_op.name()) || // Skip params that do not evaluate to floats for caching and commutation library first_params.iter().any(|p| !matches!(p, Param::Float(_))) || - second_params.iter().any(|p| !matches!(p, Param::Float(_))); + second_params.iter().any(|p| !matches!(p, Param::Float(_))) + && !SUPPORTED_OP.contains(op1.name()) + && !SUPPORTED_OP.contains(op2.name()); if skip_cache { return self.commute_matmul( @@ -297,6 +339,8 @@ impl CommutationChecker { second_op, second_params, second_qargs, + rtol, + atol, ); } @@ -331,6 +375,8 @@ impl CommutationChecker { second_op, second_params, second_qargs, + rtol, + atol, )?; // TODO: implement a LRU cache for this @@ -365,6 +411,8 @@ impl CommutationChecker { second_op: &OperationRef, second_params: &[Param], second_qargs: &[Qubit], + rtol: f64, + atol: f64, ) -> PyResult { // Compute relative positioning of qargs of the second gate to the first gate. // Since the qargs come out the same BitData, we already know there are no accidential @@ -405,8 +453,6 @@ impl CommutationChecker { None => return Ok(false), }; - let rtol = 1e-5; - let atol = 1e-8; if first_qarg == second_qarg { match first_qarg.len() { 1 => Ok(unitary_compose::commute_1q( @@ -568,6 +614,41 @@ where .any(|x| matches!(x, Param::ParameterExpression(_))) } +/// Check if a given operation can be mapped onto a generator. +/// +/// If ``op`` is in the ``SUPPORTED_ROTATIONS`` hashmap, it is a rotation and we +/// (1) check whether the rotation is so small (modulo pi) that we assume it is the +/// identity and it commutes trivially with every other operation +/// (2) otherwise, we check whether a generator of the rotation is given (e.g. X for RX) +/// and we return the generator +/// +/// Returns (operation, parameters, commutes_trivially). +fn map_rotation<'a>( + op: &'a OperationRef<'a>, + params: &'a [Param], + tol: f64, +) -> (&'a OperationRef<'a>, &'a [Param], bool) { + let name = op.name(); + if let Some(generator) = SUPPORTED_ROTATIONS.get(name) { + // if the rotation angle is below the tolerance, the gate is assumed to + // commute with everything, and we simply return the operation with the flag that + // it commutes trivially + if let Param::Float(angle) = params[0] { + if (angle % TWOPI).abs() < tol { + return (op, params, true); + }; + }; + + // otherwise, we check if a generator is given -- if not, we'll just return the operation + // itself (e.g. RXX does not have a generator and is just stored in the commutations + // dictionary) + if let Some(gate) = generator { + return (gate, &[], false); + }; + } + (op, params, false) +} + fn get_relative_placement( first_qargs: &[Qubit], second_qargs: &[Qubit], diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs new file mode 100644 index 000000000000..0fec3fa2909a --- /dev/null +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -0,0 +1,320 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use hashbrown::{HashMap, HashSet}; +use ndarray::{aview2, Array2}; +use num_complex::Complex64; +use numpy::{IntoPyArray, PyReadonlyArray2}; +use pyo3::intern; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::gate_matrix::{ONE_QUBIT_IDENTITY, TWO_QUBIT_IDENTITY}; +use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT, UNITARY_GATE}; +use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::Qubit; + +use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst}; +use crate::euler_one_qubit_decomposer::matmul_1q; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use crate::two_qubit_decompose::TwoQubitBasisDecomposer; + +fn is_supported( + target: Option<&Target>, + basis_gates: Option<&HashSet>, + name: &str, + qargs: &[Qubit], +) -> bool { + match target { + Some(target) => { + let physical_qargs = qargs.iter().map(|bit| PhysicalQubit(bit.0)).collect(); + target.instruction_supported(name, Some(&physical_qargs)) + } + None => match basis_gates { + Some(basis_gates) => basis_gates.contains(name), + None => true, + }, + } +} + +// If depth > 20, there will be 1q gates to consolidate. +const MAX_2Q_DEPTH: usize = 20; + +#[allow(clippy::too_many_arguments)] +#[pyfunction] +#[pyo3(signature = (dag, decomposer, basis_gate_name, force_consolidate, target=None, basis_gates=None, blocks=None, runs=None))] +pub(crate) fn consolidate_blocks( + py: Python, + dag: &mut DAGCircuit, + decomposer: &TwoQubitBasisDecomposer, + basis_gate_name: &str, + force_consolidate: bool, + target: Option<&Target>, + basis_gates: Option>, + blocks: Option>>, + runs: Option>>, +) -> PyResult<()> { + let blocks = match blocks { + Some(runs) => runs + .into_iter() + .map(|run| { + run.into_iter() + .map(NodeIndex::new) + .collect::>() + }) + .collect(), + // If runs are specified but blocks are none we're in a legacy configuration where external + // collection passes are being used. In this case don't collect blocks because it's + // unexpected. + None => match runs { + Some(_) => vec![], + None => dag.collect_2q_runs().unwrap(), + }, + }; + + let runs: Option>> = runs.map(|runs| { + runs.into_iter() + .map(|run| { + run.into_iter() + .map(NodeIndex::new) + .collect::>() + }) + .collect() + }); + let mut all_block_gates: HashSet = + HashSet::with_capacity(blocks.iter().map(|x| x.len()).sum()); + let mut block_qargs: HashSet = HashSet::with_capacity(2); + for block in blocks { + block_qargs.clear(); + if block.len() == 1 { + let inst_node = block[0]; + let inst = dag.dag()[inst_node].unwrap_operation(); + if !is_supported( + target, + basis_gates.as_ref(), + inst.op.name(), + dag.get_qargs(inst.qubits), + ) { + all_block_gates.insert(inst_node); + let matrix = match get_matrix_from_inst(py, inst) { + Ok(mat) => mat, + Err(_) => continue, + }; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?; + continue; + } + } + let mut basis_count: usize = 0; + let mut outside_basis = false; + for node in &block { + let inst = dag.dag()[*node].unwrap_operation(); + block_qargs.extend(dag.get_qargs(inst.qubits)); + all_block_gates.insert(*node); + if inst.op.name() == basis_gate_name { + basis_count += 1; + } + if !is_supported( + target, + basis_gates.as_ref(), + inst.op.name(), + dag.get_qargs(inst.qubits), + ) { + outside_basis = true; + } + } + if block_qargs.len() > 2 { + let mut qargs: Vec = block_qargs.iter().copied().collect(); + qargs.sort(); + let block_index_map: HashMap = qargs + .into_iter() + .enumerate() + .map(|(idx, qubit)| (qubit, idx)) + .collect(); + let circuit_data = CircuitData::from_packed_operations( + py, + block_qargs.len() as u32, + 0, + block.iter().map(|node| { + let inst = dag.dag()[*node].unwrap_operation(); + + Ok(( + inst.op.clone(), + inst.params_view().iter().cloned().collect(), + dag.get_qargs(inst.qubits) + .iter() + .map(|x| Qubit::new(block_index_map[x])) + .collect(), + vec![], + )) + }), + Param::Float(0.), + )?; + let circuit = QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (circuit_data,))?; + let array = QI_OPERATOR + .get_bound(py) + .call1((circuit,))? + .getattr(intern!(py, "data"))? + .extract::>()?; + let matrix = array.as_array(); + let identity: Array2 = Array2::eye(2usize.pow(block_qargs.len() as u32)); + if approx::abs_diff_eq!(identity, matrix) { + for node in block { + dag.remove_op_node(node); + } + } else { + let unitary_gate = + UNITARY_GATE + .get_bound(py) + .call1((array.to_object(py), py.None(), false))?; + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &block, + unitary_gate, + false, + &block_index_map, + &clbit_pos_map, + )?; + } + } else { + let block_index_map = [ + *block_qargs.iter().min().unwrap(), + *block_qargs.iter().max().unwrap(), + ]; + let matrix = blocks_to_matrix(py, dag, &block, block_index_map).ok(); + if let Some(matrix) = matrix { + if force_consolidate + || decomposer.num_basis_gates_inner(matrix.view()) < basis_count + || block.len() > MAX_2Q_DEPTH + || (basis_gates.is_some() && outside_basis) + || (target.is_some() && outside_basis) + { + if approx::abs_diff_eq!(aview2(&TWO_QUBIT_IDENTITY), matrix) { + for node in block { + dag.remove_op_node(node); + } + } else { + let array = matrix.into_pyarray_bound(py); + let unitary_gate = + UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + let qubit_pos_map = block_index_map + .into_iter() + .enumerate() + .map(|(idx, qubit)| (qubit, idx)) + .collect(); + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &block, + unitary_gate, + false, + &qubit_pos_map, + &clbit_pos_map, + )?; + } + } + } + } + } + if let Some(runs) = runs { + for run in runs { + if run.iter().any(|node| all_block_gates.contains(node)) { + continue; + } + let first_inst_node = run[0]; + let first_inst = dag.dag()[first_inst_node].unwrap_operation(); + let first_qubits = dag.get_qargs(first_inst.qubits); + + if run.len() == 1 + && !is_supported( + target, + basis_gates.as_ref(), + first_inst.op.name(), + first_qubits, + ) + { + let matrix = match get_matrix_from_inst(py, first_inst) { + Ok(mat) => mat, + Err(_) => continue, + }; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?; + continue; + } + let qubit = first_qubits[0]; + let mut matrix = ONE_QUBIT_IDENTITY; + + let mut already_in_block = false; + for node in &run { + if all_block_gates.contains(node) { + already_in_block = true; + } + let gate = dag.dag()[*node].unwrap_operation(); + let operator = match get_matrix_from_inst(py, gate) { + Ok(mat) => mat, + Err(_) => { + // Set this to skip this run because we can't compute the matrix of the + // operation. + already_in_block = true; + break; + } + }; + matmul_1q(&mut matrix, operator); + } + if already_in_block { + continue; + } + if approx::abs_diff_eq!(aview2(&ONE_QUBIT_IDENTITY), aview2(&matrix)) { + for node in run { + dag.remove_op_node(node); + } + } else { + let array = aview2(&matrix).to_owned().into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + let mut block_index_map: HashMap = HashMap::with_capacity(1); + block_index_map.insert(qubit, 0); + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &run, + unitary_gate, + false, + &block_index_map, + &clbit_pos_map, + )?; + } + } + } + + Ok(()) +} + +pub fn consolidate_blocks_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(consolidate_blocks))?; + Ok(()) +} diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index dc4d0b77c4a7..aefc5976e82f 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -12,102 +12,135 @@ use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::PyDict; -use pyo3::wrap_pyfunction; use pyo3::Python; use num_complex::Complex64; use numpy::ndarray::linalg::kron; use numpy::ndarray::{aview2, Array2, ArrayView2}; -use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; -use smallvec::SmallVec; +use numpy::PyReadonlyArray2; +use rustworkx_core::petgraph::stable_graph::NodeIndex; -use qiskit_circuit::bit_data::BitData; -use qiskit_circuit::circuit_instruction::CircuitInstruction; -use qiskit_circuit::dag_node::DAGOpNode; +use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::Operation; +use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::packed_instruction::PackedInstruction; +use qiskit_circuit::Qubit; +use crate::euler_one_qubit_decomposer::matmul_1q; use crate::QiskitError; -fn get_matrix_from_inst<'py>( - py: Python<'py>, - inst: &'py CircuitInstruction, -) -> PyResult> { - if let Some(mat) = inst.operation.matrix(&inst.params) { +#[inline] +pub fn get_matrix_from_inst(py: Python, inst: &PackedInstruction) -> PyResult> { + if let Some(mat) = inst.op.matrix(inst.params_view()) { Ok(mat) - } else if inst.operation.try_standard_gate().is_some() { + } else if inst.op.try_standard_gate().is_some() { Err(QiskitError::new_err( "Parameterized gates can't be consolidated", )) - } else { + } else if let OperationRef::Gate(gate) = inst.op.view() { Ok(QI_OPERATOR .get_bound(py) - .call1((inst.get_operation(py)?,))? + .call1((gate.gate.clone_ref(py),))? .getattr(intern!(py, "data"))? .extract::>()? .as_array() .to_owned()) + } else { + Err(QiskitError::new_err( + "Can't compute matrix of non-unitary op", + )) } } /// Return the matrix Operator resulting from a block of Instructions. -#[pyfunction] -#[pyo3(text_signature = "(op_list, /")] pub fn blocks_to_matrix( py: Python, - op_list: Vec>, - block_index_map_dict: &Bound, -) -> PyResult>> { - // Build a BitData in block_index_map_dict order. block_index_map_dict is a dict of bits to - // indices mapping the order of the qargs in the block. There should only be 2 entries since - // there are only 2 qargs here (e.g. `{Qubit(): 0, Qubit(): 1}`) so we need to ensure that - // we added the qubits to bit data in the correct index order. - let mut index_map: Vec = (0..block_index_map_dict.len()).map(|_| py.None()).collect(); - for bit_tuple in block_index_map_dict.items() { - let (bit, index): (PyObject, usize) = bit_tuple.extract()?; - index_map[index] = bit; - } - let mut bit_map: BitData = BitData::new(py, "qargs".to_string()); - for bit in index_map { - bit_map.add(py, bit.bind(py), true)?; - } - let identity = aview2(&ONE_QUBIT_IDENTITY); - let first_node = &op_list[0]; - let input_matrix = get_matrix_from_inst(py, &first_node.instruction)?; - let mut matrix: Array2 = match bit_map - .map_bits(first_node.instruction.qubits.bind(py).iter())? - .collect::>() - .as_slice() - { - [0] => kron(&identity, &input_matrix), - [1] => kron(&input_matrix, &identity), - [0, 1] => input_matrix, - [1, 0] => change_basis(input_matrix.view()), - [] => Array2::eye(4), - _ => unreachable!(), + dag: &DAGCircuit, + op_list: &[NodeIndex], + block_index_map: [Qubit; 2], +) -> PyResult> { + let map_bits = |bit: &Qubit| -> u8 { + if *bit == block_index_map[0] { + 0 + } else { + 1 + } }; - for node in op_list.into_iter().skip(1) { - let op_matrix = get_matrix_from_inst(py, &node.instruction)?; - let q_list = bit_map - .map_bits(node.instruction.qubits.bind(py).iter())? - .map(|x| x as u8) - .collect::>(); - - let result = match q_list.as_slice() { - [0] => Some(kron(&identity, &op_matrix)), - [1] => Some(kron(&op_matrix, &identity)), - [1, 0] => Some(change_basis(op_matrix.view())), - [] => Some(Array2::eye(4)), - _ => None, - }; - matrix = match result { - Some(result) => result.dot(&matrix), - None => op_matrix.dot(&matrix), - }; + let mut qubit_0 = ONE_QUBIT_IDENTITY; + let mut qubit_1 = ONE_QUBIT_IDENTITY; + let mut one_qubit_components_modified = false; + let mut output_matrix: Option> = None; + for node in op_list { + let inst = dag.dag()[*node].unwrap_operation(); + let op_matrix = get_matrix_from_inst(py, inst)?; + match dag + .get_qargs(inst.qubits) + .iter() + .map(map_bits) + .collect::>() + .as_slice() + { + [0] => { + matmul_1q(&mut qubit_0, op_matrix); + one_qubit_components_modified = true; + } + [1] => { + matmul_1q(&mut qubit_1, op_matrix); + one_qubit_components_modified = true; + } + [0, 1] => { + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + output_matrix = Some(match output_matrix { + None => op_matrix.dot(&one_qubits_combined), + Some(current) => { + let temp = one_qubits_combined.dot(¤t); + op_matrix.dot(&temp) + } + }); + qubit_0 = ONE_QUBIT_IDENTITY; + qubit_1 = ONE_QUBIT_IDENTITY; + one_qubit_components_modified = false; + } else { + output_matrix = Some(match output_matrix { + None => op_matrix, + Some(current) => op_matrix.dot(¤t), + }); + } + } + [1, 0] => { + let matrix = change_basis(op_matrix.view()); + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + output_matrix = Some(match output_matrix { + None => matrix.dot(&one_qubits_combined), + Some(current) => matrix.dot(&one_qubits_combined.dot(¤t)), + }); + qubit_0 = ONE_QUBIT_IDENTITY; + qubit_1 = ONE_QUBIT_IDENTITY; + one_qubit_components_modified = false; + } else { + output_matrix = Some(match output_matrix { + None => matrix, + Some(current) => matrix.dot(¤t), + }); + } + } + _ => unreachable!(), + } } - Ok(matrix.into_pyarray_bound(py).unbind()) + Ok(match output_matrix { + Some(matrix) => { + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + one_qubits_combined.dot(&matrix) + } else { + matrix + } + } + None => kron(&aview2(&qubit_1), &aview2(&qubit_0)), + }) } /// Switches the order of qubits in a two qubit operation. @@ -123,8 +156,3 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { } trans_matrix } - -pub fn convert_2q_block_matrix(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(blocks_to_matrix))?; - Ok(()) -} diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index c59471b3798e..dfa338fba45a 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -277,7 +277,7 @@ impl Display for EdgeData { } /// Enum that helps extract the Operation and Parameters on a Gate. -/// It is highly derivative of `PackedOperation` while also tracking the specific +/// It is highly derivative of [PackedOperation] while also tracking the specific /// parameter objects. #[derive(Debug, Clone)] pub struct GateOper { @@ -295,13 +295,15 @@ impl<'py> FromPyObject<'py> for GateOper { } } -/// Used to extract an instance of [CircuitData] from a `QuantumCircuit`. -/// It also ensures seamless conversion back to `QuantumCircuit` once sent +/// Used to extract an instance of [CircuitData] from a [`QuantumCircuit`]. +/// It also ensures seamless conversion back to [`QuantumCircuit`] once sent /// back to Python. /// -/// TODO: Remove this implementation once the `EquivalenceLibrary` is no longer +/// TODO: Remove this implementation once the [EquivalenceLibrary] is no longer /// called from Python, or once the API is able to seamlessly accept instances /// of [CircuitData]. +/// +/// [`QuantumCircuit`]: https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.QuantumCircuit #[derive(Debug, Clone)] pub struct CircuitFromPython(pub CircuitData); @@ -340,6 +342,8 @@ impl ToPyObject for CircuitFromPython { type GraphType = StableDiGraph>; type KTIType = IndexMap; +/// A library providing a one-way mapping of gates to their equivalent +/// implementations as :class:`.QuantumCircuit` instances. #[pyclass( subclass, name = "BaseEquivalenceLibrary", @@ -424,7 +428,7 @@ impl EquivalenceLibrary { /// /// Args: /// gate (Gate): A Gate instance. - /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each + /// entry (List['QuantumCircuit']) : A list of :class:`.QuantumCircuit` instances, each /// equivalently implementing the given Gate. #[pyo3(name = "set_entry")] fn py_set_entry( @@ -436,8 +440,8 @@ impl EquivalenceLibrary { self.set_entry(py, &gate.operation, &gate.params, entry) } - /// Gets the set of QuantumCircuits circuits from the library which - /// equivalently implement the given Gate. + /// Gets the set of :class:`.QuantumCircuit` instances circuits from the + /// library which equivalently implement the given :class:`.Gate`. /// /// Parameterized circuits will have their parameters replaced with the /// corresponding entries from Gate.params. @@ -446,8 +450,8 @@ impl EquivalenceLibrary { /// gate (Gate) - Gate: A Gate instance. /// /// Returns: - /// List[QuantumCircuit]: A list of equivalent QuantumCircuits. If empty, - /// library contains no known decompositions of Gate. + /// List[QuantumCircuit]: A list of equivalent :class:`.QuantumCircuit` instances. + /// If empty, library contains no known decompositions of Gate. /// /// Returned circuits will be ordered according to their insertion in /// the library, from earliest to latest, from top to base. The @@ -469,6 +473,16 @@ impl EquivalenceLibrary { } // TODO: Remove once BasisTranslator is in Rust. + /// Return graph representing the equivalence library data. + /// + /// This property should be treated as read-only as it provides + /// a reference to the internal state of the :class:`~.EquivalenceLibrary` object. + /// If the graph returned by this property is mutated it could corrupt the + /// the contents of the object. If you need to modify the output ``PyDiGraph`` + /// be sure to make a copy prior to any modification. + /// + /// Returns: + /// PyDiGraph: A graph object with equivalence data in each node. #[getter] fn get_graph(&mut self, py: Python) -> PyResult { if let Some(graph) = &self._graph { @@ -492,6 +506,10 @@ impl EquivalenceLibrary { } } + /// Return list of keys to key to node index map. + /// + /// Returns: + /// List: Keys to the key to node index map. #[pyo3(name = "keys")] fn py_keys(slf: PyRef) -> PyResult { let py_dict = PyDict::new_bound(slf.py()); @@ -501,6 +519,13 @@ impl EquivalenceLibrary { Ok(py_dict.as_any().call_method0("keys")?.into()) } + /// Return node index for a given key. + /// + /// Args: + /// key (Key): Key to an equivalence. + /// + /// Returns: + /// Int: Index to the node in the graph for the given key. #[pyo3(name = "node_index")] fn py_node_index(&self, key: &Key) -> usize { self.node_index(key).index() @@ -614,7 +639,7 @@ impl EquivalenceLibrary { Ok(()) } - /// Set the equivalence record for a Gate. Future queries for the Gate + /// Set the equivalence record for a [PackedOperation]. Future queries for the Gate /// will return only the circuits provided. pub fn set_entry( &mut self, @@ -649,21 +674,13 @@ impl EquivalenceLibrary { Ok(()) } - /// Rust native equivalent to `EquivalenceLibrary.has_entry()` - /// - /// Check if a library contains any decompositions for gate. - /// - /// # Arguments: - /// * `operation` OperationType: A Gate instance. - /// - /// # Returns: - /// `bool`: `true` if gate has a known decomposition in the library. - /// `false` otherwise. + /// Check if the [EquivalenceLibrary] instance contains any decompositions for gate. pub fn has_entry(&self, operation: &PackedOperation) -> bool { let key = Key::from_operation(operation); self.key_to_node_index.contains_key(&key) } + /// Returns an iterator with all the [Key] instances in the [EquivalenceLibrary]. pub fn keys(&self) -> impl Iterator { self.key_to_node_index.keys() } @@ -682,13 +699,7 @@ impl EquivalenceLibrary { } } - /// Retrieve the `NodeIndex` that represents a `Key` - /// - /// # Arguments: - /// * `key`: The `Key` to look for. - /// - /// # Returns: - /// `NodeIndex` + /// Retrieve the [NodeIndex] that represents a [Key]. pub fn node_index(&self, key: &Key) -> NodeIndex { self.key_to_node_index[key] } @@ -788,6 +799,8 @@ impl Display for EquivalenceError { } } +// Conversion helpers + fn to_pygraph(py: Python, pet_graph: &StableDiGraph) -> PyResult where N: IntoPy + Clone, diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index b5ed6014faaa..eb53b8309b05 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -1242,7 +1242,8 @@ pub(crate) fn optimize_1q_gates_decomposition( Ok(()) } -fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { +#[inline(always)] +pub fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { *operator = [ [ other[[0, 0]] * operator[0][0] + other[[0, 1]] * operator[1][0], diff --git a/crates/accelerate/src/gate_direction.rs b/crates/accelerate/src/gate_direction.rs old mode 100644 new mode 100755 index 3143a11a22a2..a20dfea00535 --- a/crates/accelerate/src/gate_direction.rs +++ b/crates/accelerate/src/gate_direction.rs @@ -11,18 +11,35 @@ // that they have been altered from the originals. use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::exceptions::TranspilerError; use crate::target_transpiler::Target; use hashbrown::HashSet; +use pyo3::intern; use pyo3::prelude::*; -use qiskit_circuit::imports; +use pyo3::types::PyTuple; use qiskit_circuit::operations::OperationRef; +use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::{ + circuit_instruction::CircuitInstruction, + circuit_instruction::ExtraInstructionAttributes, + converters::{circuit_to_dag, QuantumCircuitData}, dag_circuit::{DAGCircuit, NodeType}, + dag_node::{DAGNode, DAGOpNode}, + imports, + imports::get_std_gate_class, operations::Operation, + operations::Param, + operations::StandardGate, packed_instruction::PackedInstruction, Qubit, }; -use smallvec::smallvec; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use smallvec::{smallvec, SmallVec}; +use std::f64::consts::PI; + +//######################################################################### +// CheckGateDirection analysis pass functions +//######################################################################### /// Check if the two-qubit gates follow the right direction with respect to the coupling map. /// @@ -35,7 +52,7 @@ use smallvec::smallvec; /// true iff all two-qubit gates comply with the coupling constraints #[pyfunction] #[pyo3(name = "check_gate_direction_coupling")] -fn py_check_with_coupling_map( +fn py_check_direction_coupling_map( py: Python, dag: &DAGCircuit, coupling_edges: HashSet<[Qubit; 2]>, @@ -57,7 +74,7 @@ fn py_check_with_coupling_map( /// true iff all two-qubit gates comply with the target's coupling constraints #[pyfunction] #[pyo3(name = "check_gate_direction_target")] -fn py_check_with_target(py: Python, dag: &DAGCircuit, target: &Target) -> PyResult { +fn py_check_direction_target(py: Python, dag: &DAGCircuit, target: &Target) -> PyResult { let target_check = |inst: &PackedInstruction, op_args: &[Qubit]| -> bool { let qargs = smallvec![ PhysicalQubit::new(op_args[0].0), @@ -97,7 +114,7 @@ where if let OperationRef::Instruction(py_inst) = packed_inst.op.view() { if py_inst.control_flow() { - let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); // TODO: Take out of the recursion + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); let py_inst = py_inst.instruction.bind(py); for block in py_inst.getattr("blocks")?.iter()? { @@ -142,8 +159,479 @@ where Ok(true) } +//######################################################################### +// GateDirection transformation pass functions +//######################################################################### + +/// Try to swap two-qubit gate directions using pre-defined mapping to follow the right direction with respect to the coupling map. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// coupling_edges: set of edge pairs representing a directed coupling map, against which gate directionality is checked +/// +/// Returns: +/// the transformed DAGCircuit +#[pyfunction] +#[pyo3(name = "fix_gate_direction_coupling")] +fn py_fix_direction_coupling_map( + py: Python, + dag: &mut DAGCircuit, + coupling_edges: HashSet<[Qubit; 2]>, +) -> PyResult { + if coupling_edges.is_empty() { + return Ok(dag.clone()); + } + + let coupling_map_check = + |_: &PackedInstruction, op_args: &[Qubit]| -> bool { coupling_edges.contains(op_args) }; + + fix_gate_direction(py, dag, &coupling_map_check, None).cloned() +} + +/// Try to swap two-qubit gate directions using pre-defined mapping to follow the right direction with respect to the given target. +/// +/// Args: +/// dag: the DAGCircuit to analyze +/// +/// coupling_edges: set of edge pairs representing a directed coupling map, against which gate directionality is checked +/// +/// Returns: +/// the transformed DAGCircuit +#[pyfunction] +#[pyo3(name = "fix_gate_direction_target")] +fn py_fix_direction_target( + py: Python, + dag: &mut DAGCircuit, + target: &Target, +) -> PyResult { + let target_check = |inst: &PackedInstruction, op_args: &[Qubit]| -> bool { + let qargs = smallvec![ + PhysicalQubit::new(op_args[0].0), + PhysicalQubit::new(op_args[1].0) + ]; + + // Take this path so Target can check for exact match of the parameterized gate's angle + if let OperationRef::Standard(std_gate) = inst.op.view() { + match std_gate { + StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + return target + .py_instruction_supported( + py, + None, + Some(qargs), + Some( + get_std_gate_class(py, std_gate) + .expect("These gates should have Python classes") + .bind(py), + ), + Some(inst.params_view().to_vec()), + ) + .unwrap_or(false) + } + _ => {} + } + } + target.instruction_supported(inst.op.name(), Some(&qargs)) + }; + + fix_gate_direction(py, dag, &target_check, None).cloned() +} + +// The main routine for fixing gate direction. Same parameters are check_gate_direction +fn fix_gate_direction<'a, T>( + py: Python, + dag: &'a mut DAGCircuit, + gate_complies: &T, + qubit_mapping: Option<&[Qubit]>, +) -> PyResult<&'a DAGCircuit> +where + T: Fn(&PackedInstruction, &[Qubit]) -> bool, +{ + let mut nodes_to_replace: Vec<(NodeIndex, DAGCircuit)> = Vec::new(); + let mut ops_to_replace: Vec<(NodeIndex, Vec>)> = Vec::new(); + + for node in dag.op_nodes(false) { + let packed_inst = dag.dag()[node].unwrap_operation(); + + let op_args = dag.get_qargs(packed_inst.qubits); + + if let OperationRef::Instruction(py_inst) = packed_inst.op.view() { + if py_inst.control_flow() { + let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); + + let blocks = py_inst.instruction.bind(py).getattr("blocks")?; + let blocks = blocks.downcast::()?; + + let mut blocks_to_replace = Vec::with_capacity(blocks.len()); + for block in blocks { + let mut inner_dag = circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&block)?, + false, + None, + None, + )?; + + let inner_dag = if let Some(mapping) = qubit_mapping { + let mapping = op_args // Create a temp mapping for the recursive call + .iter() + .map(|q| mapping[q.index()]) + .collect::>(); + + fix_gate_direction(py, &mut inner_dag, gate_complies, Some(&mapping))? + } else { + fix_gate_direction(py, &mut inner_dag, gate_complies, Some(op_args))? + }; + + let circuit = dag_to_circuit.call1((inner_dag.clone(),))?; + blocks_to_replace.push(circuit); + } + + // Store this for replacement outside the dag.op_nodes loop + ops_to_replace.push((node, blocks_to_replace)); + + continue; + } + } + + if op_args.len() != 2 || dag.has_calibration_for_index(py, node)? { + continue; + }; + + // Take into account qubit index mapping if we're inside a control-flow block + let (op_args0, op_args1) = if let Some(mapping) = qubit_mapping { + (mapping[op_args[0].index()], mapping[op_args[1].index()]) + } else { + (op_args[0], op_args[1]) + }; + + if gate_complies(packed_inst, &[op_args0, op_args1]) { + continue; + } + + // If the op has a pre-defined replacement - replace if the other direction is supported otherwise error + // If no pre-defined replacement for the op - if the other direction is supported error saying no pre-defined rule otherwise error saying op is not supported + if let OperationRef::Standard(std_gate) = packed_inst.op.view() { + match std_gate { + StandardGate::CXGate + | StandardGate::ECRGate + | StandardGate::CZGate + | StandardGate::SwapGate + | StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + if gate_complies(packed_inst, &[op_args1, op_args0]) { + // Store this for replacement outside the dag.op_nodes loop + nodes_to_replace.push((node, replace_dag(py, std_gate, packed_inst)?)); + continue; + } else { + return Err(TranspilerError::new_err(format!( + "The circuit requires a connection between physical qubits {:?} for {}", + op_args, + packed_inst.op.name() + ))); + } + } + _ => {} + } + } + // No matching replacement found + if gate_complies(packed_inst, &[op_args1, op_args0]) + || has_calibration_for_op_node(py, dag, packed_inst, &[op_args1, op_args0])? + { + return Err(TranspilerError::new_err(format!("{} would be supported on {:?} if the direction was swapped, but no rules are known to do that. {:?} can be automatically flipped.", packed_inst.op.name(), op_args, vec!["cx", "cz", "ecr", "swap", "rzx", "rxx", "ryy", "rzz"]))); + // NOTE: Make sure to update the list of the supported gates if adding more replacements + } else { + return Err(TranspilerError::new_err(format!( + "{} with parameters {:?} is not supported on qubits {:?} in either direction.", + packed_inst.op.name(), + packed_inst.params_view(), + op_args + ))); + } + } + + for (node, op_blocks) in ops_to_replace { + let packed_inst = dag.dag()[node].unwrap_operation(); + let OperationRef::Instruction(py_inst) = packed_inst.op.view() else { + panic!("PyInstruction is expected"); + }; + let new_op = py_inst + .instruction + .bind(py) + .call_method1("replace_blocks", (op_blocks,))?; + + dag.py_substitute_node(dag.get_node(py, node)?.bind(py), &new_op, false, false)?; + } + + for (node, replacemanet_dag) in nodes_to_replace { + dag.py_substitute_node_with_dag( + py, + dag.get_node(py, node)?.bind(py), + &replacemanet_dag, + None, + true, + )?; + } + + Ok(dag) +} + +// Check whether the dag as calibration for a DAGOpNode +fn has_calibration_for_op_node( + py: Python, + dag: &DAGCircuit, + packed_inst: &PackedInstruction, + qargs: &[Qubit], +) -> PyResult { + let py_args = PyTuple::new_bound(py, dag.qubits().map_indices(qargs)); + + let dag_op_node = Py::new( + py, + ( + DAGOpNode { + instruction: CircuitInstruction { + operation: packed_inst.op.clone(), + qubits: py_args.unbind(), + clbits: PyTuple::empty_bound(py).unbind(), + params: packed_inst.params_view().iter().cloned().collect(), + extra_attrs: packed_inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: packed_inst.py_op.clone(), + }, + sort_key: "".into_py(py), + }, + DAGNode { node: None }, + ), + )?; + + dag.has_calibration_for(py, dag_op_node.borrow(py)) +} + +// Return a replacement DAG for the given standard gate in the supported list +// TODO: optimize it by caching the DAGs of the non-parametric gates and caching and +// mutating upon request the DAGs of the parametric gates +fn replace_dag( + py: Python, + std_gate: StandardGate, + inst: &PackedInstruction, +) -> PyResult { + let replacement_dag = match std_gate { + StandardGate::CXGate => cx_replacement_dag(py), + StandardGate::ECRGate => ecr_replacement_dag(py), + StandardGate::CZGate => cz_replacement_dag(py), + StandardGate::SwapGate => swap_replacement_dag(py), + StandardGate::RXXGate => rxx_replacement_dag(py, inst.params_view()), + StandardGate::RYYGate => ryy_replacement_dag(py, inst.params_view()), + StandardGate::RZZGate => rzz_replacement_dag(py, inst.params_view()), + StandardGate::RZXGate => rzx_replacement_dag(py, inst.params_view()), + _ => panic!("Mismatch in supported gates assumption"), + }; + + replacement_dag +} + +//################################################### +// Utility functions to build the replacement dags +// +// TODO: replace this once we have a Rust version of QuantumRegister +#[inline] +fn add_qreg(py: Python, dag: &mut DAGCircuit, num_qubits: u32) -> PyResult> { + let qreg = imports::QUANTUM_REGISTER + .get_bound(py) + .call1((num_qubits,))?; + dag.add_qreg(py, &qreg)?; + let mut qargs = Vec::new(); + + for i in 0..num_qubits { + let qubit = qreg.call_method1(intern!(py, "__getitem__"), (i,))?; + qargs.push( + dag.qubits() + .find(&qubit) + .expect("Qubit should have been stored in the DAGCircuit"), + ); + } + + Ok(qargs) +} + +#[inline] +fn apply_operation_back( + py: Python, + dag: &mut DAGCircuit, + gate: StandardGate, + qargs: &[Qubit], + param: Option>, +) -> PyResult<()> { + dag.apply_operation_back( + py, + PackedOperation::from_standard(gate), + qargs, + &[], + param, + ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + None, + )?; + + Ok(()) +} + +fn cx_replacement_dag(py: Python) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; + apply_operation_back( + py, + new_dag, + StandardGate::CXGate, + &[qargs[1], qargs[0]], + None, + )?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; + + Ok(new_dag.clone()) +} + +fn ecr_replacement_dag(py: Python) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + new_dag.add_global_phase(py, &Param::Float(-PI / 2.0))?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back(py, new_dag, StandardGate::SGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::SXGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::SdgGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::SdgGate, &[qargs[1]], None)?; + apply_operation_back(py, new_dag, StandardGate::SXGate, &[qargs[1]], None)?; + apply_operation_back(py, new_dag, StandardGate::SGate, &[qargs[1]], None)?; + apply_operation_back( + py, + new_dag, + StandardGate::ECRGate, + &[qargs[1], qargs[0]], + None, + )?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; + + Ok(new_dag.clone()) +} + +fn cz_replacement_dag(py: Python) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back( + py, + new_dag, + StandardGate::CZGate, + &[qargs[1], qargs[0]], + None, + )?; + + Ok(new_dag.clone()) +} + +fn swap_replacement_dag(py: Python) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back( + py, + new_dag, + StandardGate::SwapGate, + &[qargs[1], qargs[0]], + None, + )?; + + Ok(new_dag.clone()) +} + +fn rxx_replacement_dag(py: Python, param: &[Param]) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back( + py, + new_dag, + StandardGate::RXXGate, + &[qargs[1], qargs[0]], + Some(SmallVec::from(param)), + )?; + + Ok(new_dag.clone()) +} + +fn ryy_replacement_dag(py: Python, param: &[Param]) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back( + py, + new_dag, + StandardGate::RYYGate, + &[qargs[1], qargs[0]], + Some(SmallVec::from(param)), + )?; + + Ok(new_dag.clone()) +} + +fn rzz_replacement_dag(py: Python, param: &[Param]) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back( + py, + new_dag, + StandardGate::RZZGate, + &[qargs[1], qargs[0]], + Some(SmallVec::from(param)), + )?; + + Ok(new_dag.clone()) +} + +fn rzx_replacement_dag(py: Python, param: &[Param]) -> PyResult { + let new_dag = &mut DAGCircuit::new(py)?; + let qargs = add_qreg(py, new_dag, 2)?; + let qargs = qargs.as_slice(); + + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; + apply_operation_back( + py, + new_dag, + StandardGate::RZXGate, + &[qargs[1], qargs[0]], + Some(SmallVec::from(param)), + )?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[0]], None)?; + apply_operation_back(py, new_dag, StandardGate::HGate, &[qargs[1]], None)?; + + Ok(new_dag.clone()) +} + +#[pymodule] pub fn gate_direction(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(py_check_with_coupling_map))?; - m.add_wrapped(wrap_pyfunction!(py_check_with_target))?; + m.add_wrapped(wrap_pyfunction!(py_check_direction_coupling_map))?; + m.add_wrapped(wrap_pyfunction!(py_check_direction_target))?; + m.add_wrapped(wrap_pyfunction!(py_fix_direction_coupling_map))?; + m.add_wrapped(wrap_pyfunction!(py_fix_direction_target))?; Ok(()) } diff --git a/crates/accelerate/src/high_level_synthesis.rs b/crates/accelerate/src/high_level_synthesis.rs new file mode 100644 index 000000000000..74bf5370ba72 --- /dev/null +++ b/crates/accelerate/src/high_level_synthesis.rs @@ -0,0 +1,252 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use hashbrown::HashMap; +use pyo3::prelude::*; + +/// Track global qubits by their state. +/// The global qubits are numbered by consecutive integers starting at `0`, +/// and the states are distinguished into clean (:math:`|0\rangle`) +/// and dirty (unknown). +#[pyclass] +#[derive(Clone, Debug)] +pub struct QubitTracker { + /// The total number of global qubits + num_qubits: usize, + /// Stores the state for each qubit: `true` means clean, `false` means dirty + state: Vec, + /// Stores whether qubits are allowed be used + enabled: Vec, + /// Used internally for keeping the computations in `O(n)` + ignored: Vec, +} + +#[pymethods] +impl QubitTracker { + #[new] + pub fn new(num_qubits: usize) -> Self { + QubitTracker { + num_qubits, + state: vec![false; num_qubits], + enabled: vec![true; num_qubits], + ignored: vec![false; num_qubits], + } + } + + /// Sets state of the given qubits to dirty + fn set_dirty(&mut self, qubits: Vec) { + for q in qubits { + self.state[q] = false; + } + } + + /// Sets state of the given qubits to clean + fn set_clean(&mut self, qubits: Vec) { + for q in qubits { + self.state[q] = true; + } + } + + /// Disables using the given qubits + fn disable(&mut self, qubits: Vec) { + for q in qubits { + self.enabled[q] = false; + } + } + + /// Enable using the given qubits + fn enable(&mut self, qubits: Vec) { + for q in qubits { + self.enabled[q] = true; + } + } + + /// Returns the number of enabled clean qubits, ignoring the given qubits + /// ToDo: check if it's faster to avoid using ignored + fn num_clean(&mut self, ignored_qubits: Vec) -> usize { + for q in &ignored_qubits { + self.ignored[*q] = true; + } + + let count = (0..self.num_qubits) + .filter(|q| !self.ignored[*q] && self.enabled[*q] && self.state[*q]) + .count(); + + for q in &ignored_qubits { + self.ignored[*q] = false; + } + + count + } + + /// Returns the number of enabled dirty qubits, ignoring the given qubits + /// ToDo: check if it's faster to avoid using ignored + fn num_dirty(&mut self, ignored_qubits: Vec) -> usize { + for q in &ignored_qubits { + self.ignored[*q] = true; + } + + let count = (0..self.num_qubits) + .filter(|q| !self.ignored[*q] && self.enabled[*q] && !self.state[*q]) + .count(); + + for q in &ignored_qubits { + self.ignored[*q] = false; + } + + count + } + + /// Get `num_qubits` enabled qubits, excluding `ignored_qubits`, and returning the + /// clean qubits first. + /// ToDo: check if it's faster to avoid using ignored + fn borrow(&mut self, num_qubits: usize, ignored_qubits: Vec) -> Vec { + for q in &ignored_qubits { + self.ignored[*q] = true; + } + + let clean_ancillas = (0..self.num_qubits) + .filter(|q| !self.ignored[*q] && self.enabled[*q] && self.state[*q]); + let dirty_ancillas = (0..self.num_qubits) + .filter(|q| !self.ignored[*q] && self.enabled[*q] && !self.state[*q]); + let out: Vec = clean_ancillas + .chain(dirty_ancillas) + .take(num_qubits) + .collect(); + + for q in &ignored_qubits { + self.ignored[*q] = false; + } + out + } + + /// Copies the contents + fn copy(&self) -> Self { + QubitTracker { + num_qubits: self.num_qubits, + state: self.state.clone(), + enabled: self.enabled.clone(), + ignored: self.ignored.clone(), + } + } + + /// Replaces the state of the given qubits by their state in the `other` tracker + fn replace_state(&mut self, other: QubitTracker, qubits: Vec) { + for q in qubits { + self.state[q] = other.state[q] + } + } + + /// Pretty-prints + pub fn __str__(&self) -> String { + let mut out = String::from("QubitTracker("); + for q in 0..self.num_qubits { + out.push_str(&q.to_string()); + out.push(':'); + out.push(' '); + if !self.enabled[q] { + out.push('_'); + } else if self.state[q] { + out.push('0'); + } else { + out.push('*'); + } + if q != self.num_qubits - 1 { + out.push(';'); + out.push(' '); + } else { + out.push(')'); + } + } + out + } +} + +/// Correspondence between local qubits and global qubits. +/// +/// An internal class for handling recursion within `HighLevelSynthesis`. +/// Provides correspondence between the qubit indices of an internal DAG, +/// aka the "local qubits" (for instance, of the definition circuit +/// of a custom gate), and the qubit indices of the original DAG, aka the +/// "global qubits". +/// +/// Since the local qubits are consecutive integers starting at zero, +/// i.e. `0`, `1`, `2`, etc., the correspondence is kept using a vector, with +/// the entry in position `k` representing the global qubit that corresponds +/// to the local qubit `k`. +#[pyclass] +#[derive(Clone, Debug)] +pub struct QubitContext { + /// Mapping from local indices to global indices + local_to_global: Vec, +} + +#[pymethods] +impl QubitContext { + #[new] + pub fn new(local_to_global: Vec) -> Self { + QubitContext { local_to_global } + } + + /// Returns the number of local qubits + fn num_qubits(&self) -> usize { + self.local_to_global.len() + } + + /// Extends the correspondence by an additional qubit that + /// maps to the given global qubit. Returns the index of the + /// new local qubit. + fn add_qubit(&mut self, global_qubit: usize) -> usize { + let new_local_qubit = self.local_to_global.len(); + self.local_to_global.push(global_qubit); + new_local_qubit + } + + /// Returns the local-to-global mapping + fn to_global_mapping(&self) -> Vec { + self.local_to_global.clone() + } + + /// Returns the global-to-local mapping + fn to_local_mapping(&self) -> HashMap { + HashMap::from_iter( + self.local_to_global + .iter() + .enumerate() + .map(|(i, j)| (*j, i)), + ) + } + + /// Restricts the context to a subset of qubits, remapping the indices + /// to be consecutive integers starting at zero. + fn restrict(&self, qubits: Vec) -> Self { + QubitContext { + local_to_global: qubits.iter().map(|q| self.local_to_global[*q]).collect(), + } + } + + /// Returns the global qubits corresponding to the given local qubit + fn to_global(&self, qubit: usize) -> usize { + self.local_to_global[qubit] + } + + /// Returns the global qubits corresponding to the given local qubit + fn to_globals(&self, qubits: Vec) -> Vec { + qubits.iter().map(|q| self.local_to_global[*q]).collect() + } +} + +pub fn high_level_synthesis_mod(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index ed3b75d309d6..45cf047a6808 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -10,6 +10,10 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +// This stylistic lint suppression should be in `Cargo.toml`, but we can't do that until we're at an +// MSRV of 1.74 or greater. +#![allow(clippy::comparison_chain)] + use std::env; use pyo3::import_exception; @@ -21,6 +25,7 @@ pub mod circuit_library; pub mod commutation_analysis; pub mod commutation_cancellation; pub mod commutation_checker; +pub mod consolidate_blocks; pub mod convert_2q_block_matrix; pub mod dense_layout; pub mod edge_collections; @@ -31,12 +36,14 @@ pub mod euler_one_qubit_decomposer; pub mod filter_op_nodes; pub mod gate_direction; pub mod gates_in_basis; +pub mod high_level_synthesis; pub mod inverse_cancellation; pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; pub mod remove_diagonal_gates_before_measure; +pub mod remove_identity_equiv; pub mod results; pub mod sabre; pub mod sampled_exp_val; @@ -47,6 +54,7 @@ pub mod star_prerouting; pub mod stochastic_swap; pub mod synthesis; pub mod target_transpiler; +pub mod twirling; pub mod two_qubit_decompose; pub mod uc_gate; pub mod unitary_synthesis; @@ -72,3 +80,4 @@ pub fn getenv_use_multiple_threads() -> bool { } import_exception!(qiskit.exceptions, QiskitError); +import_exception!(qiskit.circuit.exceptions, CircuitError); diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs new file mode 100644 index 000000000000..a3eb921628e2 --- /dev/null +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -0,0 +1,149 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use num_complex::Complex64; +use num_complex::ComplexFloat; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::operations::OperationRef; +use qiskit_circuit::operations::Param; +use qiskit_circuit::operations::StandardGate; +use qiskit_circuit::packed_instruction::PackedInstruction; + +#[pyfunction] +#[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))] +fn remove_identity_equiv( + dag: &mut DAGCircuit, + approx_degree: Option, + target: Option<&Target>, +) { + let mut remove_list: Vec = Vec::new(); + + let get_error_cutoff = |inst: &PackedInstruction| -> f64 { + match approx_degree { + Some(degree) => { + if degree == 1.0 { + f64::EPSILON + } else { + match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err * degree, + None => f64::EPSILON.max(1. - degree), + } + } + None => f64::EPSILON.max(1. - degree), + } + } + } + None => match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err, + None => f64::EPSILON, + } + } + None => f64::EPSILON, + }, + } + }; + + for op_node in dag.op_nodes(false) { + let inst = dag.dag()[op_node].unwrap_operation(); + match inst.op.view() { + OperationRef::Standard(gate) => { + let (dim, trace) = match gate { + StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 2.; + (2., trace) + } else { + continue; + } + } + StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 4.; + (4., trace) + } else { + continue; + } + } + _ => { + // Skip global phase gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let dim = matrix.shape()[0] as f64; + let trace = matrix.diag().iter().sum::().abs(); + (dim, trace) + } else { + continue; + } + } + }; + let error = get_error_cutoff(inst); + let f_pro = (trace / dim).powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + OperationRef::Gate(gate) => { + // Skip global phase like gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let error = get_error_cutoff(inst); + let dim = matrix.shape()[0] as f64; + let trace: Complex64 = matrix.diag().iter().sum(); + let f_pro = (trace / dim).abs().powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + } + _ => continue, + } + } + for node in remove_list { + dag.remove_op_node(node); + } +} + +pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(remove_identity_equiv))?; + Ok(()) +} diff --git a/crates/accelerate/src/sabre/layout.rs b/crates/accelerate/src/sabre/layout.rs index 5ea568559119..9ab67c3fcfbc 100644 --- a/crates/accelerate/src/sabre/layout.rs +++ b/crates/accelerate/src/sabre/layout.rs @@ -62,6 +62,70 @@ pub fn sabre_layout_and_routing( &target, run_in_parallel, )); + starting_layouts.push( + (0..target.neighbors.num_qubits() as u32) + .map(Some) + .collect(), + ); + starting_layouts.push( + (0..target.neighbors.num_qubits() as u32) + .rev() + .map(Some) + .collect(), + ); + // This layout targets the largest ring on an IBM eagle device. It has been + // shown to have good results on some circuits targeting these backends. In + // all other cases this is no different from an additional random trial, + // see: https://xkcd.com/221/ + if target.neighbors.num_qubits() == 127 { + starting_layouts.push( + [ + 0, 1, 2, 3, 4, 5, 6, 15, 22, 23, 24, 25, 34, 43, 42, 41, 40, 53, 60, 59, 61, 62, + 72, 81, 80, 79, 78, 91, 98, 99, 100, 101, 102, 103, 92, 83, 82, 84, 85, 86, 73, 66, + 65, 64, 63, 54, 45, 44, 46, 47, 35, 28, 29, 27, 26, 16, 7, 8, 9, 10, 11, 12, 13, + 17, 30, 31, 32, 36, 51, 50, 49, 48, 55, 68, 67, 69, 70, 74, 89, 88, 87, 93, 106, + 105, 104, 107, 108, 112, 126, 125, 124, 123, 122, 111, 121, 120, 119, 118, 110, + 117, 116, 115, 114, 113, 109, 96, 97, 95, 94, 90, 75, 76, 77, 71, 58, 57, 56, 52, + 37, 38, 39, 33, 20, 21, 19, 18, 14, + ] + .into_iter() + .map(Some) + .collect(), + ); + } else if target.neighbors.num_qubits() == 133 { + // Same for IBM Heron 133 qubit devices. This is the ring computed by using rustworkx's + // max(simple_cycles(graph), key=len) on the connectivity graph. + starting_layouts.push( + [ + 108, 107, 94, 88, 89, 90, 75, 71, 70, 69, 56, 50, 51, 52, 37, 33, 32, 31, 18, 12, + 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 15, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 36, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 53, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 74, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 91, 95, 96, 97, + 110, 116, 117, 118, 119, 120, 111, 101, 102, 103, 104, 105, 112, 124, 125, 126, + 127, 128, 113, 109, + ] + .into_iter() + .map(Some) + .collect(), + ); + } else if target.neighbors.num_qubits() == 156 { + // Same for IBM Heron 156 qubit devices. This is the ring computed by using rustworkx's + // max(simple_cycles(graph), key=len) on the connectivity graph. + starting_layouts.push( + [ + 136, 123, 122, 121, 116, 101, 102, 103, 96, 83, 82, 81, 76, 61, 62, 63, 56, 43, 42, + 41, 36, 21, 22, 23, 16, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 19, 35, 34, + 33, 32, 31, 30, 29, 28, 27, 26, 25, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 77, 85, 86, 87, 88, 89, 90, 91, 92, + 93, 94, 95, 99, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 117, 125, + 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 139, 155, 154, 153, 152, 151, + 150, 149, 148, 147, 146, 145, 144, 143, + ] + .into_iter() + .map(Some) + .collect(), + ); + } let outer_rng = match seed { Some(seed) => Pcg64Mcg::seed_from_u64(seed), None => Pcg64Mcg::from_entropy(), diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index e452f19b3235..8cdc94f316fc 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -12,15 +12,17 @@ use std::collections::btree_map; +use hashbrown::HashSet; +use ndarray::Array2; use num_complex::Complex64; +use num_traits::Zero; use thiserror::Error; use numpy::{ - PyArray1, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyReadonlyArray1, PyReadonlyArray2, - PyUntypedArrayMethods, + PyArray1, PyArray2, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyArrayMethods, + PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods, }; - -use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::exceptions::{PyTypeError, PyValueError, PyZeroDivisionError}; use pyo3::intern; use pyo3::prelude::*; use pyo3::sync::GILOnceCell; @@ -30,6 +32,7 @@ use qiskit_circuit::imports::{ImportOnceCell, NUMPY_COPY_ONLY_IF_NEEDED}; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); +static PAULI_LIST_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "PauliList"); static SPARSE_PAULI_OP_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp"); @@ -67,7 +70,7 @@ static SPARSE_PAULI_OP_TYPE: ImportOnceCell = /// return `PyArray1` at Python-space boundaries) so that it's clear when we're doing /// the transmute, and we have to be explicit about the safety of that. #[repr(u8)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum BitTerm { /// Pauli X operator. X = 0b00_10, @@ -158,6 +161,20 @@ impl BitTerm { _ => Err(BitTermFromU8Error(value)), } } + + /// Does this term include an X component in its ZX representation? + /// + /// This is true for the operators and eigenspace projectors associated with X and Y. + pub fn has_x_component(&self) -> bool { + ((*self as u8) & (Self::X as u8)) != 0 + } + + /// Does this term include a Z component in its ZX representation? + /// + /// This is true for the operators and eigenspace projectors associated with Y and Z. + pub fn has_z_component(&self) -> bool { + ((*self as u8) & (Self::Z as u8)) != 0 + } } static BIT_TERM_PY_ENUM: GILOnceCell> = GILOnceCell::new(); @@ -242,6 +259,24 @@ impl ToPyObject for BitTerm { self.into_py(py) } } +impl<'py> FromPyObject<'py> for BitTerm { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let value = ob + .extract::() + .map_err(|_| match ob.get_type().repr() { + Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {}", repr)), + Err(err) => err, + })?; + let value_error = || { + PyValueError::new_err(format!( + "value {} is not a valid letter of the single-qubit alphabet for 'BitTerm'", + value + )) + }; + let value: u8 = value.try_into().map_err(|_| value_error())?; + value.try_into().map_err(|_| value_error()) + } +} /// The error type for a failed conversion into `BitTerm`. #[derive(Error, Debug)] @@ -263,8 +298,11 @@ impl ::std::convert::TryFrom for BitTerm { } } -/// Error cases stemming from data coherence at the point of entry into `SparseObservable` from raw -/// arrays. +/// Error cases stemming from data coherence at the point of entry into `SparseObservable` from +/// user-provided arrays. +/// +/// These most typically appear during [from_raw_parts], but can also be introduced by various +/// remapping arithmetic functions. /// /// These are generally associated with the Python-space `ValueError` because all of the /// `TypeError`-related ones are statically forbidden (within Rust) by the language, and conversion @@ -285,6 +323,10 @@ pub enum CoherenceError { DecreasingBoundaries, #[error("the values in `indices` are not term-wise increasing")] UnsortedIndices, + #[error("the input contains duplicate qubits")] + DuplicateIndices, + #[error("the provided qubit mapping does not account for all contained qubits")] + IndexMapTooSmall, } impl From for PyErr { fn from(value: CoherenceError) -> PyErr { @@ -312,6 +354,17 @@ impl From for PyErr { } } +#[derive(Error, Debug)] +pub enum ArithmeticError { + #[error("mismatched numbers of qubits: {left}, {right}")] + MismatchedQubits { left: u32, right: u32 }, +} +impl From for PyErr { + fn from(value: ArithmeticError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + /// An observable over Pauli bases that stores its data in a qubit-sparse format. /// /// Mathematics @@ -367,8 +420,8 @@ impl From for PyErr { /// The allowed alphabet forms an overcomplete basis of the operator space. This means that there /// is not a unique summation to represent a given observable. By comparison, /// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of -/// the same Pauli string, removing zeros, and sorting the terms to some canonical order) there is -/// only one representation of any operator. +/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order +/// `) there is only one representation of any operator. /// /// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making /// "efficiency of measurement" equivalent to "efficiency of representation". For example, the @@ -537,6 +590,61 @@ impl From for PyErr { /// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 /// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) /// +/// .. note:: +/// +/// The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`. +/// +/// .. _sparse-observable-canonical-order: +/// +/// Canonical ordering +/// ------------------ +/// +/// For any given mathematical observable, there are several ways of representing it with +/// :class:`SparseObservable`. For example, the same set of single-bit terms and their +/// corresponding indices might appear multiple times in the observable. Mathematically, this is +/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms +/// of the sum in a :class:`SparseObservable` can be in any order while representing the same +/// observable, since addition is commutative (although while floating-point addition is not +/// associative, :class:`SparseObservable` makes no guarantees about the summation order). +/// +/// These two categories of representation degeneracy can cause the ``==`` operator to claim that +/// two observables are not equal, despite representating the same object. In these cases, it can +/// be convenient to define some *canonical form*, which allows observables to be compared +/// structurally. +/// +/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. +/// The precise ordering of terms in canonical ordering is not specified, and may change between +/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables +/// structurally by comparing their simplified forms. +/// +/// .. note:: +/// +/// If you wish to account for floating-point tolerance in the comparison, it is safest to use +/// a recipe such as:: +/// +/// def equivalent(left, right, tol): +/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) +/// +/// .. note:: +/// +/// The canonical form produced by :meth:`simplify` will still not universally detect all +/// observables that are equivalent due to the over-complete basis alphabet; it is not +/// computationally feasible to do this at scale. For example, on observable built from ``+`` +/// and ``-`` components will not canonicalize to a single ``X`` term. +/// +/// Indexing +/// -------- +/// +/// :class:`SparseObservable` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The observable can be indexed by integers, and iterated +/// through to yield individual terms. +/// +/// Each term appears as an instance a self-contained class. The individual terms are copied out of +/// the base observable; mutations to them will not affect the observable. +/// +/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term +/// :members: /// /// Construction /// ============ @@ -565,6 +673,8 @@ impl From for PyErr { /// /// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a :class:`SparseObservable`. /// +/// :meth:`from_terms` Sum explicit single :class:`Term` instances. +/// /// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays /// `. /// ============================ ================================================================ @@ -599,7 +709,59 @@ impl From for PyErr { /// /// :meth:`identity` The identity operator on a given number of qubits. /// ============================ ================================================================ -#[pyclass(module = "qiskit.quantum_info")] +/// +/// +/// Mathematical manipulation +/// ========================= +/// +/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other +/// :mod:`~qiskit.quantum_info` operators. +/// +/// In basic arithmetic, you can: +/// +/// * add two observables using ``+`` +/// * subtract two observables using ``-`` +/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` +/// * negate all the coefficients in an observable with unary ``-`` +/// +/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, +/// which mutates the left-hand side in-place. Using these is typically more efficient than the +/// infix operators, especially for building an observable in a loop. +/// +/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of +/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be +/// equivalent to :meth:`tensor`. +/// +/// .. note:: +/// +/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware +/// that `Python's operator-precedence rules +/// `__ may cause the +/// evaluation order to be different to your expectation. In particular, the operator ``+`` +/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. +/// +/// When using the operators in mixed expressions, it is safest to use parentheses to group the +/// operands of tensor products. +/// +/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex +/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined +/// analogously to the matrix representation of other Pauli operators in Qiskit. +/// +/// +/// Efficiency notes +/// ---------------- +/// +/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating +/// growable vectors for extending the number of terms. This means that the cost of appending to an +/// observable using ``+=`` is amortised linear in the total number of terms added, rather than +/// the quadratic complexity that the binary ``+`` would require. +/// +/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic +/// "simplification" (summing of like terms), because the majority of additions to build up an +/// observable generate only a small number of duplications, and like-term detection has additional +/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, +/// or discuss further APIs with us for better building of observables. +#[pyclass(module = "qiskit.quantum_info", sequence)] #[derive(Clone, Debug, PartialEq)] pub struct SparseObservable { /// The number of qubits the operator acts on. This is not inferable from any other shape or @@ -663,7 +825,9 @@ impl SparseObservable { let indices = &indices[left..right]; if !indices.is_empty() { for (index_left, index_right) in indices[..].iter().zip(&indices[1..]) { - if index_left >= index_right { + if index_left == index_right { + return Err(CoherenceError::DuplicateIndices); + } else if index_left > index_right { return Err(CoherenceError::UnsortedIndices); } } @@ -699,12 +863,33 @@ impl SparseObservable { } } + /// Create a zero operator with pre-allocated space for the given number of summands and + /// single-qubit bit terms. + #[inline] + pub fn with_capacity(num_qubits: u32, num_terms: usize, num_bit_terms: usize) -> Self { + Self { + num_qubits, + coeffs: Vec::with_capacity(num_terms), + bit_terms: Vec::with_capacity(num_bit_terms), + indices: Vec::with_capacity(num_bit_terms), + boundaries: { + let mut boundaries = Vec::with_capacity(num_terms + 1); + boundaries.push(0); + boundaries + }, + } + } + /// Get an iterator over the individual terms of the operator. + /// + /// Recall that two [SparseObservable]s that have different term orders can still represent the + /// same object. Use [canonicalize] to apply a canonical ordering to the terms. pub fn iter(&'_ self) -> impl ExactSizeIterator> + '_ { self.coeffs.iter().enumerate().map(|(i, coeff)| { let start = self.boundaries[i]; let end = self.boundaries[i + 1]; SparseTermView { + num_qubits: self.num_qubits, coeff: *coeff, bit_terms: &self.bit_terms[start..end], indices: &self.indices[start..end], @@ -712,6 +897,104 @@ impl SparseObservable { }) } + /// Get an iterator over the individual terms of the operator that allows in-place mutation. + /// + /// The length and indices of these views cannot be mutated, since both would allow breaking + /// data coherence. + pub fn iter_mut(&mut self) -> IterMut<'_> { + self.into() + } + + /// Reduce the observable to its canonical form. + /// + /// This sums like terms, removing them if the final complex coefficient's absolute value is + /// less than or equal to the tolerance. The terms are reordered to some canonical ordering. + /// + /// This function is idempotent. + pub fn canonicalize(&self, tol: f64) -> SparseObservable { + let mut terms = btree_map::BTreeMap::new(); + for term in self.iter() { + terms + .entry((term.indices, term.bit_terms)) + .and_modify(|c| *c += term.coeff) + .or_insert(term.coeff); + } + let mut out = SparseObservable::zero(self.num_qubits); + for ((indices, bit_terms), coeff) in terms { + if coeff.norm_sqr() <= tol * tol { + continue; + } + out.coeffs.push(coeff); + out.bit_terms.extend_from_slice(bit_terms); + out.indices.extend_from_slice(indices); + out.boundaries.push(out.indices.len()); + } + out + } + + /// Tensor product of `self` with `other`. + /// + /// The bit ordering is defined such that the qubit indices of `other` will remain the same, and + /// the indices of `self` will be offset by the number of qubits in `other`. This is the same + /// convention as used by the rest of Qiskit's `quantum_info` operators. + /// + /// Put another way, in the simplest case of two observables formed of dense labels: + /// + /// ``` + /// let mut left = SparseObservable::zero(5); + /// left.add_dense_label("IXY+Z", Complex64::new(1.0, 0.0)); + /// let mut right = SparseObservable::zero(6); + /// right.add_dense_label("IIrl01", Complex64::new(1.0, 0.0)); + /// + /// // The result is the concatenation of the two labels. + /// let mut out = SparseObservable::zero(11); + /// out.add_dense_label("IXY+ZIIrl01", Complex64::new(1.0, 0.0)); + /// + /// assert_eq!(left.tensor(right), out); + /// ``` + pub fn tensor(&self, other: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits + other.num_qubits, + self.coeffs.len() * other.coeffs.len(), + other.coeffs.len() * self.bit_terms.len() + self.coeffs.len() * other.bit_terms.len(), + ); + let mut self_indices = Vec::new(); + for self_term in self.iter() { + self_indices.clear(); + self_indices.extend(self_term.indices.iter().map(|i| i + other.num_qubits)); + for other_term in other.iter() { + out.coeffs.push(self_term.coeff * other_term.coeff); + out.indices.extend_from_slice(other_term.indices); + out.indices.extend_from_slice(&self_indices); + out.bit_terms.extend_from_slice(other_term.bit_terms); + out.bit_terms.extend_from_slice(self_term.bit_terms); + out.boundaries.push(out.bit_terms.len()); + } + } + out + } + + /// Get a view onto a representation of a single sparse term. + /// + /// This is effectively an indexing operation into the [SparseObservable]. Recall that two + /// [SparseObservable]s that have different term orders can still represent the same object. + /// Use [canonicalize] to apply a canonical ordering to the terms. + /// + /// # Panics + /// + /// If the index is out of bounds. + pub fn term(&self, index: usize) -> SparseTermView { + debug_assert!(index < self.num_terms(), "index {index} out of bounds"); + let start = self.boundaries[index]; + let end = self.boundaries[index + 1]; + SparseTermView { + num_qubits: self.num_qubits, + coeff: self.coeffs[index], + bit_terms: &self.bit_terms[start..end], + indices: &self.indices[start..end], + } + } + /// Add the term implied by a dense string label onto this observable. pub fn add_dense_label>( &mut self, @@ -747,13 +1030,76 @@ impl SparseObservable { self.boundaries.push(self.bit_terms.len()); Ok(()) } + + /// Relabel the `indices` in the operator to new values. + /// + /// This fails if any of the new indices are too large, or if any mapping would cause a term to + /// contain duplicates of the same index. It may not detect if multiple qubits are mapped to + /// the same index, if those qubits never appear together in the same term. Such a mapping + /// would not cause data-coherence problems (the output observable will be valid), but is + /// unlikely to be what you intended. + /// + /// *Panics* if `new_qubits` is not long enough to map every index used in the operator. + pub fn relabel_qubits_from_slice(&mut self, new_qubits: &[u32]) -> Result<(), CoherenceError> { + for qubit in new_qubits { + if *qubit >= self.num_qubits { + return Err(CoherenceError::BitIndexTooHigh); + } + } + let mut order = btree_map::BTreeMap::new(); + for i in 0..self.num_terms() { + let start = self.boundaries[i]; + let end = self.boundaries[i + 1]; + for j in start..end { + order.insert(new_qubits[self.indices[j] as usize], self.bit_terms[j]); + } + if order.len() != end - start { + return Err(CoherenceError::DuplicateIndices); + } + for (index, dest) in order.keys().zip(&mut self.indices[start..end]) { + *dest = *index; + } + for (bit_term, dest) in order.values().zip(&mut self.bit_terms[start..end]) { + *dest = *bit_term; + } + order.clear(); + } + Ok(()) + } + + /// Add a single term to this operator. + pub fn add_term(&mut self, term: SparseTermView) -> Result<(), ArithmeticError> { + if self.num_qubits != term.num_qubits { + return Err(ArithmeticError::MismatchedQubits { + left: self.num_qubits, + right: term.num_qubits, + }); + } + self.coeffs.push(term.coeff); + self.bit_terms.extend_from_slice(term.bit_terms); + self.indices.extend_from_slice(term.indices); + self.boundaries.push(self.bit_terms.len()); + Ok(()) + } + + /// Return a suitable Python error if two observables do not have equal numbers of qubits. + fn check_equal_qubits(&self, other: &SparseObservable) -> PyResult<()> { + if self.num_qubits != other.num_qubits { + Err(PyValueError::new_err(format!( + "incompatible numbers of qubits: {} and {}", + self.num_qubits, other.num_qubits + ))) + } else { + Ok(()) + } + } } #[pymethods] impl SparseObservable { #[pyo3(signature = (data, /, num_qubits=None))] #[new] - fn py_new(data: Bound, num_qubits: Option) -> PyResult { + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { let py = data.py(); let check_num_qubits = |data: &Bound| -> PyResult<()> { let Some(num_qubits) = num_qubits else { @@ -769,11 +1115,11 @@ impl SparseObservable { }; if data.is_instance(PAULI_TYPE.get_bound(py))? { - check_num_qubits(&data)?; + check_num_qubits(data)?; return Self::py_from_pauli(data); } if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? { - check_num_qubits(&data)?; + check_num_qubits(data)?; return Self::py_from_sparse_pauli_op(data); } if let Ok(label) = data.extract::() { @@ -787,8 +1133,8 @@ impl SparseObservable { } return Self::py_from_label(&label).map_err(PyErr::from); } - if let Ok(observable) = data.downcast::() { - check_num_qubits(&data)?; + if let Ok(observable) = data.downcast_exact::() { + check_num_qubits(data)?; return Ok(observable.borrow().clone()); } // The type of `vec` is inferred from the subsequent calls to `Self::py_from_list` or @@ -805,6 +1151,12 @@ impl SparseObservable { }; return Self::py_from_sparse_list(vec, num_qubits).map_err(PyErr::from); } + if let Ok(term) = data.downcast_exact::() { + return Ok(term.borrow().to_observable()); + }; + if let Ok(observable) = Self::py_from_terms(data, num_qubits) { + return Ok(observable); + } Err(PyTypeError::new_err(format!( "unknown input format for 'SparseObservable': {}", data.get_type().repr()?, @@ -887,6 +1239,14 @@ impl SparseObservable { .map(|obj| obj.clone_ref(py)) } + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn Term(py: Python) -> Bound { + py.get_type_bound::() + } + /// Get the zero operator over the given number of qubits. /// /// The zero operator is the operator whose expectation value is zero for all quantum states. @@ -907,13 +1267,7 @@ impl SparseObservable { #[pyo3(signature = (/, num_qubits))] #[staticmethod] pub fn zero(num_qubits: u32) -> Self { - Self { - num_qubits, - coeffs: vec![], - bit_terms: vec![], - indices: vec![], - boundaries: vec![0], - } + Self::with_capacity(num_qubits, 0, 0) } /// Get the identity operator over the given number of qubits. @@ -936,6 +1290,41 @@ impl SparseObservable { } } + /// Clear all the terms from this operator, making it equal to the zero operator again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// substraction operations may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) + /// >>> obs.clear() + /// >>> assert obs == SparseObservable.zero(obs.num_qubits) + pub fn clear(&mut self) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.truncate(1); + } + + fn __len__(&self) -> usize { + self.num_terms() + } + + fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult> { + let indices = match index.with_len(self.num_terms())? { + SequenceIndex::Int(index) => return Ok(self.term(index).to_term().into_py(py)), + indices => indices, + }; + let mut out = SparseObservable::zero(self.num_qubits); + for index in indices.iter() { + out.add_term(self.term(index))?; + } + Ok(out.into_py(py)) + } + fn __repr__(&self) -> String { let num_terms = format!( "{} term{}", @@ -951,19 +1340,8 @@ impl SparseObservable { "0.0".to_owned() } else { self.iter() - .map(|term| { - let coeff = format!("{}", term.coeff).replace('i', "j"); - let paulis = term - .indices - .iter() - .zip(term.bit_terms) - .rev() - .map(|(i, op)| format!("{}_{}", op.py_label(), i)) - .collect::>() - .join(" "); - format!("({})({})", coeff, paulis) - }) - .collect::>() + .map(SparseTermView::to_sparse_str) + .collect::>() .join(" + ") }; format!( @@ -998,6 +1376,148 @@ impl SparseObservable { slf.borrow().eq(&other.borrow()) } + fn __add__(slf_: &Bound, other: &Bound) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + // This fast path is for consistency with the in-place `__iadd__`, which would otherwise + // struggle to do the addition to itself. + return Ok(<&SparseObservable as ::std::ops::Mul<_>>::mul( + &slf_.borrow(), + Complex64::new(2.0, 0.0), + ) + .into_py(py)); + } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let slf_ = slf_.borrow(); + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + Ok(<&SparseObservable as ::std::ops::Add>::add(&slf_, &other).into_py(py)) + } + fn __radd__(&self, other: &Bound) -> PyResult> { + // No need to handle the `self is other` case here, because `__add__` will get it. + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let other = other.borrow(); + self.check_equal_qubits(&other)?; + Ok((<&SparseObservable as ::std::ops::Add>::add(&other, self)).into_py(py)) + } + fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + *slf_.borrow_mut() *= Complex64::new(2.0, 0.0); + return Ok(()); + } + let mut slf_ = slf_.borrow_mut(); + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place addition of 'SparseObservable': {}", + other.repr()? + ))); + }; + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + *slf_ += &other; + Ok(()) + } + + fn __sub__(slf_: &Bound, other: &Bound) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + return Ok(SparseObservable::zero(slf_.borrow().num_qubits).into_py(py)); + } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let slf_ = slf_.borrow(); + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + Ok(<&SparseObservable as ::std::ops::Sub>::sub(&slf_, &other).into_py(py)) + } + fn __rsub__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let other = other.borrow(); + self.check_equal_qubits(&other)?; + Ok((<&SparseObservable as ::std::ops::Sub>::sub(&other, self)).into_py(py)) + } + fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + // This is not strictly the same thing as `a - a` if `a` contains non-finite + // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a + // clear view on what floating-point guarantees we're going to make right now. + slf_.borrow_mut().clear(); + return Ok(()); + } + let mut slf_ = slf_.borrow_mut(); + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place subtraction of 'SparseObservable': {}", + other.repr()? + ))); + }; + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + *slf_ -= &other; + Ok(()) + } + + fn __pos__(&self) -> SparseObservable { + self.clone() + } + fn __neg__(&self) -> SparseObservable { + -self + } + + fn __mul__(&self, other: Complex64) -> SparseObservable { + self * other + } + fn __rmul__(&self, other: Complex64) -> SparseObservable { + other * self + } + fn __imul__(&mut self, other: Complex64) { + *self *= other; + } + + fn __truediv__(&self, other: Complex64) -> PyResult { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + Ok(self / other) + } + fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + *self /= other; + Ok(()) + } + + fn __xor__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + Ok(self.tensor(&other.borrow()).into_py(py)) + } + fn __rxor__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + Ok(other.borrow().tensor(self).into_py(py)) + } + // This doesn't actually have any interaction with Python space, but uses the `py_` prefix on // its name to make it clear it's different to the Rust concept of `Copy`. /// Get a copy of this observable. @@ -1116,14 +1636,7 @@ impl SparseObservable { Some(num_qubits) => num_qubits, None => iter[0].0.len() as u32, }; - let mut out = Self { - num_qubits, - coeffs: Vec::with_capacity(iter.len()), - bit_terms: Vec::new(), - indices: Vec::new(), - boundaries: Vec::with_capacity(iter.len() + 1), - }; - out.boundaries.push(0); + let mut out = Self::with_capacity(num_qubits, iter.len(), 0); for (label, coeff) in iter { out.add_dense_label(&label, coeff)?; } @@ -1244,7 +1757,7 @@ impl SparseObservable { /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) #[staticmethod] #[pyo3(name = "from_pauli", signature = (pauli, /))] - fn py_from_pauli(pauli: Bound) -> PyResult { + fn py_from_pauli(pauli: &Bound) -> PyResult { let py = pauli.py(); let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; let z = pauli @@ -1311,7 +1824,7 @@ impl SparseObservable { /// #[staticmethod] #[pyo3(name = "from_sparse_pauli_op", signature = (op, /))] - fn py_from_sparse_pauli_op(op: Bound) -> PyResult { + fn py_from_sparse_pauli_op(op: &Bound) -> PyResult { let py = op.py(); let pauli_list_ob = op.getattr(intern!(py, "paulis"))?; let coeffs = op @@ -1378,6 +1891,42 @@ impl SparseObservable { }) } + /// Construct a :class:`SparseObservable` out of individual terms. + /// + /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument + /// must match the terms. + /// + /// No simplification is done as part of the observable creation. + /// + /// Args: + /// obj (Iterable[Term]): Iterable of individual terms to build the observable from. + /// num_qubits (int | None): The number of qubits the observable should act on. This is + /// usually inferred from the input, but can be explicitly given to handle the case + /// of an empty iterable. + /// + /// Returns: + /// The corresponding observable. + #[staticmethod] + #[pyo3(signature = (obj, /, num_qubits=None), name="from_terms")] + fn py_from_terms(obj: &Bound, num_qubits: Option) -> PyResult { + let mut iter = obj.iter()?; + let mut obs = match num_qubits { + Some(num_qubits) => SparseObservable::zero(num_qubits), + None => { + let Some(first) = iter.next() else { + return Err(PyValueError::new_err( + "cannot construct an observable from an empty list without knowing `num_qubits`", + )); + }; + first?.downcast::()?.borrow().to_observable() + } + }; + for term in iter { + obs.add_term(term?.downcast::()?.borrow().view())?; + } + Ok(obs) + } + // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then // the `bit_terms` must all be valid `BitTerm` representations. /// Construct a :class:`.SparseObservable` from raw Numpy arrays that match :ref:`the required @@ -1454,18 +2003,623 @@ impl SparseObservable { Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) } } + + /// Sum any like terms in this operator, removing them if the resulting complex coefficient has + /// an absolute value within tolerance of zero. + /// + /// As a side effect, this sorts the operator into :ref:`canonical order + /// `. + /// + /// .. note:: + /// + /// When using this for equality comparisons, note that floating-point rounding and the + /// non-associativity fo floating-point addition may cause non-zero coefficients of summed + /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to + /// compare the canonicalized difference of the two observables to zero. + /// + /// Args: + /// tol (float): after summing like terms, any coefficients whose absolute value is less + /// than the given absolute tolerance will be suppressed from the output. + /// + /// Examples: + /// + /// Using :meth:`simplify` to compare two operators that represent the same observable, but + /// would compare unequal due to the structural tests by default:: + /// + /// >>> base = SparseObservable.from_sparse_list([ + /// ... ("XZ", (2, 1), 1e-10), # value too small + /// ... ("+-", (3, 1), 2j), + /// ... ("+-", (3, 1), 2j), # can be combined with the above + /// ... ("01", (3, 1), 0.5), # out of order compared to `expected` + /// ... ], num_qubits=5) + /// >>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)]) + /// >>> assert base != expected # non-canonical comparison + /// >>> assert base.simplify() == expected.simplify() + /// + /// Note that in the above example, the coefficients are chosen such that all floating-point + /// calculations are exact, and there are no intermediate rounding or associativity + /// concerns. If this cannot be guaranteed to be the case, the safer form is:: + /// + /// >>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 + /// >>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 + /// >>> assert left.simplify() != right.simplify() + /// >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits) + #[pyo3( + signature = (/, tol=1e-8), + name = "simplify", + )] + fn py_simplify(&self, tol: f64) -> SparseObservable { + self.canonicalize(tol) + } + + /// Tensor product of two observables. + /// + /// The bit ordering is defined such that the qubit indices of the argument will remain the + /// same, and the indices of ``self`` will be offset by the number of qubits in ``other``. This + /// is the same convention as used by the rest of Qiskit's :mod:`~qiskit.quantum_info` + /// operators. + /// + /// This function is used for the infix ``^`` operator. If using this operator, beware that + /// `Python's operator-precedence rules + /// `__ may cause the + /// evaluation order to be different to your expectation. In particular, the operator ``+`` + /// binds more tightly than ``^``, just like ``*`` binds more tightly than ``+``. Use + /// parentheses to fix the evaluation order, if needed. + /// + /// The argument will be cast to :class:`SparseObservable` using its default constructor, if it + /// is not already in the correct form. + /// + /// Args: + /// + /// other: the observable to put on the right-hand side of the tensor product. + /// + /// Examples: + /// + /// The bit ordering of this is such that the tensor product of two observables made from a + /// single label "looks like" an observable made by concatenating the two strings:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl") + /// + /// You can also use the infix ``^`` operator for tensor products, which will similarly cast + /// the right-hand side of the operation if it is not already a :class:`SparseObservable`:: + /// + /// >>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ") + /// + /// See also: + /// :meth:`expand` + /// + /// The same function, but with the order of arguments flipped. This can be useful if + /// you like using the casting behavior for the argument, but you want your existing + /// :class:`SparseObservable` to be on the right-hand side of the tensor ordering. + #[pyo3(signature = (other, /), name = "tensor")] + fn py_tensor(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for tensor: {}", + other.get_type().repr()? + ))); + }; + Ok(self.tensor(&other.borrow()).into_py(py)) + } + + /// Reverse-order tensor product. + /// + /// This is equivalent to ``other.tensor(self)``, except that ``other`` will first be type-cast + /// to :class:`SparseObservable` if it isn't already one (by calling the default constructor). + /// + /// Args: + /// + /// other: the observable to put on the left-hand side of the tensor product. + /// + /// Examples: + /// + /// This is equivalent to :meth:`tensor` with the order of the arguments flipped:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == right.expand(left) + /// + /// See also: + /// :meth:`tensor` + /// + /// The same function with the order of arguments flipped. :meth:`tensor` is the more + /// standard argument ordering, and matches Qiskit's other conventions. + #[pyo3(signature = (other, /), name = "expand")] + fn py_expand(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for expand: {}", + other.get_type().repr()? + ))); + }; + Ok(other.borrow().tensor(self).into_py(py)) + } + + /// Calculate the adjoint of this observable. + /// + /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit + /// alphabet are self-adjoint, so the result of this operation is the same observable, except + /// its coefficients are all their complex conjugates. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) + /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) + /// >>> assert left.adjoint() == right + fn adjoint(&self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c.conj()).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + + /// Calculate the complex conjugation of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` conjguates to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Additionally, all coefficients are conjugated. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) + fn conjugate(&self) -> SparseObservable { + let mut out = self.transpose(); + for coeff in out.coeffs.iter_mut() { + *coeff = coeff.conj(); + } + out + } + + /// Calculate the matrix transposition of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` transposes to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) + fn transpose(&self) -> SparseObservable { + let mut out = self.clone(); + for term in out.iter_mut() { + for bit_term in term.bit_terms { + match bit_term { + BitTerm::Y => { + *term.coeff = -*term.coeff; + } + BitTerm::Right => { + *bit_term = BitTerm::Left; + } + BitTerm::Left => { + *bit_term = BitTerm::Right; + } + _ => (), + } + } + } + out + } + + /// Apply a transpiler layout to this :class:`SparseObservable`. + /// + /// Typically you will have defined your observable in terms of the virtual qubits of the + /// circuits you will use to prepare states. After transpilation, the virtual qubits are mapped + /// to particular physical qubits on a device, which may be wider than your circuit. That + /// mapping can also change over the course of the circuit. This method transforms the input + /// observable on virtual qubits to an observable that is suitable to apply immediately after + /// the fully transpiled *physical* circuit. + /// + /// Args: + /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this + /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that + /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. + /// If given as explicitly ``None``, no remapping is applied (but you can still use + /// ``num_qubits`` to expand the observable). + /// num_qubits (int | None): The number of qubits to expand the observable to. If not + /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the + /// same width as the input if the ``layout`` is given in another form. + /// + /// Returns: + /// A new :class:`SparseObservable` with the provided layout applied. + #[pyo3(signature = (/, layout, num_qubits=None), name = "apply_layout")] + fn py_apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + let py = layout.py(); + let check_inferred_qubits = |inferred: u32| -> PyResult { + if inferred < self.num_qubits { + return Err(PyValueError::new_err(format!( + "cannot shrink the qubit count in an observable from {} to {}", + self.num_qubits, inferred + ))); + } + Ok(inferred) + }; + if layout.is_none() { + let mut out = self.clone(); + out.num_qubits = check_inferred_qubits(num_qubits.unwrap_or(self.num_qubits))?; + return Ok(out); + } + let (num_qubits, layout) = if layout.is_instance( + &py.import_bound(intern!(py, "qiskit.transpiler"))? + .getattr(intern!(py, "TranspileLayout"))?, + )? { + ( + check_inferred_qubits( + layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 + )?, + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ) + } else { + ( + check_inferred_qubits(num_qubits.unwrap_or(self.num_qubits))?, + layout.extract()?, + ) + }; + if layout.len() < self.num_qubits as usize { + return Err(CoherenceError::IndexMapTooSmall.into()); + } + if layout.iter().any(|qubit| *qubit >= num_qubits) { + return Err(CoherenceError::BitIndexTooHigh.into()); + } + if layout.iter().collect::>().len() != layout.len() { + return Err(CoherenceError::DuplicateIndices.into()); + } + let mut out = self.clone(); + out.num_qubits = num_qubits; + out.relabel_qubits_from_slice(&layout)?; + Ok(out) + } + + /// Get a :class:`.PauliList` object that represents the measurement basis needed for each term + /// (in order) in this observable. + /// + /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting + /// :class:`.Pauli` is dense, in the sense that explicit identities are stored. An identity in + /// the Pauli output does not require a concrete measurement. + /// + /// This will return an entry in the Pauli list for every term in the sum. + /// + /// Returns: + /// :class:`.PauliList`: the Pauli operator list representing the necessary measurement + /// bases. + #[pyo3(name = "pauli_bases")] + fn py_pauli_bases<'py>(&self, py: Python<'py>) -> PyResult> { + let mut x = Array2::from_elem([self.num_terms(), self.num_qubits as usize], false); + let mut z = Array2::from_elem([self.num_terms(), self.num_qubits as usize], false); + for (loc, term) in self.iter().enumerate() { + let mut x_row = x.row_mut(loc); + let mut z_row = z.row_mut(loc); + for (bit_term, index) in term.bit_terms.iter().zip(term.indices) { + x_row[*index as usize] = bit_term.has_x_component(); + z_row[*index as usize] = bit_term.has_z_component(); + } + } + PAULI_LIST_TYPE + .get_bound(py) + .getattr(intern!(py, "from_symplectic"))? + .call1(( + PyArray2::from_owned_array_bound(py, z), + PyArray2::from_owned_array_bound(py, x), + )) + } +} + +impl ::std::ops::Add<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn add(mut self, rhs: &SparseObservable) -> SparseObservable { + self += rhs; + self + } +} +impl ::std::ops::Add for &SparseObservable { + type Output = SparseObservable; + + fn add(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out += rhs; + out + } +} +impl ::std::ops::AddAssign<&SparseObservable> for SparseObservable { + fn add_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to add two operators with incompatible qubit counts"); + } + self.coeffs.extend_from_slice(&rhs.coeffs); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Sub<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn sub(mut self, rhs: &SparseObservable) -> SparseObservable { + self -= rhs; + self + } +} +impl ::std::ops::Sub for &SparseObservable { + type Output = SparseObservable; + + fn sub(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out -= rhs; + out + } +} +impl ::std::ops::SubAssign<&SparseObservable> for SparseObservable { + fn sub_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to subtract two operators with incompatible qubit counts"); + } + self.coeffs.extend(rhs.coeffs.iter().map(|coeff| -coeff)); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Mul for SparseObservable { + type Output = SparseObservable; + + fn mul(mut self, rhs: Complex64) -> SparseObservable { + self *= rhs; + self + } +} +impl ::std::ops::Mul for &SparseObservable { + type Output = SparseObservable; + + fn mul(self, rhs: Complex64) -> SparseObservable { + if rhs == Complex64::new(0.0, 0.0) { + SparseObservable::zero(self.num_qubits) + } else { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c * rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + } +} +impl ::std::ops::Mul for Complex64 { + type Output = SparseObservable; + + fn mul(self, mut rhs: SparseObservable) -> SparseObservable { + rhs *= self; + rhs + } +} +impl ::std::ops::Mul<&SparseObservable> for Complex64 { + type Output = SparseObservable; + + fn mul(self, rhs: &SparseObservable) -> SparseObservable { + rhs * self + } +} +impl ::std::ops::MulAssign for SparseObservable { + fn mul_assign(&mut self, rhs: Complex64) { + if rhs == Complex64::new(0.0, 0.0) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.clear(); + self.boundaries.push(0); + } else { + self.coeffs.iter_mut().for_each(|c| *c *= rhs) + } + } +} + +impl ::std::ops::Div for SparseObservable { + type Output = SparseObservable; + + fn div(mut self, rhs: Complex64) -> SparseObservable { + self /= rhs; + self + } +} +impl ::std::ops::Div for &SparseObservable { + type Output = SparseObservable; + + fn div(self, rhs: Complex64) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c / rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::DivAssign for SparseObservable { + fn div_assign(&mut self, rhs: Complex64) { + self.coeffs.iter_mut().for_each(|c| *c /= rhs) + } +} + +impl ::std::ops::Neg for &SparseObservable { + type Output = SparseObservable; + + fn neg(self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| -c).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::Neg for SparseObservable { + type Output = SparseObservable; + + fn neg(mut self) -> SparseObservable { + self.coeffs.iter_mut().for_each(|c| *c = -*c); + self + } } /// A view object onto a single term of a `SparseObservable`. /// /// The lengths of `bit_terms` and `indices` are guaranteed to be created equal, but might be zero /// (in the case that the term is proportional to the identity). -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, PartialEq, Debug)] pub struct SparseTermView<'a> { + pub num_qubits: u32, pub coeff: Complex64, pub bit_terms: &'a [BitTerm], pub indices: &'a [u32], } +impl<'a> SparseTermView<'a> { + /// Convert this `SparseTermView` into an owning [SparseTerm] of the same data. + pub fn to_term(&self) -> SparseTerm { + SparseTerm { + num_qubits: self.num_qubits, + coeff: self.coeff, + bit_terms: self.bit_terms.into(), + indices: self.indices.into(), + } + } + + fn to_sparse_str(self) -> String { + let coeff = format!("{}", self.coeff).replace('i', "j"); + let paulis = self + .indices + .iter() + .zip(self.bit_terms) + .rev() + .map(|(i, op)| format!("{}_{}", op.py_label(), i)) + .collect::>() + .join(" "); + format!("({})({})", coeff, paulis) + } +} + +/// A mutable view object onto a single term of a [SparseObservable]. +/// +/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero +/// (in the case that the term is proportional to the identity). [indices] is not mutable because +/// this would allow data coherence to be broken. +#[derive(Debug)] +pub struct SparseTermViewMut<'a> { + pub num_qubits: u32, + pub coeff: &'a mut Complex64, + pub bit_terms: &'a mut [BitTerm], + pub indices: &'a [u32], +} + +/// Iterator type allowing in-place mutation of the [SparseObservable]. +/// +/// Created by [SparseObservable::iter_mut]. +#[derive(Debug)] +pub struct IterMut<'a> { + num_qubits: u32, + coeffs: &'a mut [Complex64], + bit_terms: &'a mut [BitTerm], + indices: &'a [u32], + boundaries: &'a [usize], + i: usize, +} +impl<'a> From<&'a mut SparseObservable> for IterMut<'a> { + fn from(value: &mut SparseObservable) -> IterMut { + IterMut { + num_qubits: value.num_qubits, + coeffs: &mut value.coeffs, + bit_terms: &mut value.bit_terms, + indices: &value.indices, + boundaries: &value.boundaries, + i: 0, + } + } +} +impl<'a> Iterator for IterMut<'a> { + type Item = SparseTermViewMut<'a>; + + fn next(&mut self) -> Option { + // The trick here is that the lifetime of the 'self' borrow is shorter than the lifetime of + // the inner borrows. We can't give out mutable references to our inner borrow, because + // after the lifetime on 'self' expired, there'd be nothing preventing somebody using the + // 'self' borrow to see _another_ mutable borrow of the inner data, which would be an + // aliasing violation. Instead, we keep splitting the inner views we took out so the + // mutable references we return don't overlap with the ones we continue to hold. + let coeffs = ::std::mem::take(&mut self.coeffs); + let (coeff, other_coeffs) = coeffs.split_first_mut()?; + self.coeffs = other_coeffs; + + let len = self.boundaries[self.i + 1] - self.boundaries[self.i]; + self.i += 1; + + let all_bit_terms = ::std::mem::take(&mut self.bit_terms); + let all_indices = ::std::mem::take(&mut self.indices); + let (bit_terms, rest_bit_terms) = all_bit_terms.split_at_mut(len); + let (indices, rest_indices) = all_indices.split_at(len); + self.bit_terms = rest_bit_terms; + self.indices = rest_indices; + + Some(SparseTermViewMut { + num_qubits: self.num_qubits, + coeff, + bit_terms, + indices, + }) + } + + fn size_hint(&self) -> (usize, Option) { + (self.coeffs.len(), Some(self.coeffs.len())) + } +} +impl<'a> ExactSizeIterator for IterMut<'a> {} +impl<'a> ::std::iter::FusedIterator for IterMut<'a> {} /// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at. #[derive(Clone, Copy, PartialEq, Eq)] @@ -1657,6 +2811,190 @@ impl ArrayView { } } +/// A single term from a complete :class:`SparseObservable`. +/// +/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. +#[pyclass(name = "Term", frozen, module = "qiskit.quantum_info")] +#[derive(Clone, Debug, PartialEq)] +pub struct SparseTerm { + /// Number of qubits the entire term applies to. + #[pyo3(get)] + num_qubits: u32, + /// The complex coefficient of the term. + #[pyo3(get)] + coeff: Complex64, + bit_terms: Box<[BitTerm]>, + indices: Box<[u32]>, +} +impl SparseTerm { + pub fn view(&self) -> SparseTermView { + SparseTermView { + num_qubits: self.num_qubits, + coeff: self.coeff, + bit_terms: &self.bit_terms, + indices: &self.indices, + } + } +} + +#[pymethods] +impl SparseTerm { + // Mark the Python class as being defined "within" the `SparseObservable` class namespace. + #[classattr] + #[pyo3(name = "__qualname__")] + fn type_qualname() -> &'static str { + "SparseObservable.Term" + } + + #[new] + #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))] + fn py_new( + num_qubits: u32, + coeff: Complex64, + bit_terms: Vec, + indices: Vec, + ) -> PyResult { + if bit_terms.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + bit_terms: bit_terms.len(), + indices: indices.len(), + } + .into()); + } + let mut order = (0..bit_terms.len()).collect::>(); + order.sort_unstable_by_key(|a| indices[*a]); + let bit_terms = order.iter().map(|i| bit_terms[*i]).collect(); + let mut sorted_indices = Vec::::with_capacity(order.len()); + for i in order { + let index = indices[i]; + if sorted_indices + .last() + .map(|prev| *prev >= index) + .unwrap_or(false) + { + return Err(CoherenceError::UnsortedIndices.into()); + } + sorted_indices.push(index) + } + Ok(Self { + num_qubits, + coeff, + bit_terms, + indices: sorted_indices.into_boxed_slice(), + }) + } + + /// Convert this term to a complete :class:`SparseObservable`. + pub fn to_observable(&self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: vec![self.coeff], + bit_terms: self.bit_terms.to_vec(), + indices: self.indices.to_vec(), + boundaries: vec![0, self.bit_terms.len()], + } + } + + fn __eq__(slf: Bound, other: Bound) -> bool { + if slf.is(&other) { + return true; + } + let Ok(other) = other.downcast_into::() else { + return false; + }; + slf.borrow().eq(&other.borrow()) + } + + fn __repr__(&self) -> String { + format!( + "<{} on {} qubit{}: {}>", + Self::type_qualname(), + self.num_qubits, + if self.num_qubits == 1 { "" } else { "s" }, + self.view().to_sparse_str(), + ) + } + + fn __getnewargs__(slf_: Bound, py: Python) -> Py { + let (num_qubits, coeff) = { + let slf_ = slf_.borrow(); + (slf_.num_qubits, slf_.coeff) + }; + ( + num_qubits, + coeff, + Self::get_bit_terms(slf_.clone()), + Self::get_indices(slf_), + ) + .into_py(py) + } + + /// Get a copy of this term. + #[pyo3(name = "copy")] + fn py_copy(&self) -> Self { + self.clone() + } + + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~SparseObservable.BitTerm`. + #[getter] + fn get_bit_terms(slf_: Bound) -> Bound> { + let bit_terms = &slf_.borrow().bit_terms; + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array_bound(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// Read-only view onto the indices of each non-identity single-qubit term. + /// + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let indices = &slf_.borrow().indices; + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array_bound(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// Get a :class:`.Pauli` object that represents the measurement basis needed for this term. + /// + /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting + /// :class:`.Pauli` is dense, in the sense that explicit identities are stored. An identity in + /// the Pauli output does not require a concrete measurement. + /// + /// Returns: + /// :class:`.Pauli`: the Pauli operator representing the necessary measurement basis. + /// + /// See also: + /// :meth:`SparseObservable.pauli_bases` + /// A similar method for an entire observable at once. + #[pyo3(name = "pauli_base")] + fn py_pauli_base<'py>(&self, py: Python<'py>) -> PyResult> { + let mut x = vec![false; self.num_qubits as usize]; + let mut z = vec![false; self.num_qubits as usize]; + for (bit_term, index) in self.bit_terms.iter().zip(self.indices.iter()) { + x[*index as usize] = bit_term.has_x_component(); + z[*index as usize] = bit_term.has_z_component(); + } + PAULI_TYPE.get_bound(py).call1((( + PyArray1::from_vec_bound(py, z), + PyArray1::from_vec_bound(py, x), + ),)) + } +} + /// Use the Numpy Python API to convert a `PyArray` into a dynamically chosen `dtype`, copying only /// if required. fn cast_array_type<'py, T>( @@ -1686,6 +3024,37 @@ fn cast_array_type<'py, T>( .map(|obj| obj.into_any()) } +/// Attempt to coerce an arbitrary Python object to a [SparseObservable]. +/// +/// This returns: +/// +/// * `Ok(Some(obs))` if the coercion was completely successful. +/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be +/// attempted. +/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python +/// exception. +/// +/// The purpose of this is for conversion the arithmetic operations, which should return +/// [PyNotImplemented] if the type is not valid for coercion. +fn coerce_to_observable<'py>( + value: &Bound<'py, PyAny>, +) -> PyResult>> { + let py = value.py(); + if let Ok(obs) = value.downcast_exact::() { + return Ok(Some(obs.clone())); + } + match SparseObservable::py_new(value, None) { + Ok(obs) => Ok(Some(Bound::new(py, obs)?)), + Err(e) => { + if e.is_instance_of::(py) { + Ok(None) + } else { + Err(e) + } + } + } +} + pub fn sparse_observable(m: &Bound) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/crates/accelerate/src/sparse_pauli_op.rs b/crates/accelerate/src/sparse_pauli_op.rs index 73c4ab7a73d5..89f033338951 100644 --- a/crates/accelerate/src/sparse_pauli_op.rs +++ b/crates/accelerate/src/sparse_pauli_op.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use ahash::RandomState; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyTuple; @@ -20,11 +21,14 @@ use numpy::prelude::*; use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods}; use hashbrown::HashMap; -use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis}; +use indexmap::IndexMap; +use ndarray::{s, ArrayView1, ArrayView2, Axis}; use num_complex::Complex64; use num_traits::Zero; -use qiskit_circuit::util::{c64, C_ONE, C_ZERO}; use rayon::prelude::*; +use thiserror::Error; + +use qiskit_circuit::util::{c64, C_ZERO}; use crate::rayon_ext::*; @@ -70,14 +74,6 @@ pub fn unordered_unique(py: Python, array: PyReadonlyArray2) -> (PyObject, ) } -#[derive(Clone, Copy)] -enum Pauli { - I, - X, - Y, - Z, -} - /// Pack a 2D array of Booleans into a given width. Returns an error if the input array is /// too large to be packed into u64. fn pack_bits(bool_arr: ArrayView2) -> Result, ()> { @@ -188,10 +184,9 @@ impl ZXPaulis { } /// Intermediate structure that represents readonly views onto the Python-space sparse Pauli data. -/// This is used in the chained methods so that the syntactical temporary lifetime extension can -/// occur; we can't have the readonly array temporaries only live within a method that returns -/// [ZXPaulisView], because otherwise the lifetimes of the [PyReadonlyArray] elements will be too -/// short. +/// This is used in the chained methods so that the lifetime extension can occur; we can't have the +/// readonly array temporaries only live within a method that returns [ZXPaulisView], because +/// otherwise the lifetimes of the [PyReadonlyArray] elements will be too short. pub struct ZXPaulisReadonly<'a> { x: PyReadonlyArray2<'a, bool>, z: PyReadonlyArray2<'a, bool>, @@ -305,7 +300,11 @@ impl MatrixCompressedPaulis { /// explicitly stored operations, if there are duplicates. After the summation, any terms that /// have become zero are dropped. pub fn combine(&mut self) { - let mut hash_table = HashMap::<(u64, u64), Complex64>::with_capacity(self.coeffs.len()); + let mut hash_table = + IndexMap::<(u64, u64), Complex64, RandomState>::with_capacity_and_hasher( + self.coeffs.len(), + RandomState::new(), + ); for (key, coeff) in self .x_like .drain(..) @@ -325,175 +324,609 @@ impl MatrixCompressedPaulis { } } +#[derive(Clone, Debug)] +struct DecomposeOut { + z: Vec, + x: Vec, + phases: Vec, + coeffs: Vec, + scale: f64, + tol: f64, + num_qubits: usize, +} + +#[derive(Error, Debug)] +enum DecomposeError { + #[error("operators must have two dimensions, not {0}")] + BadDimension(usize), + #[error("operators must be square with a power-of-two side length, not {0:?}")] + BadShape([usize; 2]), +} +impl From for PyErr { + fn from(value: DecomposeError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + /// Decompose a dense complex operator into the symplectic Pauli representation in the /// ZX-convention. /// /// This is an implementation of the "tensorized Pauli decomposition" presented in /// `Hantzko, Binkowski and Gupta (2023) `__. +/// +/// Implementation +/// -------------- +/// +/// The original algorithm was described recurisvely, allocating new matrices for each of the +/// block-wise sums (e.g. `op[top_left] + op[bottom_right]`). This implementation differs in two +/// major ways: +/// +/// - We do not allocate new matrices recursively, but instead produce a single copy of the input +/// and repeatedly overwrite subblocks of it at each point of the decomposition. +/// - The implementation is rewritten as an iteration rather than a recursion. The current "state" +/// of the iteration is encoded in a single machine word (the `PauliLocation` struct below). +/// +/// We do the decomposition in three "stages", with the stage changing whenever we need to change +/// the input/output types. The first level is mathematically the same as the middle levels, it +/// just gets handled separately because it does the double duty of moving the data out of the +/// Python-space strided array into a Rust-space contiguous array that we can modify in-place. +/// The middle levels all act in-place on this newly created scratch space. Finally, at the last +/// level, we've completed the decomposition and need to be writing the result into the output +/// data structures rather than into the scratch space. +/// +/// Each "level" is handling one qubit in the operator, equivalently to the recursive procedure +/// described in the paper referenced in the docstring. This implementation is iterative +/// stack-based and in place, rather than recursive. +/// +/// We can get away with overwriting our scratch-space matrix at each point, because each +/// element of a given subblock is used exactly twice during each decomposition - once for the `a + +/// b` case, and once for the `a - b` case. The second operand is the same in both cases. +/// Illustratively, at each step we're decomposing a submatrix blockwise, where we label the blocks +/// like this: +/// +/// +---------+---------+ +---------+---------+ +/// | | | | | | +/// | I | X | | I + Z | X + Y | +/// | | | | | | +/// +---------+---------+ =====> +---------+---------+ +/// | | | | | | +/// | Y | Z | | X - Y | I - Z | +/// | | | | | | +/// +---------+---------+ +---------+---------+ +/// +/// Each addition or subtraction is done elementwise, so as long as we iterate through the two pairs +/// of coupled blocks in order in lockstep, we can write out the answers together without +/// overwriting anything we need again. We ignore all factors of 1/2 until the very last step, and +/// apply them all at once. This minimises the number of floating-point operations we have to do. +/// +/// We store the iteration order as a stack of `PauliLocation`s, whose own docstring explains how it +/// tracks the top-left corner and the size of the submatrix it represents. #[pyfunction] pub fn decompose_dense( py: Python, operator: PyReadonlyArray2, tolerance: f64, ) -> PyResult { - let num_qubits = operator.shape()[0].ilog2() as usize; - let size = 1 << num_qubits; - if operator.shape() != [size, size] { - return Err(PyValueError::new_err(format!( - "input with shape {:?} cannot be interpreted as a multiqubit operator", - operator.shape() - ))); - } - let mut paulis = vec![]; - let mut coeffs = vec![]; - if num_qubits > 0 { - decompose_dense_inner( - C_ONE, - num_qubits, - &[], - operator.as_array(), - &mut paulis, - &mut coeffs, - tolerance * tolerance, - ); - } - if coeffs.is_empty() { - Ok(ZXPaulis { - z: PyArray2::zeros_bound(py, [0, num_qubits], false).into(), - x: PyArray2::zeros_bound(py, [0, num_qubits], false).into(), - phases: PyArray1::zeros_bound(py, [0], false).into(), - coeffs: PyArray1::zeros_bound(py, [0], false).into(), - }) - } else { - // Constructing several arrays of different shapes at once is rather awkward in iterator - // logic, so we just loop manually. - let mut z = Array2::::uninit([paulis.len(), num_qubits]); - let mut x = Array2::::uninit([paulis.len(), num_qubits]); - let mut phases = Array1::::uninit(paulis.len()); - for (i, paulis) in paulis.drain(..).enumerate() { - let mut phase = 0u8; - for (j, pauli) in paulis.into_iter().rev().enumerate() { - match pauli { - Pauli::I => { - z[[i, j]].write(false); - x[[i, j]].write(false); - } - Pauli::X => { - z[[i, j]].write(false); - x[[i, j]].write(true); - } - Pauli::Y => { - z[[i, j]].write(true); - x[[i, j]].write(true); - phase = phase.wrapping_add(1); - } - Pauli::Z => { - z[[i, j]].write(true); - x[[i, j]].write(false); - } + let array_view = operator.as_array(); + let out = py.allow_threads(|| decompose_dense_inner(array_view, tolerance))?; + Ok(ZXPaulis { + z: PyArray1::from_vec_bound(py, out.z) + .reshape([out.phases.len(), out.num_qubits])? + .into(), + x: PyArray1::from_vec_bound(py, out.x) + .reshape([out.phases.len(), out.num_qubits])? + .into(), + phases: PyArray1::from_vec_bound(py, out.phases).into(), + coeffs: PyArray1::from_vec_bound(py, out.coeffs).into(), + }) +} + +/// Rust-only inner component of the `SparsePauliOp` decomposition. +/// +/// See the top-level documentation of [decompose_dense] for more information on the internal +/// algorithm at play. +fn decompose_dense_inner( + operator: ArrayView2, + tolerance: f64, +) -> Result { + let op_shape = match operator.shape() { + [a, b] => [*a, *b], + shape => return Err(DecomposeError::BadDimension(shape.len())), + }; + if op_shape[0].is_zero() { + return Err(DecomposeError::BadShape(op_shape)); + } + let num_qubits = op_shape[0].ilog2() as usize; + let side = 1 << num_qubits; + if op_shape != [side, side] { + return Err(DecomposeError::BadShape(op_shape)); + } + if num_qubits.is_zero() { + // We have to special-case the zero-qubit operator because our `decompose_last_level` still + // needs to "consume" a qubit. + return Ok(DecomposeOut { + z: vec![], + x: vec![], + phases: vec![], + coeffs: vec![operator[[0, 0]]], + scale: 1.0, + tol: tolerance, + num_qubits: 0, + }); + } + let (stack, mut out_list, mut scratch) = decompose_first_level(operator, num_qubits); + decompose_middle_levels(stack, &mut out_list, &mut scratch, num_qubits); + Ok(decompose_last_level( + &mut out_list, + &scratch, + num_qubits, + tolerance, + )) +} + +/// Apply the matrix-addition decomposition at the first level. +/// +/// This is split out from the middle levels because it acts on an `ArrayView2`, and is responsible +/// for copying the operator over into the contiguous scratch space. We can't write over the +/// operator the user gave us (it's not ours to do that to), and anyway, we want to drop to a chunk +/// of memory that we can 100% guarantee is contiguous, so we can elide all the stride checking. +/// We split this out so we can do the first decomposition at the same time as scanning over the +/// operator to copy it. +/// +/// # Panics +/// +/// If the number of qubits in the operator is zero. +fn decompose_first_level( + in_op: ArrayView2, + num_qubits: usize, +) -> (Vec, Vec, Vec) { + let side = 1 << num_qubits; + let mut stack = Vec::::with_capacity(4); + let mut out_list = Vec::::new(); + let mut scratch = Vec::::with_capacity(side * side); + match num_qubits { + 0 => panic!("number of qubits must be greater than zero"), + 1 => { + // If we've only got one qubit, we just want to copy the data over in the correct + // continuity and let the base case of the iteration take care of outputting it. + scratch.extend(in_op.iter()); + out_list.push(PauliLocation::begin(num_qubits)); + } + _ => { + // We don't write out the operator in contiguous-index order, but we can easily + // guarantee that we'll write to each index exactly once without reading it - we still + // visit every index, just in 2x2 blockwise order, not row-by-row. + unsafe { scratch.set_len(scratch.capacity()) }; + let mut ptr = 0usize; + + let cur_qubit = num_qubits - 1; + let mid = 1 << cur_qubit; + let loc = PauliLocation::begin(num_qubits); + let mut i_nonzero = false; + let mut x_nonzero = false; + let mut y_nonzero = false; + let mut z_nonzero = false; + + let i_row_0 = loc.row(); + let i_col_0 = loc.col(); + + let x_row_0 = loc.row(); + let x_col_0 = loc.col() + mid; + + let y_row_0 = loc.row() + mid; + let y_col_0 = loc.col(); + + let z_row_0 = loc.row() + mid; + let z_col_0 = loc.col() + mid; + + for off_row in 0..mid { + let i_row = i_row_0 + off_row; + let z_row = z_row_0 + off_row; + for off_col in 0..mid { + let i_col = i_col_0 + off_col; + let z_col = z_col_0 + off_col; + let value = in_op[[i_row, i_col]] + in_op[[z_row, z_col]]; + scratch[ptr] = value; + ptr += 1; + i_nonzero = i_nonzero || (value != C_ZERO); + } + + let x_row = x_row_0 + off_row; + let y_row = y_row_0 + off_row; + for off_col in 0..mid { + let x_col = x_col_0 + off_col; + let y_col = y_col_0 + off_col; + let value = in_op[[x_row, x_col]] + in_op[[y_row, y_col]]; + scratch[ptr] = value; + ptr += 1; + x_nonzero = x_nonzero || (value != C_ZERO); } } - phases[i].write(phase % 4); + for off_row in 0..mid { + let x_row = x_row_0 + off_row; + let y_row = y_row_0 + off_row; + for off_col in 0..mid { + let x_col = x_col_0 + off_col; + let y_col = y_col_0 + off_col; + let value = in_op[[x_row, x_col]] - in_op[[y_row, y_col]]; + scratch[ptr] = value; + ptr += 1; + y_nonzero = y_nonzero || (value != C_ZERO); + } + let i_row = i_row_0 + off_row; + let z_row = z_row_0 + off_row; + for off_col in 0..mid { + let i_col = i_col_0 + off_col; + let z_col = z_col_0 + off_col; + let value = in_op[[i_row, i_col]] - in_op[[z_row, z_col]]; + scratch[ptr] = value; + ptr += 1; + z_nonzero = z_nonzero || (value != C_ZERO); + } + } + // The middle-levels `stack` is a LIFO, so if we push in this order, we'll consider the + // Pauli terms in lexicographical order, which is the canonical order from + // `SparsePauliOp.sort`. Populating the `out_list` (an initially empty `Vec`) + // effectively reverses the stack, so we want to push its elements in the IXYZ order. + if loc.qubit() == 1 { + i_nonzero.then(|| out_list.push(loc.push_i())); + x_nonzero.then(|| out_list.push(loc.push_x())); + y_nonzero.then(|| out_list.push(loc.push_y())); + z_nonzero.then(|| out_list.push(loc.push_z())); + } else { + z_nonzero.then(|| stack.push(loc.push_z())); + y_nonzero.then(|| stack.push(loc.push_y())); + x_nonzero.then(|| stack.push(loc.push_x())); + i_nonzero.then(|| stack.push(loc.push_i())); + } } - // These are safe because the above loops write into every element. It's guaranteed that - // each of the elements of the `paulis` vec will have `num_qubits` because they're all - // reading from the same base array. - let z = unsafe { z.assume_init() }; - let x = unsafe { x.assume_init() }; - let phases = unsafe { phases.assume_init() }; - Ok(ZXPaulis { - z: z.into_pyarray_bound(py).into(), - x: x.into_pyarray_bound(py).into(), - phases: phases.into_pyarray_bound(py).into(), - coeffs: PyArray1::from_vec_bound(py, coeffs).into(), - }) } + (stack, out_list, scratch) } -/// Recurse worker routine of `decompose_dense`. Should be called with at least one qubit. -fn decompose_dense_inner( - factor: Complex64, +/// Iteratively decompose the matrix at all levels other than the first and last. +/// +/// This populates the `out_list` with locations. This is mathematically the same as the first +/// level of the decomposition, except now we're acting in-place on our Rust-space contiguous +/// scratch space, rather than the strided Python-space array we were originally given. +fn decompose_middle_levels( + mut stack: Vec, + out_list: &mut Vec, + scratch: &mut [Complex64], num_qubits: usize, - paulis: &[Pauli], - block: ArrayView2, - out_paulis: &mut Vec>, - out_coeffs: &mut Vec, - square_tolerance: f64, ) { - if num_qubits == 0 { - // It would be safe to `return` here, but if it's unreachable then LLVM is allowed to - // 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. - if num_qubits == 1 { - let mut push_if_nonzero = |extra: Pauli, value: Complex64| { - if value.norm_sqr() <= square_tolerance { - return; + let side = 1 << num_qubits; + // The stack is a LIFO, which is how we implement the depth-first iteration. Depth-first + // means `stack` never grows very large; it reaches at most `3*num_qubits - 2` elements (if all + // terms are zero all the way through the first subblock decomposition). `out_list`, on the + // other hand, can be `4 ** (num_qubits - 1)` entries in the worst-case scenario of a + // completely dense (in Pauli terms) operator. + while let Some(loc) = stack.pop() { + // Here we work pairwise, writing out the new values into both I and Z simultaneously (etc + // for X and Y) so we can re-use their scratch space and avoid re-allocating. We're doing + // the multiple assignment `(I, Z) = (I + Z, I - Z)`. + // + // See the documentation of `decompose_dense` for more information on how this works. + let mid = 1 << loc.qubit(); + let mut i_nonzero = false; + let mut z_nonzero = false; + let i_row_0 = loc.row(); + let i_col_0 = loc.col(); + let z_row_0 = loc.row() + mid; + let z_col_0 = loc.col() + mid; + for off_row in 0..mid { + let i_loc_0 = (i_row_0 + off_row) * side + i_col_0; + let z_loc_0 = (z_row_0 + off_row) * side + z_col_0; + for off_col in 0..mid { + let i_loc = i_loc_0 + off_col; + let z_loc = z_loc_0 + off_col; + let add = scratch[i_loc] + scratch[z_loc]; + let sub = scratch[i_loc] - scratch[z_loc]; + scratch[i_loc] = add; + scratch[z_loc] = sub; + i_nonzero = i_nonzero || (add != C_ZERO); + z_nonzero = z_nonzero || (sub != C_ZERO); } - let paulis = { - let mut vec = Vec::with_capacity(paulis.len() + 1); - vec.extend_from_slice(paulis); - vec.push(extra); - vec - }; - out_paulis.push(paulis); - out_coeffs.push(value); - }; - push_if_nonzero(Pauli::I, 0.5 * factor * (block[[0, 0]] + block[[1, 1]])); - push_if_nonzero(Pauli::X, 0.5 * factor * (block[[0, 1]] + block[[1, 0]])); - push_if_nonzero( - Pauli::Y, - 0.5 * Complex64::i() * factor * (block[[0, 1]] - block[[1, 0]]), - ); - push_if_nonzero(Pauli::Z, 0.5 * factor * (block[[0, 0]] - block[[1, 1]])); - return; - } - let mut recurse_if_nonzero = |extra: Pauli, factor: Complex64, values: Array2| { - let mut is_zero = true; - for value in values.iter() { - if !value.is_zero() { - is_zero = false; - break; + } + + let mut x_nonzero = false; + let mut y_nonzero = false; + let x_row_0 = loc.row(); + let x_col_0 = loc.col() + mid; + let y_row_0 = loc.row() + mid; + let y_col_0 = loc.col(); + for off_row in 0..mid { + let x_loc_0 = (x_row_0 + off_row) * side + x_col_0; + let y_loc_0 = (y_row_0 + off_row) * side + y_col_0; + for off_col in 0..mid { + let x_loc = x_loc_0 + off_col; + let y_loc = y_loc_0 + off_col; + let add = scratch[x_loc] + scratch[y_loc]; + let sub = scratch[x_loc] - scratch[y_loc]; + scratch[x_loc] = add; + scratch[y_loc] = sub; + x_nonzero = x_nonzero || (add != C_ZERO); + y_nonzero = y_nonzero || (sub != C_ZERO); } } - if is_zero { - return; + // The middle-levels `stack` is a LIFO, so if we push in this order, we'll consider the + // Pauli terms in lexicographical order, which is the canonical order from + // `SparsePauliOp.sort`. Populating the `out_list` (an initially empty `Vec`) effectively + // reverses the stack, so we want to push its elements in the IXYZ order. + if loc.qubit() == 1 { + i_nonzero.then(|| out_list.push(loc.push_i())); + x_nonzero.then(|| out_list.push(loc.push_x())); + y_nonzero.then(|| out_list.push(loc.push_y())); + z_nonzero.then(|| out_list.push(loc.push_z())); + } else { + z_nonzero.then(|| stack.push(loc.push_z())); + y_nonzero.then(|| stack.push(loc.push_y())); + x_nonzero.then(|| stack.push(loc.push_x())); + i_nonzero.then(|| stack.push(loc.push_i())); } - let mut new_paulis = Vec::with_capacity(paulis.len() + 1); - new_paulis.extend_from_slice(paulis); - new_paulis.push(extra); - decompose_dense_inner( - factor, - num_qubits - 1, - &new_paulis, - values.view(), - out_paulis, - out_coeffs, - square_tolerance, - ); + } +} + +/// Write out the results of the final decomposition into the Pauli ZX form. +/// +/// The calculation here is the same as the previous two sets of decomposers, but we don't want to +/// write the result out into the scratch space to iterate needlessly once more; we want to +/// associate each non-zero coefficient with the final Pauli in the ZX format. +/// +/// This function applies all the factors of 1/2 that we've been skipping during the intermediate +/// decompositions. This means that the factors are applied to the output with `2 * output_len` +/// floating-point operations (real and imaginary), which is a huge reduction compared to repeatedly +/// doing it during the decomposition. +fn decompose_last_level( + out_list: &mut Vec, + scratch: &[Complex64], + num_qubits: usize, + tolerance: f64, +) -> DecomposeOut { + let side = 1 << num_qubits; + let scale = 0.5f64.powi(num_qubits as i32); + // Pessimistically allocate assuming that there will be no zero terms in the out list. We + // don't really pay much cost if we overallocate, but underallocating means that all four + // outputs have to copy their data across to a new allocation. + let mut out = DecomposeOut { + z: Vec::with_capacity(4 * num_qubits * out_list.len()), + x: Vec::with_capacity(4 * num_qubits * out_list.len()), + phases: Vec::with_capacity(4 * out_list.len()), + coeffs: Vec::with_capacity(4 * out_list.len()), + scale, + tol: (tolerance * tolerance) / (scale * scale), + num_qubits, }; - let mid = 1usize << (num_qubits - 1); - recurse_if_nonzero( - Pauli::I, - 0.5 * factor, - &block.slice(s![..mid, ..mid]) + &block.slice(s![mid.., mid..]), - ); - recurse_if_nonzero( - Pauli::X, - 0.5 * factor, - &block.slice(s![..mid, mid..]) + &block.slice(s![mid.., ..mid]), - ); - recurse_if_nonzero( - Pauli::Y, - 0.5 * Complex64::i() * factor, - &block.slice(s![..mid, mid..]) - &block.slice(s![mid.., ..mid]), - ); - recurse_if_nonzero( - Pauli::Z, - 0.5 * factor, - &block.slice(s![..mid, ..mid]) - &block.slice(s![mid.., mid..]), - ); + + for loc in out_list.drain(..) { + let row = loc.row(); + let col = loc.col(); + let base = row * side + col; + let i_value = scratch[base] + scratch[base + side + 1]; + let z_value = scratch[base] - scratch[base + side + 1]; + let x_value = scratch[base + 1] + scratch[base + side]; + let y_value = scratch[base + 1] - scratch[base + side]; + + let x = row ^ col; + let z = row; + let phase = (x & z).count_ones() as u8; + // Pushing the last Pauli onto the `loc` happens "forwards" to maintain lexicographical + // ordering in `out`, since this is the construction of the final object. + push_pauli_if_nonzero(x, z, phase, i_value, &mut out); + push_pauli_if_nonzero(x | 1, z, phase, x_value, &mut out); + push_pauli_if_nonzero(x | 1, z | 1, phase + 1, y_value, &mut out); + push_pauli_if_nonzero(x, z | 1, phase, z_value, &mut out); + } + // If we _wildly_ overallocated, then shrink back to a sensible size to avoid tying up too much + // memory as we return to Python space. + if out.z.capacity() / 4 > out.z.len() { + out.z.shrink_to_fit(); + out.x.shrink_to_fit(); + out.phases.shrink_to_fit(); + out.coeffs.shrink_to_fit(); + } + out +} + +// This generates lookup tables of the form +// const LOOKUP: [[bool; 2] 4] = [[false, false], [true, false], [false, true], [true, true]]; +// when called `pauli_lookup!(LOOKUP, 2, [_, _])`. The last argument is like a dummy version of +// an individual lookup rule, which is consumed to make an inner "loop" with a declarative macro. +macro_rules! pauli_lookup { + ($name:ident, $n:literal, [$head:expr$ (, $($tail:expr),*)?]) => { + static $name: [[bool; $n]; 1<<$n] = pauli_lookup!(@acc, [$($($tail),*)?], [[false], [true]]); + }; + (@acc, [$head:expr $(, $($tail:expr),*)?], [$([$($bools:tt),*]),+]) => { + pauli_lookup!(@acc, [$($($tail),*)?], [$([$($bools),*, false]),+, $([$($bools),*, true]),+]) + }; + (@acc, [], $init:expr) => { $init }; +} +pauli_lookup!(PAULI_LOOKUP_2, 2, [(), ()]); +pauli_lookup!(PAULI_LOOKUP_4, 4, [(), (), (), ()]); +pauli_lookup!(PAULI_LOOKUP_8, 8, [(), (), (), (), (), (), (), ()]); + +/// Push a complete Pauli chain into the output (`out`), if the corresponding entry is non-zero. +/// +/// `x` and `z` represent the symplectic X and Z bitvectors, packed into `usize`, where LSb n +/// corresponds to qubit `n`. +fn push_pauli_if_nonzero( + mut x: usize, + mut z: usize, + phase: u8, + value: Complex64, + out: &mut DecomposeOut, +) { + if value.norm_sqr() <= out.tol { + return; + } + + // This set of `extend` calls is effectively an 8-fold unrolling of the "natural" loop through + // each bit, where the initial `if` statements are handling the remainder (the up-to 7 + // least-significant bits). In practice, it's probably unlikely that people are decomposing + // 16q+ operators, since that's a pretty huge matrix already. + // + // The 8-fold loop unrolling is because going bit-by-bit all the way would be dominated by loop + // and bitwise-operation overhead. + + if out.num_qubits & 1 == 1 { + out.x.push(x & 1 == 1); + out.z.push(z & 1 == 1); + x >>= 1; + z >>= 1; + } + if out.num_qubits & 2 == 2 { + out.x.extend(&PAULI_LOOKUP_2[x & 0b11]); + out.z.extend(&PAULI_LOOKUP_2[z & 0b11]); + x >>= 2; + z >>= 2; + } + if out.num_qubits & 4 == 4 { + out.x.extend(&PAULI_LOOKUP_4[x & 0b1111]); + out.z.extend(&PAULI_LOOKUP_4[z & 0b1111]); + x >>= 4; + z >>= 4; + } + for _ in 0..(out.num_qubits / 8) { + out.x.extend(&PAULI_LOOKUP_8[x & 0b1111_1111]); + out.z.extend(&PAULI_LOOKUP_8[z & 0b1111_1111]); + x >>= 8; + z >>= 8; + } + + let phase = phase % 4; + let value = match phase { + 0 => Complex64::new(out.scale, 0.0) * value, + 1 => Complex64::new(0.0, out.scale) * value, + 2 => Complex64::new(-out.scale, 0.0) * value, + 3 => Complex64::new(0.0, -out.scale) * value, + _ => unreachable!("'x % 4' has only four possible values"), + }; + out.phases.push(phase); + out.coeffs.push(value); +} + +/// The "state" of an iteration step of the dense-operator decomposition routine. +/// +/// Pack the information about which row, column and qubit we're considering into a single `usize`. +/// Complex64 data is 16 bytes long and the operators are square and must be addressable in memory, +/// so the row and column are hardware limited to be of width `usize::BITS / 2 - 2` each. However, +/// we don't need to store at a granularity of 1, because the last 2x2 block we handle manually, so +/// we can remove an extra least significant bit from the row and column. Regardless of the width +/// of `usize`, we can therefore track the state for up to 30 qubits losslessly, which is greater +/// than the maximum addressable memory on a 64-bit system. +/// +/// For a 64-bit usize, the bit pattern is stored like this: +/// +/// 0b__000101__11111111111111111111111110000__11111111111111111111111110000 +/// <-6--> <------------29-------------> <------------29-------------> +/// | | | +/// | uint of the input row uint of the input column +/// | (once a 0 is appended) (once a 0 is appended) +/// | +/// current qubit under consideration +/// +/// The `qubit` field encodes the depth in the call stack that the user of the `PauliLocation` +/// should consider. When the stack is initialised (before any calculation is done), it starts at +/// the highest qubit index (`num_qubits - 1`) and decreases from there until 0. +/// +/// The `row` and `col` methods form the top-left corner of a `(2**(qubit + 1), 2**(qubit + 1))` +/// submatrix (where the top row and leftmost column are 0). The least significant `qubit + 1` +/// bits of the of row and column are therefore always zero; the 0-indexed qubit still corresponds +/// to a 2x2 block. This is why we needn't store it. +#[derive(Debug, Clone, Copy)] +struct PauliLocation(usize); + +impl PauliLocation { + // These shifts and masks are used to access the three components of the bit-packed state. + const QUBIT_SHIFT: u32 = usize::BITS - 6; + const QUBIT_MASK: usize = (usize::MAX >> Self::QUBIT_SHIFT) << Self::QUBIT_SHIFT; + const ROW_SHIFT: u32 = usize::BITS / 2 - 3; + const ROW_MASK: usize = + ((usize::MAX >> Self::ROW_SHIFT) << Self::ROW_SHIFT) & !Self::QUBIT_MASK; + const COL_SHIFT: u32 = 0; // Just for consistency. + const COL_MASK: usize = usize::MAX & !Self::ROW_MASK & !Self::QUBIT_MASK; + + /// Create the base `PauliLocation` for an entire matrix with `num_qubits` qubits. The initial + /// Pauli chain is empty. + #[inline(always)] + fn begin(num_qubits: usize) -> Self { + Self::new(0, 0, num_qubits - 1) + } + + /// Manually create a new `PauliLocation` with the given information. The logic in the rest of + /// the class assumes that `row` and `col` will end with at least `qubit + 1` zeros, since + /// these are the only valid locations. + #[inline(always)] + fn new(row: usize, col: usize, qubit: usize) -> Self { + debug_assert!(row & 1 == 0); + debug_assert!(col & 1 == 0); + debug_assert!(row < 2 * Self::ROW_SHIFT as usize); + debug_assert!(col < 2 * Self::ROW_SHIFT as usize); + debug_assert!(qubit < 64); + Self( + (qubit << Self::QUBIT_SHIFT) + | (row << Self::ROW_SHIFT >> 1) + | (col << Self::COL_SHIFT >> 1), + ) + } + + /// The row in the dense matrix that this location corresponds to. + #[inline(always)] + fn row(&self) -> usize { + ((self.0 & Self::ROW_MASK) >> Self::ROW_SHIFT) << 1 + } + + /// The column in the dense matrix that this location corresponds to. + #[inline(always)] + fn col(&self) -> usize { + ((self.0 & Self::COL_MASK) >> Self::COL_SHIFT) << 1 + } + + /// Which qubit in the Pauli chain we're currently considering. + #[inline(always)] + fn qubit(&self) -> usize { + (self.0 & Self::QUBIT_MASK) >> Self::QUBIT_SHIFT + } + + /// Create a new location corresponding to the Pauli chain so far, plus an identity on the + /// currently considered qubit. + #[inline(always)] + fn push_i(&self) -> Self { + Self::new(self.row(), self.col(), self.qubit() - 1) + } + + /// Create a new location corresponding to the Pauli chain so far, plus an X on the currently + /// considered qubit. + #[inline(always)] + fn push_x(&self) -> Self { + Self::new( + self.row(), + self.col() | (1 << self.qubit()), + self.qubit() - 1, + ) + } + + /// Create a new location corresponding to the Pauli chain so far, plus a Y on the currently + /// considered qubit. + #[inline(always)] + fn push_y(&self) -> Self { + Self::new( + self.row() | (1 << self.qubit()), + self.col(), + self.qubit() - 1, + ) + } + + /// Create a new location corresponding to the Pauli chain so far, plus a Z on the currently + /// considered qubit. + #[inline(always)] + fn push_z(&self) -> Self { + Self::new( + self.row() | (1 << self.qubit()), + self.col() | (1 << self.qubit()), + self.qubit() - 1, + ) + } } /// Convert the given [ZXPaulis] object to a dense 2D Numpy matrix. @@ -830,11 +1263,13 @@ pub fn sparse_pauli_op(m: &Bound) -> PyResult<()> { #[cfg(test)] mod tests { + use ndarray::{aview2, Array1}; + use super::*; use crate::test::*; - // The purpose of these tests is more about exercising the `unsafe` code; we test for full - // correctness from Python space. + // The purpose of these tests is more about exercising the `unsafe` code under Miri; we test for + // full numerical correctness from Python space. fn example_paulis() -> MatrixCompressedPaulis { MatrixCompressedPaulis { @@ -853,6 +1288,166 @@ mod tests { } } + /// Helper struct for the decomposition testing. This is a subset of the `DecomposeOut` + /// struct, skipping the unnecessary algorithm-state components of it. + /// + /// If we add a more Rust-friendly interface to `SparsePauliOp` in the future, hopefully this + /// can be removed. + #[derive(Clone, PartialEq, Debug)] + struct DecomposeMinimal { + z: Vec, + x: Vec, + phases: Vec, + coeffs: Vec, + num_qubits: usize, + } + impl From for DecomposeMinimal { + fn from(value: DecomposeOut) -> Self { + Self { + z: value.z, + x: value.x, + phases: value.phases, + coeffs: value.coeffs, + num_qubits: value.num_qubits, + } + } + } + impl From for DecomposeMinimal { + fn from(value: MatrixCompressedPaulis) -> Self { + let phases = value + .z_like + .iter() + .zip(value.x_like.iter()) + .map(|(z, x)| ((z & x).count_ones() % 4) as u8) + .collect::>(); + let coeffs = value + .coeffs + .iter() + .zip(phases.iter()) + .map(|(c, phase)| match phase { + 0 => *c, + 1 => Complex64::new(-c.im, c.re), + 2 => Complex64::new(-c.re, -c.im), + 3 => Complex64::new(c.im, -c.re), + _ => panic!("phase should only in [0, 4)"), + }) + .collect(); + let z = value + .z_like + .iter() + .flat_map(|digit| (0..value.num_qubits).map(move |i| (digit & (1 << i)) != 0)) + .collect(); + let x = value + .x_like + .iter() + .flat_map(|digit| (0..value.num_qubits).map(move |i| (digit & (1 << i)) != 0)) + .collect(); + Self { + z, + x, + phases, + coeffs, + num_qubits: value.num_qubits as usize, + } + } + } + + #[test] + fn decompose_empty_operator_fails() { + assert!(matches!( + decompose_dense_inner(aview2::(&[]), 0.0), + Err(DecomposeError::BadShape(_)), + )); + } + + #[test] + fn decompose_0q_operator() { + let coeff = Complex64::new(1.5, -0.5); + let arr = [[coeff]]; + let out = decompose_dense_inner(aview2(&arr), 0.0).unwrap(); + let expected = DecomposeMinimal { + z: vec![], + x: vec![], + phases: vec![], + coeffs: vec![coeff], + num_qubits: 0, + }; + assert_eq!(DecomposeMinimal::from(out), expected); + } + + #[test] + fn decompose_1q_operator() { + // Be sure that any sums are given in canonical order of the output, or there will be + // spurious test failures. + let paulis = [ + (vec![0], vec![0]), // I + (vec![1], vec![0]), // X + (vec![1], vec![1]), // Y + (vec![0], vec![1]), // Z + (vec![0, 1], vec![0, 0]), // I, X + (vec![0, 1], vec![0, 1]), // I, Y + (vec![0, 0], vec![0, 1]), // I, Z + (vec![1, 1], vec![0, 1]), // X, Y + (vec![1, 0], vec![1, 1]), // X, Z + (vec![1, 0], vec![1, 1]), // Y, Z + (vec![1, 1, 0], vec![0, 1, 1]), // X, Y, Z + ]; + let coeffs = [ + Complex64::new(1.5, -0.5), + Complex64::new(-0.25, 2.0), + Complex64::new(0.75, 0.75), + ]; + for (x_like, z_like) in paulis { + let paulis = MatrixCompressedPaulis { + num_qubits: 1, + coeffs: coeffs[0..x_like.len()].to_owned(), + x_like, + z_like, + }; + let arr = Array1::from_vec(to_matrix_dense_inner(&paulis, false)) + .into_shape((2, 2)) + .unwrap(); + let expected: DecomposeMinimal = paulis.into(); + let actual: DecomposeMinimal = decompose_dense_inner(arr.view(), 0.0).unwrap().into(); + assert_eq!(actual, expected); + } + } + + #[test] + fn decompose_3q_operator() { + // Be sure that any sums are given in canonical order of the output, or there will be + // spurious test failures. + let paulis = [ + (vec![0], vec![0]), // III + (vec![1], vec![0]), // IIX + (vec![2], vec![2]), // IYI + (vec![0], vec![4]), // ZII + (vec![6], vec![6]), // YYI + (vec![7], vec![7]), // YYY + (vec![1, 6, 7], vec![1, 6, 7]), // IIY, YYI, YYY + (vec![1, 2, 0], vec![0, 2, 4]), // IIX, IYI, ZII + ]; + let coeffs = [ + Complex64::new(1.5, -0.5), + Complex64::new(-0.25, 2.0), + Complex64::new(0.75, 0.75), + ]; + for (x_like, z_like) in paulis { + let paulis = MatrixCompressedPaulis { + num_qubits: 3, + coeffs: coeffs[0..x_like.len()].to_owned(), + x_like, + z_like, + }; + let arr = Array1::from_vec(to_matrix_dense_inner(&paulis, false)) + .into_shape((8, 8)) + .unwrap(); + let expected: DecomposeMinimal = paulis.into(); + let actual: DecomposeMinimal = decompose_dense_inner(arr.view(), 0.0).unwrap().into(); + assert_eq!(actual, expected); + } + } + #[test] fn dense_threaded_and_serial_equal() { let paulis = example_paulis(); diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs index f3a3d9454980..ac2577c2fc2c 100644 --- a/crates/accelerate/src/split_2q_unitaries.rs +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -28,20 +28,26 @@ pub fn split_2q_unitaries( dag: &mut DAGCircuit, requested_fidelity: f64, ) -> PyResult<()> { + if !dag.get_op_counts().contains_key("unitary") { + return Ok(()); + } let nodes: Vec = dag.op_nodes(false).collect(); for node in nodes { if let NodeType::Operation(inst) = &dag.dag()[node] { let qubits = dag.get_qargs(inst.qubits).to_vec(); - let matrix = inst.op.matrix(inst.params_view()); // We only attempt to split UnitaryGate objects, but this could be extended in future // -- however we need to ensure that we can compile the resulting single-qubit unitaries // to the supported basis gate set. if qubits.len() != 2 || inst.op.name() != "unitary" { continue; } + let matrix = inst + .op + .matrix(inst.params_view()) + .expect("'unitary' gates should always have a matrix form"); let decomp = TwoQubitWeylDecomposition::new_inner( - matrix.unwrap().view(), + matrix.view(), Some(requested_fidelity), None, )?; diff --git a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs index eca0b78951fd..525984b85160 100644 --- a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs +++ b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs @@ -15,8 +15,8 @@ use indexmap::IndexSet; use ndarray::{s, ArrayView2}; use smallvec::smallvec; -use crate::synthesis::clifford::utils::CliffordGatesVec; use crate::synthesis::clifford::utils::{adjust_final_pauli_gates, SymplecticMatrix}; +use crate::synthesis::clifford::utils::{Clifford, CliffordGatesVec}; use qiskit_circuit::operations::StandardGate; use qiskit_circuit::Qubit; @@ -437,3 +437,14 @@ impl GreedyCliffordSynthesis<'_> { Ok((self.num_qubits, clifford_gates)) } } + +/// Resynthesizes a clifford circuit using the greedy Clifford synthesis algorithm. +pub fn resynthesize_clifford_circuit( + num_qubits: usize, + gates: &CliffordGatesVec, +) -> Result { + let sim_clifford = Clifford::from_gate_sequence(gates, num_qubits)?; + let mut synthesis = GreedyCliffordSynthesis::new(sim_clifford.tableau.view())?; + let (_, new_gates) = synthesis.run()?; + Ok(new_gates) +} diff --git a/crates/accelerate/src/synthesis/clifford/mod.rs b/crates/accelerate/src/synthesis/clifford/mod.rs index d2c1e36fab4f..dae85de4972d 100644 --- a/crates/accelerate/src/synthesis/clifford/mod.rs +++ b/crates/accelerate/src/synthesis/clifford/mod.rs @@ -11,9 +11,9 @@ // that they have been altered from the originals. mod bm_synthesis; -mod greedy_synthesis; +pub(crate) mod greedy_synthesis; mod random_clifford; -mod utils; +pub(crate) mod utils; use crate::synthesis::clifford::bm_synthesis::synth_clifford_bm_inner; use crate::synthesis::clifford::greedy_synthesis::GreedyCliffordSynthesis; diff --git a/crates/accelerate/src/synthesis/clifford/utils.rs b/crates/accelerate/src/synthesis/clifford/utils.rs index fa2e33561a46..19cfcfaa49fd 100644 --- a/crates/accelerate/src/synthesis/clifford/utils.rs +++ b/crates/accelerate/src/synthesis/clifford/utils.rs @@ -149,6 +149,30 @@ impl Clifford { azip!((z in &mut z, &x in &x) *z ^= x); } + /// Modifies the tableau in-place by appending SX-gate + pub fn append_sx(&mut self, qubit: usize) { + let (mut x, z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= !x & z); + azip!((&z in &z, x in &mut x) *x ^= z); + } + + /// Modifies the tableau in-place by appending SXDG-gate + pub fn append_sxdg(&mut self, qubit: usize) { + let (mut x, z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & z); + azip!((&z in &z, x in &mut x) *x ^= z); + } + /// Modifies the tableau in-place by appending H-gate pub fn append_h(&mut self, qubit: usize) { let (mut x, mut z, mut p) = self.tableau.multi_slice_mut(( @@ -227,6 +251,18 @@ impl Clifford { clifford.append_s(qubits[0].index()); Ok(()) } + StandardGate::SdgGate => { + clifford.append_sdg(qubits[0].0 as usize); + Ok(()) + } + StandardGate::SXGate => { + clifford.append_sx(qubits[0].0 as usize); + Ok(()) + } + StandardGate::SXdgGate => { + clifford.append_sxdg(qubits[0].0 as usize); + Ok(()) + } StandardGate::HGate => { clifford.append_h(qubits[0].index()); Ok(()) @@ -283,21 +319,18 @@ pub fn adjust_final_pauli_gates( // add pauli gates for qubit in 0..num_qubits { if delta_phase_pre[qubit] && delta_phase_pre[qubit + num_qubits] { - // println!("=> Adding Y-gate on {}", qubit); gate_seq.push(( StandardGate::YGate, smallvec![], smallvec![Qubit::new(qubit)], )); } else if delta_phase_pre[qubit] { - // println!("=> Adding Z-gate on {}", qubit); gate_seq.push(( StandardGate::ZGate, smallvec![], smallvec![Qubit::new(qubit)], )); } else if delta_phase_pre[qubit + num_qubits] { - // println!("=> Adding X-gate on {}", qubit); gate_seq.push(( StandardGate::XGate, smallvec![], diff --git a/crates/accelerate/src/synthesis/evolution/mod.rs b/crates/accelerate/src/synthesis/evolution/mod.rs new file mode 100644 index 000000000000..d0eeca105048 --- /dev/null +++ b/crates/accelerate/src/synthesis/evolution/mod.rs @@ -0,0 +1,79 @@ +// 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 pauli_network; + +use pyo3::prelude::*; +use pyo3::types::PyList; + +use qiskit_circuit::circuit_data::CircuitData; + +use crate::synthesis::evolution::pauli_network::pauli_network_synthesis_inner; + +/// Calls Rustiq's pauli network synthesis algorithm and returns the +/// Qiskit circuit data with Clifford gates and rotations. +/// +/// # Arguments +/// +/// * py: a GIL handle, needed to add and negate rotation parameters in Python space. +/// * num_qubits: total number of qubits. +/// * pauli_network: pauli network represented in sparse format. It's a list +/// of triples such as `[("XX", [0, 3], theta), ("ZZ", [0, 1], 0.1)]`. +/// * optimize_count: if `true`, Rustiq's synthesis algorithms aims to optimize +/// the 2-qubit gate count; and if `false`, then the 2-qubit depth. +/// * preserve_order: whether the order of paulis should be preserved, up to +/// commutativity. If the order is not preserved, the returned circuit will +/// generally not be equivalent to the given pauli network. +/// * upto_clifford: if `true`, the final Clifford operator is not synthesized +/// and the returned circuit will generally not be equivalent to the given +/// pauli network. In addition, the argument `upto_phase` would be ignored. +/// * upto_phase: if `true`, the global phase of the returned circuit may differ +/// from the global phase of the given pauli network. The argument is considered +/// to be `true` when `upto_clifford` is `true`. +/// * resynth_clifford_method: describes the strategy to synthesize the final +/// Clifford operator. If `0` a naive approach is used, which doubles the number +/// of gates but preserves the global phase of the circuit. If `1`, the Clifford is +/// resynthesized using Qiskit's greedy Clifford synthesis algorithm. If `2`, it +/// is resynthesized by Rustiq itself. If `upto_phase` is `false`, the naive +/// approach is used, as neither synthesis method preserves the global phase. +/// +/// If `preserve_order` is `true` and both `upto_clifford` and `upto_phase` are `false`, +/// the returned circuit is equivalent to the given pauli network. +#[pyfunction] +#[pyo3(signature = (num_qubits, pauli_network, optimize_count=true, preserve_order=true, upto_clifford=false, upto_phase=false, resynth_clifford_method=1))] +#[allow(clippy::too_many_arguments)] +pub fn pauli_network_synthesis( + py: Python, + num_qubits: usize, + pauli_network: &Bound, + optimize_count: bool, + preserve_order: bool, + upto_clifford: bool, + upto_phase: bool, + resynth_clifford_method: usize, +) -> PyResult { + pauli_network_synthesis_inner( + py, + num_qubits, + pauli_network, + optimize_count, + preserve_order, + upto_clifford, + upto_phase, + resynth_clifford_method, + ) +} + +pub fn evolution(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(pauli_network_synthesis, m)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/evolution/pauli_network.rs b/crates/accelerate/src/synthesis/evolution/pauli_network.rs new file mode 100644 index 000000000000..b5a73262aae8 --- /dev/null +++ b/crates/accelerate/src/synthesis/evolution/pauli_network.rs @@ -0,0 +1,376 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::synthesis::clifford::greedy_synthesis::resynthesize_clifford_circuit; + +use pyo3::prelude::*; +use pyo3::types::{PyList, PyString, PyTuple}; +use smallvec::{smallvec, SmallVec}; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{multiply_param, radd_param, Param, StandardGate}; +use qiskit_circuit::Qubit; + +use rustiq_core::structures::{ + CliffordCircuit, CliffordGate, IsometryTableau, Metric, PauliLike, PauliSet, +}; +use rustiq_core::synthesis::clifford::isometry::isometry_synthesis; +use rustiq_core::synthesis::pauli_network::greedy_pauli_network; + +use rustworkx_core::petgraph::graph::NodeIndex; +use rustworkx_core::petgraph::prelude::StableDiGraph; +use rustworkx_core::petgraph::Incoming; + +/// A Qiskit gate. The quantum circuit data returned by the pauli network +/// synthesis algorithm will consist of clifford and rotation gates. +type QiskitGate = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>); + +/// Expands the sparse pauli string representation to the full representation. +/// +/// For example: for the input `sparse_pauli = "XY", qubits = [1, 3], num_qubits = 6`, +/// the function returns `"IXIYII"`. +fn expand_pauli(sparse_pauli: String, qubits: &[u32], num_qubits: usize) -> String { + let mut v: Vec = vec!['I'; num_qubits]; + for (q, p) in qubits.iter().zip(sparse_pauli.chars()) { + v[*q as usize] = p; + } + v.into_iter().collect() +} + +/// Return the Qiskit's gate corresponding to the given Rustiq's Clifford gate. +fn to_qiskit_clifford_gate(rustiq_gate: &CliffordGate) -> QiskitGate { + match rustiq_gate { + CliffordGate::CNOT(i, j) => ( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(*i as u32), Qubit(*j as u32)], + ), + CliffordGate::CZ(i, j) => ( + StandardGate::CZGate, + smallvec![], + smallvec![Qubit(*i as u32), Qubit(*j as u32)], + ), + CliffordGate::H(i) => ( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*i as u32)], + ), + CliffordGate::S(i) => ( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*i as u32)], + ), + CliffordGate::Sd(i) => ( + StandardGate::SdgGate, + smallvec![], + smallvec![Qubit(*i as u32)], + ), + CliffordGate::SqrtX(i) => ( + StandardGate::SXGate, + smallvec![], + smallvec![Qubit(*i as u32)], + ), + CliffordGate::SqrtXd(i) => ( + StandardGate::SXdgGate, + smallvec![], + smallvec![Qubit(*i as u32)], + ), + } +} + +/// Return the Qiskit rotation gate corresponding to the single-qubit Pauli rotation. +/// +/// # Arguments +/// +/// * py: a GIL handle, needed to negate rotation parameters in Python space. +/// * paulis: Rustiq's data structure storing pauli rotations. +/// * i: index of the single-qubit Pauli rotation. +/// * angle: Qiskit's rotation angle. +fn qiskit_rotation_gate(py: Python, paulis: &PauliSet, i: usize, angle: &Param) -> QiskitGate { + let (phase, pauli_str) = paulis.get(i); + for (q, c) in pauli_str.chars().enumerate() { + if c != 'I' { + let standard_gate = match c { + 'X' => StandardGate::RXGate, + 'Y' => StandardGate::RYGate, + 'Z' => StandardGate::RZGate, + _ => unreachable!("Only X, Y and Z are possible characters at this point."), + }; + // We need to negate the angle when there is a phase. + let param = match phase { + false => angle.clone(), + true => multiply_param(angle, -1.0, py), + }; + return (standard_gate, smallvec![param], smallvec![Qubit(q as u32)]); + } + } + unreachable!("The pauli rotation is guaranteed to be a single-qubit rotation.") +} + +// Note: +// The Pauli network synthesis algorithm in rustiq-core 0.0.8 only returns +// the list of Clifford gates that, when simulated, turn every Pauli rotation +// at some point to a single-qubit Pauli rotation. As an additional complication, +// the order in which the Pauli rotations are turned into single-qubit Pauli +// rotations coincides with the original order only up to commutativity between +// Pauli rotations. +// As a temporary solution, we follow the approach in Simon's private rustiq-plugin +// repository: we simulate the original Pauli network using returned Clifford gates +// to find where Pauli rotations need to be inserted, and we keep a DAG representing +// commutativity relations between Pauli rotations to make sure the single-qubit +// rotations are chosen in the correct order. +// In the future we are planning to extend the algorithm in rustiq-core to +// explicitly return the circuit with single-qubit Pauli rotations already inserted. +// When this happens, we will be able to significantly simplify the code that follows. + +/// A DAG that stores ordered Paulis, up to commutativity. +struct CommutativityDag { + /// Rustworkx's DAG + dag: StableDiGraph, +} + +impl CommutativityDag { + /// Construct a DAG corresponding to `paulis`. + /// When `add_edges` is `true`, we add an edge between pauli `i` and pauli `j` + /// iff they commute. When `add_edges` is `false`, we do not add any edges. + fn from_paulis(paulis: &PauliSet, add_edges: bool) -> Self { + let mut dag = StableDiGraph::::new(); + + let node_indices: Vec = (0..paulis.len()).map(|i| dag.add_node(i)).collect(); + + if add_edges { + for i in 0..paulis.len() { + let pauli_i = paulis.get_as_pauli(i); + for j in i + 1..paulis.len() { + let pauli_j = paulis.get_as_pauli(j); + + if !pauli_i.commutes(&pauli_j) { + dag.add_edge(node_indices[i], node_indices[j], ()); + } + } + } + } + + CommutativityDag { dag } + } + + /// Return whether the given node is a front node (i.e. has no predecessors). + fn is_front_node(&self, index: usize) -> bool { + self.dag + .neighbors_directed(NodeIndex::new(index), Incoming) + .next() + .is_none() + } + + /// Remove node from the DAG. + fn remove_node(&mut self, index: usize) { + self.dag.remove_node(NodeIndex::new(index)); + } +} + +/// Return a Qiskit circuit with Clifford gates and rotations. +/// +/// The rotations are assumed to be ordered (up to commutativity). +/// +/// # Arguments +/// +/// * py: a GIL handle, needed to negate rotation parameters in Python space. +/// * gates: the sequence of Rustiq's Clifford gates returned by Rustiq's +/// pauli network synthesis algorithm. +/// * paulis: Rustiq's data structure storing the pauli rotations. +/// * angles: Qiskit's rotation angles corresponding to the pauli rotations. +/// * preserve_order: specifies whether the order of paulis should be preserved, +/// up to commutativity. +fn inject_rotations( + py: Python, + gates: &[CliffordGate], + paulis: &PauliSet, + angles: &[Param], + preserve_order: bool, +) -> (Vec, Param) { + let mut out_gates: Vec = Vec::with_capacity(gates.len() + paulis.len()); + let mut global_phase = Param::Float(0.0); + + let mut cur_paulis = paulis.clone(); + let mut hit_paulis: Vec = vec![false; cur_paulis.len()]; + + let mut dag = CommutativityDag::from_paulis(paulis, preserve_order); + + // check which paulis are hit at the very start + for i in 0..cur_paulis.len() { + let pauli_support_size = cur_paulis.support_size(i); + if pauli_support_size == 0 { + // in case of an all-identity rotation, update global phase by subtracting + // the angle + global_phase = radd_param(global_phase, multiply_param(&angles[i], -1.0, py), py); + hit_paulis[i] = true; + dag.remove_node(i); + } else if pauli_support_size == 1 && dag.is_front_node(i) { + out_gates.push(qiskit_rotation_gate(py, &cur_paulis, i, &angles[i])); + hit_paulis[i] = true; + dag.remove_node(i); + } + } + + for gate in gates { + out_gates.push(to_qiskit_clifford_gate(gate)); + + cur_paulis.conjugate_with_gate(gate); + + // check which paulis are hit now + for i in 0..cur_paulis.len() { + if !hit_paulis[i] && cur_paulis.support_size(i) == 1 && dag.is_front_node(i) { + out_gates.push(qiskit_rotation_gate(py, &cur_paulis, i, &angles[i])); + hit_paulis[i] = true; + dag.remove_node(i); + } + } + } + + (out_gates, global_phase) +} + +/// Return the vector of Qiskit's gate corresponding to the given vector +/// of Rustiq's Clifford gate. +fn to_qiskit_clifford_gates(gates: &Vec) -> Vec { + let mut qiskit_gates: Vec = Vec::with_capacity(gates.len()); + for gate in gates { + qiskit_gates.push(to_qiskit_clifford_gate(gate)); + } + qiskit_gates +} + +/// Returns the number of CNOTs. +fn cnot_count(qgates: &[QiskitGate]) -> usize { + qgates.iter().filter(|&gate| gate.2.len() == 2).count() +} + +/// Given the Clifford circuit returned by Rustiq's pauli network synthesis algorithm, +/// generates a sequence of Qiskit gates that implements this circuit. +/// If `fix_clifford_method` is `0`, the original circuit is inverted; if `1`, it is +/// resynthesized using Qiskit; and if `2` it is resynthesized using Rustiq. +fn synthesize_final_clifford( + rcircuit: &CliffordCircuit, + resynth_clifford_method: usize, +) -> Vec { + match resynth_clifford_method { + 0 => to_qiskit_clifford_gates(&rcircuit.gates), + 1 => { + // Qiskit-based resynthesis + let qcircuit = to_qiskit_clifford_gates(&rcircuit.gates); + let new_qcircuit = resynthesize_clifford_circuit(rcircuit.nqbits, &qcircuit).unwrap(); + if cnot_count(&qcircuit) < cnot_count(&new_qcircuit) { + qcircuit + } else { + new_qcircuit + } + } + _ => { + // Rustiq-based resynthesis + let tableau = IsometryTableau::new(rcircuit.nqbits, 0); + let new_rcircuit = isometry_synthesis(&tableau, &Metric::COUNT, 1); + if new_rcircuit.cnot_count() < rcircuit.cnot_count() { + to_qiskit_clifford_gates(&new_rcircuit.gates) + } else { + to_qiskit_clifford_gates(&rcircuit.gates) + } + } + } +} + +/// Calls Rustiq's pauli network synthesis algorithm and returns the +/// Qiskit circuit data with Clifford gates and rotations. +/// +/// # Arguments +/// +/// * py: a GIL handle, needed to add and negate rotation parameters in Python space. +/// * num_qubits: total number of qubits. +/// * pauli_network: pauli network represented in sparse format. It's a list +/// of triples such as `[("XX", [0, 3], theta), ("ZZ", [0, 1], 0.1)]`. +/// * optimize_count: if `true`, Rustiq's synthesis algorithms aims to optimize +/// the 2-qubit gate count; and if `false`, then the 2-qubit depth. +/// * preserve_order: whether the order of paulis should be preserved, up to +/// commutativity. If the order is not preserved, the returned circuit will +/// generally not be equivalent to the given pauli network. +/// * upto_clifford: if `true`, the final Clifford operator is not synthesized +/// and the returned circuit will generally not be equivalent to the given +/// pauli network. In addition, the argument `upto_phase` would be ignored. +/// * upto_phase: if `true`, the global phase of the returned circuit may differ +/// from the global phase of the given pauli network. The argument is considered +/// to be `true` when `upto_clifford` is `true`. +/// * resynth_clifford_method: describes the strategy to synthesize the final +/// Clifford operator. If `0` a naive approach is used, which doubles the number +/// of gates but preserves the global phase of the circuit. If `1`, the Clifford is +/// resynthesized using Qiskit's greedy Clifford synthesis algorithm. If `2`, it +/// is resynthesized by Rustiq itself. If `upto_phase` is `false`, the naive +/// approach is used, as neither synthesis method preserves the global phase. +/// +/// If `preserve_order` is `true` and both `upto_clifford` and `upto_phase` are `false`, +/// the returned circuit is equivalent to the given pauli network. +#[allow(clippy::too_many_arguments)] +pub fn pauli_network_synthesis_inner( + py: Python, + num_qubits: usize, + pauli_network: &Bound, + optimize_count: bool, + preserve_order: bool, + upto_clifford: bool, + upto_phase: bool, + resynth_clifford_method: usize, +) -> PyResult { + let mut paulis: Vec = Vec::with_capacity(pauli_network.len()); + let mut angles: Vec = Vec::with_capacity(pauli_network.len()); + + // go over the input pauli network and extract a list of pauli rotations and + // the corresponding rotation angles + for item in pauli_network { + let tuple = item.downcast::()?; + + let sparse_pauli: String = tuple.get_item(0)?.downcast::()?.extract()?; + let qubits: Vec = tuple.get_item(1)?.extract()?; + let angle: Param = tuple.get_item(2)?.extract()?; + + paulis.push(expand_pauli(sparse_pauli, &qubits, num_qubits)); + angles.push(angle); + } + + let paulis = PauliSet::from_slice(&paulis); + let metric = match optimize_count { + true => Metric::COUNT, + false => Metric::DEPTH, + }; + + // Call Rustiq's synthesis algorithm + let circuit = greedy_pauli_network(&paulis, &metric, preserve_order, 0, false, false); + + // post-process algorithm's output, translating to Qiskit's gates and inserting rotation gates + let (mut gates, global_phase) = + inject_rotations(py, &circuit.gates, &paulis, &angles, preserve_order); + + // if the circuit needs to be synthesized exactly, we cannot use either Rustiq's + // or Qiskit's synthesis methods for Cliffords, since they do not necessarily preserve + // the global phase. + let resynth_clifford_method = match upto_phase { + true => resynth_clifford_method, + false => 0, + }; + + // synthesize the final Clifford + if !upto_clifford { + let final_clifford = synthesize_final_clifford(&circuit.dagger(), resynth_clifford_method); + for gate in final_clifford { + gates.push(gate); + } + } + + CircuitData::from_standard_gates(py, num_qubits as u32, gates, global_phase) +} diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index 6bda0667e622..212e1601ed64 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/mod.rs @@ -10,7 +10,8 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -mod clifford; +pub mod clifford; +mod evolution; pub mod linear; pub mod linear_phase; mod multi_controlled; @@ -39,5 +40,9 @@ pub fn synthesis(m: &Bound) -> PyResult<()> { multi_controlled::multi_controlled(&mc_mod)?; m.add_submodule(&mc_mod)?; + let evolution_mod = PyModule::new_bound(m.py(), "evolution")?; + evolution::evolution(&evolution_mod)?; + m.add_submodule(&evolution_mod)?; + Ok(()) } diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 9e6c220818a3..8eb9f385a672 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -43,7 +43,7 @@ use instruction_properties::InstructionProperties; use self::exceptions::TranspilerError; -mod exceptions { +pub(crate) mod exceptions { use pyo3::import_exception_bound; import_exception_bound! {qiskit.exceptions, QiskitError} import_exception_bound! {qiskit.transpiler.exceptions, TranspilerError} @@ -227,18 +227,18 @@ impl Target { #[new] #[pyo3(signature = ( description = None, - num_qubits = None, + num_qubits = 0, dt = None, - granularity = None, - min_length = None, - pulse_alignment = None, - acquire_alignment = None, + granularity = 1, + min_length = 1, + pulse_alignment = 1, + acquire_alignment = 1, qubit_properties = None, concurrent_measurements = None, ))] fn new( description: Option, - num_qubits: Option, + mut num_qubits: Option, dt: Option, granularity: Option, min_length: Option, @@ -247,10 +247,9 @@ impl Target { qubit_properties: Option>, concurrent_measurements: Option>>, ) -> PyResult { - let mut num_qubits = num_qubits; if let Some(qubit_properties) = qubit_properties.as_ref() { - if let Some(num_qubits) = num_qubits { - if num_qubits != qubit_properties.len() { + if num_qubits.is_some_and(|num_qubits| num_qubits > 0) { + if num_qubits.unwrap() != qubit_properties.len() { return Err(PyValueError::new_err( "The value of num_qubits specified does not match the \ length of the input qubit_properties list", diff --git a/crates/accelerate/src/twirling.rs b/crates/accelerate/src/twirling.rs new file mode 100644 index 000000000000..29c9da3671bc --- /dev/null +++ b/crates/accelerate/src/twirling.rs @@ -0,0 +1,484 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::f64::consts::PI; + +use hashbrown::HashMap; +use ndarray::linalg::kron; +use ndarray::prelude::*; +use ndarray::ArrayView2; +use num_complex::Complex64; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::PyList; +use pyo3::wrap_pyfunction; +use pyo3::Python; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; +use smallvec::SmallVec; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; +use qiskit_circuit::converters::dag_to_circuit; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; +use qiskit_circuit::imports::QUANTUM_CIRCUIT; +use qiskit_circuit::operations::StandardGate::{IGate, XGate, YGate, ZGate}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, PyInstruction, StandardGate}; +use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; + +use crate::euler_one_qubit_decomposer::optimize_1q_gates_decomposition; +use crate::target_transpiler::Target; +use crate::QiskitError; + +static ECR_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, YGate], 0.), + ([IGate, XGate, IGate, XGate], 0.), + ([IGate, YGate, ZGate, ZGate], PI), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, ZGate, XGate], PI), + ([ZGate, YGate, IGate, ZGate], 0.), + ([ZGate, IGate, ZGate, IGate], PI), + ([ZGate, ZGate, IGate, YGate], PI), + ([XGate, YGate, XGate, YGate], 0.), + ([XGate, IGate, YGate, XGate], PI), + ([XGate, ZGate, XGate, ZGate], 0.), + ([XGate, XGate, YGate, IGate], PI), + ([YGate, IGate, XGate, XGate], PI), + ([YGate, ZGate, YGate, ZGate], PI), + ([YGate, XGate, XGate, IGate], PI), + ([YGate, YGate, YGate, YGate], PI), +]; + +static CX_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, ZGate], 0.), + ([IGate, XGate, IGate, XGate], 0.), + ([IGate, YGate, ZGate, YGate], 0.), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, ZGate, XGate], 0.), + ([ZGate, YGate, IGate, YGate], 0.), + ([ZGate, IGate, ZGate, IGate], 0.), + ([ZGate, ZGate, IGate, ZGate], 0.), + ([XGate, YGate, YGate, ZGate], 0.), + ([XGate, IGate, XGate, XGate], 0.), + ([XGate, ZGate, YGate, YGate], PI), + ([XGate, XGate, XGate, IGate], 0.), + ([YGate, IGate, YGate, XGate], 0.), + ([YGate, ZGate, XGate, YGate], 0.), + ([YGate, XGate, YGate, IGate], 0.), + ([YGate, YGate, XGate, ZGate], PI), +]; + +static CZ_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, IGate, ZGate], 0.), + ([IGate, XGate, ZGate, XGate], 0.), + ([IGate, YGate, ZGate, YGate], 0.), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, IGate, XGate], 0.), + ([ZGate, YGate, IGate, YGate], 0.), + ([ZGate, IGate, ZGate, IGate], 0.), + ([ZGate, ZGate, ZGate, ZGate], 0.), + ([XGate, YGate, YGate, XGate], PI), + ([XGate, IGate, XGate, ZGate], 0.), + ([XGate, ZGate, XGate, IGate], 0.), + ([XGate, XGate, YGate, YGate], 0.), + ([YGate, IGate, YGate, ZGate], 0.), + ([YGate, ZGate, YGate, IGate], 0.), + ([YGate, XGate, XGate, YGate], PI), + ([YGate, YGate, XGate, XGate], 0.), +]; + +static ISWAP_TWIRL_SET: [([StandardGate; 4], f64); 16] = [ + ([IGate, ZGate, ZGate, IGate], 0.), + ([IGate, XGate, YGate, ZGate], 0.), + ([IGate, YGate, XGate, ZGate], PI), + ([IGate, IGate, IGate, IGate], 0.), + ([ZGate, XGate, YGate, IGate], 0.), + ([ZGate, YGate, XGate, IGate], PI), + ([ZGate, IGate, IGate, ZGate], 0.), + ([ZGate, ZGate, ZGate, ZGate], 0.), + ([XGate, YGate, YGate, XGate], 0.), + ([XGate, IGate, ZGate, YGate], 0.), + ([XGate, ZGate, IGate, YGate], 0.), + ([XGate, XGate, XGate, XGate], 0.), + ([YGate, IGate, ZGate, XGate], PI), + ([YGate, ZGate, IGate, XGate], PI), + ([YGate, XGate, XGate, YGate], 0.), + ([YGate, YGate, YGate, YGate], 0.), +]; + +static TWIRLING_SETS: [&[([StandardGate; 4], f64); 16]; 4] = [ + &CX_TWIRL_SET, + &CZ_TWIRL_SET, + &ECR_TWIRL_SET, + &ISWAP_TWIRL_SET, +]; + +const CX_MASK: u8 = 8; +const CZ_MASK: u8 = 4; +const ECR_MASK: u8 = 2; +const ISWAP_MASK: u8 = 1; + +#[inline(always)] +fn diff_frob_norm_sq(array: ArrayView2, gate_matrix: ArrayView2) -> f64 { + let mut res: f64 = 0.; + for i in 0..4 { + for j in 0..4 { + let gate = gate_matrix[[i, j]]; + let twirled = array[[i, j]]; + let diff = twirled - gate; + res += (diff.conj() * diff).re; + } + } + res +} + +fn generate_twirling_set(gate_matrix: ArrayView2) -> Vec<([StandardGate; 4], f64)> { + let mut out_vec = Vec::with_capacity(16); + let i_matrix = aview2(&ONE_QUBIT_IDENTITY); + let x_matrix = aview2(&qiskit_circuit::gate_matrix::X_GATE); + let y_matrix = aview2(&qiskit_circuit::gate_matrix::Y_GATE); + let z_matrix = aview2(&qiskit_circuit::gate_matrix::Z_GATE); + let iter_set = [IGate, XGate, YGate, ZGate]; + let kron_set: [Array2; 16] = [ + kron(&i_matrix, &i_matrix), + kron(&x_matrix, &i_matrix), + kron(&y_matrix, &i_matrix), + kron(&z_matrix, &i_matrix), + kron(&i_matrix, &x_matrix), + kron(&x_matrix, &x_matrix), + kron(&y_matrix, &x_matrix), + kron(&z_matrix, &x_matrix), + kron(&i_matrix, &y_matrix), + kron(&x_matrix, &y_matrix), + kron(&y_matrix, &y_matrix), + kron(&z_matrix, &y_matrix), + kron(&i_matrix, &z_matrix), + kron(&x_matrix, &z_matrix), + kron(&y_matrix, &z_matrix), + kron(&z_matrix, &z_matrix), + ]; + for (i_idx, i) in iter_set.iter().enumerate() { + for (j_idx, j) in iter_set.iter().enumerate() { + let before_matrix = kron_set[i_idx * 4 + j_idx].view(); + let half_twirled_matrix = gate_matrix.dot(&before_matrix); + for (k_idx, k) in iter_set.iter().enumerate() { + for (l_idx, l) in iter_set.iter().enumerate() { + let after_matrix = kron_set[k_idx * 4 + l_idx].view(); + let twirled_matrix = after_matrix.dot(&half_twirled_matrix); + let norm: f64 = diff_frob_norm_sq(twirled_matrix.view(), gate_matrix); + if norm.abs() < 1e-15 { + out_vec.push(([*i, *j, *k, *l], 0.)); + } else if (norm - 16.).abs() < 1e-15 { + out_vec.push(([*i, *j, *k, *l], PI)); + } + } + } + } + } + out_vec +} + +fn twirl_gate( + py: Python, + circ: &CircuitData, + rng: &mut Pcg64Mcg, + out_circ: &mut CircuitData, + twirl_set: &[([StandardGate; 4], f64)], + inst: &PackedInstruction, +) -> PyResult<()> { + let qubits = circ.get_qargs(inst.qubits); + let (twirl, twirl_phase) = twirl_set.choose(rng).unwrap(); + let bit_zero = out_circ.add_qargs(std::slice::from_ref(&qubits[0])); + let bit_one = out_circ.add_qargs(std::slice::from_ref(&qubits[1])); + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[0]), + qubits: bit_zero, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }, + )?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[1]), + qubits: bit_one, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }, + )?; + + out_circ.push(py, inst.clone())?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[2]), + qubits: bit_zero, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }, + )?; + out_circ.push( + py, + PackedInstruction { + op: PackedOperation::from_standard(twirl[3]), + qubits: bit_one, + clbits: circ.cargs_interner().get_default(), + params: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }, + )?; + + if *twirl_phase != 0. { + out_circ.add_global_phase(py, &Param::Float(*twirl_phase))?; + } + Ok(()) +} + +type CustomGateTwirlingMap = HashMap>; + +fn generate_twirled_circuit( + py: Python, + circ: &CircuitData, + rng: &mut Pcg64Mcg, + twirling_mask: u8, + custom_gate_map: Option<&CustomGateTwirlingMap>, + optimizer_target: Option<&Target>, +) -> PyResult { + let mut out_circ = CircuitData::clone_empty_like(circ, None); + + for inst in circ.data() { + if let Some(custom_gate_map) = custom_gate_map { + if let Some(twirling_set) = custom_gate_map.get(inst.op.name()) { + twirl_gate(py, circ, rng, &mut out_circ, twirling_set.as_slice(), inst)?; + continue; + } + } + match inst.op.view() { + OperationRef::Standard(gate) => match gate { + StandardGate::CXGate => { + if twirling_mask & CX_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[0], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::CZGate => { + if twirling_mask & CZ_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[1], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::ECRGate => { + if twirling_mask & ECR_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[2], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + StandardGate::ISwapGate => { + if twirling_mask & ISWAP_MASK != 0 { + twirl_gate(py, circ, rng, &mut out_circ, TWIRLING_SETS[3], inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + _ => out_circ.push(py, inst.clone())?, + }, + OperationRef::Instruction(py_inst) => { + if py_inst.control_flow() { + let new_blocks: PyResult> = py_inst + .blocks() + .iter() + .map(|block| -> PyResult { + let new_block = generate_twirled_circuit( + py, + block, + rng, + twirling_mask, + custom_gate_map, + optimizer_target, + )?; + Ok(new_block.into_py(py)) + }) + .collect(); + let new_blocks = new_blocks?; + let blocks_list = PyList::new_bound( + py, + new_blocks.iter().map(|block| { + QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (block,)) + .unwrap() + }), + ); + + let new_inst_obj = py_inst + .instruction + .bind(py) + .call_method1(intern!(py, "replace_blocks"), (blocks_list,))? + .unbind(); + let new_inst = PyInstruction { + qubits: py_inst.qubits, + clbits: py_inst.clbits, + params: py_inst.params, + op_name: py_inst.op_name.clone(), + control_flow: true, + instruction: new_inst_obj.clone_ref(py), + }; + let new_inst = PackedInstruction { + op: PackedOperation::from_instruction(Box::new(new_inst)), + qubits: inst.qubits, + clbits: inst.clbits, + params: Some(Box::new( + new_blocks + .iter() + .map(|x| Param::Obj(x.into_py(py))) + .collect::>(), + )), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: std::sync::OnceLock::new(), + }; + #[cfg(feature = "cache_pygates")] + new_inst.py_op.set(new_inst_obj).unwrap(); + out_circ.push(py, new_inst)?; + } else { + out_circ.push(py, inst.clone())?; + } + } + _ => { + out_circ.push(py, inst.clone())?; + } + } + } + if optimizer_target.is_some() { + let mut dag = DAGCircuit::from_circuit_data(py, out_circ, false)?; + optimize_1q_gates_decomposition(py, &mut dag, optimizer_target, None, None)?; + dag_to_circuit(py, &dag, false) + } else { + Ok(out_circ) + } +} + +#[pyfunction] +#[pyo3(signature=(circ, twirled_gate=None, custom_twirled_gates=None, seed=None, num_twirls=1, optimizer_target=None))] +pub(crate) fn twirl_circuit( + py: Python, + circ: &CircuitData, + twirled_gate: Option>, + custom_twirled_gates: Option>, + seed: Option, + num_twirls: usize, + optimizer_target: Option<&Target>, +) -> PyResult> { + let mut rng = match seed { + Some(seed) => Pcg64Mcg::seed_from_u64(seed), + None => Pcg64Mcg::from_entropy(), + }; + let twirling_mask: u8 = match twirled_gate { + Some(gates) => { + let mut out_mask = 0; + for gate in gates { + let new_mask = match gate { + StandardGate::CXGate => CX_MASK, + StandardGate::CZGate => CZ_MASK, + StandardGate::ECRGate => ECR_MASK, + StandardGate::ISwapGate => ISWAP_MASK, + _ => { + return Err(QiskitError::new_err( + format!("Provided gate to twirl, {}, is not currently supported you can only use cx, cz, ecr or iswap.", gate.name()) + )) + } + }; + out_mask |= new_mask; + } + out_mask + } + None => { + if custom_twirled_gates.is_none() { + 15 + } else { + 0 + } + } + }; + let custom_gate_twirling_sets: Option = + custom_twirled_gates.map(|gates| { + gates + .into_iter() + .filter_map(|gate| { + if gate.operation.num_qubits() != 2 { + return Some(Err(QiskitError::new_err( + format!( + "The provided gate to twirl {} operates on an invalid number of qubits {}, it can only be a two qubit gate", + gate.operation.name(), + gate.operation.num_qubits(), + ) + ))) + } + if gate.operation.num_params() != 0 { + return Some(Err(QiskitError::new_err( + format!( + "The provided gate to twirl {} takes a parameter, it can only be an unparameterized gate", + gate.operation.name(), + ) + ))) + } + let matrix = gate.operation.matrix(&gate.params); + if let Some(matrix) = matrix { + let twirl_set = generate_twirling_set(matrix.view()); + if twirl_set.is_empty() { + None + } else { + Some(Ok((gate.operation.name().to_string(), twirl_set))) + } + } else { + Some(Err(QiskitError::new_err( + format!("Provided gate to twirl, {}, does not have a matrix defined and can't be twirled", gate.operation.name()) + ))) + } + }) + .collect() + }).transpose()?; + (0..num_twirls) + .map(|_| { + generate_twirled_circuit( + py, + circ, + &mut rng, + twirling_mask, + custom_gate_twirling_sets.as_ref(), + optimizer_target, + ) + }) + .collect() +} + +pub fn twirling(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(twirl_circuit))?; + Ok(()) +} diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index fb8c58baab9d..4410d6f35e07 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -35,9 +35,10 @@ use numpy::{IntoPyArray, ToPyArray}; use numpy::{PyArray2, PyArrayLike2, PyReadonlyArray1, PyReadonlyArray2}; use pyo3::exceptions::PyValueError; +use pyo3::intern; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; -use pyo3::types::PyList; +use pyo3::types::{PyList, PyTuple, PyType}; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ @@ -54,7 +55,7 @@ use rand_pcg::Pcg64Mcg; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; -use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; @@ -1337,6 +1338,17 @@ pub struct TwoQubitBasisDecomposer { q2r: Array2, } impl TwoQubitBasisDecomposer { + /// Return the KAK gate name + pub fn gate_name(&self) -> &str { + self.gate.as_str() + } + + /// Compute the number of basis gates needed for a given unitary + pub fn num_basis_gates_inner(&self, unitary: ArrayView2) -> usize { + let u = unitary.into_faer_complex(); + __num_basis_gates(self.basis_decomposer.b, self.basis_fidelity, u) + } + fn decomp1_inner( &self, target: &TwoQubitWeylDecomposition, @@ -2394,6 +2406,484 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { Ok([g0_equiv + 0., g1_equiv + 0., g2_equiv + 0.]) } +/// invert 1q gate sequence +fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, SmallVec<[f64; 3]>) { + let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); + let inv_gate = gate + .0 + .inverse(&gate_params) + .expect("An unexpected standard gate was inverted"); + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => unreachable!("Parameterized inverse generated from non-parameterized gate."), + }) + .collect::>(); + (inv_gate.0, inv_gate_params) +} + +#[derive(Clone, Debug, FromPyObject)] +pub enum RXXEquivalent { + Standard(StandardGate), + CustomPython(Py), +} + +impl RXXEquivalent { + fn matrix(&self, py: Python, param: f64) -> PyResult> { + match self { + Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), + Self::CustomPython(gate_cls) => { + let gate_obj = gate_cls.bind(py).call1((param,))?; + let raw_matrix = gate_obj + .call_method0(intern!(py, "to_matrix"))? + .extract::>()?; + Ok(raw_matrix.as_array().to_owned()) + } + } + } +} + +#[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] +pub struct TwoQubitControlledUDecomposer { + rxx_equivalent_gate: RXXEquivalent, + #[pyo3(get)] + scale: f64, +} + +const DEFAULT_ATOL: f64 = 1e-12; +type InverseReturn = (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); + +/// Decompose two-qubit unitary in terms of a desired +/// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` +/// gate that is locally equivalent to an :class:`.RXXGate`. +impl TwoQubitControlledUDecomposer { + /// invert 2q gate sequence + fn invert_2q_gate( + &self, + py: Python, + gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), + ) -> PyResult { + let (gate, params, qubits) = gate; + if let Some(gate) = gate { + let inv_gate = gate + .inverse(¶ms.into_iter().map(Param::Float).collect::>()) + .unwrap(); + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => { + unreachable!("Parameterized inverse generated from non-parameterized gate.") + } + }) + .collect::>(); + Ok((Some(inv_gate.0), inv_gate_params, qubits)) + } else { + match &self.rxx_equivalent_gate { + RXXEquivalent::Standard(gate) => { + let inv_gate = gate + .inverse(¶ms.into_iter().map(Param::Float).collect::>()) + .unwrap(); + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => unreachable!( + "Parameterized inverse generated from non-parameterized gate." + ), + }) + .collect::>(); + Ok((Some(inv_gate.0), inv_gate_params, qubits)) + } + RXXEquivalent::CustomPython(gate_cls) => { + let gate_obj = gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; + let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; + let inverse: OperationFromPython = raw_inverse.extract()?; + let params: SmallVec<[f64; 3]> = inverse + .params + .into_iter() + .map(|x| match x { + Param::Float(val) => val, + _ => panic!("Inverse has invalid parameter"), + }) + .collect(); + if let Some(gate) = inverse.operation.try_standard_gate() { + Ok((Some(gate), params, qubits)) + } else if raw_inverse.is_instance(gate_cls.bind(py))? { + Ok((None, params, qubits)) + } else { + Err(QiskitError::new_err( + "rxx gate inverse is not valid for this decomposer", + )) + } + } + } + } + } + + /// Takes an angle and returns the circuit equivalent to an RXXGate with the + /// RXX equivalent gate as the two-qubit unitary. + /// Args: + /// angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) + /// Returns: + /// Circuit: Circuit equivalent to an RXXGate. + /// Raises: + /// QiskitError: If the circuit is not equivalent to an RXXGate. + fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate + // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl + // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. + // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters + // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. + + let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let decomposer_inv = + TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; + + let euler_basis = EulerBasis::ZYZ; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(euler_basis); + + // Express the RXXGate in terms of the user-provided RXXGate equivalent gate. + let mut gates = Vec::with_capacity(13); + let mut global_phase = -decomposer_inv.global_phase; + + let decomp_k1r = decomposer_inv.K1r.view(); + let decomp_k2r = decomposer_inv.K2r.view(); + let decomp_k1l = decomposer_inv.K1l.view(); + let decomp_k2l = decomposer_inv.K2l.view(); + + let unitary_k1r = + unitary_to_gate_sequence_inner(decomp_k1r, &target_1q_basis_list, 0, None, true, None); + let unitary_k2r = + unitary_to_gate_sequence_inner(decomp_k2r, &target_1q_basis_list, 0, None, true, None); + let unitary_k1l = + unitary_to_gate_sequence_inner(decomp_k1l, &target_1q_basis_list, 0, None, true, None); + let unitary_k2l = + unitary_to_gate_sequence_inner(decomp_k2l, &target_1q_basis_list, 0, None, true, None); + + if let Some(unitary_k2r) = unitary_k2r { + global_phase -= unitary_k2r.global_phase; + for gate in unitary_k2r.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + if let Some(unitary_k2l) = unitary_k2l { + global_phase -= unitary_k2l.global_phase; + for gate in unitary_k2l.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); + } + } + gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); + + if let Some(unitary_k1r) = unitary_k1r { + global_phase += unitary_k1r.global_phase; + for gate in unitary_k1r.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + if let Some(unitary_k1l) = unitary_k1l { + global_phase += unitary_k1l.global_phase; + for gate in unitary_k1l.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); + } + } + + Ok(TwoQubitGateSequence { + gates, + global_phase, + }) + } + + /// Appends U_d(a, b, c) to the circuit. + fn weyl_gate( + &self, + py: Python, + circ: &mut TwoQubitGateSequence, + target_decomposed: TwoQubitWeylDecomposition, + atol: f64, + ) -> PyResult<()> { + let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + circ.gates.extend(circ_a.gates); + let mut global_phase = circ_a.global_phase; + + // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. + if (target_decomposed.b).abs() > atol { + let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + global_phase += circ_b.global_phase; + circ.gates + .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::SdgGate), smallvec![], smallvec![1])); + circ.gates.extend(circ_b.gates); + circ.gates + .push((Some(StandardGate::SGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::SGate), smallvec![], smallvec![1])); + } + + // # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. + if (target_decomposed.c).abs() > atol { + // Since the Weyl chamber is here defined as a > b > |c| we may have + // negative c. This will cause issues in _to_rxx_gate + // as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). + // We therefore produce RZZGate(|c|) and append its inverse to the + // circuit if c < 0. + let mut gamma = -2.0 * target_decomposed.c; + if gamma <= 0.0 { + let circ_c = self.to_rxx_gate(py, gamma)?; + global_phase += circ_c.global_phase; + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + circ.gates.extend(circ_c.gates); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + } else { + // invert the circuit above + gamma *= -1.0; + let circ_c = self.to_rxx_gate(py, gamma)?; + global_phase -= circ_c.global_phase; + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + for gate in circ_c.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params, inv_gate_qubits) = + self.invert_2q_gate(py, gate)?; + circ.gates + .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); + } + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + } + } + + circ.global_phase = global_phase; + Ok(()) + } + + /// Returns the Weyl decomposition in circuit form. + /// Note: atol is passed to OneQubitEulerDecomposer. + fn call_inner( + &self, + py: Python, + unitary: ArrayView2, + atol: f64, + ) -> PyResult { + let target_decomposed = + TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; + + let euler_basis = EulerBasis::ZYZ; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(euler_basis); + + let c1r = target_decomposed.K1r.view(); + let c2r = target_decomposed.K2r.view(); + let c1l = target_decomposed.K1l.view(); + let c2l = target_decomposed.K2l.view(); + + let unitary_c1r = + unitary_to_gate_sequence_inner(c1r, &target_1q_basis_list, 0, None, true, None); + let unitary_c2r = + unitary_to_gate_sequence_inner(c2r, &target_1q_basis_list, 0, None, true, None); + let unitary_c1l = + unitary_to_gate_sequence_inner(c1l, &target_1q_basis_list, 0, None, true, None); + let unitary_c2l = + unitary_to_gate_sequence_inner(c2l, &target_1q_basis_list, 0, None, true, None); + + let mut gates = Vec::with_capacity(59); + let mut global_phase = target_decomposed.global_phase; + + if let Some(unitary_c2r) = unitary_c2r { + global_phase += unitary_c2r.global_phase; + for gate in unitary_c2r.gates.into_iter() { + gates.push((Some(gate.0), gate.1, smallvec![0])); + } + } + if let Some(unitary_c2l) = unitary_c2l { + global_phase += unitary_c2l.global_phase; + for gate in unitary_c2l.gates.into_iter() { + gates.push((Some(gate.0), gate.1, smallvec![1])); + } + } + let mut gates1 = TwoQubitGateSequence { + gates, + global_phase, + }; + self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + global_phase += gates1.global_phase; + + if let Some(unitary_c1r) = unitary_c1r { + global_phase -= unitary_c1r.global_phase; + for gate in unitary_c1r.gates.into_iter() { + gates1.gates.push((Some(gate.0), gate.1, smallvec![0])); + } + } + if let Some(unitary_c1l) = unitary_c1l { + global_phase -= unitary_c1l.global_phase; + for gate in unitary_c1l.gates.into_iter() { + gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); + } + } + + gates1.global_phase = global_phase; + Ok(gates1) + } +} + +#[pymethods] +impl TwoQubitControlledUDecomposer { + /// Initialize the KAK decomposition. + /// Args: + /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: + /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + /// Raises: + /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. + #[new] + #[pyo3(signature=(rxx_equivalent_gate))] + pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + let atol = DEFAULT_ATOL; + let test_angles = [0.2, 0.3, PI2]; + + let scales: PyResult> = test_angles + .into_iter() + .map(|test_angle| { + match &rxx_equivalent_gate { + RXXEquivalent::Standard(gate) => { + if gate.num_params() != 1 { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter.", + )); + } + } + RXXEquivalent::CustomPython(gate_cls) => { + if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter.", + )); + } + } + }; + let mat = rxx_equivalent_gate.matrix(py, test_angle)?; + let decomp = + TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; + let mat_rxx = StandardGate::RXXGate + .matrix(&[Param::Float(test_angle)]) + .unwrap(); + let decomposer_rxx = TwoQubitWeylDecomposition::new_inner( + mat_rxx.view(), + None, + Some(Specialization::ControlledEquiv), + )?; + let decomposer_equiv = TwoQubitWeylDecomposition::new_inner( + mat.view(), + Some(DEFAULT_FIDELITY), + Some(Specialization::ControlledEquiv), + )?; + let scale_a = decomposer_rxx.a / decomposer_equiv.a; + if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol { + return Err(QiskitError::new_err( + "The provided gate is not equivalent to an RXXGate.", + )); + } + Ok(scale_a) + }) + .collect(); + let scales = scales?; + + let scale = scales[0]; + + // Check that all three tested angles give the same scale + for scale_val in &scales { + if !abs_diff_eq!(scale_val, &scale, epsilon = atol) { + return Err(QiskitError::new_err( + "Inconsistent scaling parameters in check.", + )); + } + } + + Ok(TwoQubitControlledUDecomposer { + scale, + rxx_equivalent_gate, + }) + } + + #[pyo3(signature=(unitary, atol))] + fn __call__( + &self, + py: Python, + unitary: PyReadonlyArray2, + atol: f64, + ) -> PyResult { + let sequence = self.call_inner(py, unitary.as_array(), atol)?; + match &self.rxx_equivalent_gate { + RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( + py, + 2, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => ( + gate, + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + None => ( + *rxx_gate, + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + }), + Param::Float(sequence.global_phase), + ), + RXXEquivalent::CustomPython(gate_cls) => CircuitData::from_packed_operations( + py, + 2, + 0, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => Ok(( + PackedOperation::from_standard(gate), + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + )), + None => { + let raw_gate_obj = + gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; + let op: OperationFromPython = raw_gate_obj.extract()?; + + Ok(( + op.operation, + op.params, + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + )) + } + }), + Param::Float(sequence.global_phase), + ), + } + } +} + pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(_num_basis_gates))?; m.add_wrapped(wrap_pyfunction!(py_decompose_two_qubit_product_gate))?; @@ -2407,5 +2897,6 @@ pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 1931f1e97b2b..62f41c78084c 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -11,9 +11,9 @@ // that they have been altered from the originals. #![allow(clippy::too_many_arguments)] -#[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; use std::f64::consts::PI; +#[cfg(feature = "cache_pygates")] +use std::sync::OnceLock; use approx::relative_eq; use hashbrown::{HashMap, HashSet}; @@ -27,7 +27,7 @@ use smallvec::{smallvec, SmallVec}; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyDict, PyList, PyString}; +use pyo3::types::{IntoPyDict, PyDict, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; @@ -149,7 +149,7 @@ fn apply_synth_sequence( params: new_params, extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }; instructions.push(instruction); } @@ -225,7 +225,7 @@ fn py_run_main_loop( qubit_indices: Vec, min_qubits: usize, target: &Target, - coupling_edges: &Bound<'_, PyList>, + coupling_edges: HashSet<[PhysicalQubit; 2]>, approximation_degree: Option, natural_direction: Option, ) -> PyResult { @@ -268,7 +268,7 @@ fn py_run_main_loop( new_ids, min_qubits, target, - coupling_edges, + coupling_edges.clone(), approximation_degree, natural_direction, )?; @@ -352,7 +352,7 @@ fn py_run_main_loop( py, unitary, ref_qubits, - coupling_edges, + &coupling_edges, target, approximation_degree, natural_direction, @@ -383,7 +383,7 @@ fn run_2q_unitary_synthesis( py: Python, unitary: Array2, ref_qubits: &[PhysicalQubit; 2], - coupling_edges: &Bound<'_, PyList>, + coupling_edges: &HashSet<[PhysicalQubit; 2]>, target: &Target, approximation_degree: Option, natural_direction: Option, @@ -794,7 +794,7 @@ fn preferred_direction( decomposer: &DecomposerElement, ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, - coupling_edges: &Bound<'_, PyList>, + coupling_edges: &HashSet<[PhysicalQubit; 2]>, target: &Target, ) -> PyResult> { // Returns: @@ -830,14 +830,8 @@ fn preferred_direction( Some(false) => None, _ => { // None or Some(true) - let mut edge_set = HashSet::new(); - for item in coupling_edges.iter() { - if let Ok(tuple) = item.extract::<(usize, usize)>() { - edge_set.insert(tuple); - } - } - let zero_one = edge_set.contains(&(qubits[0].0 as usize, qubits[1].0 as usize)); - let one_zero = edge_set.contains(&(qubits[1].0 as usize, qubits[0].0 as usize)); + let zero_one = coupling_edges.contains(&qubits); + let one_zero = coupling_edges.contains(&[qubits[1], qubits[0]]); match (zero_one, one_zero) { (true, false) => Some(true), diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index f3577d767897..0cda94cb6ffb 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -11,12 +11,13 @@ // that they have been altered from the originals. #[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; +use std::sync::OnceLock; use crate::bit_data::BitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, }; +use crate::dag_circuit::add_global_phase; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; use crate::interner::{Interned, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; @@ -156,6 +157,7 @@ impl CircuitData { self_.clbits.cached().clone_ref(py), None::<()>, self_.data.len(), + self_.global_phase.clone(), ) }; Ok((ty, args, None::<()>, self_.iter()?).into_py(py)) @@ -303,7 +305,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }); } } else if copy_instructions { @@ -315,7 +317,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }); } } else { @@ -734,13 +736,19 @@ impl CircuitData { } /// Assign all uses of the circuit parameters as keys `mapping` to their corresponding values. + /// + /// Any items in the mapping that are not present in the circuit are skipped; it's up to Python + /// space to turn extra bindings into an error, if they choose to do it. fn assign_parameters_mapping(&mut self, mapping: Bound) -> PyResult<()> { let py = mapping.py(); let mut items = Vec::new(); for item in mapping.call_method0("items")?.iter()? { let (param_ob, value) = item?.extract::<(Py, AssignParam)>()?; let uuid = ParameterUuid::from_parameter(param_ob.bind(py))?; - items.push((param_ob, value.0, self.param_table.pop(uuid)?)); + // It's fine if the mapping contains parameters that we don't have - just skip those. + if let Ok(uses) = self.param_table.pop(uuid) { + items.push((param_ob, value.0, uses)); + } } self.assign_parameters_inner(py, items) } @@ -778,6 +786,16 @@ impl CircuitData { if slf.len()? != other.len()? { return Ok(false); } + + if let Ok(other_dc) = other.downcast::() { + if !slf + .getattr("global_phase")? + .eq(other_dc.getattr("global_phase")?)? + { + return Ok(false); + } + } + // Implemented using generic iterators on both sides // for simplicity. let mut ours_itr = slf.iter()?; @@ -924,7 +942,7 @@ impl CircuitData { params, extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }); res.track_instruction_parameters(py, res.data.len() - 1)?; } @@ -1033,7 +1051,7 @@ impl CircuitData { params, extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }); res.track_instruction_parameters(py, res.data.len() - 1)?; } @@ -1091,7 +1109,7 @@ impl CircuitData { params, extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }); Ok(()) } @@ -1270,6 +1288,11 @@ impl CircuitData { self.qargs_interner().get(index) } + /// Insert qargs into the interner and return the interned value + pub fn add_qargs(&mut self, qubits: &[Qubit]) -> Interned<[Qubit]> { + self.qargs_interner.insert(qubits) + } + /// Unpacks from InternerIndex to `[Clbit]` pub fn get_cargs(&self, index: Interned<[Clbit]>) -> &[Clbit] { self.cargs_interner().get(index) @@ -1483,6 +1506,60 @@ impl CircuitData { pub fn get_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { self.param_table.py_parameter_by_uuid(uuid) } + + /// Get an immutable view of the instructions in the circuit data + pub fn data(&self) -> &[PackedInstruction] { + &self.data + } + + /// Clone an empty CircuitData from a given reference. + /// + /// The new copy will have the global properties from the provided `CircuitData`. + /// The the bit data fields and interners, global phase, etc will be copied to + /// the new returned `CircuitData`, but the `data` field's instruction list will + /// be empty. This can be useful for scenarios where you want to rebuild a copy + /// of the circuit from a reference but insert new gates in the middle. + /// + /// # Arguments + /// + /// * other - The other `CircuitData` to clone an empty `CircuitData` from. + /// * capacity - The capacity for instructions to use in the output `CircuitData` + /// If `None` the length of `other` will be used, if `Some` the integer + /// value will be used as the capacity. + pub fn clone_empty_like(other: &Self, capacity: Option) -> Self { + CircuitData { + data: Vec::with_capacity(capacity.unwrap_or(other.data.len())), + qargs_interner: other.qargs_interner.clone(), + cargs_interner: other.cargs_interner.clone(), + qubits: other.qubits.clone(), + clbits: other.clbits.clone(), + param_table: ParameterTable::new(), + global_phase: other.global_phase.clone(), + } + } + + /// Append a PackedInstruction to the circuit data. + /// + /// # Arguments + /// + /// * packed: The new packed instruction to insert to the end of the CircuitData + /// The qubits and clbits **must** already be present in the interner for this + /// function to work. If they are not this will corrupt the circuit. + pub fn push(&mut self, py: Python, packed: PackedInstruction) -> PyResult<()> { + let new_index = self.data.len(); + self.data.push(packed); + self.track_instruction_parameters(py, new_index) + } + + /// Add a param to the current global phase of the circuit + pub fn add_global_phase(&mut self, py: Python, value: &Param) -> PyResult<()> { + match value { + Param::Obj(_) => Err(PyTypeError::new_err( + "Invalid parameter type, only float and parameter expression are supported", + )), + _ => self.set_global_phase(py, add_global_phase(py, &self.global_phase, value)?), + } + } } /// Helper struct for `assign_parameters` to allow use of `Param::extract_no_coerce` in diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 255343ac186c..e449ad660e38 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -11,7 +11,7 @@ // that they have been altered from the originals. #[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; +use std::sync::OnceLock; use numpy::IntoPyArray; use pyo3::basic::CompareOp; @@ -236,7 +236,7 @@ pub struct CircuitInstruction { pub params: SmallVec<[Param; 3]>, pub extra_attrs: ExtraInstructionAttributes, #[cfg(feature = "cache_pygates")] - pub py_op: OnceCell>, + pub py_op: OnceLock>, } impl CircuitInstruction { @@ -301,7 +301,7 @@ impl CircuitInstruction { params, extra_attrs: ExtraInstructionAttributes::new(label, None, None, None), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }) } @@ -656,7 +656,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython { ob.getattr(intern!(py, "label"))?.extract()?, duration, unit, - ob.getattr(intern!(py, "condition"))?.extract()?, + ob.getattr(intern!(py, "_condition"))?.extract()?, )) }; diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index dea366d02ff6..030a582bdad7 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -11,7 +11,7 @@ // that they have been altered from the originals. #[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; +use std::sync::OnceLock; use ::pyo3::prelude::*; use hashbrown::HashMap; @@ -126,7 +126,7 @@ pub fn dag_to_circuit( )), extra_attrs: instr.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }) } else { Ok(instr.clone()) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 73345f710083..25e52831683c 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -10,7 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use ahash::RandomState; use smallvec::SmallVec; @@ -68,19 +68,60 @@ use std::convert::Infallible; use std::f64::consts::PI; #[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; +use std::sync::OnceLock; static CONTROL_FLOW_OP_NAMES: [&str; 4] = ["for_loop", "while_loop", "if_else", "switch_case"]; static SEMANTIC_EQ_SYMMETRIC: [&str; 4] = ["barrier", "swap", "break_loop", "continue_loop"]; +/// An opaque key type that identifies a variable within a [DAGCircuit]. +/// +/// When a new variable is added to the DAG, it is associated internally +/// with one of these keys. When enumerating DAG nodes and edges, you can +/// retrieve the associated variable instance via [DAGCircuit::get_var]. +/// +/// These keys are [Eq], but this is semantically valid only for keys +/// from the same [DAGCircuit] instance. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Var(BitType); + +impl Var { + /// Construct a new [Var] object from a usize. if you have a u32 you can + /// create a [Var] object directly with `Var(0u32)`. This will panic + /// if the `usize` index exceeds `u32::MAX`. + #[inline(always)] + fn new(index: usize) -> Self { + Var(index + .try_into() + .unwrap_or_else(|_| panic!("Index value '{}' exceeds the maximum bit width!", index))) + } + + /// Get the index of the [Var] + #[inline(always)] + fn index(&self) -> usize { + self.0 as usize + } +} + +impl From for Var { + fn from(value: BitType) -> Self { + Var(value) + } +} + +impl From for BitType { + fn from(value: Var) -> Self { + value.0 + } +} + #[derive(Clone, Debug)] pub enum NodeType { QubitIn(Qubit), QubitOut(Qubit), ClbitIn(Clbit), ClbitOut(Clbit), - VarIn(PyObject), - VarOut(PyObject), + VarIn(Var), + VarOut(Var), Operation(PackedInstruction), } @@ -97,45 +138,21 @@ impl NodeType { } } -#[derive(Clone, Debug)] +#[derive(Hash, Eq, PartialEq, Clone, Debug)] pub enum Wire { Qubit(Qubit), Clbit(Clbit), - Var(PyObject), -} - -impl PartialEq for Wire { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Wire::Qubit(q1), Wire::Qubit(q2)) => q1 == q2, - (Wire::Clbit(c1), Wire::Clbit(c2)) => c1 == c2, - (Wire::Var(v1), Wire::Var(v2)) => { - v1.is(v2) || Python::with_gil(|py| v1.bind(py).eq(v2).unwrap()) - } - _ => false, - } - } -} - -impl Eq for Wire {} - -impl Hash for Wire { - fn hash(&self, state: &mut H) { - match self { - Self::Qubit(qubit) => qubit.hash(state), - Self::Clbit(clbit) => clbit.hash(state), - Self::Var(var) => Python::with_gil(|py| var.bind(py).hash().unwrap().hash(state)), - } - } + Var(Var), } impl Wire { fn to_pickle(&self, py: Python) -> PyObject { match self { - Self::Qubit(bit) => (0, bit.0.into_py(py)).into_py(py), - Self::Clbit(bit) => (1, bit.0.into_py(py)).into_py(py), - Self::Var(var) => (2, var.clone_ref(py)).into_py(py), + Self::Qubit(bit) => (0, bit.0.into_py(py)), + Self::Clbit(bit) => (1, bit.0.into_py(py)), + Self::Var(var) => (2, var.0.into_py(py)), } + .into_py(py) } fn from_pickle(b: &Bound) -> PyResult { @@ -146,84 +163,13 @@ impl Wire { } else if wire_type == 1 { Ok(Self::Clbit(Clbit(tuple.get_item(1)?.extract()?))) } else if wire_type == 2 { - Ok(Self::Var(tuple.get_item(1)?.unbind())) + Ok(Self::Var(Var(tuple.get_item(1)?.extract()?))) } else { Err(PyTypeError::new_err("Invalid wire type")) } } } -// TODO: Remove me. -// This is a temporary map type used to store a mapping of -// Var to NodeIndex to hold us over until Var is ported to -// Rust. Currently, we need this because PyObject cannot be -// used as the key to an IndexMap. -// -// Once we've got Var ported, Wire should also become Hash + Eq -// and we can consider combining input/output nodes maps. -#[derive(Clone, Debug)] -struct _VarIndexMap { - dict: Py, -} - -impl _VarIndexMap { - pub fn new(py: Python) -> Self { - Self { - dict: PyDict::new_bound(py).unbind(), - } - } - - pub fn keys(&self, py: Python) -> impl Iterator { - self.dict - .bind(py) - .keys() - .into_iter() - .map(|k| k.unbind()) - .collect::>() - .into_iter() - } - - pub fn contains_key(&self, py: Python, key: &PyObject) -> bool { - self.dict.bind(py).contains(key).unwrap() - } - - pub fn get(&self, py: Python, key: &PyObject) -> Option { - self.dict - .bind(py) - .get_item(key) - .unwrap() - .map(|v| NodeIndex::new(v.extract().unwrap())) - } - - pub fn insert(&mut self, py: Python, key: PyObject, value: NodeIndex) { - self.dict - .bind(py) - .set_item(key, value.index().into_py(py)) - .unwrap() - } - - pub fn remove(&mut self, py: Python, key: &PyObject) -> Option { - let bound_dict = self.dict.bind(py); - let res = bound_dict - .get_item(key.clone_ref(py)) - .unwrap() - .map(|v| NodeIndex::new(v.extract().unwrap())); - let _del_result = bound_dict.del_item(key); - res - } - pub fn values<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { - let values = self.dict.bind(py).values(); - values.iter().map(|x| NodeIndex::new(x.extract().unwrap())) - } - - pub fn iter<'py>(&self, py: Python<'py>) -> impl Iterator + 'py { - self.dict - .bind(py) - .iter() - .map(|(var, index)| (var.unbind(), NodeIndex::new(index.extract().unwrap()))) - } -} - /// Quantum circuit as a directed acyclic graph. /// /// There are 3 types of nodes in the graph: inputs, outputs, and operations. @@ -257,6 +203,8 @@ pub struct DAGCircuit { qubits: BitData, /// Clbits registered in the circuit. clbits: BitData, + /// Variables registered in the circuit. + vars: BitData, /// Global phase. global_phase: Param, /// Duration. @@ -281,11 +229,8 @@ pub struct DAGCircuit { /// Map from clbit to input and output nodes of the graph. clbit_io_map: Vec<[NodeIndex; 2]>, - // TODO: use IndexMap once Var is ported to Rust - /// Map from var to input nodes of the graph. - var_input_map: _VarIndexMap, - /// Map from var to output nodes of the graph. - var_output_map: _VarIndexMap, + /// Map from var to input and output nodes of the graph. + var_io_map: Vec<[NodeIndex; 2]>, /// Operation kind to count op_names: IndexMap, @@ -438,6 +383,7 @@ impl DAGCircuit { cargs_interner: Interner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), + vars: BitData::new(py, "vars".to_string()), global_phase: Param::Float(0.), duration: None, unit: "dt".to_string(), @@ -445,8 +391,7 @@ impl DAGCircuit { clbit_locations: PyDict::new_bound(py).unbind(), qubit_io_map: Vec::new(), clbit_io_map: Vec::new(), - var_input_map: _VarIndexMap::new(py), - var_output_map: _VarIndexMap::new(py), + var_io_map: Vec::new(), op_names: IndexMap::default(), control_flow_module: PyControlFlowModule::new(py)?, vars_info: HashMap::new(), @@ -522,10 +467,15 @@ impl DAGCircuit { self.get_node(py, indices[0])?, )?; } - for (var, index) in self.var_input_map.dict.bind(py).iter() { + for (var, indices) in self + .var_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Var::new(idx), indices)) + { out_dict.set_item( - var, - self.get_node(py, NodeIndex::new(index.extract::()?))?, + self.vars.get(var).unwrap().clone_ref(py), + self.get_node(py, indices[0])?, )?; } Ok(out_dict.unbind()) @@ -556,10 +506,15 @@ impl DAGCircuit { self.get_node(py, indices[1])?, )?; } - for (var, index) in self.var_output_map.dict.bind(py).iter() { + for (var, indices) in self + .var_io_map + .iter() + .enumerate() + .map(|(idx, indices)| (Var::new(idx), indices)) + { out_dict.set_item( - var, - self.get_node(py, NodeIndex::new(index.extract::()?))?, + self.vars.get(var).unwrap().clone_ref(py), + self.get_node(py, indices[1])?, )?; } Ok(out_dict.unbind()) @@ -579,7 +534,7 @@ impl DAGCircuit { .iter() .enumerate() .map(|(k, v)| (k, [v[0].index(), v[1].index()])) - .collect::>(), + .into_py_dict_bound(py), )?; out_dict.set_item( "clbit_io_map", @@ -587,10 +542,16 @@ impl DAGCircuit { .iter() .enumerate() .map(|(k, v)| (k, [v[0].index(), v[1].index()])) - .collect::>(), + .into_py_dict_bound(py), + )?; + out_dict.set_item( + "var_io_map", + self.var_io_map + .iter() + .enumerate() + .map(|(k, v)| (k, [v[0].index(), v[1].index()])) + .into_py_dict_bound(py), )?; - out_dict.set_item("var_input_map", self.var_input_map.dict.clone_ref(py))?; - out_dict.set_item("var_output_map", self.var_output_map.dict.clone_ref(py))?; out_dict.set_item("op_name", self.op_names.clone())?; out_dict.set_item( "vars_info", @@ -607,11 +568,12 @@ impl DAGCircuit { ), ) }) - .collect::>(), + .into_py_dict_bound(py), )?; out_dict.set_item("vars_by_type", self.vars_by_type.clone())?; out_dict.set_item("qubits", self.qubits.bits())?; out_dict.set_item("clbits", self.clbits.bits())?; + out_dict.set_item("vars", self.vars.bits())?; let mut nodes: Vec = Vec::with_capacity(self.dag.node_count()); for node_idx in self.dag.node_indices() { let node_data = self.get_node(py, node_idx)?; @@ -656,12 +618,6 @@ impl DAGCircuit { self.cregs = dict_state.get_item("cregs")?.unwrap().extract()?; self.global_phase = dict_state.get_item("global_phase")?.unwrap().extract()?; self.op_names = dict_state.get_item("op_name")?.unwrap().extract()?; - self.var_input_map = _VarIndexMap { - dict: dict_state.get_item("var_input_map")?.unwrap().extract()?, - }; - self.var_output_map = _VarIndexMap { - dict: dict_state.get_item("var_output_map")?.unwrap().extract()?, - }; self.vars_by_type = dict_state.get_item("vars_by_type")?.unwrap().extract()?; let binding = dict_state.get_item("vars_info")?.unwrap(); let vars_info_raw = binding.downcast::().unwrap(); @@ -692,6 +648,11 @@ impl DAGCircuit { for bit in clbits_raw.iter() { self.clbits.add(py, &bit, false)?; } + let binding = dict_state.get_item("vars")?.unwrap(); + let vars_raw = binding.downcast::().unwrap(); + for bit in vars_raw.iter() { + self.vars.add(py, &bit, false)?; + } let binding = dict_state.get_item("qubit_io_map")?.unwrap(); let qubit_index_map_raw = binding.downcast::().unwrap(); self.qubit_io_map = Vec::with_capacity(qubit_index_map_raw.len()); @@ -703,12 +664,19 @@ impl DAGCircuit { let binding = dict_state.get_item("clbit_io_map")?.unwrap(); let clbit_index_map_raw = binding.downcast::().unwrap(); self.clbit_io_map = Vec::with_capacity(clbit_index_map_raw.len()); - for (_k, v) in clbit_index_map_raw.iter() { let indices: [usize; 2] = v.extract()?; self.clbit_io_map .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); } + let binding = dict_state.get_item("var_io_map")?.unwrap(); + let var_index_map_raw = binding.downcast::().unwrap(); + self.var_io_map = Vec::with_capacity(var_index_map_raw.len()); + for (_k, v) in var_index_map_raw.iter() { + let indices: [usize; 2] = v.extract()?; + self.var_io_map + .push([NodeIndex::new(indices[0]), NodeIndex::new(indices[1])]); + } // Rebuild Graph preserving index holes: let binding = dict_state.get_item("nodes")?.unwrap(); let nodes_lst = binding.downcast::()?; @@ -920,6 +888,8 @@ impl DAGCircuit { /// /// Raises: /// Exception: if the gate is of type string and params is None. + /// + /// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0 #[pyo3(signature=(gate, qubits, schedule, params=None))] fn add_calibration<'py>( &mut self, @@ -993,7 +963,7 @@ def _format(operand): /// case, the operation does not need to be translated to the device basis. /// /// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0 - fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { + pub fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { emit_pulse_dependency_deprecation( py, "method ``qiskit.dagcircuit.dagcircuit.DAGCircuit.has_calibration_for``", @@ -1237,7 +1207,7 @@ def _format(operand): let clbits: HashSet = bit_iter.collect(); let mut busy_bits = Vec::new(); for bit in clbits.iter() { - if !self.is_wire_idle(py, &Wire::Clbit(*bit))? { + if !self.is_wire_idle(&Wire::Clbit(*bit))? { busy_bits.push(self.clbits.get(*bit).unwrap()); } } @@ -1264,7 +1234,7 @@ def _format(operand): // Remove DAG in/out nodes etc. for bit in clbits.iter() { - self.remove_idle_wire(py, Wire::Clbit(*bit))?; + self.remove_idle_wire(Wire::Clbit(*bit))?; } // Copy the current clbit mapping so we can use it while remapping @@ -1445,7 +1415,7 @@ def _format(operand): let mut busy_bits = Vec::new(); for bit in qubits.iter() { - if !self.is_wire_idle(py, &Wire::Qubit(*bit))? { + if !self.is_wire_idle(&Wire::Qubit(*bit))? { busy_bits.push(self.qubits.get(*bit).unwrap()); } } @@ -1472,7 +1442,7 @@ def _format(operand): // Remove DAG in/out nodes etc. for bit in qubits.iter() { - self.remove_idle_wire(py, Wire::Qubit(*bit))?; + self.remove_idle_wire(Wire::Qubit(*bit))?; } // Copy the current qubit mapping so we can use it while remapping @@ -2198,7 +2168,7 @@ def _format(operand): let wires = (0..self.qubit_io_map.len()) .map(|idx| Wire::Qubit(Qubit::new(idx))) .chain((0..self.clbit_io_map.len()).map(|idx| Wire::Clbit(Clbit::new(idx)))) - .chain(self.var_input_map.keys(py).map(Wire::Var)); + .chain((0..self.var_io_map.len()).map(|idx| Wire::Var(Var::new(idx)))); match ignore { Some(ignore) => { // Convert the list to a Rust set. @@ -2207,7 +2177,7 @@ def _format(operand): .map(|s| s.extract()) .collect::>>()?; for wire in wires { - let nodes_found = self.nodes_on_wire(py, &wire, true).into_iter().any(|node| { + let nodes_found = self.nodes_on_wire(&wire, true).into_iter().any(|node| { let weight = self.dag.node_weight(node).unwrap(); if let NodeType::Operation(packed) = weight { !ignore_set.contains(packed.op.name()) @@ -2220,18 +2190,18 @@ def _format(operand): result.push(match wire { Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(var).unwrap().clone_ref(py), }); } } } None => { for wire in wires { - if self.is_wire_idle(py, &wire)? { + if self.is_wire_idle(&wire)? { result.push(match wire { Wire::Qubit(qubit) => self.qubits.get(qubit).unwrap().clone_ref(py), Wire::Clbit(clbit) => self.clbits.get(clbit).unwrap().clone_ref(py), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(var).unwrap().clone_ref(py), }); } } @@ -2665,8 +2635,18 @@ def _format(operand): [NodeType::ClbitIn(bit1), NodeType::ClbitIn(bit2)] => Ok(bit1 == bit2), [NodeType::QubitOut(bit1), NodeType::QubitOut(bit2)] => Ok(bit1 == bit2), [NodeType::ClbitOut(bit1), NodeType::ClbitOut(bit2)] => Ok(bit1 == bit2), - [NodeType::VarIn(var1), NodeType::VarIn(var2)] => var1.bind(py).eq(var2), - [NodeType::VarOut(var1), NodeType::VarOut(var2)] => var1.bind(py).eq(var2), + [NodeType::VarIn(var1), NodeType::VarIn(var2)] => self + .vars + .get(*var1) + .unwrap() + .bind(py) + .eq(other.vars.get(*var2).unwrap()), + [NodeType::VarOut(var1), NodeType::VarOut(var2)] => self + .vars + .get(*var1) + .unwrap() + .bind(py) + .eq(other.vars.get(*var2).unwrap()), _ => Ok(false), } }; @@ -2828,117 +2808,14 @@ def _format(operand): } let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); - - let mut block_op_names = Vec::new(); - let mut block_qargs: HashSet = HashSet::new(); - let mut block_cargs: HashSet = HashSet::new(); - for nd in &block_ids { - let weight = self.dag.node_weight(*nd); - match weight { - Some(NodeType::Operation(packed)) => { - block_op_names.push(packed.op.name().to_string()); - block_qargs.extend(self.qargs_interner.get(packed.qubits)); - block_cargs.extend(self.cargs_interner.get(packed.clbits)); - - if let Some(condition) = packed.condition() { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .condition_resources(condition.bind(py))? - .clbits - .bind(py), - )?, - ); - continue; - } - - // Add classical bits from SwitchCaseOp, if applicable. - if let OperationRef::Instruction(op) = packed.op.view() { - if op.name() == "switch_case" { - let op_bound = op.instruction.bind(py); - let target = op_bound.getattr(intern!(py, "target"))?; - if target.is_instance(imports::CLBIT.get_bound(py))? { - block_cargs.insert(self.clbits.find(&target).unwrap()); - } else if target - .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? - { - block_cargs.extend( - self.clbits - .map_bits(target.extract::>>()?)?, - ); - } else { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .node_resources(&target)? - .clbits - .bind(py), - )?, - ); - } - } - } - } - Some(_) => { - return Err(DAGCircuitError::new_err( - "Nodes in 'node_block' must be of type 'DAGOpNode'.", - )) - } - None => { - return Err(DAGCircuitError::new_err( - "Node in 'node_block' not found in DAG.", - )) - } - } - } - - let mut block_qargs: Vec = block_qargs - .into_iter() - .filter(|q| qubit_pos_map.contains_key(q)) - .collect(); - block_qargs.sort_by_key(|q| qubit_pos_map[q]); - - let mut block_cargs: Vec = block_cargs - .into_iter() - .filter(|c| clbit_pos_map.contains_key(c)) - .collect(); - block_cargs.sort_by_key(|c| clbit_pos_map[c]); - - let py_op = op.extract::()?; - - if py_op.operation.num_qubits() as usize != block_qargs.len() { - return Err(DAGCircuitError::new_err(format!( - "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() - ))); - } - - let op_name = py_op.operation.name().to_string(); - let qubits = self.qargs_interner.insert_owned(block_qargs); - let clbits = self.cargs_interner.insert_owned(block_cargs); - let weight = NodeType::Operation(PackedInstruction { - op: py_op.operation, - qubits, - clbits, - params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), - extra_attrs: py_op.extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: op.unbind().into(), - }); - - let new_node = self - .dag - .contract_nodes(block_ids, weight, cycle_check) - .map_err(|e| match e { - ContractError::DAGWouldCycle => DAGCircuitError::new_err( - "Replacing the specified node block would introduce a cycle", - ), - })?; - - self.increment_op(op_name.as_str()); - for name in block_op_names { - self.decrement_op(name.as_str()); - } - + let new_node = self.replace_block_with_py_op( + py, + &block_ids, + op, + cycle_check, + &qubit_pos_map, + &clbit_pos_map, + )?; self.get_node(py, new_node) } @@ -3130,7 +3007,7 @@ def _format(operand): .edges_directed(node_index, Incoming) .find(|edge| { if let Wire::Var(var) = edge.weight() { - contracted_var.eq(var).unwrap() + contracted_var.eq(self.vars.get(*var)).unwrap() } else { false } @@ -3141,7 +3018,7 @@ def _format(operand): .edges_directed(node_index, Outgoing) .find(|edge| { if let Wire::Var(var) = edge.weight() { - contracted_var.eq(var).unwrap() + contracted_var.eq(self.vars.get(*var)).unwrap() } else { false } @@ -3150,7 +3027,7 @@ def _format(operand): self.dag.add_edge( pred.source(), succ.target(), - Wire::Var(contracted_var.unbind()), + Wire::Var(self.vars.find(&contracted_var).unwrap()), ); } @@ -3467,141 +3344,22 @@ def _format(operand): }; let py = op.py(); let node_index = node.as_ref().node.unwrap(); - // Extract information from node that is going to be replaced - let old_packed = match self.dag.node_weight(node_index) { - Some(NodeType::Operation(old_packed)) => old_packed.clone(), - Some(_) => { - return Err(DAGCircuitError::new_err( - "'node' must be of type 'DAGOpNode'.", - )) + self.substitute_node_with_py_op(py, node_index, op, propagate_condition)?; + if inplace { + let new_weight = self.dag[node_index].unwrap_operation(); + let temp: OperationFromPython = op.extract()?; + node.instruction.operation = temp.operation; + node.instruction.params = new_weight.params_view().iter().cloned().collect(); + node.instruction.extra_attrs = new_weight.extra_attrs.clone(); + #[cfg(feature = "cache_pygates")] + { + node.instruction.py_op = new_weight.py_op.clone(); } - None => return Err(DAGCircuitError::new_err("'node' not found in DAG.")), - }; - // Extract information from new op - let new_op = op.extract::()?; - let current_wires: HashSet = self - .dag - .edges(node_index) - .map(|e| e.weight().clone()) - .collect(); - let mut new_wires: HashSet = self - .qargs_interner - .get(old_packed.qubits) - .iter() - .map(|x| Wire::Qubit(*x)) - .chain( - self.cargs_interner - .get(old_packed.clbits) - .iter() - .map(|x| Wire::Clbit(*x)), - ) - .collect(); - let (additional_clbits, additional_vars) = - self.additional_wires(py, new_op.operation.view(), new_op.extra_attrs.condition())?; - new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); - new_wires.extend(additional_vars.iter().map(|x| Wire::Var(x.clone_ref(py)))); - - if old_packed.op.num_qubits() != new_op.operation.num_qubits() - || old_packed.op.num_clbits() != new_op.operation.num_clbits() - { - return Err(DAGCircuitError::new_err( - format!( - "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", - old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() - ))); + Ok(node.into_py(py)) + } else { + self.get_node(py, node_index) } - - #[cfg(feature = "cache_pygates")] - let mut py_op_cache = Some(op.clone().unbind()); - - let mut extra_attrs = new_op.extra_attrs.clone(); - // If either operation is a control-flow operation, propagate_condition is ignored - if propagate_condition - && !(node.instruction.operation.control_flow() || new_op.operation.control_flow()) - { - // if new_op has a condition, the condition can't be propagated from the old node - if new_op.extra_attrs.condition().is_some() { - return Err(DAGCircuitError::new_err( - "Cannot propagate a condition to an operation that already has one.", - )); - } - if let Some(old_condition) = old_packed.condition() { - if matches!(new_op.operation.view(), OperationRef::Operation(_)) { - return Err(DAGCircuitError::new_err( - "Cannot add a condition on a generic Operation.", - )); - } - extra_attrs.set_condition(Some(old_condition.clone_ref(py))); - - let binding = self - .control_flow_module - .condition_resources(old_condition.bind(py))?; - let condition_clbits = binding.clbits.bind(py); - for bit in condition_clbits { - new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); - } - let op_ref = new_op.operation.view(); - if let OperationRef::Instruction(inst) = op_ref { - inst.instruction - .bind(py) - .setattr(intern!(py, "condition"), old_condition)?; - } else if let OperationRef::Gate(gate) = op_ref { - gate.gate.bind(py).call_method1( - intern!(py, "c_if"), - old_condition.downcast_bound::(py)?, - )?; - } - #[cfg(feature = "cache_pygates")] - { - py_op_cache = None; - } - } - }; - if new_wires != current_wires { - // The new wires must be a non-strict subset of the current wires; if they add new - // wires, we'd not know where to cut the existing wire to insert the new dependency. - return Err(DAGCircuitError::new_err(format!( - "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires - ))); - } - - if inplace { - node.instruction.operation = new_op.operation.clone(); - node.instruction.params = new_op.params.clone(); - node.instruction.extra_attrs = extra_attrs.clone(); - #[cfg(feature = "cache_pygates")] - { - node.instruction.py_op = py_op_cache - .as_ref() - .map(|ob| OnceCell::from(ob.clone_ref(py))) - .unwrap_or_default(); - } - } - // Clone op data, as it will be moved into the PackedInstruction - let new_weight = NodeType::Operation(PackedInstruction { - op: new_op.operation.clone(), - qubits: old_packed.qubits, - clbits: old_packed.clbits, - params: (!new_op.params.is_empty()).then(|| new_op.params.into()), - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), - }); - let node_index = node.as_ref().node.unwrap(); - if let Some(weight) = self.dag.node_weight_mut(node_index) { - *weight = new_weight; - } - - // Update self.op_names - self.decrement_op(old_packed.op.name()); - self.increment_op(new_op.operation.name()); - - if inplace { - Ok(node.into_py(py)) - } else { - self.get_node(py, node_index) - } - } + } /// Decompose the circuit into sets of qubits with no gates connecting them. /// @@ -3659,11 +3417,11 @@ def _format(operand): non_classical = true; } NodeType::VarIn(v) => { - let var_in = new_dag.var_input_map.get(py, v).unwrap(); + let var_in = new_dag.var_io_map[v.index()][0]; node_map.insert(*node, var_in); } NodeType::VarOut(v) => { - let var_out = new_dag.var_output_map.get(py, v).unwrap(); + let var_out = new_dag.var_io_map[v.index()][1]; node_map.insert(*node, var_out); } NodeType::Operation(pi) => { @@ -3717,12 +3475,11 @@ def _format(operand): .add_edge(*in_node, *out_node, Wire::Clbit(clbit)); } } - for (var, in_node) in new_dag.var_input_map.iter(py) { + for (var_index, &[in_node, out_node]) in new_dag.var_io_map.iter().enumerate() { if new_dag.dag.edges(in_node).next().is_none() { - let out_node = new_dag.var_output_map.get(py, &var).unwrap(); new_dag .dag - .add_edge(in_node, out_node, Wire::Var(var.clone_ref(py))); + .add_edge(in_node, out_node, Wire::Var(Var::new(var_index))); } } if remove_idle_qubits { @@ -3892,7 +3649,7 @@ def _format(operand): match edge.weight() { Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(*var).unwrap(), }, )) } @@ -4326,7 +4083,7 @@ def _format(operand): /// Return a list of op nodes in the first layer of this dag. #[pyo3(name = "front_layer")] fn py_front_layer(&self, py: Python) -> PyResult> { - let native_front_layer = self.front_layer(py); + let native_front_layer = self.front_layer(); let front_layer_list = PyList::empty_bound(py); for node in native_front_layer { front_layer_list.append(self.get_node(py, node)?)?; @@ -4353,7 +4110,7 @@ def _format(operand): #[pyo3(signature = (*, vars_mode="captures"))] fn layers(&self, py: Python, vars_mode: &str) -> PyResult> { let layer_list = PyList::empty_bound(py); - let mut graph_layers = self.multigraph_layers(py); + let mut graph_layers = self.multigraph_layers(); if graph_layers.next().is_none() { return Ok(PyIterator::from_bound_object(&layer_list)?.into()); } @@ -4453,7 +4210,7 @@ def _format(operand): /// Yield layers of the multigraph. #[pyo3(name = "multigraph_layers")] fn py_multigraph_layers(&self, py: Python) -> PyResult> { - let graph_layers = self.multigraph_layers(py).map(|layer| -> Vec { + let graph_layers = self.multigraph_layers().map(|layer| -> Vec { layer .into_iter() .filter_map(|index| self.get_node(py, index).ok()) @@ -4568,10 +4325,8 @@ def _format(operand): self.qubits.find(wire).map(Wire::Qubit) } else if wire.is_instance(imports::CLBIT.get_bound(py))? { self.clbits.find(wire).map(Wire::Clbit) - } else if self.var_input_map.contains_key(py, &wire.clone().unbind()) { - Some(Wire::Var(wire.clone().unbind())) } else { - None + self.vars.find(wire).map(Wire::Var) } .ok_or_else(|| { DAGCircuitError::new_err(format!( @@ -4581,7 +4336,7 @@ def _format(operand): })?; let nodes = self - .nodes_on_wire(py, &wire, only_ops) + .nodes_on_wire(&wire, only_ops) .into_iter() .map(|n| self.get_node(py, n)) .collect::>>()?; @@ -4793,7 +4548,8 @@ def _format(operand): "cannot add inputs to a circuit with captures", )); } - self.add_var(py, var, DAGVarType::Input) + self.add_var(py, var, DAGVarType::Input)?; + Ok(()) } /// Add a captured variable to the circuit. @@ -4809,7 +4565,8 @@ def _format(operand): "cannot add captures to a circuit with inputs", )); } - self.add_var(py, var, DAGVarType::Capture) + self.add_var(py, var, DAGVarType::Capture)?; + Ok(()) } /// Add a declared local variable to the circuit. @@ -4817,7 +4574,8 @@ def _format(operand): /// Args: /// var: the variable to add. fn add_declared_var(&mut self, py: Python, var: &Bound) -> PyResult<()> { - self.add_var(py, var, DAGVarType::Declare) + self.add_var(py, var, DAGVarType::Declare)?; + Ok(()) } /// Total number of classical variables tracked by the circuit. @@ -4926,7 +4684,7 @@ def _format(operand): match wire.weight() { Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(*var).unwrap(), }, ) .into_py(py) @@ -4944,7 +4702,7 @@ def _format(operand): match wire.weight() { Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(*var).unwrap(), }, ) .into_py(py) @@ -4958,7 +4716,7 @@ def _format(operand): .map(|wire| match wire.weight() { Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(*var).unwrap(), }) .collect() } @@ -4969,7 +4727,7 @@ def _format(operand): .map(|wire| match wire.weight() { Wire::Qubit(qubit) => self.qubits.get(*qubit).unwrap(), Wire::Clbit(clbit) => self.clbits.get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => self.vars.get(*var).unwrap(), }) .collect() } @@ -4989,7 +4747,7 @@ def _format(operand): let weight = match e.weight() { Wire::Qubit(q) => self.qubits.get(*q).unwrap(), Wire::Clbit(c) => self.clbits.get(*c).unwrap(), - Wire::Var(v) => v, + Wire::Var(v) => self.vars.get(*v).unwrap(), }; if edge_checker.call1((weight,))?.extract::()? { result.push(self.get_node(py, e.target())?); @@ -5006,7 +4764,7 @@ def _format(operand): match wire { Wire::Qubit(qubit) => self.qubits.get(*qubit).to_object(py), Wire::Clbit(clbit) => self.clbits.get(*clbit).to_object(py), - Wire::Var(var) => var.clone_ref(py), + Wire::Var(var) => self.vars.get(*var).to_object(py), } }) .collect() @@ -5050,6 +4808,12 @@ impl DAGCircuit { &self.clbits } + /// Returns an immutable view of the Variable wires registered in the circuit + #[inline(always)] + pub fn vars(&self) -> &BitData { + &self.vars + } + /// Return an iterator of gate runs with non-conditional op nodes of given names pub fn collect_runs( &self, @@ -5208,11 +4972,12 @@ impl DAGCircuit { .iter() .map(|c| self.clbit_io_map.get(c.index()).map(|x| x[1]).unwrap()), ) - .chain( - vars.iter() - .flatten() - .map(|v| self.var_output_map.get(py, v).unwrap()), - ) + .chain(vars.iter().flatten().map(|v| { + self.var_io_map + .get(self.vars.find(v.bind(py)).unwrap().index()) + .map(|x| x[1]) + .unwrap() + })) .collect(); for output_node in output_nodes { @@ -5273,7 +5038,7 @@ impl DAGCircuit { .collect(); if let Some(vars) = vars { for var in vars { - input_nodes.push(self.var_input_map.get(py, &var).unwrap()); + input_nodes.push(self.var_io_map[self.vars.find(var.bind(py)).unwrap().index()][0]); } } @@ -5382,7 +5147,7 @@ impl DAGCircuit { let py_op = if let Some(py_op) = py_op { py_op.into() } else { - OnceCell::new() + OnceLock::new() }; let packed_instruction = PackedInstruction { op, @@ -5469,7 +5234,7 @@ impl DAGCircuit { .any(|x| self.op_names.contains_key(&x.to_string())) } - fn is_wire_idle(&self, py: Python, wire: &Wire) -> PyResult { + fn is_wire_idle(&self, wire: &Wire) -> PyResult { let (input_node, output_node) = match wire { Wire::Qubit(qubit) => ( self.qubit_io_map[qubit.index()][0], @@ -5480,8 +5245,8 @@ impl DAGCircuit { self.clbit_io_map[clbit.index()][1], ), Wire::Var(var) => ( - self.var_input_map.get(py, var).unwrap(), - self.var_output_map.get(py, var).unwrap(), + self.var_io_map[var.index()][0], + self.var_io_map[var.index()][1], ), }; @@ -5616,9 +5381,12 @@ impl DAGCircuit { /// /// This adds a pair of in and out nodes connected by an edge. /// + /// Returns: + /// The input and output node indices of the added wire, respectively. + /// /// Raises: /// DAGCircuitError: if trying to add duplicate wire - fn add_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + fn add_wire(&mut self, wire: Wire) -> PyResult<(NodeIndex, NodeIndex)> { let (in_node, out_node) = match wire { Wire::Qubit(qubit) => { if (qubit.index()) >= self.qubit_io_map.len() { @@ -5640,33 +5408,31 @@ impl DAGCircuit { Err(DAGCircuitError::new_err("classical wire already exists!")) } } - Wire::Var(ref var) => { - if self.var_input_map.contains_key(py, var) - || self.var_output_map.contains_key(py, var) - { + Wire::Var(var) => { + if var.index() >= self.var_io_map.len() { + let in_node = self.dag.add_node(NodeType::VarIn(var)); + let out_node = self.dag.add_node(NodeType::VarOut(var)); + self.var_io_map.push([in_node, out_node]); + Ok((in_node, out_node)) + } else { return Err(DAGCircuitError::new_err("var wire already exists!")); } - let in_node = self.dag.add_node(NodeType::VarIn(var.clone_ref(py))); - let out_node = self.dag.add_node(NodeType::VarOut(var.clone_ref(py))); - self.var_input_map.insert(py, var.clone_ref(py), in_node); - self.var_output_map.insert(py, var.clone_ref(py), out_node); - Ok((in_node, out_node)) } }?; self.dag.add_edge(in_node, out_node, wire); - Ok(()) + Ok((in_node, out_node)) } /// Get the nodes on the given wire. /// /// Note: result is empty if the wire is not in the DAG. - pub fn nodes_on_wire(&self, py: Python, wire: &Wire, only_ops: bool) -> Vec { + pub fn nodes_on_wire(&self, wire: &Wire, only_ops: bool) -> Vec { let mut nodes = Vec::new(); let mut current_node = match wire { Wire::Qubit(qubit) => self.qubit_io_map.get(qubit.index()).map(|x| x[0]), Wire::Clbit(clbit) => self.clbit_io_map.get(clbit.index()).map(|x| x[0]), - Wire::Var(var) => self.var_input_map.get(py, var), + Wire::Var(var) => self.var_io_map.get(var.index()).map(|x| x[0]), }; while let Some(node) = current_node { @@ -5691,14 +5457,11 @@ impl DAGCircuit { nodes } - fn remove_idle_wire(&mut self, py: Python, wire: Wire) -> PyResult<()> { + fn remove_idle_wire(&mut self, wire: Wire) -> PyResult<()> { let [in_node, out_node] = match wire { Wire::Qubit(qubit) => self.qubit_io_map[qubit.index()], Wire::Clbit(clbit) => self.clbit_io_map[clbit.index()], - Wire::Var(var) => [ - self.var_input_map.remove(py, &var).unwrap(), - self.var_output_map.remove(py, &var).unwrap(), - ], + Wire::Var(var) => self.var_io_map[var.index()], }; self.dag.remove_node(in_node); self.dag.remove_node(out_node); @@ -5717,7 +5480,7 @@ impl DAGCircuit { }, )?, )?; - self.add_wire(py, Wire::Qubit(qubit))?; + self.add_wire(Wire::Qubit(qubit))?; Ok(qubit) } @@ -5733,7 +5496,7 @@ impl DAGCircuit { }, )?, )?; - self.add_wire(py, Wire::Clbit(clbit))?; + self.add_wire(Wire::Clbit(clbit))?; Ok(clbit) } @@ -5802,7 +5565,7 @@ impl DAGCircuit { } else if wire.is_instance(imports::CLBIT.get_bound(py))? { NodeType::ClbitIn(self.clbits.find(wire).unwrap()) } else { - NodeType::VarIn(wire.clone().unbind()) + NodeType::VarIn(self.vars.find(wire).unwrap()) } } else if let Ok(out_node) = b.downcast::() { let out_node = out_node.borrow(); @@ -5812,7 +5575,7 @@ impl DAGCircuit { } else if wire.is_instance(imports::CLBIT.get_bound(py))? { NodeType::ClbitOut(self.clbits.find(wire).unwrap()) } else { - NodeType::VarIn(wire.clone().unbind()) + NodeType::VarIn(self.vars.find(wire).unwrap()) } } else if let Ok(op_node) = b.downcast::() { let op_node = op_node.borrow(); @@ -5890,12 +5653,16 @@ impl DAGCircuit { )? .into_any() } - NodeType::VarIn(var) => { - Py::new(py, DAGInNode::new(py, id, var.clone_ref(py)))?.into_any() - } - NodeType::VarOut(var) => { - Py::new(py, DAGOutNode::new(py, id, var.clone_ref(py)))?.into_any() - } + NodeType::VarIn(var) => Py::new( + py, + DAGInNode::new(py, id, self.vars.get(*var).unwrap().clone_ref(py)), + )? + .into_any(), + NodeType::VarOut(var) => Py::new( + py, + DAGOutNode::new(py, id, self.vars.get(*var).unwrap().clone_ref(py)), + )? + .into_any(), }; Ok(dag_node) } @@ -5980,10 +5747,10 @@ impl DAGCircuit { } /// Returns an iterator over a list layers of the `DAGCircuit``. - pub fn multigraph_layers(&self, py: Python) -> impl Iterator> + '_ { + pub fn multigraph_layers(&self) -> impl Iterator> + '_ { let mut first_layer: Vec<_> = self.qubit_io_map.iter().map(|x| x[0]).collect(); first_layer.extend(self.clbit_io_map.iter().map(|x| x[0])); - first_layer.extend(self.var_input_map.values(py)); + first_layer.extend(self.var_io_map.iter().map(|x| x[0])); // A DAG is by definition acyclical, therefore unwrapping the layer should never fail. layers(&self.dag, first_layer).map(|layer| match layer { Ok(layer) => layer, @@ -5992,17 +5759,14 @@ impl DAGCircuit { } /// Returns an iterator over the first layer of the `DAGCircuit``. - pub fn front_layer<'a>(&'a self, py: Python) -> Box + 'a> { - let mut graph_layers = self.multigraph_layers(py); + pub fn front_layer(&self) -> impl Iterator + '_ { + let mut graph_layers = self.multigraph_layers(); graph_layers.next(); - - let next_layer = graph_layers.next(); - match next_layer { - Some(layer) => Box::new(layer.into_iter().filter(|node| { - matches!(self.dag.node_weight(*node).unwrap(), NodeType::Operation(_)) - })), - None => Box::new(vec![].into_iter()), - } + graph_layers + .next() + .into_iter() + .flatten() + .filter(|node| matches!(self.dag.node_weight(*node).unwrap(), NodeType::Operation(_))) } fn substitute_node_with_subgraph( @@ -6090,7 +5854,7 @@ impl DAGCircuit { .any(|edge| match edge.weight() { Wire::Qubit(qubit) => !qubit_map.contains_key(qubit), Wire::Clbit(clbit) => !clbit_map.contains_key(clbit), - Wire::Var(var) => !bound_var_map.contains(var).unwrap(), + Wire::Var(var) => !bound_var_map.contains(other.vars.get(*var)).unwrap(), }), _ => false, } @@ -6154,7 +5918,11 @@ impl DAGCircuit { match edge.weight() { Wire::Qubit(qubit) => Wire::Qubit(qubit_map[qubit]), Wire::Clbit(clbit) => Wire::Clbit(clbit_map[clbit]), - Wire::Var(var) => Wire::Var(bound_var_map.get_item(var)?.unwrap().unbind()), + Wire::Var(var) => Wire::Var( + self.vars + .find(&bound_var_map.get_item(other.vars.get(*var))?.unwrap()) + .unwrap(), + ), }, ); } @@ -6174,9 +5942,13 @@ impl DAGCircuit { .clbit_io_map .get(reverse_clbit_map[&clbit].index()) .map(|x| x[0]), - Wire::Var(ref var) => { - let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); - other.var_input_map.get(py, index) + Wire::Var(var) => { + let index = other + .vars + .find(&reverse_var_map.get_item(self.vars.get(var))?.unwrap()) + .unwrap() + .index(); + other.var_io_map.get(index).map(|x| x[0]) } }; let old_index = @@ -6210,9 +5982,13 @@ impl DAGCircuit { .clbit_io_map .get(reverse_clbit_map[&clbit].index()) .map(|x| x[1]), - Wire::Var(ref var) => { - let index = &reverse_var_map.get_item(var)?.unwrap().unbind(); - other.var_output_map.get(py, index) + Wire::Var(var) => { + let index = other + .vars + .find(&reverse_var_map.get_item(self.vars.get(var))?.unwrap()) + .unwrap() + .index(); + other.var_io_map.get(index).map(|x| x[1]) } }; let old_index = @@ -6239,7 +6015,14 @@ impl DAGCircuit { Ok(out_map) } - fn add_var(&mut self, py: Python, var: &Bound, type_: DAGVarType) -> PyResult<()> { + /// Retrieve a variable given its unique [Var] key within the DAG. + /// + /// The provided [Var] must be from this [DAGCircuit]. + pub fn get_var<'py>(&self, py: Python<'py>, var: Var) -> Option> { + self.vars.get(var).map(|v| v.bind(py).clone()) + } + + fn add_var(&mut self, py: Python, var: &Bound, type_: DAGVarType) -> PyResult { // The setup of the initial graph structure between an "in" and an "out" node is the same as // the bit-related `_add_wire`, but this logically needs to do different bookkeeping around // tracking the properties @@ -6257,16 +6040,9 @@ impl DAGCircuit { "cannot add var as its name shadows an existing var", )); } - let in_node = NodeType::VarIn(var.clone().unbind()); - let out_node = NodeType::VarOut(var.clone().unbind()); - let in_index = self.dag.add_node(in_node); - let out_index = self.dag.add_node(out_node); - self.dag - .add_edge(in_index, out_index, Wire::Var(var.clone().unbind())); - self.var_input_map - .insert(py, var.clone().unbind(), in_index); - self.var_output_map - .insert(py, var.clone().unbind(), out_index); + + let var_idx = self.vars.add(py, var, true)?; + let (in_index, out_index) = self.add_wire(Wire::Var(var_idx))?; self.vars_by_type[type_ as usize] .bind(py) .add(var.clone().unbind())?; @@ -6279,7 +6055,7 @@ impl DAGCircuit { out_node: out_index, }, ); - Ok(()) + Ok(var_idx) } fn check_op_addition(&self, py: Python, inst: &PackedInstruction) -> PyResult<()> { @@ -6316,7 +6092,8 @@ impl DAGCircuit { } } for v in vars { - if !self.var_output_map.contains_key(py, &v) { + let var_idx = self.vars.find(v.bind(py)).unwrap(); + if !self.var_io_map.len() - 1 < var_idx.index() { return Err(DAGCircuitError::new_err(format!( "var {} not found in output map", v @@ -6369,6 +6146,7 @@ impl DAGCircuit { cargs_interner: Interner::with_capacity(num_clbits), qubits: BitData::with_capacity(py, "qubits".to_string(), num_qubits), clbits: BitData::with_capacity(py, "clbits".to_string(), num_clbits), + vars: BitData::with_capacity(py, "vars".to_string(), num_vars), global_phase: Param::Float(0.), duration: None, unit: "dt".to_string(), @@ -6376,8 +6154,7 @@ impl DAGCircuit { clbit_locations: PyDict::new_bound(py).unbind(), qubit_io_map: Vec::with_capacity(num_qubits), clbit_io_map: Vec::with_capacity(num_clbits), - var_input_map: _VarIndexMap::new(py), - var_output_map: _VarIndexMap::new(py), + var_io_map: Vec::with_capacity(num_vars), op_names: IndexMap::default(), control_flow_module: PyControlFlowModule::new(py)?, vars_info: HashMap::with_capacity(num_vars), @@ -6416,7 +6193,7 @@ impl DAGCircuit { .then(|| Box::new(new_gate.1.iter().map(|x| Param::Float(*x)).collect())), extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), } } else { panic!("This method only works if provided index is an op node"); @@ -6499,12 +6276,12 @@ impl DAGCircuit { }; #[cfg(feature = "cache_pygates")] let py_op = match new_op.operation.view() { - OperationRef::Standard(_) => OnceCell::new(), - OperationRef::Gate(gate) => OnceCell::from(gate.gate.clone_ref(py)), + OperationRef::Standard(_) => OnceLock::new(), + OperationRef::Gate(gate) => OnceLock::from(gate.gate.clone_ref(py)), OperationRef::Instruction(instruction) => { - OnceCell::from(instruction.instruction.clone_ref(py)) + OnceLock::from(instruction.instruction.clone_ref(py)) } - OperationRef::Operation(op) => OnceCell::from(op.operation.clone_ref(py)), + OperationRef::Operation(op) => OnceLock::from(op.operation.clone_ref(py)), }; let inst = PackedInstruction { op: new_op.operation, @@ -6737,7 +6514,8 @@ impl DAGCircuit { } else { // If the var is not in the last nodes collection, the edge between the output node and its predecessor. // Then, store the predecessor's NodeIndex in the last nodes collection. - let output_node = self.var_output_map.get(py, var).unwrap(); + let var_idx = self.vars.find(var.bind(py)).unwrap(); + let output_node = self.var_io_map.get(var_idx.index()).unwrap()[1]; let (edge_id, predecessor_node) = self .dag .edges_directed(output_node, Incoming) @@ -6754,8 +6532,11 @@ impl DAGCircuit { if var_last_node == new_node { continue; } - self.dag - .add_edge(var_last_node, new_node, Wire::Var(var.clone_ref(py))); + self.dag.add_edge( + var_last_node, + new_node, + Wire::Var(self.vars.find(var.bind(py)).unwrap()), + ); } } @@ -6774,7 +6555,8 @@ impl DAGCircuit { // Add the output_nodes back to vars for item in vars_last_nodes.items() { let (var, node): (PyObject, usize) = item.extract()?; - let output_node = self.var_output_map.get(py, &var).unwrap(); + let var = self.vars.find(var.bind(py)).unwrap(); + let output_node = self.var_io_map.get(var.index()).unwrap()[1]; self.dag .add_edge(NodeIndex::new(node), output_node, Wire::Var(var)); } @@ -6950,7 +6732,7 @@ impl DAGCircuit { params: instr.params.clone(), extra_attrs: instr.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }) }) .collect::>>()?; @@ -6980,11 +6762,254 @@ impl DAGCircuit { }; Self::from_circuit(py, circ, copy_op, None, None) } + + /// Replace a block of node indices with a new python operation + pub fn replace_block_with_py_op( + &mut self, + py: Python, + block_ids: &[NodeIndex], + op: Bound, + cycle_check: bool, + qubit_pos_map: &HashMap, + clbit_pos_map: &HashMap, + ) -> PyResult { + let mut block_op_names = Vec::with_capacity(block_ids.len()); + let mut block_qargs: HashSet = HashSet::new(); + let mut block_cargs: HashSet = HashSet::new(); + for nd in block_ids { + let weight = self.dag.node_weight(*nd); + match weight { + Some(NodeType::Operation(packed)) => { + block_op_names.push(packed.op.name().to_string()); + block_qargs.extend(self.qargs_interner.get(packed.qubits)); + block_cargs.extend(self.cargs_interner.get(packed.clbits)); + + if let Some(condition) = packed.condition() { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + continue; + } + + // Add classical bits from SwitchCaseOp, if applicable. + if let OperationRef::Instruction(op) = packed.op.view() { + if op.name() == "switch_case" { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + } + } + } + Some(_) => { + return Err(DAGCircuitError::new_err( + "Nodes in 'node_block' must be of type 'DAGOpNode'.", + )) + } + None => { + return Err(DAGCircuitError::new_err( + "Node in 'node_block' not found in DAG.", + )) + } + } + } + + let mut block_qargs: Vec = block_qargs + .into_iter() + .filter(|q| qubit_pos_map.contains_key(q)) + .collect(); + block_qargs.sort_by_key(|q| qubit_pos_map[q]); + + let mut block_cargs: Vec = block_cargs + .into_iter() + .filter(|c| clbit_pos_map.contains_key(c)) + .collect(); + block_cargs.sort_by_key(|c| clbit_pos_map[c]); + + let py_op = op.extract::()?; + + if py_op.operation.num_qubits() as usize != block_qargs.len() { + return Err(DAGCircuitError::new_err(format!( + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + ))); + } + + let op_name = py_op.operation.name().to_string(); + let qubits = self.qargs_interner.insert_owned(block_qargs); + let clbits = self.cargs_interner.insert_owned(block_cargs); + let weight = NodeType::Operation(PackedInstruction { + op: py_op.operation, + qubits, + clbits, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }); + + let new_node = self + .dag + .contract_nodes(block_ids.iter().copied(), weight, cycle_check) + .map_err(|e| match e { + ContractError::DAGWouldCycle => DAGCircuitError::new_err( + "Replacing the specified node block would introduce a cycle", + ), + })?; + + self.increment_op(op_name.as_str()); + for name in block_op_names { + self.decrement_op(name.as_str()); + } + Ok(new_node) + } + + /// Substitute a give node in the dag with a new operation from python + pub fn substitute_node_with_py_op( + &mut self, + py: Python, + node_index: NodeIndex, + op: &Bound, + propagate_condition: bool, + ) -> PyResult<()> { + // Extract information from node that is going to be replaced + let old_packed = self.dag[node_index].unwrap_operation(); + let op_name = old_packed.op.name().to_string(); + // Extract information from new op + let new_op = op.extract::()?; + let current_wires: HashSet = self + .dag + .edges(node_index) + .map(|e| e.weight().clone()) + .collect(); + let mut new_wires: HashSet = self + .qargs_interner + .get(old_packed.qubits) + .iter() + .map(|x| Wire::Qubit(*x)) + .chain( + self.cargs_interner + .get(old_packed.clbits) + .iter() + .map(|x| Wire::Clbit(*x)), + ) + .collect(); + let (additional_clbits, additional_vars) = + self.additional_wires(py, new_op.operation.view(), new_op.extra_attrs.condition())?; + new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); + new_wires.extend( + additional_vars + .iter() + .map(|x| Wire::Var(self.vars.find(x.bind(py)).unwrap())), + ); + + if old_packed.op.num_qubits() != new_op.operation.num_qubits() + || old_packed.op.num_clbits() != new_op.operation.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() + ))); + } + + #[cfg(feature = "cache_pygates")] + let mut py_op_cache = Some(op.clone().unbind()); + + let mut extra_attrs = new_op.extra_attrs.clone(); + // If either operation is a control-flow operation, propagate_condition is ignored + if propagate_condition && !(old_packed.op.control_flow() || new_op.operation.control_flow()) + { + // if new_op has a condition, the condition can't be propagated from the old node + if new_op.extra_attrs.condition().is_some() { + return Err(DAGCircuitError::new_err( + "Cannot propagate a condition to an operation that already has one.", + )); + } + if let Some(old_condition) = old_packed.condition() { + if matches!(new_op.operation.view(), OperationRef::Operation(_)) { + return Err(DAGCircuitError::new_err( + "Cannot add a condition on a generic Operation.", + )); + } + extra_attrs.set_condition(Some(old_condition.clone_ref(py))); + + let binding = self + .control_flow_module + .condition_resources(old_condition.bind(py))?; + let condition_clbits = binding.clbits.bind(py); + for bit in condition_clbits { + new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); + } + let op_ref = new_op.operation.view(); + if let OperationRef::Instruction(inst) = op_ref { + inst.instruction + .bind(py) + .setattr(intern!(py, "condition"), old_condition)?; + } else if let OperationRef::Gate(gate) = op_ref { + gate.gate.bind(py).call_method1( + intern!(py, "c_if"), + old_condition.downcast_bound::(py)?, + )?; + } + #[cfg(feature = "cache_pygates")] + { + py_op_cache = None; + } + } + }; + if new_wires != current_wires { + // The new wires must be a non-strict subset of the current wires; if they add new + // wires, we'd not know where to cut the existing wire to insert the new dependency. + return Err(DAGCircuitError::new_err(format!( + "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires + ))); + } + let new_op_name = new_op.operation.name().to_string(); + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op.operation, + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!new_op.params.is_empty()).then(|| new_op.params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op_cache.map(OnceLock::from).unwrap_or_default(), + }); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(op_name.as_str()); + self.increment_op(new_op_name.as_str()); + Ok(()) + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this /// does not handle the full possibility of parameter values. -fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { +pub(crate) fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { Ok(match [phase, other] { [Param::Float(a), Param::Float(b)] => Param::Float(a + b), [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 6c3a2d15fbad..2fdfcdcbaef2 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -10,9 +10,9 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -#[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; use std::hash::Hasher; +#[cfg(feature = "cache_pygates")] +use std::sync::OnceLock; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::QUANTUM_CIRCUIT; @@ -241,7 +241,7 @@ impl DAGOpNode { instruction.operation = instruction.operation.py_deepcopy(py, None)?; #[cfg(feature = "cache_pygates")] { - instruction.py_op = OnceCell::new(); + instruction.py_op = OnceLock::new(); } } let base = PyClassInitializer::from(DAGNode { node: None }); @@ -293,7 +293,7 @@ impl DAGOpNode { params: self.instruction.params.clone(), extra_attrs: self.instruction.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), + py_op: OnceLock::new(), }) } diff --git a/crates/circuit/src/dot_utils.rs b/crates/circuit/src/dot_utils.rs index 8558535788a0..c31488e92c81 100644 --- a/crates/circuit/src/dot_utils.rs +++ b/crates/circuit/src/dot_utils.rs @@ -59,7 +59,7 @@ where let edge_weight = match edge.weight() { Wire::Qubit(qubit) => dag.qubits().get(*qubit).unwrap(), Wire::Clbit(clbit) => dag.clbits().get(*clbit).unwrap(), - Wire::Var(var) => var, + Wire::Var(var) => dag.vars().get(*var).unwrap(), }; writeln!( file, diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 6b04b8512fb0..c6eabf1064fe 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -19,6 +19,12 @@ use crate::util::{ }; pub static ONE_QUBIT_IDENTITY: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_ONE]]; +pub static TWO_QUBIT_IDENTITY: 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_ONE], +]; // Utility for generating static matrices for controlled gates with "n" control qubits. // Assumptions: diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 8705d52d1aa7..a4064d44b917 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,7 +17,7 @@ pub mod converters; pub mod dag_circuit; pub mod dag_node; mod dot_utils; -mod error; +pub mod error; pub mod gate_matrix; pub mod imports; pub mod interner; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 67edb3aa0adc..a7b147c7e371 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -27,7 +27,7 @@ use smallvec::{smallvec, SmallVec}; use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyTuple}; +use pyo3::types::{IntoPyDict, PyFloat, PyIterator, PyList, PyTuple}; use pyo3::{intern, IntoPy, Python}; #[derive(Clone, Debug)] @@ -705,6 +705,11 @@ impl StandardGate { self.num_qubits() } + #[getter] + pub fn get_num_ctrl_qubits(&self) -> u32 { + self.num_ctrl_qubits() + } + #[getter] pub fn get_num_clbits(&self) -> u32 { self.num_clbits() @@ -720,6 +725,24 @@ impl StandardGate { self.name() } + #[getter] + pub fn is_controlled_gate(&self) -> bool { + self.num_ctrl_qubits() > 0 + } + + #[getter] + pub fn get_gate_class(&self, py: Python) -> PyResult> { + get_std_gate_class(py, *self) + } + + #[staticmethod] + pub fn all_gates(py: Python) -> Bound { + PyList::new_bound( + py, + (0..STANDARD_GATE_SIZE as u8).map(::bytemuck::checked::cast::<_, Self>), + ) + } + pub fn __hash__(&self) -> isize { *self as isize } @@ -2282,7 +2305,7 @@ pub fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { .call_method1(py, intern!(py, "__rmul__"), (mult,)) .expect("Multiplication of Parameter expression by float failed."), ), - Param::Obj(_) => unreachable!(), + Param::Obj(_) => unreachable!("Unsupported multiplication of a Param::Obj."), } } @@ -2316,7 +2339,7 @@ pub fn add_param(param: &Param, summand: f64, py: Python) -> Param { } } -fn radd_param(param1: Param, param2: Param, py: Python) -> Param { +pub fn radd_param(param1: Param, param2: Param, py: Python) -> Param { match [param1, param2] { [Param::Float(theta), Param::Float(lambda)] => Param::Float(theta + lambda), [Param::ParameterExpression(theta), Param::ParameterExpression(lambda)] => { diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index af72b3226a7c..4da706280336 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -10,9 +10,9 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -#[cfg(feature = "cache_pygates")] -use std::cell::OnceCell; use std::ptr::NonNull; +#[cfg(feature = "cache_pygates")] +use std::sync::OnceLock; use pyo3::intern; use pyo3::prelude::*; @@ -504,17 +504,17 @@ pub struct PackedInstruction { pub extra_attrs: ExtraInstructionAttributes, #[cfg(feature = "cache_pygates")] - /// This is hidden in a `OnceCell` because it's just an on-demand cache; we don't create this - /// unless asked for it. A `OnceCell` of a non-null pointer type (like `Py`) is the same + /// This is hidden in a `OnceLock` because it's just an on-demand cache; we don't create this + /// unless asked for it. A `OnceLock` of a non-null pointer type (like `Py`) is the same /// size as a pointer and there are no runtime checks on access beyond the initialisation check, /// which is a simple null-pointer check. /// - /// WARNING: remember that `OnceCell`'s `get_or_init` method is no-reentrant, so the initialiser + /// WARNING: remember that `OnceLock`'s `get_or_init` method is no-reentrant, so the initialiser /// must not yield the GIL to Python space. We avoid using `GILOnceCell` here because it /// requires the GIL to even `get` (of course!), which makes implementing `Clone` hard for us. /// We can revisit once we're on PyO3 0.22+ and have been able to disable its `py-clone` /// feature. - pub py_op: OnceCell>, + pub py_op: OnceLock>, } impl PackedInstruction { @@ -581,7 +581,7 @@ impl PackedInstruction { } }; - // `OnceCell::get_or_init` and the non-stabilised `get_or_try_init`, which would otherwise + // `OnceLock::get_or_init` and the non-stabilised `get_or_try_init`, which would otherwise // be nice here are both non-reentrant. This is a problem if the init yields control to the // Python interpreter as this one does, since that can allow CPython to freeze the thread // and for another to attempt the initialisation. diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 560541455138..d8e59e04e51e 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -35,7 +35,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?; add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?; add_submodule(m, ::qiskit_accelerate::commutation_checker::commutation_checker, "commutation_checker")?; - add_submodule(m, ::qiskit_accelerate::convert_2q_block_matrix::convert_2q_block_matrix, "convert_2q_block_matrix")?; + add_submodule(m, ::qiskit_accelerate::consolidate_blocks::consolidate_blocks_mod, "consolidate_blocks")?; add_submodule(m, ::qiskit_accelerate::dense_layout::dense_layout, "dense_layout")?; add_submodule(m, ::qiskit_accelerate::equivalence::equivalence, "equivalence")?; add_submodule(m, ::qiskit_accelerate::error_map::error_map, "error_map")?; @@ -49,7 +49,9 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::nlayout::nlayout, "nlayout")?; add_submodule(m, ::qiskit_accelerate::optimize_1q_gates::optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, ::qiskit_accelerate::pauli_exp_val::pauli_expval, "pauli_expval")?; + add_submodule(m, ::qiskit_accelerate::high_level_synthesis::high_level_synthesis_mod, "high_level_synthesis")?; add_submodule(m, ::qiskit_accelerate::remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, "remove_diagonal_gates_before_measure")?; + add_submodule(m, ::qiskit_accelerate::remove_identity_equiv::remove_identity_equiv_mod, "remove_identity_equiv")?; add_submodule(m, ::qiskit_accelerate::results::results, "results")?; add_submodule(m, ::qiskit_accelerate::sabre::sabre, "sabre")?; add_submodule(m, ::qiskit_accelerate::sampled_exp_val::sampled_exp_val, "sampled_exp_val")?; @@ -60,6 +62,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::stochastic_swap::stochastic_swap, "stochastic_swap")?; add_submodule(m, ::qiskit_accelerate::synthesis::synthesis, "synthesis")?; add_submodule(m, ::qiskit_accelerate::target_transpiler::target, "target")?; + add_submodule(m, ::qiskit_accelerate::twirling::twirling, "twirling")?; add_submodule(m, ::qiskit_accelerate::two_qubit_decompose::two_qubit_decompose, "two_qubit_decompose")?; add_submodule(m, ::qiskit_accelerate::unitary_synthesis::unitary_synthesis, "unitary_synthesis")?; add_submodule(m, ::qiskit_accelerate::uc_gate::uc_gate, "uc_gate")?; diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 8e42f3f0701b..eb7b7137c7ab 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -13,5 +13,5 @@ doctest = false pyo3.workspace = true indexmap.workspace = true hashbrown.workspace = true -oq3_semantics = "0.6.0" +oq3_semantics = "0.7.0" ahash.workspace = true diff --git a/docs/conf.py b/docs/conf.py index 9ea64b61e73f..1c188de24bed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,9 +30,9 @@ author = "Qiskit Development Team" # The short X.Y version -version = "1.3" +version = "2.0" # The full version, including alpha/beta/rc tags -release = "1.3.0" +release = "2.0.0" language = "en" @@ -120,6 +120,14 @@ napoleon_google_docstring = True napoleon_numpy_docstring = False +# Autosummary generates stub filenames based on the import name. +# Sometimes, two distinct interfaces only differ in capitalization; this +# creates a problem on case-insensitive OS/filesystems like macOS. So, +# we manually avoid the clash by renaming one of the files. +autosummary_filename_map = { + "qiskit.circuit.library.iqp": "qiskit.circuit.library.iqp_function", +} + # ---------------------------------------------------------------------------------- # Doctest diff --git a/docs/release_notes.rst b/docs/release_notes.rst index fddd907195fc..37e1127c5d8b 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -10,4 +10,4 @@ Qiskit |version| release notes `:earliest-version:` should be set to the rc1 release for the current minor release series. For example, the stable/1.1 branch should set it to 1.1.0rc1. If on `main`, set to the prior minor version's rc1, like `1.0.0rc1`. .. release-notes:: - :earliest-version: 1.0.0rc1 + :earliest-version: 1.1.0rc1 diff --git a/pyproject.toml b/pyproject.toml index 326d28f2a273..bdabeda6f146 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,22 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "qft.line" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisLine" "qft.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:QFTSynthesisFull" "permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.hls_plugins:TokenSwapperSynthesisPermutation" +"ModularAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:ModularAdderSynthesisDefault" +"ModularAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:ModularAdderSynthesisC04" +"ModularAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:ModularAdderSynthesisV95" +"ModularAdder.qft_d00" = "qiskit.transpiler.passes.synthesis.hls_plugins:ModularAdderSynthesisD00" +"HalfAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisDefault" +"HalfAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisC04" +"HalfAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisV95" +"HalfAdder.qft_d00" = "qiskit.transpiler.passes.synthesis.hls_plugins:HalfAdderSynthesisD00" +"FullAdder.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" +"FullAdder.ripple_c04" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisC04" +"FullAdder.ripple_v95" = "qiskit.transpiler.passes.synthesis.hls_plugins:FullAdderSynthesisV95" +"Multiplier.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisR17" +"Multiplier.qft_r17" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisR17" +"Multiplier.cumulative_h18" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisH18" +"PauliEvolution.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:PauliEvolutionSynthesisDefault" +"PauliEvolution.rustiq" = "qiskit.transpiler.passes.synthesis.hls_plugins:PauliEvolutionSynthesisRustiq" [project.entry-points."qiskit.transpiler.init"] default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index f0bb29e76388..227cea215648 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -1.3.0 +2.0.0 diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 63e5a2a48a47..af543bea80c7 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -58,7 +58,6 @@ sys.modules["qiskit._accelerate.converters"] = _accelerate.converters sys.modules["qiskit._accelerate.basis"] = _accelerate.basis sys.modules["qiskit._accelerate.basis.basis_translator"] = _accelerate.basis.basis_translator -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.equivalence"] = _accelerate.equivalence sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map @@ -97,7 +96,9 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.commutation_cancellation"] = _accelerate.commutation_cancellation +sys.modules["qiskit._accelerate.consolidate_blocks"] = _accelerate.consolidate_blocks sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.synthesis.evolution"] = _accelerate.synthesis.evolution sys.modules["qiskit._accelerate.synthesis.multi_controlled"] = ( _accelerate.synthesis.multi_controlled ) @@ -106,6 +107,9 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling +sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis +sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 32365b2ecb94..f4d533b7a2f2 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -792,10 +792,10 @@ .. code-block:: text ┌─────────┐ ┌─────────┐ ┌─────────┐ - q_0: ┤ Rz(0.5) ├──■──┤ Rz(1.2) ├──■── q_0: ┤ Rz(1.7) ├ - └─────────┘┌─┴─┐└──┬───┬──┘┌─┴─┐ = └──┬───┬──┘ - q_1: ───────────┤ X ├───┤ X ├───┤ X ├ q_1: ───┤ X ├─── - └───┘ └───┘ └───┘ └───┘ + q_0: ┤ Rz(0.5) ├──■──┤ Rz(1.2) ├──■── q_0: ┤ Rz(1.7) ├ + └─────────┘┌─┴─┐└──┬───┬──┘┌─┴─┐ = └──┬───┬──┘ + q_1: ───────────┤ X ├───┤ X ├───┤ X ├ q_1: ───┤ X ├─── + └───┘ └───┘ └───┘ └───┘ Performing these optimizations are part of the transpiler, but the tools to investigate commutations are available in the :class:`CommutationChecker`. @@ -805,7 +805,7 @@ CommutationChecker - + .. _circuit-custom-gates: Creating custom instructions @@ -1051,6 +1051,24 @@ def __array__(self, dtype=None, copy=None): .. autofunction:: random_circuit .. currentmodule:: qiskit.circuit +Apply Pauli twirling to a circuit +--------------------------------- + +There are two primary types of noise when executing quantum circuits. The first is stochastic, +or incoherent, noise that is mainly due to the unwanted interaction between the quantum processor +and the external environment in which it resides. The second is known as coherent error, and these +errors arise due to imperfect control of a quantum system. This can be unwanted terms in a system +Hamiltonian, i.e. incorrect unitary evolution, or errors from incorrect temporal control of the +quantum system, which includes things like incorrect pulse-shapes for gates. + +Pauli twirling is a quantum error suppression technique that uses randomization to shape coherent +error into stochastic errors by combining the results from many random, but logically equivalent +circuits, together. Qiskit provides a function to apply Pauli twirling to a given circuit for +standard two qubit gates. For more details you can refer to the documentation of the function +below: + +.. autofunction:: qiskit.circuit.pauli_twirl_2q_gates + Exceptions ========== @@ -1296,3 +1314,4 @@ def __array__(self, dtype=None, copy=None): ) from .annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier +from .twirling import pauli_twirl_2q_gates diff --git a/qiskit/circuit/_standard_gates_commutations.py b/qiskit/circuit/_standard_gates_commutations.py index 9dc95b675db7..12899207b7f3 100644 --- a/qiskit/circuit/_standard_gates_commutations.py +++ b/qiskit/circuit/_standard_gates_commutations.py @@ -47,6 +47,7 @@ first cx. """ + standard_gates_commutations = { ("id", "id"): True, ("id", "sx"): True, @@ -70,6 +71,10 @@ ("id", "iswap"): True, ("id", "sxdg"): True, ("id", "tdg"): True, + ("id", "rxx"): True, + ("id", "ryy"): True, + ("id", "rzz"): True, + ("id", "rzx"): True, ("sx", "sx"): True, ("sx", "cx"): { (0,): False, @@ -109,6 +114,13 @@ ("sx", "iswap"): False, ("sx", "sxdg"): True, ("sx", "tdg"): False, + ("sx", "rxx"): True, + ("sx", "ryy"): False, + ("sx", "rzz"): False, + ("sx", "rzx"): { + (0,): False, + (1,): True, + }, ("x", "id"): True, ("x", "sx"): True, ("x", "x"): True, @@ -152,6 +164,13 @@ ("x", "tdg"): False, ("x", "y"): False, ("x", "z"): False, + ("x", "rxx"): True, + ("x", "ryy"): False, + ("x", "rzz"): False, + ("x", "rzx"): { + (0,): False, + (1,): True, + }, ("cx", "cx"): { (0, 1): True, (0, None): True, @@ -303,6 +322,31 @@ }, ("cx", "swap"): False, ("cx", "iswap"): False, + ("cx", "rxx"): { + (0, 1): False, + (0, None): False, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): True, + }, + ("cx", "ryy"): False, + ("cx", "rzz"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("cx", "rzx"): { + (0, 1): True, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): False, + (None, 1): True, + }, ("c3sx", "c3sx"): { (0, 1, 2, 3): True, (0, 1, 2, None): True, @@ -1029,6 +1073,10 @@ ("dcx", "csdg"): False, ("dcx", "swap"): False, ("dcx", "iswap"): False, + ("dcx", "rxx"): False, + ("dcx", "ryy"): False, + ("dcx", "rzz"): False, + ("dcx", "rzx"): False, ("ch", "cx"): { (0, 1): False, (0, None): True, @@ -1189,6 +1237,24 @@ }, ("ch", "swap"): False, ("ch", "iswap"): False, + ("ch", "rxx"): False, + ("ch", "ryy"): False, + ("ch", "rzz"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("ch", "rzx"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, ("cswap", "c3sx"): { (0, 1, 2): True, (0, 1, 3): False, @@ -1499,6 +1565,31 @@ }, ("csx", "swap"): False, ("csx", "iswap"): False, + ("csx", "rxx"): { + (0, 1): False, + (0, None): False, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): True, + }, + ("csx", "ryy"): False, + ("csx", "rzz"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("csx", "rzx"): { + (0, 1): True, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): False, + (None, 1): True, + }, ("cy", "c3sx"): { (0, 1): False, (0, 2): False, @@ -1635,6 +1726,31 @@ }, ("cy", "swap"): False, ("cy", "iswap"): False, + ("cy", "rxx"): False, + ("cy", "ryy"): { + (0, 1): False, + (0, None): False, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): True, + }, + ("cy", "rzz"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("cy", "rzx"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, ("cz", "c3sx"): { (0, 1): True, (0, 2): True, @@ -1750,6 +1866,17 @@ (None, 0): False, (None, 1): False, }, + ("cz", "rxx"): False, + ("cz", "ryy"): False, + ("cz", "rzz"): True, + ("cz", "rzx"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): False, + }, ("ccz", "c3sx"): { (0, 1, 2): True, (0, 1, 3): False, @@ -2000,6 +2127,10 @@ ("h", "tdg"): False, ("h", "y"): False, ("h", "z"): False, + ("h", "rxx"): False, + ("h", "ryy"): False, + ("h", "rzz"): False, + ("h", "rzx"): False, ("rccx", "c3sx"): { (0, 1, 2): False, (0, 1, 3): False, @@ -2479,6 +2610,24 @@ ("ecr", "csdg"): False, ("ecr", "swap"): False, ("ecr", "iswap"): False, + ("ecr", "rxx"): { + (0, 1): False, + (0, None): False, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): True, + }, + ("ecr", "ryy"): False, + ("ecr", "rzz"): False, + ("ecr", "rzx"): { + (0, 1): False, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): True, + }, ("s", "id"): True, ("s", "sx"): False, ("s", "x"): False, @@ -2540,6 +2689,13 @@ ("s", "tdg"): True, ("s", "y"): False, ("s", "z"): True, + ("s", "rxx"): False, + ("s", "ryy"): False, + ("s", "rzz"): True, + ("s", "rzx"): { + (0,): True, + (1,): False, + }, ("sdg", "cx"): { (0,): True, (1,): False, @@ -2594,6 +2750,13 @@ ("sdg", "iswap"): False, ("sdg", "sxdg"): False, ("sdg", "tdg"): True, + ("sdg", "rxx"): False, + ("sdg", "ryy"): False, + ("sdg", "rzz"): True, + ("sdg", "rzx"): { + (0,): True, + (1,): False, + }, ("cs", "cx"): { (0, 1): False, (0, None): True, @@ -2726,6 +2889,17 @@ (None, 0): False, (None, 1): False, }, + ("cs", "rxx"): False, + ("cs", "ryy"): False, + ("cs", "rzz"): True, + ("cs", "rzx"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): False, + (None, 0): True, + (None, 1): False, + }, ("csdg", "c3sx"): { (0, 1): True, (0, 2): True, @@ -3064,6 +3238,13 @@ ("sxdg", "swap"): False, ("sxdg", "iswap"): False, ("sxdg", "sxdg"): True, + ("sxdg", "rxx"): True, + ("sxdg", "ryy"): False, + ("sxdg", "rzz"): False, + ("sxdg", "rzx"): { + (0,): False, + (1,): True, + }, ("t", "id"): True, ("t", "sx"): False, ("t", "x"): False, @@ -3124,6 +3305,13 @@ ("t", "tdg"): True, ("t", "y"): False, ("t", "z"): True, + ("t", "rxx"): False, + ("t", "ryy"): False, + ("t", "rzz"): True, + ("t", "rzx"): { + (0,): True, + (1,): False, + }, ("tdg", "cx"): { (0,): True, (1,): False, @@ -3177,6 +3365,13 @@ ("tdg", "iswap"): False, ("tdg", "sxdg"): False, ("tdg", "tdg"): True, + ("tdg", "rxx"): False, + ("tdg", "ryy"): False, + ("tdg", "rzz"): True, + ("tdg", "rzx"): { + (0,): True, + (1,): False, + }, ("y", "id"): True, ("y", "sx"): False, ("y", "cx"): False, @@ -3204,6 +3399,10 @@ ("y", "tdg"): False, ("y", "y"): True, ("y", "z"): False, + ("y", "rxx"): False, + ("y", "ryy"): True, + ("y", "rzz"): False, + ("y", "rzx"): False, ("z", "id"): True, ("z", "sx"): False, ("z", "cx"): { @@ -3261,4 +3460,390 @@ ("z", "sxdg"): False, ("z", "tdg"): True, ("z", "z"): True, + ("z", "rxx"): False, + ("z", "ryy"): False, + ("z", "rzz"): True, + ("z", "rzx"): { + (0,): True, + (1,): False, + }, + ("rxx", "c3sx"): { + (0, 1): False, + (0, 2): False, + (0, 3): False, + (0, None): False, + (1, 0): False, + (1, 2): False, + (1, 3): False, + (1, None): False, + (2, 0): False, + (2, 1): False, + (2, 3): False, + (2, None): False, + (3, 0): False, + (3, 1): False, + (3, 2): False, + (3, None): True, + (None, 0): False, + (None, 1): False, + (None, 2): False, + (None, 3): True, + }, + ("rxx", "ccx"): { + (0, 1): False, + (0, 2): False, + (0, None): False, + (1, 0): False, + (1, 2): False, + (1, None): False, + (2, 0): False, + (2, 1): False, + (2, None): True, + (None, 0): False, + (None, 1): False, + (None, 2): True, + }, + ("rxx", "cswap"): { + (0, 1): False, + (0, 2): False, + (0, None): False, + (1, 0): False, + (1, 2): True, + (1, None): False, + (2, 0): False, + (2, 1): True, + (2, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + }, + ("rxx", "ccz"): False, + ("rxx", "rccx"): False, + ("rxx", "rcccx"): False, + ("rxx", "csdg"): False, + ("rxx", "swap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rxx", "iswap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rxx", "rxx"): True, + ("rxx", "ryy"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rxx", "rzz"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rxx", "rzx"): { + (0, 1): False, + (0, None): False, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): True, + }, + ("ryy", "c3sx"): False, + ("ryy", "ccx"): False, + ("ryy", "cswap"): { + (0, 1): False, + (0, 2): False, + (0, None): False, + (1, 0): False, + (1, 2): True, + (1, None): False, + (2, 0): False, + (2, 1): True, + (2, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + }, + ("ryy", "ccz"): False, + ("ryy", "rccx"): False, + ("ryy", "rcccx"): False, + ("ryy", "csdg"): False, + ("ryy", "swap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("ryy", "iswap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("ryy", "ryy"): True, + ("ryy", "rzz"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("ryy", "rzx"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rzz", "c3sx"): { + (0, 1): True, + (0, 2): True, + (0, 3): False, + (0, None): True, + (1, 0): True, + (1, 2): True, + (1, 3): False, + (1, None): True, + (2, 0): True, + (2, 1): True, + (2, 3): False, + (2, None): True, + (3, 0): False, + (3, 1): False, + (3, 2): False, + (3, None): False, + (None, 0): True, + (None, 1): True, + (None, 2): True, + (None, 3): False, + }, + ("rzz", "ccx"): { + (0, 1): True, + (0, 2): False, + (0, None): True, + (1, 0): True, + (1, 2): False, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, None): False, + (None, 0): True, + (None, 1): True, + (None, 2): False, + }, + ("rzz", "cswap"): { + (0, 1): False, + (0, 2): False, + (0, None): True, + (1, 0): False, + (1, 2): True, + (1, None): False, + (2, 0): False, + (2, 1): True, + (2, None): False, + (None, 0): True, + (None, 1): False, + (None, 2): False, + }, + ("rzz", "ccz"): True, + ("rzz", "rccx"): { + (0, 1): True, + (0, 2): False, + (0, None): True, + (1, 0): True, + (1, 2): False, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, None): False, + (None, 0): True, + (None, 1): True, + (None, 2): False, + }, + ("rzz", "rcccx"): { + (0, 1): True, + (0, 2): True, + (0, 3): False, + (0, None): True, + (1, 0): True, + (1, 2): True, + (1, 3): False, + (1, None): True, + (2, 0): True, + (2, 1): True, + (2, 3): False, + (2, None): True, + (3, 0): False, + (3, 1): False, + (3, 2): False, + (3, None): False, + (None, 0): True, + (None, 1): True, + (None, 2): True, + (None, 3): False, + }, + ("rzz", "csdg"): True, + ("rzz", "swap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rzz", "iswap"): { + (0, 1): True, + (0, None): False, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): False, + }, + ("rzz", "rzz"): True, + ("rzx", "c3sx"): { + (0, 1): False, + (0, 2): False, + (0, 3): True, + (0, None): True, + (1, 0): False, + (1, 2): False, + (1, 3): True, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, 3): True, + (2, None): True, + (3, 0): False, + (3, 1): False, + (3, 2): False, + (3, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + (None, 3): True, + }, + ("rzx", "ccx"): { + (0, 1): False, + (0, 2): True, + (0, None): True, + (1, 0): False, + (1, 2): True, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): True, + }, + ("rzx", "cswap"): { + (0, 1): False, + (0, 2): False, + (0, None): True, + (1, 0): False, + (1, 2): False, + (1, None): False, + (2, 0): False, + (2, 1): False, + (2, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + }, + ("rzx", "ccz"): { + (0, 1): False, + (0, 2): False, + (0, None): True, + (1, 0): False, + (1, 2): False, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, None): True, + (None, 0): False, + (None, 1): False, + (None, 2): False, + }, + ("rzx", "rccx"): { + (0, 1): False, + (0, 2): False, + (0, None): True, + (1, 0): False, + (1, 2): False, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + }, + ("rzx", "rcccx"): { + (0, 1): False, + (0, 2): False, + (0, 3): False, + (0, None): True, + (1, 0): False, + (1, 2): False, + (1, 3): False, + (1, None): True, + (2, 0): False, + (2, 1): False, + (2, 3): False, + (2, None): True, + (3, 0): False, + (3, 1): False, + (3, 2): False, + (3, None): False, + (None, 0): False, + (None, 1): False, + (None, 2): False, + (None, 3): False, + }, + ("rzx", "csdg"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("rzx", "swap"): False, + ("rzx", "iswap"): False, + ("rzx", "rzz"): { + (0, 1): False, + (0, None): True, + (1, 0): False, + (1, None): True, + (None, 0): False, + (None, 1): False, + }, + ("rzx", "rzx"): { + (0, 1): True, + (0, None): True, + (1, 0): True, + (1, None): False, + (None, 0): False, + (None, 1): True, + }, } diff --git a/qiskit/circuit/barrier.py b/qiskit/circuit/barrier.py index c339066b4dce..d04769b68d39 100644 --- a/qiskit/circuit/barrier.py +++ b/qiskit/circuit/barrier.py @@ -19,6 +19,7 @@ from __future__ import annotations from qiskit.exceptions import QiskitError +from qiskit.utils import deprecate_func from .instruction import Instruction @@ -44,5 +45,6 @@ def inverse(self, annotated: bool = False): """Special case. Return self.""" return Barrier(self.num_qubits) + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical, val): raise QiskitError("Barriers are compiler directives and cannot be conditional.") diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index ab464a50ca67..458fabec49ef 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -284,7 +284,7 @@ def _copy_mutable_properties(self, instruction: Instruction) -> Instruction: The same instruction instance that was passed, but mutated to propagate the tracked changes to this class. """ - instruction.condition = self.condition + instruction._condition = self._condition return instruction # Provide some better error messages, just in case something goes wrong during development and @@ -639,8 +639,8 @@ def update_registers(index, op): # a register is already present, so we use our own tracking. self.add_register(register) out.add_register(register) - if getattr(op, "condition", None) is not None: - for register in condition_resources(op.condition).cregs: + if getattr(op, "_condition", None) is not None: + for register in condition_resources(op._condition).cregs: if register not in self.registers: self.add_register(register) out.add_register(register) diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index dd639c65f4b5..f5cdc9a2e1ae 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -87,12 +87,20 @@ def __init__( super().__init__("if_else", num_qubits, num_clbits, [true_body, false_body], label=label) - self.condition = validate_condition(condition) + self._condition = validate_condition(condition) @property def params(self): return self._params + @property + def condition(self): + return self._condition + + @condition.setter + def condition(self, value): + self._condition = value + @params.setter def params(self, parameters): # pylint: disable=cyclic-import @@ -152,7 +160,7 @@ def replace_blocks(self, blocks: Iterable[QuantumCircuit]) -> "IfElseOp": true_body, false_body = ( ablock for ablock, _ in itertools.zip_longest(blocks, range(2), fillvalue=None) ) - return IfElseOp(self.condition, true_body, false_body=false_body, label=self.label) + return IfElseOp(self._condition, true_body, false_body=false_body, label=self.label) def c_if(self, classical, val): raise NotImplementedError( @@ -200,7 +208,7 @@ def __init__( "if_else", len(self.__resources.qubits), len(self.__resources.clbits), [], label=label ) # Set the condition after super().__init__() has initialized it to None. - self.condition = validate_condition(condition) + self._condition = validate_condition(condition) def with_false_block(self, false_block: ControlFlowBuilderBlock) -> "IfElsePlaceholder": """Return a new placeholder instruction, with the false block set to the given value, @@ -225,7 +233,7 @@ def with_false_block(self, false_block: ControlFlowBuilderBlock) -> "IfElsePlace false_bits = false_block.qubits() | false_block.clbits() true_block.add_bits(false_bits - true_bits) false_block.add_bits(true_bits - false_bits) - return type(self)(self.condition, true_block, false_block, label=self.label) + return type(self)(self._condition, true_block, false_block, label=self.label) def registers(self): """Get the registers used by the interior blocks.""" @@ -288,7 +296,7 @@ def concrete_instruction(self, qubits, clbits): ) return ( self._copy_mutable_properties( - IfElseOp(self.condition, true_body, false_body, label=self.label) + IfElseOp(self._condition, true_body, false_body, label=self.label) ), InstructionResources( qubits=tuple(true_body.qubits), diff --git a/qiskit/circuit/controlflow/while_loop.py b/qiskit/circuit/controlflow/while_loop.py index 488a7a8b7025..7dcc2c22f6ee 100644 --- a/qiskit/circuit/controlflow/while_loop.py +++ b/qiskit/circuit/controlflow/while_loop.py @@ -53,12 +53,20 @@ def __init__( num_clbits = body.num_clbits super().__init__("while_loop", num_qubits, num_clbits, [body], label=label) - self.condition = validate_condition(condition) + self._condition = validate_condition(condition) @property def params(self): return self._params + @property + def condition(self): + return self._condition + + @condition.setter + def condition(self, value): + self._condition = value + @params.setter def params(self, parameters): # pylint: disable=cyclic-import @@ -88,7 +96,7 @@ def blocks(self): def replace_blocks(self, blocks): (body,) = blocks - return WhileLoopOp(self.condition, body, label=self.label) + return WhileLoopOp(self._condition, body, label=self.label) def c_if(self, classical, val): raise NotImplementedError( diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 7dab3a6839f9..2f0613acb932 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit import _utils from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.utils import deprecate_func @_utils.with_gate_array(np.eye(2, dtype=complex)) @@ -46,6 +47,7 @@ def inverse(self, annotated: bool = False): """Special case. Return self.""" return self + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical, val): raise CircuitError("Conditional Delay is not yet implemented.") diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 2f1c09668252..122db0973124 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -89,7 +89,9 @@ def power(self, exponent: float, annotated: bool = False): from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate if not annotated: - return UnitaryGate(Operator(self).power(exponent), label=f"{self.name}^{exponent}") + return UnitaryGate( + Operator(self).power(exponent, assume_unitary=True), label=f"{self.name}^{exponent}" + ) else: return AnnotatedOperation(self, PowerModifier(exponent)) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index f2879e2be5cd..e1df54c2cb99 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -176,6 +176,7 @@ def to_mutable(self): return self.copy() @property + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0", is_property=True) def condition(self): """The classical condition on the instruction.""" return self._condition @@ -410,8 +411,8 @@ def _assemble(self): # Add condition parameters for assembler. This is needed to convert # to a qobj conditional instruction at assemble time and after # conversion will be deleted by the assembler. - if self.condition: - instruction._condition = self.condition + if self._condition: + instruction._condition = self._condition return instruction @property @@ -517,6 +518,7 @@ def inverse(self, annotated: bool = False): inverse_gate.definition = inverse_definition return inverse_gate + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical, val): """Set a classical equality condition on this instruction between the register or cbit ``classical`` and value ``val``. @@ -632,7 +634,7 @@ def repeat(self, n): qargs = tuple(qc.qubits) cargs = tuple(qc.clbits) base = self.copy() - if self.condition: + if self._condition: # Condition is handled on the outer instruction. base = base.to_mutable() base.condition = None @@ -640,18 +642,19 @@ def repeat(self, n): qc._append(CircuitInstruction(base, qargs, cargs)) instruction.definition = qc - if self.condition: - instruction = instruction.c_if(*self.condition) + if self._condition: + instruction = instruction.c_if(*self._condition) return instruction @property + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0", is_property=True) def condition_bits(self) -> List[Clbit]: """Get Clbits in condition.""" from qiskit.circuit.controlflow import condition_resources # pylint: disable=cyclic-import - if self.condition is None: + if self._condition is None: return [] - return list(condition_resources(self.condition).clbits) + return list(condition_resources(self._condition).clbits) @property def name(self): diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index cc8a050fd2b0..a3f06a90040c 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -20,6 +20,7 @@ from typing import Callable from qiskit.circuit.exceptions import CircuitError +from qiskit.utils import deprecate_func from .classicalregister import Clbit, ClassicalRegister from .operation import Operation from .quantumcircuitdata import CircuitInstruction @@ -105,6 +106,7 @@ def inverse(self, annotated: bool = False): ) return self + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "InstructionSet": """Set a classical equality condition on all the instructions in this set between the :obj:`.ClassicalRegister` or :obj:`.Clbit` ``classical`` and value ``val``. diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index fc01cb118535..969ccfc5e5a6 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -36,7 +36,11 @@ circuit.append(gate, [0, 1, 4, 2, 3]) circuit.draw('mpl') -The library is organized in several sections. +The library is organized in several sections. The function +:func:`.get_standard_gate_name_mapping` allows you to see the available standard gates and operations. + +.. autofunction:: get_standard_gate_name_mapping + Standard gates ============== @@ -128,6 +132,7 @@ ZGate GlobalPhaseGate + Standard Directives =================== @@ -156,12 +161,12 @@ :include-source: :nofigs: - from qiskit.circuit.library import Diagonal + from qiskit.circuit.library import DiagonalGate - diagonal = Diagonal([1, 1]) + diagonal = DiagonalGate([1, 1j]) print(diagonal.num_qubits) - diagonal = Diagonal([1, 1, 1, 1]) + diagonal = DiagonalGate([1, 1, 1, -1]) print(diagonal.num_qubits) .. code-block:: text @@ -215,9 +220,15 @@ :template: autosummary/class_no_inherited_members.rst AND + AndGate OR + OrGate XOR + BitwiseXorGate + random_bitwise_xor InnerProduct + InnerProductGate + Basis Change Circuits ===================== @@ -273,6 +284,9 @@ CDKMRippleCarryAdder VBERippleCarryAdder WeightedAdder + ModularAdderGate + HalfAdderGate + FullAdderGate Multipliers ----------- @@ -283,6 +297,7 @@ HRSCumulativeMultiplier RGQFTMultiplier + MultiplierGate Comparators ----------- @@ -314,16 +329,19 @@ Particular Quantum Circuits =========================== +The following gates and quantum circuits define specific +quantum circuits of interest: + .. autosummary:: :toctree: ../stubs/ :template: autosummary/class_no_inherited_members.rst FourierChecking GraphState + GraphStateGate HiddenLinearFunction IQP QuantumVolume - quantum_volume PhaseEstimation GroverOperator PhaseOracle @@ -331,10 +349,42 @@ HamiltonianGate UnitaryOverlap +For circuits that have a well-defined structure it is preferrable +to use the following functions to construct them: + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + fourier_checking + hidden_linear_function + iqp + random_iqp + quantum_volume + phase_estimation + grover_operator + unitary_overlap + N-local circuits ================ +The following functions return a parameterized :class:`.QuantumCircuit` to use as ansatz in +a broad set of variational quantum algorithms: + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + n_local + efficient_su2 + real_amplitudes + pauli_two_design + excitation_preserving + qaoa_ansatz + hamiltonian_variational_ansatz + evolved_operator_ansatz + These :class:`~qiskit.circuit.library.BlueprintCircuit` subclasses are used as parameterized models (a.k.a. ansatzes or variational forms) in variational algorithms. They are heavily used in near-term algorithms in e.g. Chemistry, Physics or Optimization. @@ -356,6 +406,17 @@ Data encoding circuits ====================== +The following functions return a parameterized :class:`.QuantumCircuit` to use as data +encoding circuits in a series of variational quantum algorithms: + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + pauli_feature_map + z_feature_map + zz_feature_map + These :class:`~qiskit.circuit.library.BlueprintCircuit` encode classical data in quantum states and are used as feature maps for classification. @@ -366,6 +427,17 @@ PauliFeatureMap ZFeatureMap ZZFeatureMap + + +Data preparation circuits +========================= + +The following operations are used for state preparation: + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + StatePreparation Initialize @@ -529,12 +601,21 @@ from .hamiltonian_gate import HamiltonianGate from .boolean_logic import ( AND, + AndGate, OR, + OrGate, XOR, + BitwiseXorGate, + random_bitwise_xor, InnerProduct, + InnerProductGate, ) from .basis_change import QFT, QFTGate from .arithmetic import ( + ModularAdderGate, + HalfAdderGate, + FullAdderGate, + MultiplierGate, FunctionalPauliRotations, LinearPauliRotations, PiecewiseLinearPauliRotations, @@ -554,13 +635,21 @@ ) from .n_local import ( + n_local, NLocal, TwoLocal, + pauli_two_design, PauliTwoDesign, + real_amplitudes, RealAmplitudes, + efficient_su2, EfficientSU2, + hamiltonian_variational_ansatz, + evolved_operator_ansatz, EvolvedOperatorAnsatz, + excitation_preserving, ExcitationPreserving, + qaoa_ansatz, QAOAAnsatz, ) from .data_preparation import ( @@ -574,11 +663,12 @@ Initialize, ) from .quantum_volume import QuantumVolume, quantum_volume -from .fourier_checking import FourierChecking -from .graph_state import GraphState -from .hidden_linear_function import HiddenLinearFunction -from .iqp import IQP -from .phase_estimation import PhaseEstimation -from .grover_operator import GroverOperator +from .fourier_checking import FourierChecking, fourier_checking +from .graph_state import GraphState, GraphStateGate +from .hidden_linear_function import HiddenLinearFunction, hidden_linear_function +from .iqp import IQP, iqp, random_iqp +from .phase_estimation import PhaseEstimation, phase_estimation +from .grover_operator import GroverOperator, grover_operator from .phase_oracle import PhaseOracle -from .overlap import UnitaryOverlap +from .overlap import UnitaryOverlap, unitary_overlap +from .standard_gates import get_standard_gate_name_mapping diff --git a/qiskit/circuit/library/arithmetic/__init__.py b/qiskit/circuit/library/arithmetic/__init__.py index 71c6cb7f5df5..ede35a9451e1 100644 --- a/qiskit/circuit/library/arithmetic/__init__.py +++ b/qiskit/circuit/library/arithmetic/__init__.py @@ -21,7 +21,14 @@ from .weighted_adder import WeightedAdder from .quadratic_form import QuadraticForm from .linear_amplitude_function import LinearAmplitudeFunction -from .adders import VBERippleCarryAdder, CDKMRippleCarryAdder, DraperQFTAdder +from .adders import ( + VBERippleCarryAdder, + CDKMRippleCarryAdder, + DraperQFTAdder, + ModularAdderGate, + HalfAdderGate, + FullAdderGate, +) from .piecewise_chebyshev import PiecewiseChebyshev -from .multipliers import HRSCumulativeMultiplier, RGQFTMultiplier +from .multipliers import HRSCumulativeMultiplier, RGQFTMultiplier, MultiplierGate from .exact_reciprocal import ExactReciprocal diff --git a/qiskit/circuit/library/arithmetic/adders/__init__.py b/qiskit/circuit/library/arithmetic/adders/__init__.py index 1b53c707318d..03c042ab70c5 100644 --- a/qiskit/circuit/library/arithmetic/adders/__init__.py +++ b/qiskit/circuit/library/arithmetic/adders/__init__.py @@ -15,3 +15,4 @@ from .cdkm_ripple_carry_adder import CDKMRippleCarryAdder from .draper_qft_adder import DraperQFTAdder from .vbe_ripple_carry_adder import VBERippleCarryAdder +from .adder import ModularAdderGate, HalfAdderGate, FullAdderGate diff --git a/qiskit/circuit/library/arithmetic/adders/adder.py b/qiskit/circuit/library/arithmetic/adders/adder.py index 2e1a814d92cc..7fa3411d0436 100644 --- a/qiskit/circuit/library/arithmetic/adders/adder.py +++ b/qiskit/circuit/library/arithmetic/adders/adder.py @@ -12,13 +12,16 @@ """Compute the sum of two equally sized qubit registers.""" -from qiskit.circuit import QuantumCircuit +from __future__ import annotations + +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.utils.deprecation import deprecate_func class Adder(QuantumCircuit): r"""Compute the sum of two equally sized qubit registers. - For two registers :math:`|a\rangle_n` and :math:|b\rangle_n` with :math:`n` qubits each, an + For two registers :math:`|a\rangle_n` and :math:`|b\rangle_n` with :math:`n` qubits each, an adder performs the following operation .. math:: @@ -39,6 +42,16 @@ class Adder(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use the adder gates provided in qiskit.circuit.library.arithmetic instead. " + "The gate type depends on the adder kind: fixed, half, full are represented by " + "ModularAdderGate, HalfAdderGate, FullAdderGate, respectively. For different adder " + "implementations, see https://docs.quantum.ibm.com/api/qiskit/synthesis.", + ), + pending=True, + ) def __init__(self, num_state_qubits: int, name: str = "Adder") -> None: """ Args: @@ -56,3 +69,142 @@ def num_state_qubits(self) -> int: The number of state qubits. """ return self._num_state_qubits + + +class HalfAdderGate(Gate): + r"""Compute the sum of two equally-sized qubit registers, including a carry-out bit. + + For two registers :math:`|a\rangle_n` and :math:`|b\rangle_n` with :math:`n` qubits each, an + adder performs the following operation + + .. math:: + + |a\rangle_n |b\rangle_n \mapsto |a\rangle_n |a + b\rangle_{n + 1}. + + The quantum register :math:`|a\rangle_n` (and analogously :math:`|b\rangle_n`) + + .. math:: + + |a\rangle_n = |a_0\rangle \otimes \cdots \otimes |a_{n - 1}\rangle, + + for :math:`a_i \in \{0, 1\}`, is associated with the integer value + + .. math:: + + a = 2^{0}a_{0} + 2^{1}a_{1} + \cdots + 2^{n - 1}a_{n - 1}. + + """ + + def __init__(self, num_state_qubits: int, label: str | None = None) -> None: + """ + Args: + num_state_qubits: The number of qubits in each of the registers. + name: The name of the circuit. + """ + if num_state_qubits < 1: + raise ValueError("Need at least 1 state qubit.") + + super().__init__("HalfAdder", 2 * num_state_qubits + 1, [], label=label) + self._num_state_qubits = num_state_qubits + + @property + def num_state_qubits(self) -> int: + """The number of state qubits, i.e. the number of bits in each input register. + + Returns: + The number of state qubits. + """ + return self._num_state_qubits + + +class ModularAdderGate(Gate): + r"""Compute the sum modulo :math:`2^n` of two :math:`n`-sized qubit registers. + + For two registers :math:`|a\rangle_n` and :math:`|b\rangle_n` with :math:`n` qubits each, an + adder performs the following operation + + .. math:: + + |a\rangle_n |b\rangle_n \mapsto |a\rangle_n |a + b \text{ mod } 2^n\rangle_n. + + The quantum register :math:`|a\rangle_n` (and analogously :math:`|b\rangle_n`) + + .. math:: + + |a\rangle_n = |a_0\rangle \otimes \cdots \otimes |a_{n - 1}\rangle, + + for :math:`a_i \in \{0, 1\}`, is associated with the integer value + + .. math:: + + a = 2^{0}a_{0} + 2^{1}a_{1} + \cdots + 2^{n - 1}a_{n - 1}. + + """ + + def __init__(self, num_state_qubits: int, label: str | None = None) -> None: + """ + Args: + num_state_qubits: The number of qubits in each of the registers. + name: The name of the circuit. + """ + if num_state_qubits < 1: + raise ValueError("Need at least 1 state qubit.") + + super().__init__("ModularAdder", 2 * num_state_qubits, [], label=label) + self._num_state_qubits = num_state_qubits + + @property + def num_state_qubits(self) -> int: + """The number of state qubits, i.e. the number of bits in each input register. + + Returns: + The number of state qubits. + """ + return self._num_state_qubits + + +class FullAdderGate(Gate): + r"""Compute the sum of two :math:`n`-sized qubit registers, including carry-in and -out bits. + + For two registers :math:`|a\rangle_n` and :math:`|b\rangle_n` with :math:`n` qubits each, an + adder performs the following operation + + .. math:: + + |c_{\text{in}}\rangle_1 |a\rangle_n |b\rangle_n + \mapsto |a\rangle_n |c_{\text{in}} + a + b \rangle_{n + 1}. + + The quantum register :math:`|a\rangle_n` (and analogously :math:`|b\rangle_n`) + + .. math:: + + |a\rangle_n = |a_0\rangle \otimes \cdots \otimes |a_{n - 1}\rangle, + + for :math:`a_i \in \{0, 1\}`, is associated with the integer value + + .. math:: + + a = 2^{0}a_{0} + 2^{1}a_{1} + \cdots + 2^{n - 1}a_{n - 1}. + + """ + + def __init__(self, num_state_qubits: int, label: str | None = None) -> None: + """ + Args: + num_state_qubits: The number of qubits in each of the registers. + name: The name of the circuit. + """ + if num_state_qubits < 1: + raise ValueError("Need at least 1 state qubit.") + + super().__init__("FullAdder", 2 * num_state_qubits + 2, [], label=label) + self._num_state_qubits = num_state_qubits + + @property + def num_state_qubits(self) -> int: + """The number of state qubits, i.e. the number of bits in each input register. + + Returns: + The number of state qubits. + """ + return self._num_state_qubits diff --git a/qiskit/circuit/library/arithmetic/adders/cdkm_ripple_carry_adder.py b/qiskit/circuit/library/arithmetic/adders/cdkm_ripple_carry_adder.py index 3e18791e1cb6..7f610302adb8 100644 --- a/qiskit/circuit/library/arithmetic/adders/cdkm_ripple_carry_adder.py +++ b/qiskit/circuit/library/arithmetic/adders/cdkm_ripple_carry_adder.py @@ -12,8 +12,7 @@ """Compute the sum of two qubit registers using ripple-carry approach.""" -from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister - +from qiskit.synthesis.arithmetic import adder_ripple_c04 from .adder import Adder @@ -75,6 +74,21 @@ class CDKMRippleCarryAdder(Adder): It has one less qubit than the full-adder since it doesn't have the carry-out, but uses a helper qubit instead of the carry-in, so it only has one less qubit, not two. + .. seealso:: + + The following generic gate objects perform additions, like this circuit class, + but allow the compiler to select the optimal decomposition based on the context. + Specific implementations can be set via the :class:`.HLSConfig`, e.g. this circuit + can be chosen via ``Adder=["ripple_c04"]``. + + :class:`.ModularAdderGate`: A generic inplace adder, modulo :math:`2^n`. This + is functionally equivalent to ``kind="fixed"``. + + :class:`.AdderGate`: A generic inplace adder. This + is functionally equivalent to ``kind="half"``. + + :class:`.FullAdderGate`: A generic inplace adder, with a carry-in bit. This + is functionally equivalent to ``kind="full"``. **References:** @@ -102,58 +116,8 @@ def __init__( Raises: ValueError: If ``num_state_qubits`` is lower than 1. """ - if num_state_qubits < 1: - raise ValueError("The number of qubits must be at least 1.") - super().__init__(num_state_qubits, name=name) + circuit = adder_ripple_c04(num_state_qubits, kind) - if kind == "full": - qr_c = QuantumRegister(1, name="cin") - self.add_register(qr_c) - else: - qr_c = AncillaRegister(1, name="help") - - qr_a = QuantumRegister(num_state_qubits, name="a") - qr_b = QuantumRegister(num_state_qubits, name="b") - self.add_register(qr_a, qr_b) - - if kind in ["full", "half"]: - qr_z = QuantumRegister(1, name="cout") - self.add_register(qr_z) - - if kind != "full": - self.add_register(qr_c) - - # build carry circuit for majority of 3 bits in-place - # corresponds to MAJ gate in [1] - qc_maj = QuantumCircuit(3, name="MAJ") - qc_maj.cx(0, 1) - qc_maj.cx(0, 2) - qc_maj.ccx(2, 1, 0) - maj_gate = qc_maj.to_gate() - - # build circuit for reversing carry operation - # corresponds to UMA gate in [1] - qc_uma = QuantumCircuit(3, name="UMA") - qc_uma.ccx(2, 1, 0) - qc_uma.cx(0, 2) - qc_uma.cx(2, 1) - uma_gate = qc_uma.to_gate() - - circuit = QuantumCircuit(*self.qregs, name=name) - - # build ripple-carry adder circuit - circuit.append(maj_gate, [qr_a[0], qr_b[0], qr_c[0]]) - - for i in range(num_state_qubits - 1): - circuit.append(maj_gate, [qr_a[i + 1], qr_b[i + 1], qr_a[i]]) - - if kind in ["full", "half"]: - circuit.cx(qr_a[-1], qr_z[0]) - - for i in reversed(range(num_state_qubits - 1)): - circuit.append(uma_gate, [qr_a[i + 1], qr_b[i + 1], qr_a[i]]) - - circuit.append(uma_gate, [qr_a[0], qr_b[0], qr_c[0]]) - + self.add_register(*circuit.qregs) self.append(circuit.to_gate(), self.qubits) diff --git a/qiskit/circuit/library/arithmetic/adders/draper_qft_adder.py b/qiskit/circuit/library/arithmetic/adders/draper_qft_adder.py index c213b85c422f..62f8c88ffcdc 100644 --- a/qiskit/circuit/library/arithmetic/adders/draper_qft_adder.py +++ b/qiskit/circuit/library/arithmetic/adders/draper_qft_adder.py @@ -44,6 +44,19 @@ class DraperQFTAdder(Adder): cout_0: ┤2 ├────────────────────────■────────■───────┤2 ├ └──────┘ └───────┘ + .. seealso:: + + The following generic gate objects perform additions, like this circuit class, + but allow the compiler to select the optimal decomposition based on the context. + Specific implementations can be set via the :class:`.HLSConfig`, e.g. this + circuit can be chosen via ``Adder=["qft_d00"]``. + + :class:`.ModularAdderGate`: A generic inplace adder, modulo :math:`2^n`. This + is functionally equivalent to ``kind="fixed"``. + + :class:`.AdderGate`: A generic inplace adder. This + is functionally equivalent to ``kind="half"``. + **References:** [1] T. G. Draper, Addition on a Quantum Computer, 2000. diff --git a/qiskit/circuit/library/arithmetic/adders/vbe_ripple_carry_adder.py b/qiskit/circuit/library/arithmetic/adders/vbe_ripple_carry_adder.py index 0279738cb94f..2f8d9ed31e2c 100644 --- a/qiskit/circuit/library/arithmetic/adders/vbe_ripple_carry_adder.py +++ b/qiskit/circuit/library/arithmetic/adders/vbe_ripple_carry_adder.py @@ -11,11 +11,9 @@ # that they have been altered from the originals. """Compute the sum of two qubit registers using Classical Addition.""" -from __future__ import annotations -from qiskit.circuit.bit import Bit - -from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister +from __future__ import annotations +from qiskit.synthesis.arithmetic import adder_ripple_v95 from .adder import Adder @@ -52,6 +50,22 @@ class VBERippleCarryAdder(Adder): This is different ordering as compared to Figure 2 in [1], which leads to a different drawing of the circuit. + .. seealso:: + + The following generic gate objects perform additions, like this circuit class, + but allow the compiler to select the optimal decomposition based on the context. + Specific implementations can be set via the :class:`.HLSConfig`, e.g. this circuit + can be chosen via ``Adder=["ripple_v95"]``. + + :class:`.ModularAdderGate`: A generic inplace adder, modulo :math:`2^n`. This + is functionally equivalent to ``kind="fixed"``. + + :class:`.AdderGate`: A generic inplace adder. This + is functionally equivalent to ``kind="half"``. + + :class:`.FullAdderGate`: A generic inplace adder, with a carry-in bit. This + is functionally equivalent to ``kind="full"``. + **References:** [1] Vedral et al., Quantum Networks for Elementary Arithmetic Operations, 1995. @@ -74,92 +88,8 @@ def __init__( Raises: ValueError: If ``num_state_qubits`` is lower than 1. """ - if num_state_qubits < 1: - raise ValueError("The number of qubits must be at least 1.") - super().__init__(num_state_qubits, name=name) + circuit = adder_ripple_v95(num_state_qubits, kind) - # define the input registers - registers: list[QuantumRegister | list[Bit]] = [] - if kind == "full": - qr_cin = QuantumRegister(1, name="cin") - registers.append(qr_cin) - else: - qr_cin = QuantumRegister(0) - - qr_a = QuantumRegister(num_state_qubits, name="a") - qr_b = QuantumRegister(num_state_qubits, name="b") - - registers += [qr_a, qr_b] - - if kind in ["half", "full"]: - qr_cout = QuantumRegister(1, name="cout") - registers.append(qr_cout) - else: - qr_cout = QuantumRegister(0) - - self.add_register(*registers) - - if num_state_qubits > 1: - qr_help = AncillaRegister(num_state_qubits - 1, name="helper") - self.add_register(qr_help) - else: - qr_help = AncillaRegister(0) - - # the code is simplified a lot if we create a list of all carries and helpers - carries = qr_cin[:] + qr_help[:] + qr_cout[:] - - # corresponds to Carry gate in [1] - qc_carry = QuantumCircuit(4, name="Carry") - qc_carry.ccx(1, 2, 3) - qc_carry.cx(1, 2) - qc_carry.ccx(0, 2, 3) - carry_gate = qc_carry.to_gate() - carry_gate_dg = carry_gate.inverse() - - # corresponds to Sum gate in [1] - qc_sum = QuantumCircuit(3, name="Sum") - qc_sum.cx(1, 2) - qc_sum.cx(0, 2) - sum_gate = qc_sum.to_gate() - - circuit = QuantumCircuit(*self.qregs, name=name) - - # handle all cases for the first qubits, depending on whether cin is available - i = 0 - if kind == "half": - i += 1 - circuit.ccx(qr_a[0], qr_b[0], carries[0]) - elif kind == "fixed": - i += 1 - if num_state_qubits == 1: - circuit.cx(qr_a[0], qr_b[0]) - else: - circuit.ccx(qr_a[0], qr_b[0], carries[0]) - - for inp, out in zip(carries[:-1], carries[1:]): - circuit.append(carry_gate, [inp, qr_a[i], qr_b[i], out]) - i += 1 - - if kind in ["full", "half"]: # final CX (cancels for the 'fixed' case) - circuit.cx(qr_a[-1], qr_b[-1]) - - if len(carries) > 1: - circuit.append(sum_gate, [carries[-2], qr_a[-1], qr_b[-1]]) - - i -= 2 - for j, (inp, out) in enumerate(zip(reversed(carries[:-1]), reversed(carries[1:]))): - if j == 0: - if kind == "fixed": - i += 1 - else: - continue - circuit.append(carry_gate_dg, [inp, qr_a[i], qr_b[i], out]) - circuit.append(sum_gate, [inp, qr_a[i], qr_b[i]]) - i -= 1 - - if kind in ["half", "fixed"] and num_state_qubits > 1: - circuit.ccx(qr_a[0], qr_b[0], carries[0]) - circuit.cx(qr_a[0], qr_b[0]) - + self.add_register(*circuit.qregs) self.append(circuit.to_gate(), self.qubits) diff --git a/qiskit/circuit/library/arithmetic/multipliers/__init__.py b/qiskit/circuit/library/arithmetic/multipliers/__init__.py index 64a7edb0e7e0..c708d6d051fe 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/__init__.py +++ b/qiskit/circuit/library/arithmetic/multipliers/__init__.py @@ -14,3 +14,4 @@ from .hrs_cumulative_multiplier import HRSCumulativeMultiplier from .rg_qft_multiplier import RGQFTMultiplier +from .multiplier import MultiplierGate diff --git a/qiskit/circuit/library/arithmetic/multipliers/hrs_cumulative_multiplier.py b/qiskit/circuit/library/arithmetic/multipliers/hrs_cumulative_multiplier.py index ba9ed8c89cdc..3a0029d7e896 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/hrs_cumulative_multiplier.py +++ b/qiskit/circuit/library/arithmetic/multipliers/hrs_cumulative_multiplier.py @@ -59,6 +59,13 @@ class HRSCumulativeMultiplier(Multiplier): a series of shifted additions using one of the input registers while the qubits from the other input register act as control qubits for the adders. + .. seealso:: + + The :class:`.MultiplierGate` objects represents a multiplication, like this circuit class, + but allows the compiler to select the optimal decomposition based on the context. + Specific implementations can be set via the :class:`.HLSConfig`, e.g. this circuit + can be chosen via ``Multiplier=["cumulative_h18"]``. + **References:** [1] Häner et al., Optimizing Quantum Circuits for Arithmetic, 2018. diff --git a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py index a56240438829..4089cc35452a 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/multiplier.py +++ b/qiskit/circuit/library/arithmetic/multipliers/multiplier.py @@ -12,9 +12,10 @@ """Compute the product of two equally sized qubit registers.""" -from typing import Optional +from __future__ import annotations -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.utils.deprecation import deprecate_func class Multiplier(QuantumCircuit): @@ -45,10 +46,19 @@ class Multiplier(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use the MultiplierGate provided in qiskit.circuit.library.arithmetic instead. " + "For different multiplier implementations, see " + "https://docs.quantum.ibm.com/api/qiskit/synthesis.", + ), + pending=True, + ) def __init__( self, num_state_qubits: int, - num_result_qubits: Optional[int] = None, + num_result_qubits: int | None = None, name: str = "Multiplier", ) -> None: """ @@ -99,3 +109,84 @@ def num_result_qubits(self) -> int: The number of result qubits. """ return self._num_result_qubits + + +class MultiplierGate(Gate): + r"""Compute the product of two equally sized qubit registers into a new register. + + For two input registers :math:`|a\rangle_n`, :math:`|b\rangle_n` with :math:`n` qubits each + and an output register with :math:`2n` qubits, a multiplier performs the following operation + + .. math:: + + |a\rangle_n |b\rangle_n |0\rangle_{t} \mapsto |a\rangle_n |b\rangle_n |a \cdot b\rangle_t + + where :math:`t` is the number of bits used to represent the result. To completely store the result + of the multiplication without overflow we need :math:`t = 2n` bits. + + The quantum register :math:`|a\rangle_n` (analogously :math:`|b\rangle_n` and + output register) + + .. math:: + + |a\rangle_n = |a_0\rangle \otimes \cdots \otimes |a_{n - 1}\rangle, + + for :math:`a_i \in \{0, 1\}`, is associated with the integer value + + .. math:: + + a = 2^{0}a_{0} + 2^{1}a_{1} + \cdots + 2^{n - 1}a_{n - 1}. + + """ + + def __init__( + self, + num_state_qubits: int, + num_result_qubits: int | None = None, + label: str | None = None, + ) -> None: + """ + Args: + num_state_qubits: The number of qubits in each of the input registers. + num_result_qubits: The number of result qubits to limit the output to. + Default value is ``2 * num_state_qubits`` to represent any possible + result from the multiplication of the two inputs. + name: The name of the circuit. + Raises: + ValueError: If ``num_state_qubits`` is smaller than 1. + ValueError: If ``num_result_qubits`` is smaller than ``num_state_qubits``. + ValueError: If ``num_result_qubits`` is larger than ``2 * num_state_qubits``. + """ + if num_state_qubits < 1: + raise ValueError("The number of state qubits must be at least 1.") + + if num_result_qubits is None: + num_result_qubits = 2 * num_state_qubits + elif num_result_qubits < num_state_qubits or num_result_qubits > 2 * num_state_qubits: + raise ValueError( + f"num_result_qubits ({num_result_qubits}) must be in between num_state_qubits " + f"({num_state_qubits}) and 2 * num_state_qubits ({2 * num_state_qubits})" + ) + + super().__init__("Multiplier", 2 * num_state_qubits + num_result_qubits, [], label=label) + + self._num_state_qubits = num_state_qubits + self._num_result_qubits = num_result_qubits + + @property + def num_state_qubits(self) -> int: + """The number of state qubits, i.e. the number of bits in each input register. + + Returns: + The number of state qubits. + """ + return self._num_state_qubits + + @property + def num_result_qubits(self) -> int: + """The number of result qubits to limit the output to. + + Returns: + The number of result qubits. + """ + return self._num_result_qubits diff --git a/qiskit/circuit/library/arithmetic/multipliers/rg_qft_multiplier.py b/qiskit/circuit/library/arithmetic/multipliers/rg_qft_multiplier.py index 4bd2799733f2..ad213cd4dce5 100644 --- a/qiskit/circuit/library/arithmetic/multipliers/rg_qft_multiplier.py +++ b/qiskit/circuit/library/arithmetic/multipliers/rg_qft_multiplier.py @@ -48,6 +48,13 @@ class RGQFTMultiplier(Multiplier): out_1: ┤1 ├─────────■───────────────■──────────────■─────────────■───────┤1 ├ └──────┘ └───────┘ + .. seealso:: + + The :class:`.MultiplierGate` objects represents a multiplication, like this circuit class, + but allows the compiler to select the optimal decomposition based on the context. + Specific implementations can be set via the :class:`.HLSConfig`, e.g. this circuit + can be chosen via ``Multiplier=["qft_r17"]``. + **References:** [1] Ruiz-Perez et al., Quantum arithmetic with the Quantum Fourier Transform, 2017. diff --git a/qiskit/circuit/library/basis_change/qft.py b/qiskit/circuit/library/basis_change/qft.py index 15668ec51e11..cb14977b4463 100644 --- a/qiskit/circuit/library/basis_change/qft.py +++ b/qiskit/circuit/library/basis_change/qft.py @@ -13,10 +13,10 @@ """Define a Quantum Fourier Transform circuit (QFT) and a native gate (QFTGate).""" from __future__ import annotations -import warnings import numpy as np -from qiskit.circuit.quantumcircuit import QuantumCircuit, QuantumRegister, CircuitInstruction, Gate +from qiskit.circuit.quantumcircuit import QuantumRegister, CircuitInstruction, Gate +from qiskit.utils.deprecation import deprecate_func from ..blueprintcircuit import BlueprintCircuit @@ -72,6 +72,14 @@ class QFT(BlueprintCircuit): """ + @deprecate_func( + since="1.3", + additional_msg=( + "Use qiskit.circuit.library.QFTGate or qiskit.synthesis.qft.synth_qft_full instead, " + "for access to all previous arguments.", + ), + pending=True, + ) def __init__( self, num_qubits: int | None = None, @@ -232,22 +240,6 @@ def inverse(self, annotated: bool = False) -> "QFT": inverted._inverse = not self._inverse return inverted - def _warn_if_precision_loss(self): - """Issue a warning if constructing the circuit will lose precision. - - If we need an angle smaller than ``pi * 2**-1022``, we start to lose precision by going into - the subnormal numbers. We won't lose _all_ precision until an exponent of about 1075, but - beyond 1022 we're using fractional bits to represent leading zeros.""" - max_num_entanglements = self.num_qubits - self.approximation_degree - 1 - if max_num_entanglements > -np.finfo(float).minexp: # > 1022 for doubles. - warnings.warn( - "precision loss in QFT." - f" The rotation needed to represent {max_num_entanglements} entanglements" - " is smaller than the smallest normal floating-point number.", - category=RuntimeWarning, - stacklevel=3, - ) - def _check_configuration(self, raise_on_failure: bool = True) -> bool: """Check if the current configuration is valid.""" valid = True @@ -255,7 +247,6 @@ def _check_configuration(self, raise_on_failure: bool = True) -> bool: valid = False if raise_on_failure: raise AttributeError("The number of qubits has not been set.") - self._warn_if_precision_loss() return valid def _build(self) -> None: @@ -270,25 +261,16 @@ def _build(self) -> None: if num_qubits == 0: return - circuit = QuantumCircuit(*self.qregs, name=self.name) - for j in reversed(range(num_qubits)): - circuit.h(j) - num_entanglements = max(0, j - max(0, self.approximation_degree - (num_qubits - j - 1))) - for k in reversed(range(j - num_entanglements, j)): - # Use negative exponents so that the angle safely underflows to zero, rather than - # using a temporary variable that overflows to infinity in the worst case. - lam = np.pi * (2.0 ** (k - j)) - circuit.cp(lam, j, k) - - if self.insert_barriers: - circuit.barrier() - - if self._do_swaps: - for i in range(num_qubits // 2): - circuit.swap(i, num_qubits - i - 1) - - if self._inverse: - circuit = circuit.inverse() + from qiskit.synthesis.qft import synth_qft_full + + circuit = synth_qft_full( + num_qubits, + do_swaps=self._do_swaps, + insert_barriers=self._insert_barriers, + approximation_degree=self._approximation_degree, + inverse=self._inverse, + name=self.name, + ) wrapped = circuit.to_instruction() if self.insert_barriers else circuit.to_gate() self.compose(wrapped, qubits=self.qubits, inplace=True) diff --git a/qiskit/circuit/library/boolean_logic/__init__.py b/qiskit/circuit/library/boolean_logic/__init__.py index b273f0d6a245..736f983b05f9 100644 --- a/qiskit/circuit/library/boolean_logic/__init__.py +++ b/qiskit/circuit/library/boolean_logic/__init__.py @@ -12,7 +12,7 @@ """The Boolean logic circuit library.""" -from .quantum_and import AND -from .quantum_or import OR -from .quantum_xor import XOR -from .inner_product import InnerProduct +from .quantum_and import AND, AndGate +from .quantum_or import OR, OrGate +from .quantum_xor import XOR, BitwiseXorGate, random_bitwise_xor +from .inner_product import InnerProduct, InnerProductGate diff --git a/qiskit/circuit/library/boolean_logic/inner_product.py b/qiskit/circuit/library/boolean_logic/inner_product.py index dcaced069119..84b3807fb56c 100644 --- a/qiskit/circuit/library/boolean_logic/inner_product.py +++ b/qiskit/circuit/library/boolean_logic/inner_product.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 @@ -11,10 +11,11 @@ # that they have been altered from the originals. -"""InnerProduct circuit.""" +"""InnerProduct circuit and gate.""" -from qiskit.circuit import QuantumRegister, QuantumCircuit +from qiskit.circuit import QuantumRegister, QuantumCircuit, Gate +from qiskit.utils.deprecation import deprecate_func class InnerProduct(QuantumCircuit): @@ -61,6 +62,11 @@ class InnerProduct(QuantumCircuit): _generate_circuit_library_visualization(circuit) """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.InnerProductGate instead.", + pending=True, + ) def __init__(self, num_qubits: int) -> None: """Return a circuit to compute the inner product of 2 n-qubit registers. @@ -76,3 +82,74 @@ def __init__(self, num_qubits: int) -> None: super().__init__(*inner.qregs, name="inner_product") self.compose(inner.to_gate(), qubits=self.qubits, inplace=True) + + +class InnerProductGate(Gate): + r"""A 2n-qubit Boolean function that computes the inner product of + two n-qubit vectors over :math:`F_2`. + + This implementation is a phase oracle which computes the following transform. + + .. math:: + + \mathcal{IP}_{2n} : F_2^{2n} \rightarrow {-1, 1} + \mathcal{IP}_{2n}(x_1, \cdots, x_n, y_1, \cdots, y_n) = (-1)^{x.y} + + The corresponding unitary is a diagonal, which induces a -1 phase on any inputs + where the inner product of the top and bottom registers is 1. Otherwise, it keeps + the input intact. + + .. parsed-literal:: + + + q0_0: ─■────────── + │ + q0_1: ─┼──■─────── + │ │ + q0_2: ─┼──┼──■──── + │ │ │ + q0_3: ─┼──┼──┼──■─ + │ │ │ │ + q1_0: ─■──┼──┼──┼─ + │ │ │ + q1_1: ────■──┼──┼─ + │ │ + q1_2: ───────■──┼─ + │ + q1_3: ──────────■─ + + + Reference Circuit: + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import InnerProductGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(8) + circuit.append(InnerProductGate(4), [0, 1, 2, 3, 4, 5, 6, 7]) + _generate_circuit_library_visualization(circuit) + """ + + def __init__( + self, + num_qubits: int, + ) -> None: + """ + Args: + num_qubits: width of top and bottom registers (half total number of qubits). + """ + super().__init__("inner_product", 2 * num_qubits, []) + + def _define(self): + num_qubits = self.num_qubits // 2 + qr_a = QuantumRegister(num_qubits, name="x") + qr_b = QuantumRegister(num_qubits, name="y") + + circuit = QuantumCircuit(qr_a, qr_b, name="inner_product") + for i in range(num_qubits): + circuit.cz(qr_a[i], qr_b[i]) + + self.definition = circuit + + def __eq__(self, other): + return isinstance(other, InnerProductGate) and self.num_qubits == other.num_qubits diff --git a/qiskit/circuit/library/boolean_logic/quantum_and.py b/qiskit/circuit/library/boolean_logic/quantum_and.py index ce2d253ca85a..53099aa7be84 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_and.py +++ b/qiskit/circuit/library/boolean_logic/quantum_and.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 @@ -11,11 +11,13 @@ # that they have been altered from the originals. -"""Implementations of boolean logic quantum circuits.""" +"""Boolean AND circuit and gate.""" + from __future__ import annotations -from qiskit.circuit import QuantumRegister, QuantumCircuit, AncillaRegister +from qiskit.circuit import QuantumRegister, QuantumCircuit, AncillaRegister, Gate from qiskit.circuit.library.standard_gates import MCXGate +from qiskit.utils.deprecation import deprecate_func class AND(QuantumCircuit): @@ -49,6 +51,11 @@ class AND(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.AndGate instead.", + pending=True, + ) def __init__( self, num_variable_qubits: int, @@ -58,7 +65,7 @@ def __init__( """Create a new logical AND circuit. Args: - num_variable_qubits: The qubits of which the OR is computed. The result will be written + num_variable_qubits: The qubits of which the AND is computed. The result will be written into an additional result qubit. flags: A list of +1/0/-1 marking negations or omissions of qubits. mcx_mode: The mode to be used to implement the multi-controlled X gate. @@ -95,3 +102,99 @@ def __init__( super().__init__(*circuit.qregs, name="and") self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + + +class AndGate(Gate): + r"""A gate representing the logical AND operation on a number of qubits. + + For the AND operation the state :math:`|1\rangle` is interpreted as ``True``. The result + qubit is flipped, if the state of all variable qubits is ``True``. In this format, the AND + operation equals a multi-controlled X gate, which is controlled on all variable qubits. + Using a list of flags however, qubits can be skipped or negated. Practically, the flags + allow to skip controls or to apply pre- and post-X gates to the negated qubits. + + The AndGate gate without special flags equals the multi-controlled-X gate: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import AndGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(6) + circuit.append(AndGate(5), [0, 1, 2, 3, 4, 5]) + _generate_circuit_library_visualization(circuit) + + Using flags we can negate qubits or skip them. For instance, if we have 5 qubits and want to + return ``True`` if the first qubit is ``False`` and the last two are ``True`` we use the flags + ``[-1, 0, 0, 1, 1]``. + + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import AndGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(6) + circuit.append(AndGate(5, flags=[-1, 0, 0, 1, 1]), [0, 1, 2, 3, 4, 5]) + _generate_circuit_library_visualization(circuit) + + """ + + def __init__( + self, + num_variable_qubits: int, + flags: list[int] | None = None, + ) -> None: + """ + Args: + num_variable_qubits: The qubits of which the AND is computed. The result will be written + into an additional result qubit. + flags: A list of +1/0/-1 marking negations or omissions of qubits. + """ + super().__init__("and", num_variable_qubits + 1, []) + self.num_variable_qubits = num_variable_qubits + self.flags = flags + + def _define(self): + # add registers + qr_variable = QuantumRegister(self.num_variable_qubits, name="variable") + qr_result = QuantumRegister(1, name="result") + + # determine the control qubits: all that have a nonzero flag + flags = self.flags or [1] * self.num_variable_qubits + control_qubits = [q for q, flag in zip(qr_variable, flags) if flag != 0] + + # determine the qubits that need to be flipped (if a flag is < 0) + flip_qubits = [q for q, flag in zip(qr_variable, flags) if flag < 0] + + # create the definition circuit + circuit = QuantumCircuit(qr_variable, qr_result, name="and") + + if len(flip_qubits) > 0: + circuit.x(flip_qubits) + circuit.mcx(control_qubits, qr_result[:]) + if len(flip_qubits) > 0: + circuit.x(flip_qubits) + + self.definition = circuit + + # pylint: disable=unused-argument + def inverse(self, annotated: bool = False): + r"""Return inverted AND gate (itself). + + Args: + annotated: when set to ``True``, this is typically used to return an + :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete + :class:`.Gate`. However, for this class this argument is ignored as this gate + is self-inverse. + + Returns: + AndGate: inverse gate (self-inverse). + """ + return AndGate(self.num_variable_qubits, self.flags) + + def __eq__(self, other): + return ( + isinstance(other, AndGate) + and self.num_variable_qubits == other.num_variable_qubits + and self.flags == other.flags + ) diff --git a/qiskit/circuit/library/boolean_logic/quantum_or.py b/qiskit/circuit/library/boolean_logic/quantum_or.py index 91d6c4fbdd52..95b346b34846 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_or.py +++ b/qiskit/circuit/library/boolean_logic/quantum_or.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 @@ -11,12 +11,14 @@ # that they have been altered from the originals. -"""Implementations of boolean logic quantum circuits.""" +"""Boolean OR circuit and gate.""" + from __future__ import annotations from typing import List, Optional -from qiskit.circuit import QuantumRegister, QuantumCircuit, AncillaRegister +from qiskit.circuit import QuantumRegister, QuantumCircuit, AncillaRegister, Gate from qiskit.circuit.library.standard_gates import MCXGate +from qiskit.utils.deprecation import deprecate_func class OR(QuantumCircuit): @@ -50,6 +52,11 @@ class OR(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.OrGate instead.", + pending=True, + ) def __init__( self, num_variable_qubits: int, @@ -96,3 +103,100 @@ def __init__( super().__init__(*circuit.qregs, name="or") self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + + +class OrGate(Gate): + r"""A gate representing the logical OR operation on a number of qubits. + + For the OR operation the state :math:`|1\rangle` is interpreted as ``True``. The result + qubit is flipped, if the state of any variable qubit is ``True``. The OR is implemented using + a multi-open-controlled X gate (i.e. flips if the state is :math:`|0\rangle`) and + applying an X gate on the result qubit. + Using a list of flags, qubits can be skipped or negated. + + The OrGate gate without special flags: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import OrGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(6) + circuit.append(OrGate(5), [0, 1, 2, 3, 4, 5]) + _generate_circuit_library_visualization(circuit) + + Using flags we can negate qubits or skip them. For instance, if we have 5 qubits and want to + return ``True`` if the first qubit is ``False`` or one of the last two are ``True`` we use the + flags ``[-1, 0, 0, 1, 1]``. + + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import OrGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(6) + circuit.append(OrGate(5, flags=[-1, 0, 0, 1, 1]), [0, 1, 2, 3, 4, 5]) + _generate_circuit_library_visualization(circuit) + + """ + + def __init__( + self, + num_variable_qubits: int, + flags: list[int] | None = None, + ) -> None: + """ + Args: + num_variable_qubits: The qubits of which the AND is computed. The result will be written + into an additional result qubit. + flags: A list of +1/0/-1 marking negations or omissions of qubits. + """ + super().__init__("and", num_variable_qubits + 1, []) + self.num_variable_qubits = num_variable_qubits + self.flags = flags + + def _define(self): + # add registers + qr_variable = QuantumRegister(self.num_variable_qubits, name="variable") + qr_result = QuantumRegister(1, name="result") + + # determine the control qubits: all that have a nonzero flag + flags = self.flags or [1] * self.num_variable_qubits + control_qubits = [q for q, flag in zip(qr_variable, flags) if flag != 0] + + # determine the qubits that need to be flipped (if a flag is > 0) + flip_qubits = [q for q, flag in zip(qr_variable, flags) if flag > 0] + + # create the definition circuit + circuit = QuantumCircuit(qr_variable, qr_result, name="or") + + circuit.x(qr_result) + if len(flip_qubits) > 0: + circuit.x(flip_qubits) + circuit.mcx(control_qubits, qr_result[:]) + if len(flip_qubits) > 0: + circuit.x(flip_qubits) + + self.definition = circuit + + # pylint: disable=unused-argument + def inverse(self, annotated: bool = False): + r"""Return inverted OR gate (itself). + + Args: + annotated: when set to ``True``, this is typically used to return an + :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete + :class:`.Gate`. However, for this class this argument is ignored as this gate + is self-inverse. + + Returns: + OrGate: inverse gate (self-inverse). + """ + return OrGate(self.num_variable_qubits, self.flags) + + def __eq__(self, other): + return ( + isinstance(other, OrGate) + and self.num_variable_qubits == other.num_variable_qubits + and self.flags == other.flags + ) diff --git a/qiskit/circuit/library/boolean_logic/quantum_xor.py b/qiskit/circuit/library/boolean_logic/quantum_xor.py index 5b230345137d..73a2178830bc 100644 --- a/qiskit/circuit/library/boolean_logic/quantum_xor.py +++ b/qiskit/circuit/library/boolean_logic/quantum_xor.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 @@ -11,13 +11,14 @@ # that they have been altered from the originals. -"""XOR circuit.""" +"""Bitwise XOR circuit and gate.""" from typing import Optional import numpy as np -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func class XOR(QuantumCircuit): @@ -28,6 +29,12 @@ class XOR(QuantumCircuit): This circuit can also represent addition by ``amount`` over the finite field GF(2). """ + @deprecate_func( + since="1.3", + additional_msg="Instead, for xor-ing with a specified amount, use BitwiseXorGate," + "and for xor-ing with a random amount, use random_bitwise_xor.", + pending=True, + ) def __init__( self, num_qubits: int, @@ -69,3 +76,90 @@ def __init__( super().__init__(*circuit.qregs, name="xor") self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + + +class BitwiseXorGate(Gate): + """An n-qubit gate for bitwise xor-ing the input with some integer ``amount``. + + The ``amount`` is xor-ed in bitstring form with the input. + + This gate can also represent addition by ``amount`` over the finite field GF(2). + + Reference Circuit: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import BitwiseXorGate + from qiskit.visualization.library import _generate_circuit_library_visualization + circuit = QuantumCircuit(5) + circuit.append(BitwiseXorGate(5, amount=12), [0, 1, 2, 3, 4]) + _generate_circuit_library_visualization(circuit) + + """ + + def __init__( + self, + num_qubits: int, + amount: int, + ) -> None: + """ + Args: + num_qubits: the width of circuit. + amount: the xor amount in decimal form. + + Raises: + CircuitError: if the xor bitstring exceeds available qubits. + """ + if len(bin(amount)[2:]) > num_qubits: + raise CircuitError("Bits in 'amount' exceed circuit width") + + super().__init__("xor", num_qubits, []) + self.amount = amount + + def _define(self): + circuit = QuantumCircuit(self.num_qubits, name="xor") + amount = self.amount + for i in range(self.num_qubits): + bit = amount & 1 + amount = amount >> 1 + if bit == 1: + circuit.x(i) + + self.definition = circuit + + def __eq__(self, other): + return ( + isinstance(other, BitwiseXorGate) + and self.num_qubits == other.num_qubits + and self.amount == other.amount + ) + + # pylint: disable=unused-argument + def inverse(self, annotated: bool = False): + r"""Return inverted BitwiseXorGate gate (itself). + + Args: + annotated: when set to ``True``, this is typically used to return an + :class:`.AnnotatedOperation` with an inverse modifier set instead of a concrete + :class:`.Gate`. However, for this class this argument is ignored as this gate + is self-inverse. + + Returns: + BitwiseXorGate: inverse gate (self-inverse). + """ + return BitwiseXorGate(self.num_qubits, self.amount) + + +def random_bitwise_xor(num_qubits: int, seed: int) -> BitwiseXorGate: + """ + Create a random BitwiseXorGate. + + Args: + num_qubits: the width of circuit. + seed: random seed in case a random xor is requested. + """ + + rng = np.random.default_rng(seed) + amount = rng.integers(0, 2**num_qubits) + return BitwiseXorGate(num_qubits, amount) diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index 8c04f37ca9b0..b511edd9513e 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -160,9 +160,9 @@ def pauli_feature_map( data_map_func=data_map_func, alpha=alpha, insert_barriers=insert_barriers, - ) + ), + name=name, ) - circuit.name = name return circuit @@ -585,7 +585,10 @@ def basis_change(circuit, inverse=False): if pauli == "X": circuit.h(i) elif pauli == "Y": - circuit.rx(-np.pi / 2 if inverse else np.pi / 2, i) + if inverse: + circuit.sxdg(i) + else: + circuit.sx(i) def cx_chain(circuit, inverse=False): num_cx = len(indices) - 1 diff --git a/qiskit/circuit/library/fourier_checking.py b/qiskit/circuit/library/fourier_checking.py index 9035fbc360d4..b5012b4beeef 100644 --- a/qiskit/circuit/library/fourier_checking.py +++ b/qiskit/circuit/library/fourier_checking.py @@ -12,13 +12,14 @@ """Fourier checking circuit.""" -from typing import List - +from collections.abc import Sequence import math + from qiskit.circuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func -from .generalized_gates.diagonal import Diagonal +from .generalized_gates.diagonal import Diagonal, DiagonalGate class FourierChecking(QuantumCircuit): @@ -52,7 +53,12 @@ class FourierChecking(QuantumCircuit): `arXiv:1411.5729 `_ """ - def __init__(self, f: List[int], g: List[int]) -> None: + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.fourier_checking instead.", + pending=True, + ) + def __init__(self, f: Sequence[int], g: Sequence[int]) -> None: """Create Fourier checking circuit. Args: @@ -81,17 +87,72 @@ def __init__(self, f: List[int], g: List[int]) -> None: "{1, -1}." ) - circuit = QuantumCircuit(num_qubits, name=f"fc: {f}, {g}") - + # This definition circuit is not replaced by the circuit produced by fourier_checking, + # as the latter produces a slightly different circuit, with DiagonalGates instead + # of Diagonal circuits. + circuit = QuantumCircuit(int(num_qubits), name=f"fc: {f}, {g}") circuit.h(circuit.qubits) - circuit.compose(Diagonal(f), inplace=True) - circuit.h(circuit.qubits) - circuit.compose(Diagonal(g), inplace=True) - circuit.h(circuit.qubits) - super().__init__(*circuit.qregs, name=circuit.name) self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + + +def fourier_checking(f: Sequence[int], g: Sequence[int]) -> QuantumCircuit: + """Fourier checking circuit. + + The circuit for the Fourier checking algorithm, introduced in [1], + involves a layer of Hadamards, the function :math:`f`, another layer of + Hadamards, the function :math:`g`, followed by a final layer of Hadamards. + The functions :math:`f` and :math:`g` are classical functions realized + as phase oracles (diagonal operators with {-1, 1} on the diagonal). + + The probability of observing the all-zeros string is :math:`p(f,g)`. + The algorithm solves the promise Fourier checking problem, + which decides if f is correlated with the Fourier transform + of g, by testing if :math:`p(f,g) <= 0.01` or :math:`p(f,g) >= 0.05`, + promised that one or the other of these is true. + + The functions :math:`f` and :math:`g` are currently implemented + from their truth tables but could be represented concisely and + implemented efficiently for special classes of functions. + + Fourier checking is a special case of :math:`k`-fold forrelation [2]. + + **Reference Circuit:** + + .. plot:: + :include-source: + + from qiskit.circuit.library import fourier_checking + circuit = fourier_checking([1, -1, -1, -1], [1, 1, -1, -1]) + circuit.draw('mpl') + + **Reference:** + + [1] S. Aaronson, BQP and the Polynomial Hierarchy, 2009 (Section 3.2). + `arXiv:0910.4698 `_ + + [2] S. Aaronson, A. Ambainis, Forrelation: a problem that + optimally separates quantum from classical computing, 2014. + `arXiv:1411.5729 `_ + """ + num_qubits = math.log2(len(f)) + + if len(f) != len(g) or num_qubits == 0 or not num_qubits.is_integer(): + raise CircuitError( + "The functions f and g must be given as truth " + "tables, each as a list of 2**n entries of " + "{1, -1}." + ) + num_qubits = int(num_qubits) + + circuit = QuantumCircuit(num_qubits, name=f"fc: {f}, {g}") + circuit.h(circuit.qubits) + circuit.append(DiagonalGate(f), range(num_qubits)) + circuit.h(circuit.qubits) + circuit.append(DiagonalGate(g), range(num_qubits)) + circuit.h(circuit.qubits) + return circuit diff --git a/qiskit/circuit/library/generalized_gates/diagonal.py b/qiskit/circuit/library/generalized_gates/diagonal.py index ba85157c4439..b478bb58e96b 100644 --- a/qiskit/circuit/library/generalized_gates/diagonal.py +++ b/qiskit/circuit/library/generalized_gates/diagonal.py @@ -23,6 +23,8 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier +from qiskit.utils.deprecation import deprecate_func from .ucrz import UCRZGate @@ -30,19 +32,28 @@ class Diagonal(QuantumCircuit): - r"""Diagonal circuit. + """Circuit implementing a diagonal transformation.""" - Circuit symbol: + @deprecate_func(since="1.3", additional_msg="Use DiagonalGate instead.", pending=True) + def __init__(self, diag: Sequence[complex]) -> None: + r""" + Args: + diag: List of the :math:`2^k` diagonal entries (for a diagonal gate on :math:`k` qubits). + + Raises: + CircuitError: if the list of the diagonal entries or the qubit list is in bad format; + if the number of diagonal entries is not :math:`2^k`, where :math:`k` denotes the + number of qubits. + """ + DiagonalGate._check_input(diag) + num_qubits = int(math.log2(len(diag))) + + super().__init__(num_qubits, name="Diagonal") + self.append(DiagonalGate(diag), self.qubits) - .. code-block:: text - ┌───────────┐ - q_0: ┤0 ├ - │ │ - q_1: ┤1 Diagonal ├ - │ │ - q_2: ┤2 ├ - └───────────┘ +class DiagonalGate(Gate): + r"""A generic diagonal quantum gate. Matrix form: @@ -80,30 +91,28 @@ class Diagonal(QuantumCircuit): def __init__(self, diag: Sequence[complex]) -> None: r""" Args: - diag: List of the :math:`2^k` diagonal entries (for a diagonal gate on :math:`k` qubits). - - Raises: - CircuitError: if the list of the diagonal entries or the qubit list is in bad format; - if the number of diagonal entries is not :math:`2^k`, where :math:`k` denotes the - number of qubits. + diag: list of the :math:`2^k` diagonal entries (for a diagonal gate on :math:`k` qubits). """ self._check_input(diag) num_qubits = int(math.log2(len(diag))) - circuit = QuantumCircuit(num_qubits, name="Diagonal") + super().__init__("diagonal", num_qubits, diag) + def _define(self): # Since the diagonal is a unitary, all its entries have absolute value # one and the diagonal is fully specified by the phases of its entries. - diag_phases = [cmath.phase(z) for z in diag] - n = len(diag) + diag_phases = [cmath.phase(z) for z in self.params] + n = len(diag_phases) + circuit = QuantumCircuit(self.num_qubits) + while n >= 2: angles_rz = [] for i in range(0, n, 2): diag_phases[i // 2], rz_angle = _extract_rz(diag_phases[i], diag_phases[i + 1]) angles_rz.append(rz_angle) num_act_qubits = int(math.log2(n)) - ctrl_qubits = list(range(num_qubits - num_act_qubits + 1, num_qubits)) - target_qubit = num_qubits - num_act_qubits + ctrl_qubits = list(range(self.num_qubits - num_act_qubits + 1, self.num_qubits)) + target_qubit = self.num_qubits - num_act_qubits ucrz = UCRZGate(angles_rz) circuit.append(ucrz, [target_qubit] + ctrl_qubits) @@ -111,36 +120,7 @@ def __init__(self, diag: Sequence[complex]) -> None: n //= 2 circuit.global_phase += diag_phases[0] - super().__init__(num_qubits, name="Diagonal") - self.append(circuit.to_gate(), self.qubits) - - @staticmethod - def _check_input(diag): - """Check if ``diag`` is in valid format.""" - if not isinstance(diag, (list, np.ndarray)): - raise CircuitError("Diagonal entries must be in a list or numpy array.") - num_qubits = math.log2(len(diag)) - if num_qubits < 1 or not num_qubits.is_integer(): - raise CircuitError("The number of diagonal entries is not a positive power of 2.") - if not np.allclose(np.abs(diag), 1, atol=_EPS): - raise CircuitError("A diagonal element does not have absolute value one.") - - -class DiagonalGate(Gate): - """Gate implementing a diagonal transformation.""" - - def __init__(self, diag: Sequence[complex]) -> None: - r""" - Args: - diag: list of the :math:`2^k` diagonal entries (for a diagonal gate on :math:`k` qubits). - """ - Diagonal._check_input(diag) - num_qubits = int(math.log2(len(diag))) - - super().__init__("diagonal", num_qubits, diag) - - def _define(self): - self.definition = Diagonal(self.params).decompose() + self.definition = circuit def validate_parameter(self, parameter): """Diagonal Gate parameter should accept complex @@ -152,8 +132,22 @@ def validate_parameter(self, parameter): def inverse(self, annotated: bool = False): """Return the inverse of the diagonal gate.""" + if annotated: + return AnnotatedOperation(self.copy(), InverseModifier) + return DiagonalGate([np.conj(entry) for entry in self.params]) + @staticmethod + def _check_input(diag): + """Check if ``diag`` is in valid format.""" + if not isinstance(diag, (list, np.ndarray)): + raise CircuitError("Diagonal entries must be in a list or numpy array.") + num_qubits = math.log2(len(diag)) + if num_qubits < 1 or not num_qubits.is_integer(): + raise CircuitError("The number of diagonal entries is not a positive power of 2.") + if not np.allclose(np.abs(diag), 1, atol=_EPS): + raise CircuitError("A diagonal element does not have absolute value one.") + def _extract_rz(phi1, phi2): """ diff --git a/qiskit/circuit/library/generalized_gates/gms.py b/qiskit/circuit/library/generalized_gates/gms.py index 964a99d18b2b..bdf01757abd0 100644 --- a/qiskit/circuit/library/generalized_gates/gms.py +++ b/qiskit/circuit/library/generalized_gates/gms.py @@ -15,13 +15,16 @@ Global Mølmer–Sørensen gate. """ -from typing import Union, List +from __future__ import annotations +from collections.abc import Sequence import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.library.standard_gates import RXXGate from qiskit.circuit.gate import Gate +from qiskit.utils.deprecation import deprecate_func class GMS(QuantumCircuit): @@ -74,7 +77,8 @@ class GMS(QuantumCircuit): `arXiv:1707.06356 `_ """ - def __init__(self, num_qubits: int, theta: Union[List[List[float]], np.ndarray]) -> None: + @deprecate_func(since="1.3", additional_msg="Use the MSGate instead.", pending=True) + def __init__(self, num_qubits: int, theta: list[list[float]] | np.ndarray) -> None: """Create a new Global Mølmer–Sørensen (GMS) gate. Args: @@ -94,28 +98,77 @@ def __init__(self, num_qubits: int, theta: Union[List[List[float]], np.ndarray]) class MSGate(Gate): - """MSGate has been deprecated. - Please use ``GMS`` in ``qiskit.circuit.generalized_gates`` instead. + r"""The Mølmer–Sørensen gate. - Global Mølmer–Sørensen gate. + The Mølmer–Sørensen gate is native to ion-trap systems. The global MS + can be applied to multiple ions to entangle multiple qubits simultaneously [1]. - The Mølmer–Sørensen gate is native to ion-trap systems. The global MS can be - applied to multiple ions to entangle multiple qubits simultaneously. + In the two-qubit case, this is equivalent to an XX interaction, + and is thus reduced to the :class:`.RXXGate`. The global MS gate is a sum of XX + interactions on all pairs [2]. - In the two-qubit case, this is equivalent to an XX(theta) interaction, - and is thus reduced to the RXXGate. + .. math:: + + MS(\chi_{12}, \chi_{13}, ..., \chi_{n-1 n}) = + exp(-i \sum_{i=1}^{n} \sum_{j=i+1}^{n} X{\otimes}X \frac{\chi_{ij}}{2}) + + Example:: + + import numpy as np + from qiskit.circuit.library import MSGate + from qiskit.quantum_info import Operator + + gate = MSGate(num_qubits=3, theta=[[0, np.pi/4, np.pi/8], + [0, 0, np.pi/2], + [0, 0, 0]]) + print(Operator(gate)) + + + **References:** + + [1] Sørensen, A. and Mølmer, K., Multi-particle entanglement of hot trapped ions. + Physical Review Letters. 82 (9): 1835–1838. + `arXiv:9810040 `_ + + [2] Maslov, D. and Nam, Y., Use of global interactions in efficient quantum circuit + constructions. New Journal of Physics, 20(3), p.033018. + `arXiv:1707.06356 `_ """ - def __init__(self, num_qubits, theta, label=None): - """Create new MS gate.""" + def __init__( + self, + num_qubits: int, + theta: ParameterValueType | Sequence[Sequence[ParameterValueType]], + label: str | None = None, + ): + """ + Args: + num_qubits: The number of qubits the MS gate acts on. + theta: The XX rotation angles. If a single value, the same angle is used on all + interactions. Alternatively an upper-triangular, square matrix with width + ``num_qubits`` can be provided with interaction angles for each qubit pair. + label: A gate label. + """ super().__init__("ms", num_qubits, [theta], label=label) def _define(self): - theta = self.params[0] - q = QuantumRegister(self.num_qubits, "q") + thetas = self.params[0] + q = QuantumRegister(self.num_qubits, name="q") qc = QuantumCircuit(q, name=self.name) for i in range(self.num_qubits): for j in range(i + 1, self.num_qubits): + # if theta is just a single angle, use that, otherwise use the correct index + theta = thetas if not isinstance(thetas, Sequence) else thetas[i][j] qc._append(RXXGate(theta), [q[i], q[j]], []) self.definition = qc + + def validate_parameter(self, parameter): + if isinstance(parameter, Sequence): + # pylint: disable=super-with-arguments + return [ + [super(MSGate, self).validate_parameter(theta) for theta in row] + for row in parameter + ] + + return super().validate_parameter(parameter) diff --git a/qiskit/circuit/library/generalized_gates/isometry.py b/qiskit/circuit/library/generalized_gates/isometry.py index e6b4f6fb21cc..6df08320fa9f 100644 --- a/qiskit/circuit/library/generalized_gates/isometry.py +++ b/qiskit/circuit/library/generalized_gates/isometry.py @@ -31,7 +31,7 @@ from qiskit.quantum_info.operators.predicates import is_isometry from qiskit._accelerate import isometry as isometry_rs -from .diagonal import Diagonal +from .diagonal import DiagonalGate from .uc import UCGate from .mcg_up_to_diagonal import MCGupDiag @@ -167,7 +167,7 @@ def _gates_to_uncompute(self): if len(diag) > 1 and not isometry_rs.diag_is_identity_up_to_global_phase( diag, self._epsilon ): - diagonal = Diagonal(np.conj(diag)) + diagonal = DiagonalGate(np.conj(diag)) circuit.append(diagonal, q_input) return circuit diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index 73b86bffb03a..90cee1b9e9be 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -17,6 +17,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.library.generalized_gates.permutation import PermutationGate +from qiskit.utils.deprecation import deprecate_func # pylint: disable=cyclic-import from qiskit.quantum_info import Clifford @@ -67,7 +68,7 @@ class LinearFunction(Gate): def __init__( self, linear: ( - list[list] + list[list[bool]] | np.ndarray[bool] | QuantumCircuit | LinearFunction @@ -221,17 +222,22 @@ def validate_parameter(self, parameter): def _define(self): """Populates self.definition with a decomposition of this gate.""" - self.definition = self.synthesize() + from qiskit.synthesis.linear import synth_cnot_count_full_pmh + + self.definition = synth_cnot_count_full_pmh(self.linear) + @deprecate_func( + since="1.3", + pending=True, + additional_msg="Call LinearFunction.definition instead, or compile the circuit.", + ) def synthesize(self): """Synthesizes the linear function into a quantum circuit. Returns: QuantumCircuit: A circuit implementing the evolution. """ - from qiskit.synthesis.linear import synth_cnot_count_full_pmh - - return synth_cnot_count_full_pmh(self.linear) + return self.definition @property def linear(self): diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index 285590962c8d..bea863f78242 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -22,11 +22,13 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumcircuit import Gate from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func class Permutation(QuantumCircuit): """An n_qubit circuit that permutes qubits.""" + @deprecate_func(since="1.3", pending=True, additional_msg="Use PermutationGate instead.") def __init__( self, num_qubits: int, @@ -117,11 +119,11 @@ def __init__( from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.library import PermutationGate - A = [2,4,3,0,1] + A = [2, 4, 3, 0, 1] permutation = PermutationGate(A) circuit = QuantumCircuit(5) circuit.append(permutation, [0, 1, 2, 3, 4]) - circuit.draw('mpl') + circuit.draw("mpl") Expanded Circuit: .. plot:: @@ -129,7 +131,7 @@ def __init__( from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.library import PermutationGate from qiskit.visualization.library import _generate_circuit_library_visualization - A = [2,4,3,0,1] + A = [2, 4, 3, 0, 1] permutation = PermutationGate(A) circuit = QuantumCircuit(5) circuit.append(permutation, [0, 1, 2, 3, 4]) @@ -168,11 +170,11 @@ def validate_parameter(self, parameter): return parameter @property - def pattern(self): + def pattern(self) -> np.ndarray[bool]: """Returns the permutation pattern defining this permutation.""" return self.params[0] - def inverse(self, annotated: bool = False): + def inverse(self, annotated: bool = False) -> PermutationGate: """Returns the inverse of the permutation.""" # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/generalized_gates/rv.py b/qiskit/circuit/library/generalized_gates/rv.py index 58f49c533138..55a203fddd33 100644 --- a/qiskit/circuit/library/generalized_gates/rv.py +++ b/qiskit/circuit/library/generalized_gates/rv.py @@ -51,14 +51,13 @@ class RVGate(Gate): \end{pmatrix} """ - def __init__(self, v_x, v_y, v_z, basis="U"): - """Create new rv single-qubit gate. - + def __init__(self, v_x: float, v_y: float, v_z: float, basis: str = "U"): + """ Args: - v_x (float): x-component - v_y (float): y-component - v_z (float): z-component - basis (str, optional): basis (see + v_x: x-component + v_y: y-component + v_z: z-component + basis: basis (see :class:`~qiskit.synthesis.one_qubit.one_qubit_decompose.OneQubitEulerDecomposer`) """ # pylint: disable=cyclic-import @@ -80,7 +79,7 @@ def inverse(self, annotated: bool = False): vx, vy, vz = self.params return RVGate(-vx, -vy, -vz) - def to_matrix(self): + def to_matrix(self) -> numpy.ndarray: """Return a numpy.array for the R(v) gate.""" v = numpy.asarray(self.params, dtype=float) angle = math.sqrt(v.dot(v)) diff --git a/qiskit/circuit/library/generalized_gates/uc.py b/qiskit/circuit/library/generalized_gates/uc.py index c81494da3eec..d9f4dbe9cf1f 100644 --- a/qiskit/circuit/library/generalized_gates/uc.py +++ b/qiskit/circuit/library/generalized_gates/uc.py @@ -60,14 +60,21 @@ class UCGate(Gate): The decomposition is based on Ref. [1]. + Unnecessary controls and repeated operators can be removed as described in Ref [2]. + **References:** [1] Bergholm et al., Quantum circuits with uniformly controlled one-qubit gates (2005). `Phys. Rev. A 71, 052330 `__. + [2] de Carvalho et al., Quantum multiplexer simplification for state preparation (2024). + `arXiv:2409.05618 `__. + """ - def __init__(self, gate_list: list[np.ndarray], up_to_diagonal: bool = False): + def __init__( + self, gate_list: list[np.ndarray], up_to_diagonal: bool = False, mux_simp: bool = True + ): r""" Args: gate_list: List of two qubit unitaries :math:`[U_0, ..., U_{2^{k-1}}]`, where each @@ -76,6 +83,9 @@ def __init__(self, gate_list: list[np.ndarray], up_to_diagonal: bool = False): or if it is decomposed completely (default: False). If the ``UCGate`` :math:`U` is decomposed up to a diagonal :math:`D`, this means that the circuit implements a unitary :math:`U'` such that :math:`D U' = U`. + mux_simp: Determines whether the search for repetitions is conducted (default: True). + The intention is to perform a possible simplification in the number of controls + and operators. Raises: QiskitError: in case of bad input to the constructor @@ -101,10 +111,67 @@ def __init__(self, gate_list: list[np.ndarray], up_to_diagonal: bool = False): if not is_unitary_matrix(gate, _EPS): raise QiskitError("A controlled gate is not unitary.") + new_controls = set() + if mux_simp: + new_controls, gate_list = self._simplify(gate_list, num_contr) + self.simp_contr = (mux_simp, new_controls) + # Create new gate. super().__init__("multiplexer", int(num_contr) + 1, gate_list) self.up_to_diagonal = up_to_diagonal + def _simplify(self, gate_list, num_contr): + """https://arxiv.org/abs/2409.05618""" + + c = set() + nc = set() + mux_copy = gate_list.copy() + + for i in range(int(num_contr)): + c.add(i + 1) + + if len(gate_list) > 1: + nc, mux_copy = self._repetition_search(gate_list, num_contr, mux_copy) + + new_controls = {x for x in c if x not in nc} + new_mux = [gate for gate in mux_copy if gate is not None] + return new_controls, new_mux + + def _repetition_search(self, mux, level, mux_copy): + nc = set() + d = 1 + while d <= len(mux) / 2: + disentanglement = False + if np.allclose(mux[d], mux[0]): + mux_org = mux_copy.copy() + repetitions = len(mux) / (2 * d) + p = 0 + while repetitions > 0: + repetitions -= 1 + valid, mux_copy = self._repetition_verify(p, d, mux, mux_copy) + p = p + 2 * d + if not valid: + mux_copy = mux_org + break + if repetitions == 0: + disentanglement = True + + if disentanglement: + removed_contr = level - math.log2(d) + nc.add(removed_contr) + d = 2 * d + return nc, mux_copy + + def _repetition_verify(self, base, d, mux, mux_copy): + i = 0 + next_base = base + d + while i < d: + if not np.allclose(mux[base], mux[next_base]): + return False, mux_copy + mux_copy[next_base] = None + base, next_base, i = base + 1, next_base + 1, i + 1 + return True, mux_copy + def inverse(self, annotated: bool = False) -> Gate: """Return the inverse. @@ -135,6 +202,19 @@ def _get_diagonal(self): # q[k-1],...,q[0],q_target, decreasingly ordered with respect to the # significance of the qubit in the computational basis _, diag = self._dec_ucg() + if self.simp_contr[1]: + q_controls = [self.num_qubits - i for i in self.simp_contr[1]] + q_controls.reverse() + for i in range(self.num_qubits): + if i not in [0] + q_controls: + d = 2**i + new_diag = [] + n = len(diag) + for j in range(n): + new_diag.append(diag[j]) + if (j + 1) % d == 0: + new_diag.extend(diag[j + 1 - d : j + 1]) + diag = np.array(new_diag) return diag def _define(self): @@ -149,12 +229,19 @@ def _dec_ucg(self): """ diag = np.ones(2**self.num_qubits).tolist() q = QuantumRegister(self.num_qubits, "q") - q_controls = q[1:] q_target = q[0] + mux_simplify = self.simp_contr[0] + + if mux_simplify: + q_controls = [q[self.num_qubits - i] for i in self.simp_contr[1]] + q_controls.reverse() + else: + q_controls = q[1:] + 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]) + circuit.unitary(self.params[0], q[0]) return circuit, diag # If there is at least one control, first, # we find the single qubit gates of the decomposition. @@ -190,7 +277,8 @@ def _dec_ucg(self): # q[k-1],...,q[0],q_target (ordered with decreasing significance), # where q[i] are the control qubits and t denotes the target qubit. diagonal = Diagonal(diag) - circuit.append(diagonal, q) + + circuit.append(diagonal, [q_target] + q_controls) return circuit, diag def _dec_ucg_help(self): @@ -199,6 +287,8 @@ def _dec_ucg_help(self): https://arxiv.org/pdf/quant-ph/0410066.pdf. """ single_qubit_gates = [gate.astype(complex) for gate in self.params] + if self.simp_contr[0]: + return uc_gate.dec_ucg_help(single_qubit_gates, len(self.simp_contr[1]) + 1) return uc_gate.dec_ucg_help(single_qubit_gates, self.num_qubits) @staticmethod diff --git a/qiskit/circuit/library/graph_state.py b/qiskit/circuit/library/graph_state.py index 89d1edb035ff..79c0dfbc864d 100644 --- a/qiskit/circuit/library/graph_state.py +++ b/qiskit/circuit/library/graph_state.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (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 @@ -10,13 +10,14 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Graph State circuit.""" +"""Graph State circuit and gate.""" from __future__ import annotations import numpy as np -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func class GraphState(QuantumCircuit): @@ -56,6 +57,11 @@ class GraphState(QuantumCircuit): `arXiv:1512.07892 `_ """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.GraphStateGate instead.", + pending=True, + ) def __init__(self, adjacency_matrix: list | np.ndarray) -> None: """Create graph state preparation circuit. @@ -73,14 +79,91 @@ def __init__(self, adjacency_matrix: list | np.ndarray) -> None: if not np.allclose(adjacency_matrix, adjacency_matrix.transpose()): raise CircuitError("The adjacency matrix must be symmetric.") + graph_state_gate = GraphStateGate(adjacency_matrix) + super().__init__(graph_state_gate.num_qubits, name=f"graph: {adjacency_matrix}") + self.compose(graph_state_gate, qubits=self.qubits, inplace=True) + + +class GraphStateGate(Gate): + r"""A gate representing a graph state. + + Given a graph G = (V, E), with the set of vertices V and the set of edges E, + the corresponding graph state is defined as + + .. math:: + + |G\rangle = \prod_{(a,b) \in E} CZ_{(a,b)} {|+\rangle}^{\otimes V} + + Such a state can be prepared by first preparing all qubits in the :math:`+` + state, then applying a :math:`CZ` gate for each corresponding graph edge. + + Graph state preparation circuits are Clifford circuits, and thus + easy to simulate classically. However, by adding a layer of measurements + in a product basis at the end, there is evidence that the circuit becomes + hard to simulate [2]. + + **Reference Circuit:** + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import GraphStateGate + import rustworkx as rx + + G = rx.generators.cycle_graph(5) + circuit = QuantumCircuit(5) + circuit.append(GraphStateGate(rx.adjacency_matrix(G)), [0, 1, 2, 3, 4]) + circuit.decompose().draw('mpl') + + **References:** + + [1] M. Hein, J. Eisert, H.J. Briegel, Multi-party Entanglement in Graph States, + `arXiv:0307130 `_ + [2] D. Koh, Further Extensions of Clifford Circuits & their Classical Simulation Complexities. + `arXiv:1512.07892 `_ + """ + + def __init__(self, adjacency_matrix: list | np.ndarray) -> None: + """ + Args: + adjacency_matrix: input graph as n-by-n list of 0-1 lists + + Raises: + CircuitError: If adjacency_matrix is not symmetric. + + The gate represents a graph state with the given adjacency matrix. + """ + + adjacency_matrix = np.asarray(adjacency_matrix) + if not np.allclose(adjacency_matrix, adjacency_matrix.transpose()): + raise CircuitError("The adjacency matrix must be symmetric.") num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name=f"graph: {adjacency_matrix}") - circuit.h(range(num_qubits)) - for i in range(num_qubits): - for j in range(i + 1, num_qubits): + super().__init__(name="graph_state", num_qubits=num_qubits, params=[adjacency_matrix]) + + def _define(self): + adjacency_matrix = self.adjacency_matrix + circuit = QuantumCircuit(self.num_qubits, name=self.name) + circuit.h(range(self.num_qubits)) + for i in range(self.num_qubits): + for j in range(i + 1, self.num_qubits): if adjacency_matrix[i][j] == 1: circuit.cz(i, j) - - super().__init__(*circuit.qregs, name=circuit.name) - self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + self.definition = circuit + + def validate_parameter(self, parameter): + """Parameter validation""" + return parameter + + @property + def adjacency_matrix(self): + """Returns the adjacency matrix.""" + return self.params[0] + + def __eq__(self, other): + return ( + isinstance(other, GraphStateGate) + and self.num_qubits == other.num_qubits + and np.all(self.adjacency_matrix == other.adjacency_matrix) + ) diff --git a/qiskit/circuit/library/grover_operator.py b/qiskit/circuit/library/grover_operator.py index d40deefdf679..d55a59249a3d 100644 --- a/qiskit/circuit/library/grover_operator.py +++ b/qiskit/circuit/library/grover_operator.py @@ -16,10 +16,266 @@ from typing import List, Optional, Union import numpy -from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister, AncillaQubit from qiskit.exceptions import QiskitError from qiskit.quantum_info import Statevector, Operator, DensityMatrix +from qiskit.utils.deprecation import deprecate_func from .standard_gates import MCXGate +from .generalized_gates import DiagonalGate + + +def grover_operator( + oracle: QuantumCircuit | Statevector, + state_preparation: QuantumCircuit | None = None, + zero_reflection: QuantumCircuit | DensityMatrix | Operator | None = None, + reflection_qubits: list[int] | None = None, + insert_barriers: bool = False, + name: str = "Q", +): + r"""Construct the Grover operator. + + Grover's search algorithm [1, 2] consists of repeated applications of the so-called + Grover operator used to amplify the amplitudes of the desired output states. + This operator, :math:`\mathcal{Q}`, consists of the phase oracle, :math:`\mathcal{S}_f`, + zero phase-shift or zero reflection, :math:`\mathcal{S}_0`, and an + input state preparation :math:`\mathcal{A}`: + + .. math:: + \mathcal{Q} = \mathcal{A} \mathcal{S}_0 \mathcal{A}^\dagger \mathcal{S}_f + + In the standard Grover search we have :math:`\mathcal{A} = H^{\otimes n}`: + + .. math:: + \mathcal{Q} = H^{\otimes n} \mathcal{S}_0 H^{\otimes n} \mathcal{S}_f + = D \mathcal{S_f} + + The operation :math:`D = H^{\otimes n} \mathcal{S}_0 H^{\otimes n}` is also referred to as + diffusion operator. In this formulation we can see that Grover's operator consists of two + steps: first, the phase oracle multiplies the good states by -1 (with :math:`\mathcal{S}_f`) + and then the whole state is reflected around the mean (with :math:`D`). + + This class allows setting a different state preparation, as in quantum amplitude + amplification (a generalization of Grover's algorithm), :math:`\mathcal{A}` might not be + a layer of Hardamard gates [3]. + + The action of the phase oracle :math:`\mathcal{S}_f` is defined as + + .. math:: + \mathcal{S}_f: |x\rangle \mapsto (-1)^{f(x)}|x\rangle + + where :math:`f(x) = 1` if :math:`x` is a good state and 0 otherwise. To highlight the fact + that this oracle flips the phase of the good states and does not flip the state of a result + qubit, we call :math:`\mathcal{S}_f` a phase oracle. + + Note that you can easily construct a phase oracle from a bitflip oracle by sandwiching the + controlled X gate on the result qubit by a X and H gate. For instance + + .. parsed-literal:: + + Bitflip oracle Phaseflip oracle + q_0: ──■── q_0: ────────────■──────────── + ┌─┴─┐ ┌───┐┌───┐┌─┴─┐┌───┐┌───┐ + out: ┤ X ├ out: ┤ X ├┤ H ├┤ X ├┤ H ├┤ X ├ + └───┘ └───┘└───┘└───┘└───┘└───┘ + + There is some flexibility in defining the oracle and :math:`\mathcal{A}` operator. Before the + Grover operator is applied in Grover's algorithm, the qubits are first prepared with one + application of the :math:`\mathcal{A}` operator (or Hadamard gates in the standard formulation). + Thus, we always have operation of the form + :math:`\mathcal{A} \mathcal{S}_f \mathcal{A}^\dagger`. Therefore it is possible to move + bitflip logic into :math:`\mathcal{A}` and leaving the oracle only to do phaseflips via Z gates + based on the bitflips. One possible use-case for this are oracles that do not uncompute the + state qubits. + + The zero reflection :math:`\mathcal{S}_0` is usually defined as + + .. math:: + \mathcal{S}_0 = 2 |0\rangle^{\otimes n} \langle 0|^{\otimes n} - \mathbb{I}_n + + where :math:`\mathbb{I}_n` is the identity on :math:`n` qubits. + By default, this class implements the negative version + :math:`2 |0\rangle^{\otimes n} \langle 0|^{\otimes n} - \mathbb{I}_n`, since this can simply + be implemented with a multi-controlled Z sandwiched by X gates on the target qubit and the + introduced global phase does not matter for Grover's algorithm. + + Examples: + + We can construct a Grover operator just from the phase oracle: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(2) + oracle.z(0) # good state = first qubit is |1> + grover_op = grover_operator(oracle, insert_barriers=True) + grover_op.draw("mpl") + + We can also modify the state preparation: + + .. plot:: + :include-source: + :context: + + oracle = QuantumCircuit(1) + oracle.z(0) # the qubit state |1> is the good state + state_preparation = QuantumCircuit(1) + state_preparation.ry(0.2, 0) # non-uniform state preparation + grover_op = grover_operator(oracle, state_preparation) + grover_op.draw("mpl") + + In addition, we can also mark which qubits the zero reflection should act on. This + is useful in case that some qubits are just used as scratch space but should not affect + the oracle: + + .. plot:: + :include-source: + :context: + + oracle = QuantumCircuit(4) + oracle.z(3) + reflection_qubits = [0, 3] + state_preparation = QuantumCircuit(4) + state_preparation.cry(0.1, 0, 3) + state_preparation.ry(0.5, 3) + grover_op = grover_operator(oracle, state_preparation, reflection_qubits=reflection_qubits) + grover_op.draw("mpl") + + + The oracle and the zero reflection can also be passed as :mod:`qiskit.quantum_info` + objects: + + .. plot:: + :include-source: + :context: + + from qiskit.quantum_info import Statevector, DensityMatrix, Operator + + mark_state = Statevector.from_label("011") + reflection = 2 * DensityMatrix.from_label("000") - Operator.from_label("III") + grover_op = grover_operator(oracle=mark_state, zero_reflection=reflection) + grover_op.draw("mpl") + + For a large number of qubits, the multi-controlled X gate used for the zero-reflection + can be synthesized in different fashions. Depending on the number of available qubits, + the compiler will choose a different implementation: + + .. code-block:: python + + from qiskit import transpile, Qubit + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(10) + oracle.z(oracle.qubits) + grover_op = grover_operator(oracle) + + # without extra qubit space, the MCX synthesis is expensive + basis_gates = ["u", "cx"] + tqc = transpile(grover_op, basis_gates=basis_gates) + is_2q = lambda inst: len(inst.qubits) == 2 + print("2q depth w/o scratch qubits:", tqc.depth(filter_function=is_2q)) # > 350 + + # add extra bits that can be used as scratch space + grover_op.add_bits([Qubit() for _ in range(num_qubits)]) + tqc = transpile(grover_op, basis_gates=basis_gates) + print("2q depth w/ scratch qubits:", tqc.depth(filter_function=is_2q)) # < 100 + + Args: + oracle: The phase oracle implementing a reflection about the bad state. Note that this + is not a bitflip oracle, see the docstring for more information. + state_preparation: The operator preparing the good and bad state. + For Grover's algorithm, this is a n-qubit Hadamard gate and for amplitude + amplification or estimation the operator :math:`\mathcal{A}`. + zero_reflection: The reflection about the zero state, :math:`\mathcal{S}_0`. + reflection_qubits: Qubits on which the zero reflection acts on. + insert_barriers: Whether barriers should be inserted between the reflections and A. + name: The name of the circuit. + + References: + [1]: L. K. Grover (1996), A fast quantum mechanical algorithm for database search, + `arXiv:quant-ph/9605043 `_. + [2]: I. Chuang & M. Nielsen, Quantum Computation and Quantum Information, + Cambridge: Cambridge University Press, 2000. Chapter 6.1.2. + [3]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). + Quantum Amplitude Amplification and Estimation. + `arXiv:quant-ph/0005055 `_. + """ + # We inherit the ancillas/qubits structure from the oracle, if it is given as circuit. + if isinstance(oracle, QuantumCircuit): + circuit = oracle.copy_empty_like(name=name, vars_mode="drop") + else: + circuit = QuantumCircuit(oracle.num_qubits, name=name) + + # (1) Add the oracle. + # If the oracle is given as statevector, turn it into a circuit that implements the + # reflection about the state. + if isinstance(oracle, Statevector): + diagonal = DiagonalGate((-1) ** oracle.data) + circuit.append(diagonal, circuit.qubits) + else: + circuit.compose(oracle, inplace=True) + + if insert_barriers: + circuit.barrier() + + # (2) Add the inverse state preparation. + # For this we need to know the target qubits that we apply the zero reflection to. + # If the reflection qubits are not given, we assume they are the qubits that are not + # of type ``AncillaQubit`` in the oracle. + if reflection_qubits is None: + reflection_qubits = [ + i for i, qubit in enumerate(circuit.qubits) if not isinstance(qubit, AncillaQubit) + ] + + if state_preparation is None: + circuit.h(reflection_qubits) # H is self-inverse + else: + circuit.compose(state_preparation.inverse(), inplace=True) + + if insert_barriers: + circuit.barrier() + + # (3) Add the zero reflection. + if zero_reflection is None: + num_reflection = len(reflection_qubits) + + circuit.x(reflection_qubits) + if num_reflection == 1: + circuit.z( + reflection_qubits[0] + ) # MCX does not support 0 controls, hence this is separate + else: + mcx = MCXGate(num_reflection - 1) + + circuit.h(reflection_qubits[-1]) + circuit.append(mcx, reflection_qubits) + circuit.h(reflection_qubits[-1]) + circuit.x(reflection_qubits) + + elif isinstance(zero_reflection, (Operator, DensityMatrix)): + diagonal = DiagonalGate(zero_reflection.data.diagonal()) + circuit.append(diagonal, circuit.qubits) + + else: + circuit.compose(zero_reflection, inplace=True) + + if insert_barriers: + circuit.barrier() + + # (4) Add the state preparation. + if state_preparation is None: + circuit.h(reflection_qubits) + else: + circuit.compose(state_preparation, inplace=True) + + # minus sign + circuit.global_phase = numpy.pi + + return circuit class GroverOperator(QuantumCircuit): @@ -150,6 +406,13 @@ class GroverOperator(QuantumCircuit): «state_2: ┤2 ├┤1 ├┤ UCRZ(pi/4) ├┤ H ├ « └─────────────────┘└───────────────┘└────────────┘└───┘ + .. seealso:: + + The :func:`.grover_operator` implements the same functionality but keeping the + :class:`.MCXGate` abstract, such that the compiler may choose the optimal decomposition. + We recommend using :func:`.grover_operator` for performance reasons, which does not + wrap the circuit into an opaque gate. + References: [1]: L. K. Grover (1996), A fast quantum mechanical algorithm for database search, `arXiv:quant-ph/9605043 `_. @@ -160,6 +423,11 @@ class GroverOperator(QuantumCircuit): `arXiv:quant-ph/0005055 `_. """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.grover_operator instead.", + pending=True, + ) def __init__( self, oracle: Union[QuantumCircuit, Statevector], diff --git a/qiskit/circuit/library/hidden_linear_function.py b/qiskit/circuit/library/hidden_linear_function.py index b68fda7f8fc5..7acaaa7aa72e 100644 --- a/qiskit/circuit/library/hidden_linear_function.py +++ b/qiskit/circuit/library/hidden_linear_function.py @@ -12,11 +12,12 @@ """Hidden Linear Function circuit.""" -from typing import Union, List +from __future__ import annotations import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func class HiddenLinearFunction(QuantumCircuit): @@ -67,7 +68,12 @@ class HiddenLinearFunction(QuantumCircuit): `arXiv:1704.00690 `_ """ - def __init__(self, adjacency_matrix: Union[List[List[int]], np.ndarray]) -> None: + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.hidden_linear_function instead.", + pending=True, + ) + def __init__(self, adjacency_matrix: list | np.ndarray) -> None: """Create new HLF circuit. Args: @@ -77,22 +83,79 @@ def __init__(self, adjacency_matrix: Union[List[List[int]], np.ndarray]) -> None Raises: CircuitError: If A is not symmetric. """ - adjacency_matrix = np.asarray(adjacency_matrix) - if not np.allclose(adjacency_matrix, adjacency_matrix.transpose()): - raise CircuitError("The adjacency matrix must be symmetric.") - - num_qubits = len(adjacency_matrix) - circuit = QuantumCircuit(num_qubits, name=f"hlf: {adjacency_matrix}") - - circuit.h(range(num_qubits)) - for i in range(num_qubits): - for j in range(i + 1, num_qubits): - if adjacency_matrix[i][j]: - circuit.cz(i, j) - for i in range(num_qubits): - if adjacency_matrix[i][i]: - circuit.s(i) - circuit.h(range(num_qubits)) - + circuit = hidden_linear_function(adjacency_matrix) super().__init__(*circuit.qregs, name=circuit.name) - self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + self.append(circuit.to_gate(), self.qubits) + + +def hidden_linear_function(adjacency_matrix: list | np.ndarray) -> QuantumCircuit: + r"""Circuit to solve the hidden linear function problem. + + The 2D Hidden Linear Function problem is determined by a 2D adjacency + matrix A, where only elements that are nearest-neighbor on a grid have + non-zero entries. Each row/column corresponds to one binary variable + :math:`x_i`. + + The hidden linear function problem is as follows: + + Consider the quadratic form + + .. math:: + + q(x) = \sum_{i,j=1}^{n}{x_i x_j} ~(\mathrm{mod}~ 4) + + and restrict :math:`q(x)` onto the nullspace of A. This results in a linear + function. + + .. math:: + + 2 \sum_{i=1}^{n}{z_i x_i} ~(\mathrm{mod}~ 4) \forall x \in \mathrm{Ker}(A) + + and the goal is to recover this linear function (equivalently a vector + :math:`[z_0, ..., z_{n-1}]`). There can be multiple solutions. + + In [1] it is shown that the present circuit solves this problem + on a quantum computer in constant depth, whereas any corresponding + solution on a classical computer would require circuits that grow + logarithmically with :math:`n`. Thus this circuit is an example + of quantum advantage with shallow circuits. + + **Reference Circuit:** + + .. plot:: + :include-source: + + from qiskit.circuit.library import hidden_linear_function + A = [[1, 1, 0], [1, 0, 1], [0, 1, 1]] + circuit = hidden_linear_function(A) + circuit.draw('mpl') + + Args: + adjacency_matrix: a symmetric n-by-n list of 0-1 lists. + n will be the number of qubits. + + Raises: + CircuitError: If A is not symmetric. + + **Reference:** + + [1] S. Bravyi, D. Gosset, R. Koenig, Quantum Advantage with Shallow Circuits, 2017. + `arXiv:1704.00690 `_ + """ + adjacency_matrix = np.asarray(adjacency_matrix) + if not np.allclose(adjacency_matrix, adjacency_matrix.transpose()): + raise CircuitError("The adjacency matrix must be symmetric.") + + num_qubits = len(adjacency_matrix) + circuit = QuantumCircuit(num_qubits, name=f"hlf: {adjacency_matrix}") + + circuit.h(range(num_qubits)) + for i in range(num_qubits): + for j in range(i + 1, num_qubits): + if adjacency_matrix[i][j]: + circuit.cz(i, j) + for i in range(num_qubits): + if adjacency_matrix[i][i]: + circuit.s(i) + circuit.h(range(num_qubits)) + return circuit diff --git a/qiskit/circuit/library/iqp.py b/qiskit/circuit/library/iqp.py index db65fbe294ad..62082fd5c446 100644 --- a/qiskit/circuit/library/iqp.py +++ b/qiskit/circuit/library/iqp.py @@ -13,10 +13,12 @@ """Instantaneous quantum polynomial circuit.""" from __future__ import annotations +from collections.abc import Sequence import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func +from qiskit._accelerate.circuit_library import py_iqp, py_random_iqp class IQP(QuantumCircuit): @@ -60,6 +62,11 @@ class IQP(QuantumCircuit): `arXiv:1504.07999 `_ """ + @deprecate_func( + since="1.3", + additional_msg="Use the qiskit.circuit.library.iqp function instead.", + pending=True, + ) def __init__(self, interactions: list | np.ndarray) -> None: """Create IQP circuit. @@ -69,28 +76,100 @@ def __init__(self, interactions: list | np.ndarray) -> None: Raises: CircuitError: if the inputs is not as symmetric matrix. """ - num_qubits = len(interactions) - interactions = np.array(interactions) - if not np.allclose(interactions, interactions.transpose()): - raise CircuitError("The interactions matrix is not symmetric") + circuit = iqp(interactions) + super().__init__(*circuit.qregs, name=circuit.name) + self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) - a_str = np.array_str(interactions) - a_str.replace("\n", ";") - name = "iqp:" + a_str.replace("\n", ";") - circuit = QuantumCircuit(num_qubits, name=name) +def iqp( + interactions: Sequence[Sequence[int]], +) -> QuantumCircuit: + r"""Instantaneous quantum polynomial time (IQP) circuit. - circuit.h(range(num_qubits)) - for i in range(num_qubits): - for j in range(i + 1, num_qubits): - if interactions[i][j] % 4 != 0: - circuit.cp(interactions[i][j] * np.pi / 2, i, j) + The circuit consists of a column of Hadamard gates, a column of powers of T gates, + a sequence of powers of CS gates (up to :math:`\frac{n^2-n}{2}` of them), and a final column of + Hadamard gates, as introduced in [1]. - for i in range(num_qubits): - if interactions[i][i] % 8 != 0: - circuit.p(interactions[i][i] * np.pi / 8, i) + The circuit is parameterized by an :math:`n \times n` interactions matrix. The powers of each + T gate are given by the diagonal elements of the interactions matrix. The powers of the CS gates + are given by the upper triangle of the interactions matrix. - circuit.h(range(num_qubits)) + **Reference Circuit:** - super().__init__(*circuit.qregs, name=circuit.name) - self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + .. plot:: + + from qiskit.circuit.library import iqp + A = [[6, 5, 3], [5, 4, 5], [3, 5, 1]] + circuit = iqp(A) + circuit.draw("mpl") + + **Expanded Circuit:** + + .. plot:: + + from qiskit.circuit.library import iqp + from qiskit.visualization.library import _generate_circuit_library_visualization + A = [[6, 5, 3], [5, 4, 5], [3, 5, 1]] + circuit = iqp(A) + _generate_circuit_library_visualization(circuit) + + **References:** + + [1] M. J. Bremner et al. Average-case complexity versus approximate + simulation of commuting quantum computations, + Phys. Rev. Lett. 117, 080501 (2016). + `arXiv:1504.07999 `_ + + Args: + interactions: The interactions as symmetric square matrix. If ``None``, then the + ``num_qubits`` argument must be set and a random IQP circuit will be generated. + + Returns: + An IQP circuit. + """ + # if no interactions are given, generate them + num_qubits = len(interactions) + interactions = np.asarray(interactions).astype(np.int64) + + # set the label -- if the number of qubits is too large, do not show the interactions matrix + if num_qubits < 5 and interactions is not None: + label = np.array_str(interactions) + name = "iqp:" + label.replace("\n", ";") + else: + name = "iqp" + + circuit = QuantumCircuit._from_circuit_data(py_iqp(interactions), add_regs=True) + circuit.name = name + return circuit + + +def random_iqp( + num_qubits: int, + seed: int | None = None, +) -> QuantumCircuit: + r"""A random instantaneous quantum polynomial time (IQP) circuit. + + See :func:`iqp` for more details on the IQP circuit. + + Example: + + .. plot:: + :include-source: + + from qiskit.circuit.library import random_iqp + + circuit = random_iqp(3) + circuit.draw("mpl") + + Args: + num_qubits: The number of qubits in the circuit. + seed: A seed for the random number generator, in case the interactions matrix is + randomly generated. + + Returns: + An IQP circuit. + """ + # set the label -- if the number of qubits is too large, do not show the interactions matrix + circuit = QuantumCircuit._from_circuit_data(py_random_iqp(num_qubits, seed), add_regs=True) + circuit.name = "iqp" + return circuit diff --git a/qiskit/circuit/library/n_local/__init__.py b/qiskit/circuit/library/n_local/__init__.py index 4a238dcedbb8..cd7e50dbb6e9 100644 --- a/qiskit/circuit/library/n_local/__init__.py +++ b/qiskit/circuit/library/n_local/__init__.py @@ -12,22 +12,34 @@ """The circuit library module containing N-local circuits.""" -from .n_local import NLocal +from .n_local import NLocal, n_local from .two_local import TwoLocal -from .pauli_two_design import PauliTwoDesign -from .real_amplitudes import RealAmplitudes -from .efficient_su2 import EfficientSU2 -from .evolved_operator_ansatz import EvolvedOperatorAnsatz -from .excitation_preserving import ExcitationPreserving -from .qaoa_ansatz import QAOAAnsatz +from .pauli_two_design import PauliTwoDesign, pauli_two_design +from .real_amplitudes import RealAmplitudes, real_amplitudes +from .efficient_su2 import EfficientSU2, efficient_su2 +from .evolved_operator_ansatz import ( + EvolvedOperatorAnsatz, + evolved_operator_ansatz, + hamiltonian_variational_ansatz, +) +from .excitation_preserving import ExcitationPreserving, excitation_preserving +from .qaoa_ansatz import QAOAAnsatz, qaoa_ansatz __all__ = [ + "n_local", "NLocal", "TwoLocal", + "real_amplitudes", "RealAmplitudes", + "pauli_two_design", "PauliTwoDesign", + "efficient_su2", "EfficientSU2", + "hamiltonian_variational_ansatz", + "evolved_operator_ansatz", "EvolvedOperatorAnsatz", + "excitation_preserving", "ExcitationPreserving", + "qaoa_ansatz", "QAOAAnsatz", ] diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index 69398dca1e90..53698a3e18a6 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -14,18 +14,121 @@ from __future__ import annotations import typing -from collections.abc import Callable +from collections.abc import Callable, Iterable from numpy import pi -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Gate from qiskit.circuit.library.standard_gates import RYGate, RZGate, CXGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import +def efficient_su2( + num_qubits: int, + su2_gates: str | Gate | Iterable[str | Gate] | None = None, + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "reverse_linear", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "EfficientSU2", +): + r"""The hardware-efficient :math:`SU(2)` 2-local circuit. + + The ``efficient_su2`` circuit consists of layers of single qubit operations spanned by + :math:`SU(2)` and CX entanglements. This is a heuristic pattern that can be used to prepare trial + wave functions for variational quantum algorithms or classification circuit for machine learning. + + :math:`SU(2)` is the special unitary group of degree 2, its elements are :math:`2 \times 2` + unitary matrices with determinant 1, such as the Pauli rotation gates. + + On 3 qubits and using the Pauli :math:`Y` and :math:`Z` rotations as single qubit gates, the + this circuit is represented by: + + .. parsed-literal:: + + ┌──────────┐┌──────────┐ ░ ░ ░ ┌───────────┐┌───────────┐ + ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├─░────────■───░─ ... ─░─┤ RY(θ[12]) ├┤ RZ(θ[15]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[1]) ├┤ RZ(θ[4]) ├─░───■──┤ X ├─░─ ... ─░─┤ RY(θ[13]) ├┤ RZ(θ[16]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐└───┘ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─░─┤ X ├──────░─ ... ─░─┤ RY(θ[14]) ├┤ RZ(θ[17]) ├ + └──────────┘└──────────┘ ░ └───┘ ░ ░ └───────────┘└───────────┘ + + Examples: + + Per default, the ``"reverse_linear"`` entanglement is used, which, in the case of + CX gates, is equivalent to an all-to-all entanglement: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import efficient_su2 + + circuit = efficient_su2(3, reps=1) + circuit.draw("mpl") + + To specify which SU(2) gates should be used in the rotation layer, we can set the + ``su2_gates`` argument. In addition, we can change the entanglement structure. + For example: + + .. plot:: + :include-source: + :context: + + circuit = efficient_su2(4, su2_gates=["rx", "y"], entanglement="circular", reps=1) + circuit.draw("mpl") + + Args: + num_qubits: The number of qubits. + su2_gates: The :math:`SU(2)` single qubit gates to apply in single qubit gate layers. + 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. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + An efficient-SU(2) circuit. + """ + if su2_gates is None: + su2_gates = ["ry", "rz"] + + return n_local( + num_qubits, + su2_gates, + ["cx"], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class EfficientSU2(TwoLocal): r"""The hardware efficient SU(2) 2-local circuit. @@ -55,7 +158,7 @@ class EfficientSU2(TwoLocal): Examples: >>> circuit = EfficientSU2(3, reps=1) - >>> print(circuit) + >>> print(circuit.decompose()) ┌──────────┐┌──────────┐ ┌──────────┐┌──────────┐ q_0: ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├──■────■──┤ RY(θ[6]) ├┤ RZ(θ[9]) ├───────────── ├──────────┤├──────────┤┌─┴─┐ │ └──────────┘├──────────┤┌───────────┐ @@ -64,7 +167,8 @@ class EfficientSU2(TwoLocal): q_2: ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─────┤ X ├───┤ X ├────┤ RY(θ[8]) ├┤ RZ(θ[11]) ├ └──────────┘└──────────┘ └───┘ └───┘ └──────────┘└───────────┘ - >>> ansatz = EfficientSU2(4, su2_gates=['rx', 'y'], entanglement='circular', reps=1) + >>> ansatz = EfficientSU2(4, su2_gates=['rx', 'y'], entanglement='circular', reps=1, + ... flatten=True) >>> qc = QuantumCircuit(4) # create a circuit and append the RY variational form >>> qc.compose(ansatz, inplace=True) >>> qc.draw() @@ -78,8 +182,17 @@ class EfficientSU2(TwoLocal): q_3: ┤ RX(θ[3]) ├┤ Y ├──■──────────────────────┤ X ├────┤ RX(θ[7]) ├┤ Y ├ └──────────┘└───┘ └───┘ └──────────┘└───┘ + .. seealso:: + + The :func:`.efficient_su2` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.efficient_su2 instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py index 4bc6bcc58a13..086bbda31749 100644 --- a/qiskit/circuit/library/n_local/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/n_local/evolved_operator_ansatz.py @@ -15,16 +15,263 @@ from __future__ import annotations from collections.abc import Sequence +import typing +import warnings +import itertools import numpy as np from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate from qiskit.circuit.parameter import Parameter +from qiskit.circuit.parametervector import ParameterVector from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info import Operator, Pauli, SparsePauliOp +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from qiskit._accelerate.circuit_library import pauli_evolution from .n_local import NLocal +if typing.TYPE_CHECKING: + from qiskit.synthesis.evolution import EvolutionSynthesis + + +def evolved_operator_ansatz( + operators: BaseOperator | Sequence[BaseOperator], + reps: int = 1, + evolution: EvolutionSynthesis | None = None, + insert_barriers: bool = False, + name: str = "EvolvedOps", + parameter_prefix: str | Sequence[str] = "t", + remove_identities: bool = True, + flatten: bool | None = None, +) -> QuantumCircuit: + r"""Construct an ansatz out of operator evolutions. + + For a set of operators :math:`[O_1, ..., O_J]` and :math:`R` repetitions (``reps``), this circuit + is defined as + + .. math:: + + \prod_{r=1}^{R} \left( \prod_{j=J}^1 e^{-i\theta_{j, r} O_j} \right) + + where the exponentials :math:`exp(-i\theta O_j)` are expanded using the product formula + specified by ``evolution``. + + Examples: + + .. plot:: + :include-source: + + from qiskit.circuit.library import evolved_operator_ansatz + from qiskit.quantum_info import Pauli + + ops = [Pauli("ZZI"), Pauli("IZZ"), Pauli("IXI")] + ansatz = evolved_operator_ansatz(ops, reps=3, insert_barriers=True) + ansatz.draw("mpl") + + Args: + operators: The operators to evolve. Can be a single operator or a sequence thereof. + reps: The number of times to repeat the evolved operators. + evolution: A specification of which evolution synthesis to use for the + :class:`.PauliEvolutionGate`. Defaults to first order Trotterization. Note, that + operators of type :class:`.Operator` are evolved using the :class:`.HamiltonianGate`, + as there are no Hamiltonian terms to expand in Trotterization. + insert_barriers: Whether to insert barriers in between each evolution. + name: The name of the circuit. + parameter_prefix: Set the names of the circuit parameters. If a string, the same prefix + will be used for each parameters. Can also be a list to specify a prefix per + operator. + remove_identities: If ``True``, ignore identity operators (note that we do not check + :class:`.Operator` inputs). This will also remove parameters associated with identities. + flatten: If ``True``, a flat circuit is returned instead of nesting it inside multiple + layers of gate objects. Setting this to ``False`` is significantly less performant, + especially for parameter binding, but can be desirable for a cleaner visualization. + """ + if reps < 0: + raise ValueError("reps must be a non-negative integer.") + + if isinstance(operators, BaseOperator): + operators = [operators] + elif len(operators) == 0: + return QuantumCircuit() + + num_operators = len(operators) + if not isinstance(parameter_prefix, str): + if num_operators != len(parameter_prefix): + raise ValueError( + f"Mismatching number of operators ({len(operators)}) and parameter_prefix " + f"({len(parameter_prefix)})." + ) + + num_qubits = operators[0].num_qubits + if remove_identities: + operators, parameter_prefix = _remove_identities(operators, parameter_prefix) + + if any(op.num_qubits != num_qubits for op in operators): + raise ValueError("Inconsistent numbers of qubits in the operators.") + + # get the total number of parameters + if isinstance(parameter_prefix, str): + parameters = ParameterVector(parameter_prefix, reps * num_operators) + param_iter = iter(parameters) + else: + # this creates the parameter vectors per operator, e.g. + # [[a0, a1, a2, ...], [b0, b1, b2, ...], [c0, c1, c2, ...]] + # and turns them into an iterator + # a0 -> b0 -> c0 -> a1 -> b1 -> c1 -> a2 -> ... + per_operator = [ParameterVector(prefix, reps).params for prefix in parameter_prefix] + param_iter = itertools.chain.from_iterable(zip(*per_operator)) + + # fast, Rust-path + if ( + flatten is not False # captures None and True + and evolution is None + and all(isinstance(op, SparsePauliOp) for op in operators) + ): + sparse_labels = [op.to_sparse_list() for op in operators] + expanded_paulis = [] + for _ in range(reps): + for term in sparse_labels: + param = next(param_iter) + expanded_paulis += [ + (pauli, indices, 2 * coeff * param) for pauli, indices, coeff in term + ] + + data = pauli_evolution(num_qubits, expanded_paulis, insert_barriers, False) + circuit = QuantumCircuit._from_circuit_data(data, add_regs=True) + circuit.name = name + + return circuit + + # slower, Python-path + if evolution is None: + from qiskit.synthesis.evolution import LieTrotter + + evolution = LieTrotter(insert_barriers=insert_barriers) + + circuit = QuantumCircuit(num_qubits, name=name) + + # pylint: disable=cyclic-import + from qiskit.circuit.library.hamiltonian_gate import HamiltonianGate + + for rep in range(reps): + for i, op in enumerate(operators): + if isinstance(op, Operator): + gate = HamiltonianGate(op, next(param_iter)) + if flatten: + warnings.warn( + "Cannot flatten the evolution of an Operator, flatten is set to " + "False for this operator." + ) + flatten_operator = False + + elif isinstance(op, BaseOperator): + gate = PauliEvolutionGate(op, next(param_iter), synthesis=evolution) + flatten_operator = flatten is True or flatten is None + else: + raise ValueError(f"Unsupported operator type: {type(op)}") + + if flatten_operator: + circuit.compose(gate.definition, inplace=True) + else: + circuit.append(gate, circuit.qubits) + + if insert_barriers and (rep < reps - 1 or i < num_operators - 1): + circuit.barrier() + + return circuit + + +def hamiltonian_variational_ansatz( + hamiltonian: SparsePauliOp | Sequence[SparsePauliOp], + reps: int = 1, + insert_barriers: bool = False, + name: str = "HVA", + parameter_prefix: str = "t", +) -> QuantumCircuit: + r"""Construct a Hamiltonian variational ansatz. + + For a Hamiltonian :math:`H = \sum_{k=1}^K H_k` where the terms :math:`H_k` consist of only + commuting Paulis, but the terms do not commute among each other :math:`[H_k, H_{k'}] \neq 0`, the + Hamiltonian variational ansatz (HVA) is + + .. math:: + + \prod_{r=1}^{R} \left( \prod_{k=K}^1 e^{-i\theta_{k, r} H_k} \right) + + where the exponentials :math:`exp(-i\theta H_k)` are implemented exactly [1, 2]. Note that this + differs from :func:`.evolved_operator_ansatz`, where no assumptions on the structure of the + operators are done. + + The Hamiltonian can be passed as :class:`.SparsePauliOp`, in which case we split the Hamiltonian + into commuting terms :math:`\{H_k\}_k`. Note, that this may not be optimal and if the + minimal set of commuting terms is known it can be passed as sequence into this function. + + Examples: + + A single operator will be split into commuting terms automatically: + + .. plot:: + :include-source: + + from qiskit.quantum_info import SparsePauliOp + from qiskit.circuit.library import hamiltonian_variational_ansatz + + # this Hamiltonian will be split into the two terms [ZZI, IZZ] and [IXI] + hamiltonian = SparsePauliOp(["ZZI", "IZZ", "IXI"]) + ansatz = hamiltonian_variational_ansatz(hamiltonian, reps=2) + ansatz.draw("mpl") + + Alternatively, we can directly provide the terms: + + .. plot:: + :include-source: + + from qiskit.quantum_info import SparsePauliOp + from qiskit.circuit.library import hamiltonian_variational_ansatz + + zz = SparsePauliOp(["ZZI", "IZZ"]) + x = SparsePauliOp(["IXI"]) + ansatz = hamiltonian_variational_ansatz([zz, x], reps=2) + ansatz.draw("mpl") + + + Args: + hamiltonian: The Hamiltonian to evolve. If given as single operator, it will be split into + commuting terms. If a sequence of :class:`.SparsePauliOp`, then it is assumed that + each element consists of commuting terms, but the elements do not commute among each + other. + reps: The number of times to repeat the evolved operators. + insert_barriers: Whether to insert barriers in between each evolution. + name: The name of the circuit. + parameter_prefix: Set the names of the circuit parameters. If a string, the same prefix + will be used for each parameters. Can also be a list to specify a prefix per + operator. + + References: + + [1] D. Wecker et al. Progress towards practical quantum variational algorithms (2015) + `Phys Rev A 92, 042303 `__ + [2] R. Wiersema et al. Exploring entanglement and optimization within the Hamiltonian + Variational Ansatz (2020) `arXiv:2008.02941 `__ + + """ + # If a single operator is given, check if it is a sum of operators (a SparsePauliOp), + # and split it into commuting terms. Otherwise treat it as single operator. + if isinstance(hamiltonian, SparsePauliOp): + hamiltonian = hamiltonian.group_commuting() + + return evolved_operator_ansatz( + hamiltonian, + reps=reps, + evolution=None, + insert_barriers=insert_barriers, + name=name, + parameter_prefix=parameter_prefix, + flatten=True, + ) + class EvolvedOperatorAnsatz(NLocal): """The evolved operator ansatz.""" @@ -254,3 +501,15 @@ def _is_pauli_identity(operator): if isinstance(operator, Pauli): return not np.any(np.logical_or(operator.x, operator.z)) return False + + +def _remove_identities(operators, prefixes): + identity_ops = {index for index, op in enumerate(operators) if _is_pauli_identity(op)} + + if len(identity_ops) == 0: + return operators, prefixes + + cleaned_ops = [op for i, op in enumerate(operators) if i not in identity_ops] + cleaned_prefix = [prefix for i, prefix in enumerate(prefixes) if i not in identity_ops] + + return cleaned_ops, cleaned_prefix diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index 4a3e7445fa10..49bfdb07f017 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -13,14 +13,127 @@ """The ExcitationPreserving 2-local circuit.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from numpy import pi from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library.standard_gates import RZGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal +def excitation_preserving( + num_qubits: int, + mode: str = "iswap", + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "full", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "ExcitationPreserving", +): + r"""The heuristic excitation-preserving wave function ansatz. + + The ``excitation_preserving`` circuit preserves the ratio of :math:`|00\rangle`, + :math:`|01\rangle + |10\rangle` and :math:`|11\rangle` states. To this end, this circuit + uses two-qubit interactions of the form + + .. math:: + + \newcommand{\rotationangle}{\theta/2} + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos\left(\rotationangle\right) & -i\sin\left(\rotationangle\right) & 0 \\ + 0 & -i\sin\left(\rotationangle\right) & \cos\left(\rotationangle\right) & 0 \\ + 0 & 0 & 0 & e^{-i\phi} + \end{pmatrix} + + for the mode ``"fsim"`` or with :math:`e^{-i\phi} = 1` for the mode ``"iswap"``. + + Note that other wave functions, such as UCC-ansatzes, are also excitation preserving. + However these can become complex quickly, while this heuristically motivated circuit follows + a simpler pattern. + + This trial wave function consists of layers of :math:`Z` rotations with 2-qubit entanglements. + The entangling is creating using :math:`XX+YY` rotations and optionally a controlled-phase + gate for the mode ``"fsim"``. + + Examples: + + With linear entanglement, this circuit is given by: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import excitation_preserving + + ansatz = excitation_preserving(3, reps=1, insert_barriers=True, entanglement="linear") + ansatz.draw("mpl") + + The entanglement structure can be explicitly specified with the ``entanglement`` + argument. The ``"fsim"`` mode includes an additional parameterized :class:`.CPhaseGate` + in each block: + + .. plot:: + :include-source: + :context: + + ansatz = excitation_preserving(3, reps=1, mode="fsim", entanglement=[[0, 2]]) + ansatz.draw("mpl") + + Args: + num_qubits: The number of qubits. + mode: Choose the entangler mode, can be `"iswap"` or `"fsim"`. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + An excitation-preserving circuit. + """ + supported_modes = ["iswap", "fsim"] + if mode not in supported_modes: + raise ValueError(f"Unsupported mode {mode}, choose one of {supported_modes}") + + theta = Parameter("θ") + swap = QuantumCircuit(2, name="Interaction") + swap.rxx(theta, 0, 1) + swap.ryy(theta, 0, 1) + if mode == "fsim": + phi = Parameter("φ") + swap.cp(phi, 0, 1) + + return n_local( + num_qubits, + ["rz"], + [swap.to_gate()], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class ExcitationPreserving(TwoLocal): r"""The heuristic excitation-preserving wave function ansatz. @@ -57,7 +170,7 @@ class ExcitationPreserving(TwoLocal): Examples: >>> ansatz = ExcitationPreserving(3, reps=1, insert_barriers=True, entanglement='linear') - >>> print(ansatz) # show the circuit + >>> print(ansatz.decompose()) # show the circuit ┌──────────┐ ░ ┌────────────┐┌────────────┐ ░ ┌──────────┐ q_0: ┤ RZ(θ[0]) ├─░─┤0 ├┤0 ├─────────────────────────────░─┤ RZ(θ[5]) ├ ├──────────┤ ░ │ RXX(θ[3]) ││ RYY(θ[3]) │┌────────────┐┌────────────┐ ░ ├──────────┤ @@ -66,10 +179,10 @@ class ExcitationPreserving(TwoLocal): q_2: ┤ RZ(θ[2]) ├─░─────────────────────────────┤1 ├┤1 ├─░─┤ RZ(θ[7]) ├ └──────────┘ ░ └────────────┘└────────────┘ ░ └──────────┘ - >>> ansatz = ExcitationPreserving(2, reps=1) + >>> ansatz = ExcitationPreserving(2, reps=1, flatten=True) >>> qc = QuantumCircuit(2) # create a circuit and append the RY variational form >>> qc.cry(0.2, 0, 1) # do some previous operation - >>> qc.compose(ansatz, inplace=True) # add the swaprz + >>> qc.compose(ansatz, inplace=True) # add the excitation-preserving >>> qc.draw() ┌──────────┐┌────────────┐┌────────────┐┌──────────┐ q_0: ─────■─────┤ RZ(θ[0]) ├┤0 ├┤0 ├┤ RZ(θ[3]) ├ @@ -78,8 +191,8 @@ class ExcitationPreserving(TwoLocal): └─────────┘└──────────┘└────────────┘└────────────┘└──────────┘ >>> ansatz = ExcitationPreserving(3, reps=1, mode='fsim', entanglement=[[0,2]], - ... insert_barriers=True) - >>> print(ansatz) + ... insert_barriers=True, flatten=True) + >>> print(ansatz.decompose()) ┌──────────┐ ░ ┌────────────┐┌────────────┐ ░ ┌──────────┐ q_0: ┤ RZ(θ[0]) ├─░─┤0 ├┤0 ├─■──────░─┤ RZ(θ[5]) ├ ├──────────┤ ░ │ ││ │ │ ░ ├──────────┤ @@ -87,8 +200,19 @@ class ExcitationPreserving(TwoLocal): ├──────────┤ ░ │ ││ │ │θ[4] ░ ├──────────┤ q_2: ┤ RZ(θ[2]) ├─░─┤1 ├┤1 ├─■──────░─┤ RZ(θ[7]) ├ └──────────┘ ░ └────────────┘└────────────┘ ░ └──────────┘ + + .. seealso:: + + The :func:`.excitation_preserving` function constructs a functionally equivalent circuit, + but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.excitation_preserving instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 9eb79d367f3b..8c0b4d285086 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -17,21 +17,28 @@ import collections import itertools import typing -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence, Iterable import numpy -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.gate import Gate +from qiskit.circuit.quantumcircuit import QuantumCircuit, ParameterValueType +from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit import ( Instruction, Parameter, - ParameterVector, ParameterExpression, CircuitInstruction, ) from qiskit.exceptions import QiskitError from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map +from qiskit.utils.deprecation import deprecate_func + +from qiskit._accelerate.circuit_library import ( + Block, + py_n_local, + get_entangler_map as fast_entangler_map, +) from ..blueprintcircuit import BlueprintCircuit @@ -39,6 +46,219 @@ if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import +# entanglement for an individual block, e.g. if the block is CXGate() and we have +# 3 qubits, this could be [(0, 1), (1, 2), (2, 0)] +BlockEntanglement = typing.Union[str, Iterable[Iterable[int]]] + + +def n_local( + num_qubits: int, + rotation_blocks: str | Gate | Iterable[str | Gate], + entanglement_blocks: str | Gate | Iterable[str | Gate], + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "full", + reps: int = 3, + insert_barriers: bool = False, + parameter_prefix: str = "θ", + overwrite_block_parameters: bool = True, + skip_final_rotation_layer: bool = False, + skip_unentangled_qubits: bool = False, + name: str | None = "nlocal", +) -> QuantumCircuit: + r"""Construct an n-local variational circuit. + + The structure of the n-local circuit are alternating rotation and entanglement layers. + In both layers, parameterized circuit-blocks act on the circuit in a defined way. + In the rotation layer, the blocks are applied stacked on top of each other, while in the + entanglement layer according to the ``entanglement`` strategy. + The circuit blocks can have arbitrary sizes (smaller equal to the number of qubits in the + circuit). Each layer is repeated ``reps`` times, and by default a final rotation layer is + appended. + + For instance, a rotation block on 2 qubits and an entanglement block on 4 qubits using + ``"linear"`` entanglement yields the following circuit. + + .. parsed-literal:: + + ┌──────┐ ░ ┌──────┐ ░ ┌──────┐ + ┤0 ├─░─┤0 ├──────────────── ... ─░─┤0 ├ + │ Rot │ ░ │ │┌──────┐ ░ │ Rot │ + ┤1 ├─░─┤1 ├┤0 ├──────── ... ─░─┤1 ├ + ├──────┤ ░ │ Ent ││ │┌──────┐ ░ ├──────┤ + ┤0 ├─░─┤2 ├┤1 ├┤0 ├ ... ─░─┤0 ├ + │ Rot │ ░ │ ││ Ent ││ │ ░ │ Rot │ + ┤1 ├─░─┤3 ├┤2 ├┤1 ├ ... ─░─┤1 ├ + ├──────┤ ░ └──────┘│ ││ Ent │ ░ ├──────┤ + ┤0 ├─░─────────┤3 ├┤2 ├ ... ─░─┤0 ├ + │ Rot │ ░ └──────┘│ │ ░ │ Rot │ + ┤1 ├─░─────────────────┤3 ├ ... ─░─┤1 ├ + └──────┘ ░ └──────┘ ░ └──────┘ + + | | + +---------------------------------+ + repeated reps times + + Entanglement: + + The entanglement describes the connections of the gates in the entanglement layer. + For a two-qubit gate for example, the entanglement contains pairs of qubits on which the + gate should acts, e.g. ``[[ctrl0, target0], [ctrl1, target1], ...]``. + A set of default entanglement strategies is provided and can be selected by name: + + * ``"full"`` entanglement is each qubit is entangled with all the others. + * ``"linear"`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, + for all :math:`i \in \{0, 1, ... , n - 2\}`, where :math:`n` is the total number of qubits. + * ``"reverse_linear"`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, + for all :math:`i \in \{n-2, n-3, ... , 1, 0\}`, where :math:`n` is the total number of qubits. + Note that if ``entanglement_blocks=="cx"`` then this option provides the same unitary as + ``"full"`` with fewer entangling gates. + * ``"pairwise"`` entanglement is one layer where qubit :math:`i` is entangled with qubit + :math:`i + 1`, for all even values of :math:`i`, and then a second layer where qubit :math:`i` + is entangled with qubit :math:`i + 1`, for all odd values of :math:`i`. + * ``"circular"`` entanglement is linear entanglement but with an additional entanglement of the + first and last qubit before the linear part. + * ``"sca"`` (shifted-circular-alternating) entanglement is a generalized and modified version + of the proposed circuit 14 in `Sim et al. `__. + It consists of circular entanglement where the "long" entanglement connecting the first with + the last qubit is shifted by one each block. Furthermore the role of control and target + qubits are swapped every block (therefore alternating). + + If an entanglement layer contains multiple blocks, then the entanglement should be + given as list of entanglements for each block. For example:: + + entanglement_blocks = ["rxx", "ryy"] + entanglement = ["full", "linear"] # full for rxx and linear for ryy + + or:: + + structure_rxx = [[0, 1], [2, 3]] + structure_ryy = [[0, 2]] + entanglement = [structure_rxx, structure_ryy] + + Finally, the entanglement can vary in each repetition of the circuit. For this, we + support passing a callable that takes as input the layer index and returns the entanglement + for the layer in the above format. See the examples below for a concrete example. + + Examples: + + The rotation and entanglement gates can be specified via single strings, if they + are made up of a single block per layer: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import n_local + + circuit = n_local(3, "ry", "cx", "linear", reps=2, insert_barriers=True) + circuit.draw("mpl") + + Multiple gates per layer can be set by passing a list. Here, for example, we use + Pauli-Y and Pauli-Z rotations in the rotation layer: + + .. plot:: + :include-source: + :context: + + circuit = n_local(3, ["ry", "rz"], "cz", "full", reps=1, insert_barriers=True) + circuit.draw("mpl") + + To omit rotation or entanglement layers, the block can be set to an empty list: + + .. plot:: + :include-source: + :context: + + circuit = n_local(4, [], "cry", reps=2) + circuit.draw("mpl") + + The entanglement can be set explicitly via the ``entanglement`` argument: + + .. plot:: + :include-source: + :context: + + entangler_map = [[0, 1], [2, 0]] + circuit = n_local(3, "x", "crx", entangler_map, reps=2) + circuit.draw("mpl") + + We can set different entanglements per layer, by specifing a callable that takes + as input the current layer index, and returns the entanglement structure. For example, + the following uses different entanglements for odd and even layers: + + .. plot: + :include-source: + :context: + + def entanglement(layer_index): + if layer_index % 2 == 0: + return [[0, 1], [0, 2]] + return [[1, 2]] + + circuit = n_local(3, "x", "cx", entanglement, reps=3, insert_barriers=True) + circuit.draw("mpl") + + + Args: + num_qubits: The number of qubits of the circuit. + rotation_blocks: The blocks used in the rotation layers. If multiple are passed, + these will be applied one after another (like new sub-layers). + entanglement_blocks: The blocks used in the entanglement layers. If multiple are passed, + these will be applied one after another. + entanglement: The indices specifying on which qubits the input blocks act. This is + specified by string describing an entanglement strategy (see the additional info) + or a list of qubit connections. + If a list of entanglement blocks is passed, different entanglement for each block can + be specified by passing a list of entanglements. To specify varying entanglement for + each repetition, pass a callable that takes as input the layer and returns the + entanglement for that layer. + Defaults to ``"full"``, meaning an all-to-all entanglement structure. + reps: Specifies how often the rotation blocks and entanglement blocks are repeated. + insert_barriers: If ``True``, barriers are inserted in between each layer. If ``False``, + no barriers are inserted. + parameter_prefix: The prefix used if default parameters are generated. + overwrite_block_parameters: If the parameters in the added blocks should be overwritten. + If ``False``, the parameters in the blocks are not changed. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + name: The name of the circuit. + + Returns: + An n-local circuit. + """ + if reps < 0: + # this is an important check, since we cast this to an unsigned integer Rust-side + raise ValueError(f"reps must be non-negative, but is {reps}") + + supported_gates = get_standard_gate_name_mapping() + rotation_blocks = _normalize_blocks( + rotation_blocks, supported_gates, overwrite_block_parameters + ) + entanglement_blocks = _normalize_blocks( + entanglement_blocks, supported_gates, overwrite_block_parameters + ) + + entanglement = _normalize_entanglement(entanglement, len(entanglement_blocks)) + + data = py_n_local( + num_qubits=num_qubits, + rotation_blocks=rotation_blocks, + entanglement_blocks=entanglement_blocks, + entanglement=entanglement, + reps=reps, + insert_barriers=insert_barriers, + parameter_prefix=parameter_prefix, + skip_final_rotation_layer=skip_final_rotation_layer, + skip_unentangled_qubits=skip_unentangled_qubits, + ) + circuit = QuantumCircuit._from_circuit_data(data, add_regs=True, name=name) + + return circuit + class NLocal(BlueprintCircuit): """The n-local circuit class. @@ -76,8 +296,18 @@ class NLocal(BlueprintCircuit): If specified, barriers can be inserted in between every block. If an initial state object is provided, it is added in front of the NLocal. + + .. seealso:: + + The :func:`.n_local` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.n_local instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, @@ -1069,3 +1299,174 @@ def _stdlib_gate_from_simple_block(block: QuantumCircuit) -> _StdlibGateResult | ): return None return _StdlibGateResult(instruction.operation.base_class, len(instruction.operation.params)) + + +def _normalize_entanglement( + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ), + num_entanglement_blocks: int, +) -> list[str | list[tuple[int]]] | Callable[[int], list[str | list[tuple[int]]]]: + """If the entanglement is Iterable[Iterable], normalize to list[tuple].""" + if isinstance(entanglement, str): + return [entanglement] * num_entanglement_blocks + + if callable(entanglement): + return lambda offset: _normalize_entanglement(entanglement(offset), num_entanglement_blocks) + + # here, entanglement is an Iterable + if len(entanglement) == 0: + # handle edge cases when entanglement is set to an empty list + return [[]] + + # if the entanglement is Iterable[Iterable[int]], normalize to Iterable[Iterable[Iterable[int]]] + try: + # if users e.g. gave Iterable[int] this in invalid and will raise a TypeError + if isinstance(entanglement[0][0], (int, numpy.integer)): + entanglement = [entanglement] + except TypeError as exc: + raise TypeError(f"Invalid entanglement type: {entanglement}.") from exc + + # ensure the number of block entanglements matches the number of blocks + if len(entanglement) != num_entanglement_blocks: + raise QiskitError( + f"Number of block-entanglements ({len(entanglement)}) must match number of " + f"entanglement blocks ({num_entanglement_blocks})!" + ) + + # normalize the data: str remains, and Iterable[Iterable[int]] becomes list[tuple[int]] + normalized = [] + for block in entanglement: + if isinstance(block, str): + normalized.append(block) + else: + normalized.append([tuple(connections) for connections in block]) + + return normalized + + +def _normalize_blocks( + blocks: str | Gate | Iterable[str | Gate], + supported_gates: dict[str, Gate], + overwrite_block_parameters: bool, +) -> list[Block]: + # normalize the input into an iterable -- we add an extra check for a circuit as + # courtesy to the users, since the NLocal class used to accept circuits + if isinstance(blocks, (str, Gate, QuantumCircuit)): + blocks = [blocks] + + normalized = [] + for block in blocks: + # since the NLocal circuit accepted circuits as inputs, we raise a warning here + # to simplify the transition (even though, strictly speaking, quantum circuits are + # not a supported input type) + if isinstance(block, QuantumCircuit): + raise ValueError( + "The blocks should be of type Gate or str, but you passed a QuantumCircuit. " + "You can call .to_gate() on the circuit to turn it into a Gate object." + ) + + is_standard = False + if isinstance(block, str): + if block not in supported_gates: + raise ValueError(f"Unsupported gate: {block}") + block = supported_gates[block] + is_standard = True + elif isinstance(block, Gate) and getattr(block, "_standard_gate", None) is not None: + if len(block.params) == 0: + is_standard = True + # the fast path will always overwrite block parameters + elif overwrite_block_parameters: + # if all parameters are plain Parameter objects, this is a plain + # standard gate we do not need to propagate parameterizations for + is_standard = all(isinstance(p, Parameter) for p in block.params) + + if is_standard: + block = Block.from_standard_gate(block._standard_gate) + else: + if overwrite_block_parameters: + num_parameters, builder = _get_gate_builder(block) + else: + num_parameters, builder = _trivial_builder(block) + + block = Block.from_callable(block.num_qubits, num_parameters, builder) + + normalized.append(block) + + return normalized + + +def _trivial_builder( + gate: Gate, +) -> tuple[int, Callable[list[Parameter], tuple[Gate, list[ParameterValueType]]]]: + + def builder(_): + copied = gate.copy() + return copied, copied.params + + return 0, builder + + +def _get_gate_builder( + gate: Gate, +) -> tuple[int, Callable[list[Parameter], tuple[Gate, list[ParameterValueType]]]]: + """Construct a callable that handles parameter-rebinding. + + For a given gate, this return the number of free parameters and a callable that can be + used to obtain a re-parameterized version of the gate. For example:: + + x, y = Parameter("x"), Parameter("y") + gate = CUGate(x, 2 * y, 0.5, 0.) + + num_parameters, builder = _build_gate(gate) + print(num_parameters) # prints 2 + + a, b = Parameter("a"), Parameter("b") + new_gate, new_params = builder([a, b]) + print(new_gate) # CUGate(a, 2 * b, 0.5, 0) + print(new_params) # [a, 2 * b, 0.5, 0] + + """ + free_parameters = set() + for p in gate.params: + if isinstance(p, ParameterExpression): + free_parameters |= set(p.parameters) + + num_parameters = len(free_parameters) + + sorted_parameters = _sort_parameters(free_parameters) + + def builder(new_parameters): + out = gate.copy() + + # re-bind the ``Gate.params`` attribute + param_dict = dict(zip(sorted_parameters, new_parameters)) + bound_params = gate.params.copy() + for i, expr in enumerate(gate.params): + if isinstance(expr, ParameterExpression): + for parameter in expr.parameters: + expr = expr.assign(parameter, param_dict[parameter]) + bound_params[i] = expr + + out.params = bound_params + + # if the definition exists, rebind it + if out._definition is not None: + out._definition.assign_parameters(param_dict, inplace=True) + + return out, bound_params + + return num_parameters, builder + + +def _sort_parameters(parameters): + """Sort a list of Parameter objects.""" + + def key(parameter): + if isinstance(parameter, ParameterVectorElement): + return (parameter.vector.name, parameter.index) + return (parameter.name,) + + return sorted(parameters, key=key) diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index 8bb003f4f690..f0deeb68b287 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -16,9 +16,104 @@ import numpy as np from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library.standard_gates import RXGate, RYGate, RZGate, CZGate +from qiskit.utils.deprecation import deprecate_func +from qiskit._accelerate.circuit_library import Block, py_n_local +from .two_local import TwoLocal -from .two_local import TwoLocal +def pauli_two_design( + num_qubits: int, + reps: int = 3, + seed: int | None = None, + insert_barriers: bool = False, + parameter_prefix: str = "θ", + name: str = "PauliTwoDesign", +) -> QuantumCircuit: + r"""Construct a Pauli 2-design ansatz. + + This class implements a particular form of a 2-design circuit [1], which is frequently studied + in quantum machine learning literature, such as, e.g., the investigation of Barren plateaus in + variational algorithms [2]. + + The circuit consists of alternating rotation and entanglement layers with + an initial layer of :math:`\sqrt{H} = RY(\pi/4)` gates. + The rotation layers contain single qubit Pauli rotations, where the axis is chosen uniformly + at random to be X, Y or Z. The entanglement layers is compromised of pairwise CZ gates + with a total depth of 2. + + For instance, the circuit could look like this: + + .. parsed-literal:: + + ┌─────────┐┌──────────┐ ░ ┌──────────┐ ░ ┌──────────┐ + q_0: ┤ RY(π/4) ├┤ RZ(θ[0]) ├─■─────░─┤ RY(θ[4]) ├─■─────░──┤ RZ(θ[8]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ├──────────┤ + q_1: ┤ RY(π/4) ├┤ RZ(θ[1]) ├─■──■──░─┤ RY(θ[5]) ├─■──■──░──┤ RX(θ[9]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ┌┴──────────┤ + q_2: ┤ RY(π/4) ├┤ RX(θ[2]) ├─■──■──░─┤ RY(θ[6]) ├─■──■──░─┤ RX(θ[10]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ├───────────┤ + q_3: ┤ RY(π/4) ├┤ RZ(θ[3]) ├─■─────░─┤ RX(θ[7]) ├─■─────░─┤ RY(θ[11]) ├ + └─────────┘└──────────┘ ░ └──────────┘ ░ └───────────┘ + + Examples: + + .. plot:: + :include-source: + + from qiskit.circuit.library import pauli_two_design + circuit = pauli_two_design(4, reps=2, seed=5, insert_barriers=True) + circuit.draw("mpl") + + Args: + num_qubits: The number of qubits of the Pauli Two-Design circuit. + reps: Specifies how often a block consisting of a rotation layer and entanglement + layer is repeated. + seed: The seed for randomly choosing the axes of the Pauli rotations. + parameter_prefix: The prefix used for the rotation parameters. + insert_barriers: If ``True``, barriers are inserted in between each layer. If ``False``, + no barriers are inserted. Defaults to ``False``. + name: The circuit name. + + Returns: + A Pauli 2-design circuit. + + References: + + [1]: Nakata et al., Unitary 2-designs from random X- and Z-diagonal unitaries. + `arXiv:1502.07514 `_ + + [2]: McClean et al., Barren plateaus in quantum neural network training landscapes. + `arXiv:1803.11173 `_ + """ + rng = np.random.default_rng(seed) + random_block = Block.from_callable(1, 1, lambda params: _random_pauli_builder(params, rng)) + cz_block = Block.from_standard_gate(CZGate._standard_gate) + + data = py_n_local( + num_qubits=num_qubits, + reps=reps, + rotation_blocks=[random_block], + entanglement_blocks=[cz_block], + entanglement=["pairwise"], + insert_barriers=insert_barriers, + skip_final_rotation_layer=False, + skip_unentangled_qubits=False, + parameter_prefix=parameter_prefix, + ) + two_design = QuantumCircuit._from_circuit_data(data) + + circuit = QuantumCircuit(num_qubits, name=name) + circuit.ry(np.pi / 4, circuit.qubits) + circuit.compose(two_design, inplace=True, copy=False) + + return circuit + + +def _random_pauli_builder(params, rng): + gate_cls = rng.choice([RXGate, RYGate, RZGate]) + gate = gate_cls(params[0]) + return gate, gate.params class PauliTwoDesign(TwoLocal): @@ -58,6 +153,10 @@ class PauliTwoDesign(TwoLocal): circuit = PauliTwoDesign(4, reps=2, seed=5, insert_barriers=True) circuit.draw('mpl') + .. seealso:: + + The :func:`.pauli_two_design` function constructs the functionally same circuit, but faster. + References: [1]: Nakata et al., Unitary 2-designs from random X- and Z-diagonal unitaries. @@ -67,6 +166,11 @@ class PauliTwoDesign(TwoLocal): `arXiv:1803.11173 `_ """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.pauli_two_design instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, @@ -85,8 +189,6 @@ def __init__( no barriers are inserted. Defaults to ``False``. """ - from qiskit.circuit.library import RYGate # pylint: disable=cyclic-import - # store a random number generator self._seed = seed self._rng = np.random.default_rng(seed) diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index 43869c0c54c9..b200d7ea7e15 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -21,8 +21,87 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .evolved_operator_ansatz import ( + EvolvedOperatorAnsatz, + _is_pauli_identity, + evolved_operator_ansatz, +) + + +def qaoa_ansatz( + cost_operator: BaseOperator, + reps: int = 1, + initial_state: QuantumCircuit | None = None, + mixer_operator: BaseOperator | None = None, + insert_barriers: bool = False, + name: str = "QAOA", + flatten: bool = True, +) -> QuantumCircuit: + r"""A generalized QAOA quantum circuit with a support of custom initial states and mixers. + + Examples: + + To define the QAOA ansatz we require a cost Hamiltonian, encoding the classical + optimization problem: + + .. plot:: + :include-source: + + from qiskit.quantum_info import SparsePauliOp + from qiskit.circuit.library import qaoa_ansatz + + cost_operator = SparsePauliOp(["ZZII", "IIZZ", "ZIIZ"]) + ansatz = qaoa_ansatz(cost_operator, reps=3, insert_barriers=True) + ansatz.draw("mpl") + + Args: + cost_operator: The operator representing the cost of the optimization problem, denoted as + :math:`U(C, \gamma)` in [1]. + reps: The integer determining the depth of the circuit, called :math:`p` in [1]. + initial_state: An optional initial state to use, which defaults to a layer of + Hadamard gates preparing the :math:`|+\rangle^{\otimes n}` state. + If a custom mixer is chosen, this circuit should be set to prepare its ground state, + to appropriately fulfill the annealing conditions. + mixer_operator: An optional custom mixer, which defaults to global Pauli-:math:`X` + rotations. This is denoted as :math:`U(B, \beta)` in [1]. If this is set, + the ``initial_state`` might also require modification. + insert_barriers: Whether to insert barriers in-between the cost and mixer operators. + name: The name of the circuit. + flatten: If ``True``, a flat circuit is returned instead of nesting it inside multiple + layers of gate objects. Setting this to ``False`` is significantly less performant, + especially for parameter binding, but can be desirable for a cleaner visualization. -from .evolved_operator_ansatz import EvolvedOperatorAnsatz, _is_pauli_identity + References: + + [1]: Farhi et al., A Quantum Approximate Optimization Algorithm. + `arXiv:1411.4028 `_ + """ + num_qubits = cost_operator.num_qubits + + if initial_state is None: + initial_state = QuantumCircuit(num_qubits) + initial_state.h(range(num_qubits)) + + if mixer_operator is None: + mixer_operator = SparsePauliOp.from_sparse_list( + [("X", [i], 1) for i in range(num_qubits)], num_qubits + ) + + parameter_prefix = ["γ", "β"] + + return initial_state.compose( + evolved_operator_ansatz( + [cost_operator, mixer_operator], + reps=reps, + insert_barriers=insert_barriers, + parameter_prefix=parameter_prefix, + name=name, + flatten=flatten, + ), + copy=False, + ) class QAOAAnsatz(EvolvedOperatorAnsatz): diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index 2b18bac0eb3d..f1ba10604e9d 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -13,15 +13,123 @@ """The real-amplitudes 2-local circuit.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import RYGate, CXGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal +def real_amplitudes( + num_qubits: int, + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "reverse_linear", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "RealAmplitudes", +) -> QuantumCircuit: + r"""Construct a real-amplitudes 2-local circuit. + + This circuit is a heuristic trial wave function used, e.g., as ansatz in chemistry, optimization + or machine learning applications. The circuit consists of alternating layers of :math:`Y` + rotations and :math:`CX` entanglements. The entanglement pattern can be user-defined or selected + from a predefined set. This circuit is "real amplitudes" since the prepared quantum states will + only have real amplitudes. + + For example a ``real_amplitudes`` circuit with 2 repetitions on 3 qubits with ``"reverse_linear"`` + entanglement is + + .. parsed-literal:: + + ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ + ┤ Ry(θ[0]) ├─░────────■───░─┤ Ry(θ[3]) ├─░────────■───░─┤ Ry(θ[6]) ├ + ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ + ┤ Ry(θ[1]) ├─░───■──┤ X ├─░─┤ Ry(θ[4]) ├─░───■──┤ X ├─░─┤ Ry(θ[7]) ├ + ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ + ┤ Ry(θ[2]) ├─░─┤ X ├──────░─┤ Ry(θ[5]) ├─░─┤ X ├──────░─┤ Ry(θ[8]) ├ + └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ + + The entanglement can be set using the ``entanglement`` keyword as string or a list of + index-pairs. See the documentation of :func:`.n_local`. Additional options that can be set include + the number of repetitions, skipping rotation gates on qubits that are not entangled, leaving out + the final rotation layer and inserting barriers in between the rotation and entanglement + layers. + + Examples: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import real_amplitudes + + ansatz = real_amplitudes(3, reps=2) # create the circuit on 3 qubits + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(3, entanglement="full", reps=2) # it is the same unitary as above + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(3, entanglement="linear", reps=2, insert_barriers=True) + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(4, reps=2, entanglement=[[0,3], [0,2]], skip_unentangled_qubits=True) + ansatz.draw("mpl") + + Args: + num_qubits: The number of qubits of the RealAmplitudes circuit. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + A real-amplitudes circuit. + """ + + return n_local( + num_qubits, + ["ry"], + ["cx"], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class RealAmplitudes(TwoLocal): r"""The real-amplitudes 2-local circuit. @@ -59,7 +167,7 @@ class RealAmplitudes(TwoLocal): Examples: >>> ansatz = RealAmplitudes(3, reps=2) # create the circuit on 3 qubits - >>> print(ansatz) + >>> print(ansatz.decompose()) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├──────────■──────┤ Ry(θ[3]) ├──────────■──────┤ Ry(θ[6]) ├ ├──────────┤ ┌─┴─┐ ├──────────┤ ┌─┴─┐ ├──────────┤ @@ -68,7 +176,7 @@ class RealAmplitudes(TwoLocal): q_2: ┤ Ry(θ[2]) ├┤ X ├┤ Ry(θ[5]) ├────────────┤ X ├┤ Ry(θ[8]) ├──────────── └──────────┘└───┘└──────────┘ └───┘└──────────┘ - >>> ansatz = RealAmplitudes(3, entanglement='full', reps=2) # it is the same unitary as above + >>> ansatz = RealAmplitudes(3, entanglement='full', reps=2, flatten=True) >>> print(ansatz) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ RY(θ[0]) ├──■────■──┤ RY(θ[3]) ├──────────────■────■──┤ RY(θ[6]) ├──────────── @@ -78,7 +186,8 @@ class RealAmplitudes(TwoLocal): q_2: ┤ RY(θ[2]) ├─────┤ X ├───┤ X ├────┤ RY(θ[5]) ├─────┤ X ├───┤ X ├────┤ RY(θ[8]) ├ └──────────┘ └───┘ └───┘ └──────────┘ └───┘ └───┘ └──────────┘ - >>> ansatz = RealAmplitudes(3, entanglement='linear', reps=2, insert_barriers=True) + >>> ansatz = RealAmplitudes(3, entanglement='linear', reps=2, insert_barriers=True, + ... flatten=True) >>> qc = QuantumCircuit(3) # create a circuit and append the RY variational form >>> qc.compose(ansatz, inplace=True) >>> qc.draw() @@ -90,7 +199,8 @@ class RealAmplitudes(TwoLocal): q_2: ┤ RY(θ[2]) ├─░──────┤ X ├─░─┤ RY(θ[5]) ├─░──────┤ X ├─░─┤ RY(θ[8]) ├ └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ - >>> ansatz = RealAmplitudes(4, reps=1, entanglement='circular', insert_barriers=True) + >>> ansatz = RealAmplitudes(4, reps=1, entanglement='circular', insert_barriers=True, + ... flatten=True) >>> print(ansatz) ┌──────────┐ ░ ┌───┐ ░ ┌──────────┐ q_0: ┤ RY(θ[0]) ├─░─┤ X ├──■─────────────░─┤ RY(θ[4]) ├ @@ -103,7 +213,7 @@ class RealAmplitudes(TwoLocal): └──────────┘ ░ └───┘ ░ └──────────┘ >>> ansatz = RealAmplitudes(4, reps=2, entanglement=[[0,3], [0,2]], - ... skip_unentangled_qubits=True) + ... skip_unentangled_qubits=True, flatten=True) >>> print(ansatz) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ RY(θ[0]) ├──■───────■──────┤ RY(θ[3]) ├──■───────■──────┤ RY(θ[6]) ├ @@ -115,8 +225,17 @@ class RealAmplitudes(TwoLocal): q_3: ┤ RY(θ[2]) ├┤ X ├┤ RY(θ[5]) ├────────────┤ X ├┤ RY(θ[8]) ├──────────── └──────────┘└───┘└──────────┘ └───┘└──────────┘ + .. seealso:: + + The :func:`.real_amplitudes` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.real_amplitudes instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index f3822d53243f..fcbc5e30bb8d 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -18,6 +18,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit import Gate, Instruction +from qiskit.utils.deprecation import deprecate_func from .n_local import NLocal from ..standard_gates import get_standard_gate_name_mapping @@ -76,7 +77,7 @@ class TwoLocal(NLocal): Examples: >>> two = TwoLocal(3, 'ry', 'cx', 'linear', reps=2, insert_barriers=True) - >>> print(two) # decompose the layers into standard gates + >>> print(two.decompose()) # decompose the layers into standard gates ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├─░───■────────░─┤ Ry(θ[3]) ├─░───■────────░─┤ Ry(θ[6]) ├ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ @@ -85,10 +86,10 @@ class TwoLocal(NLocal): q_2: ┤ Ry(θ[2]) ├─░──────┤ X ├─░─┤ Ry(θ[5]) ├─░──────┤ X ├─░─┤ Ry(θ[8]) ├ └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ - >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True) + >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True, flatten=True) >>> qc = QuantumCircuit(3) >>> qc &= two - >>> print(qc.decompose().draw()) + >>> print(qc.draw()) ┌──────────┐┌──────────┐ ░ ░ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├─░──■──■─────░─┤ Ry(θ[6]) ├─┤ Rz(θ[9]) ├ ├──────────┤├──────────┤ ░ │ │ ░ ├──────────┤┌┴──────────┤ @@ -98,7 +99,7 @@ class TwoLocal(NLocal): └──────────┘└──────────┘ ░ ░ └──────────┘└───────────┘ >>> entangler_map = [[0, 1], [1, 2], [2, 0]] # circular entanglement for 3 qubits - >>> two = TwoLocal(3, 'x', 'crx', entangler_map, reps=1) + >>> two = TwoLocal(3, 'x', 'crx', entangler_map, reps=1, flatten=True) >>> print(two) # note: no barriers inserted this time! ┌───┐ ┌──────────┐┌───┐ q_0: |0>┤ X ├─────■───────────────────────┤ Rx(θ[2]) ├┤ X ├ @@ -109,9 +110,9 @@ class TwoLocal(NLocal): └───┘ └──────────┘ └───┘ >>> entangler_map = [[0, 3], [0, 2]] # entangle the first and last two-way - >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1) + >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1, flatten=True) >>> circuit = two.compose(two) - >>> print(circuit.decompose().draw()) # note, that the parameters are the same! + >>> print(circuit.draw()) # note, that the parameters are the same! q_0: ─────■───────────■───────────■───────────■────── │ │ │ │ q_1: ─────┼───────────┼───────────┼───────────┼────── @@ -123,7 +124,8 @@ class TwoLocal(NLocal): >>> layer_1 = [(0, 1), (0, 2)] >>> layer_2 = [(1, 2)] - >>> two = TwoLocal(3, 'x', 'cx', [layer_1, layer_2], reps=2, insert_barriers=True) + >>> two = TwoLocal(3, 'x', 'cx', [layer_1, layer_2], reps=2, insert_barriers=True, + ... flatten=True) >>> print(two) ┌───┐ ░ ░ ┌───┐ ░ ░ ┌───┐ q_0: ┤ X ├─░───■────■───░─┤ X ├─░───────░─┤ X ├ @@ -135,6 +137,11 @@ class TwoLocal(NLocal): """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.n_local instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/overlap.py b/qiskit/circuit/library/overlap.py index f6ae5fd6ebdd..6f444bebf0fb 100644 --- a/qiskit/circuit/library/overlap.py +++ b/qiskit/circuit/library/overlap.py @@ -16,6 +16,7 @@ from qiskit.circuit.parametervector import ParameterVector from qiskit.circuit.exceptions import CircuitError from qiskit.circuit import Barrier +from qiskit.utils.deprecation import deprecate_func class UnitaryOverlap(QuantumCircuit): @@ -41,13 +42,13 @@ class UnitaryOverlap(QuantumCircuit): from qiskit.circuit.library import EfficientSU2, UnitaryOverlap from qiskit.primitives import Sampler - # get two circuit to prepare states of which we comput the overlap + # get two circuit to prepare states of which we compute the overlap circuit = EfficientSU2(2, reps=1) unitary1 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) unitary2 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) # create the overlap circuit - overlap = UnitaryOverap(unitary1, unitary2) + overlap = UnitaryOverlap(unitary1, unitary2) # sample from the overlap sampler = Sampler(options={"shots": 100}) @@ -58,6 +59,11 @@ class UnitaryOverlap(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.unitary_overlap instead.", + pending=True, + ) def __init__( self, unitary1: QuantumCircuit, @@ -80,30 +86,9 @@ def __init__( CircuitError: Number of qubits in ``unitary1`` and ``unitary2`` does not match. CircuitError: Inputs contain measurements and/or resets. """ - # check inputs are valid - if unitary1.num_qubits != unitary2.num_qubits: - raise CircuitError( - f"Number of qubits in unitaries does " - f"not match: {unitary1.num_qubits} != {unitary2.num_qubits}." - ) - - unitaries = [unitary1, unitary2] - for unitary in unitaries: - _check_unitary(unitary) - - # Vectors of new parameters, if any. Need the unitaries in a list here to ensure - # we can overwrite them. - for i, prefix in enumerate([prefix1, prefix2]): - if unitaries[i].num_parameters > 0: - new_params = ParameterVector(prefix, unitaries[i].num_parameters) - unitaries[i] = unitaries[i].assign_parameters(new_params) - - # 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) + circuit = unitary_overlap(unitary1, unitary2, prefix1, prefix2, insert_barrier) + super().__init__(*circuit.qregs, name=circuit.name) + self.compose(circuit, qubits=self.qubits, inplace=True) def _check_unitary(circuit): @@ -115,3 +100,83 @@ def _check_unitary(circuit): "One or more instructions cannot be converted to" f' a gate. "{instruction.operation.name}" is not a gate instruction' ) + + +def unitary_overlap( + unitary1: QuantumCircuit, + unitary2: QuantumCircuit, + prefix1: str = "p1", + prefix2: str = "p2", + insert_barrier: bool = False, +) -> QuantumCircuit: + r"""Circuit that returns the overlap between two unitaries :math:`U_2^{\dag} U_1`. + + The input quantum circuits must represent unitary operations, since they must be invertible. + If the inputs will have parameters, they are replaced by :class:`.ParameterVector`\s with + names `"p1"` (for circuit ``unitary1``) and `"p2"` (for circuit ``unitary_2``) in the output + circuit. + + This circuit is usually employed in computing the fidelity: + + .. math:: + + \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|`. + + **Reference Circuit:** + + .. plot:: + :include-source: + + import numpy as np + from qiskit.circuit.library import EfficientSU2, unitary_overlap + + # get two circuit to prepare states of which we compute the overlap + circuit = EfficientSU2(2, reps=1) + unitary1 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) + unitary2 = circuit.assign_parameters(np.random.random(circuit.num_parameters)) + + # create the overlap circuit + overlap = unitary_overlap(unitary1, unitary2) + overlap.draw('mpl') + + Args: + unitary1: Unitary acting on the ket vector. + unitary2: Unitary whose inverse operates on the bra vector. + prefix1: The name of the parameter vector associated to ``unitary1``, + 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. + CircuitError: Inputs contain measurements and/or resets. + """ + # check inputs are valid + if unitary1.num_qubits != unitary2.num_qubits: + raise CircuitError( + f"Number of qubits in unitaries does " + f"not match: {unitary1.num_qubits} != {unitary2.num_qubits}." + ) + + unitaries = [unitary1, unitary2] + for unitary in unitaries: + _check_unitary(unitary) + + # Vectors of new parameters, if any. Need the unitaries in a list here to ensure + # we can overwrite them. + for i, prefix in enumerate([prefix1, prefix2]): + if unitaries[i].num_parameters > 0: + new_params = ParameterVector(prefix, unitaries[i].num_parameters) + unitaries[i] = unitaries[i].assign_parameters(new_params) + + # Generate the actual overlap circuit + circuit = QuantumCircuit(unitaries[0].num_qubits, name="UnitaryOverlap") + circuit.compose(unitaries[0], inplace=True) + if insert_barrier: + circuit.barrier() + circuit.compose(unitaries[1].inverse(), inplace=True) + return circuit diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index d954f283dfd0..e5c0c46bd312 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -14,10 +14,11 @@ from __future__ import annotations -from typing import Union, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING import numpy as np from qiskit.circuit.gate import Gate +from qiskit.circuit.quantumcircuit import ParameterValueType from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.quantum_info import Pauli, SparsePauliOp @@ -89,10 +90,10 @@ class PauliEvolutionGate(Gate): def __init__( self, - operator, - time: Union[int, float, ParameterExpression] = 1.0, - label: Optional[str] = None, - synthesis: Optional[EvolutionSynthesis] = None, + operator: Pauli | SparsePauliOp | list[Pauli | SparsePauliOp], + time: ParameterValueType = 1.0, + label: str | None = None, + synthesis: EvolutionSynthesis | None = None, ) -> None: """ Args: @@ -113,21 +114,23 @@ class docstring for an example. else: operator = _to_sparse_pauli_op(operator) - if synthesis is None: - from qiskit.synthesis.evolution import LieTrotter - - synthesis = LieTrotter() - if label is None: label = _get_default_label(operator) num_qubits = operator[0].num_qubits if isinstance(operator, list) else operator.num_qubits super().__init__(name="PauliEvolution", num_qubits=num_qubits, params=[time], label=label) self.operator = operator + + if synthesis is None: + # pylint: disable=cyclic-import + from qiskit.synthesis.evolution import LieTrotter + + synthesis = LieTrotter() + self.synthesis = synthesis @property - def time(self) -> Union[float, ParameterExpression]: + def time(self) -> ParameterValueType: """Return the evolution time as stored in the gate parameters. Returns: @@ -136,7 +139,7 @@ def time(self) -> Union[float, ParameterExpression]: return self.params[0] @time.setter - def time(self, time: Union[float, ParameterExpression]) -> None: + def time(self, time: ParameterValueType) -> None: """Set the evolution time. Args: @@ -148,9 +151,7 @@ def _define(self): """Unroll, where the default synthesis is matrix based.""" self.definition = self.synthesis.synthesize(self) - def validate_parameter( - self, parameter: Union[int, float, ParameterExpression] - ) -> Union[float, ParameterExpression]: + def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueType: """Gate parameters should be int, float, or ParameterExpression""" if isinstance(parameter, int): parameter = float(parameter) diff --git a/qiskit/circuit/library/phase_estimation.py b/qiskit/circuit/library/phase_estimation.py index 3311adf46324..8ff441eb705e 100644 --- a/qiskit/circuit/library/phase_estimation.py +++ b/qiskit/circuit/library/phase_estimation.py @@ -12,11 +12,11 @@ """Phase estimation circuit.""" -from typing import Optional +from __future__ import annotations from qiskit.circuit import QuantumCircuit, QuantumRegister - -from .basis_change import QFT +from qiskit.utils.deprecation import deprecate_func +from qiskit.circuit.library import QFT class PhaseEstimation(QuantumCircuit): @@ -49,11 +49,16 @@ class PhaseEstimation(QuantumCircuit): """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.phase_estimation instead.", + pending=True, + ) def __init__( self, num_evaluation_qubits: int, unitary: QuantumCircuit, - iqft: Optional[QuantumCircuit] = None, + iqft: QuantumCircuit | None = None, name: str = "QPE", ) -> None: """ @@ -97,3 +102,74 @@ def __init__( super().__init__(*circuit.qregs, name=circuit.name) self.compose(circuit.to_gate(), qubits=self.qubits, inplace=True) + + +def phase_estimation( + num_evaluation_qubits: int, + unitary: QuantumCircuit, + name: str = "QPE", +) -> QuantumCircuit: + r"""Phase Estimation circuit. + + In the Quantum Phase Estimation (QPE) algorithm [1, 2, 3], the Phase Estimation circuit is used + to estimate the phase :math:`\phi` of an eigenvalue :math:`e^{2\pi i\phi}` of a unitary operator + :math:`U`, provided with the corresponding eigenstate :math:`|\psi\rangle`. + That is + + .. math:: + + U|\psi\rangle = e^{2\pi i\phi} |\psi\rangle + + This estimation (and thereby this circuit) is a central routine to several well-known + algorithms, such as Shor's algorithm or Quantum Amplitude Estimation. + + Args: + num_evaluation_qubits: The number of evaluation qubits. + unitary: The unitary operation :math:`U` which will be repeated and controlled. + name: The name of the circuit. + + **Reference Circuit:** + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import phase_estimation + unitary = QuantumCircuit(2) + unitary.x(0) + unitary.y(1) + circuit = phase_estimation(3, unitary) + circuit.draw('mpl') + + **References:** + + [1]: Kitaev, A. Y. (1995). Quantum measurements and the Abelian Stabilizer Problem. 1–22. + `quant-ph/9511026 `_ + + [2]: Michael A. Nielsen and Isaac L. Chuang. 2011. + Quantum Computation and Quantum Information: 10th Anniversary Edition (10th ed.). + Cambridge University Press, New York, NY, USA. + + [3]: Qiskit + `textbook `_ + + """ + # pylint: disable=cyclic-import + from qiskit.circuit.library import PermutationGate, QFTGate + + qr_eval = QuantumRegister(num_evaluation_qubits, "eval") + qr_state = QuantumRegister(unitary.num_qubits, "q") + circuit = QuantumCircuit(qr_eval, qr_state, name=name) + + circuit.h(qr_eval) # hadamards on evaluation qubits + + for j in range(num_evaluation_qubits): # controlled powers + circuit.compose(unitary.power(2**j).control(), qubits=[j] + qr_state[:], inplace=True) + + circuit.append(QFTGate(num_evaluation_qubits).inverse(), qr_eval[:]) + + reversal_pattern = list(reversed(range(num_evaluation_qubits))) + circuit.append(PermutationGate(reversal_pattern), qr_eval[:]) + + return circuit diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index 19a2bf770da2..be0e9dd04449 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -48,7 +48,26 @@ def get_standard_gate_name_mapping(): """Return a dictionary mapping the name of standard gates and instructions to an object for - that name.""" + that name. + + Examples: + + .. code-block:: python + + from qiskit.circuit.library import get_standard_gate_name_mapping + + gate_name_map = get_standard_gate_name_mapping() + cx_object = gate_name_map["cx"] + + print(cx_object) + print(type(cx_object)) + + .. code-block:: text + + Instruction(name='cx', num_qubits=2, num_clbits=0, params=[]) + _SingletonCXGate + """ + from qiskit.circuit.parameter import Parameter from qiskit.circuit.measure import Measure from qiskit.circuit.delay import Delay diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 9f689e46ca2b..a3ea7167a34f 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -36,32 +36,32 @@ class PhaseGate(Gate): .. code-block:: text ┌──────┐ - q_0: ┤ P(λ) ├ + q_0: ┤ P(θ) ├ └──────┘ **Matrix Representation:** .. math:: - P(\lambda) = + P(\theta) = \begin{pmatrix} 1 & 0 \\ - 0 & e^{i\lambda} + 0 & e^{i\theta} \end{pmatrix} **Examples:** .. math:: - P(\lambda = \pi) = Z + P(\theta = \pi) = Z .. math:: - P(\lambda = \pi/2) = S + P(\theta = \pi/2) = S .. math:: - P(\lambda = \pi/4) = T + P(\theta = \pi/4) = T .. seealso:: @@ -70,7 +70,7 @@ class PhaseGate(Gate): .. math:: - P(\lambda) = e^{i{\lambda}/2} RZ(\lambda) + P(\theta) = e^{i{\theta}/2} RZ(\theta) Reference for virtual Z gate implementation: `1612.00858 `_ @@ -175,7 +175,7 @@ class CPhaseGate(ControlledGate): q_0: ─■── - │λ + │θ q_1: ─■── @@ -189,7 +189,7 @@ class CPhaseGate(ControlledGate): 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\lambda} + 0 & 0 & 0 & e^{i\theta} \end{pmatrix} .. seealso:: diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index c1b16e1d99e4..e7efeafd24c5 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -178,20 +178,20 @@ class CRZGate(ControlledGate): q_0: ────■──── ┌───┴───┐ - q_1: ┤ Rz(λ) ├ + q_1: ┤ Rz(θ) ├ └───────┘ **Matrix representation:** .. math:: - CRZ(\lambda)\ q_0, q_1 = - I \otimes |0\rangle\langle 0| + RZ(\lambda) \otimes |1\rangle\langle 1| = + CRZ(\theta)\ q_0, q_1 = + I \otimes |0\rangle\langle 0| + RZ(\theta) \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & e^{-i\frac{\lambda}{2}} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\frac{\lambda}{2}} + 0 & 0 & 0 & e^{i\frac{\theta}{2}} \end{pmatrix} .. note:: @@ -205,19 +205,19 @@ class CRZGate(ControlledGate): .. code-block:: text ┌───────┐ - q_0: ┤ Rz(λ) ├ + q_0: ┤ Rz(θ) ├ └───┬───┘ q_1: ────■──── .. math:: - CRZ(\lambda)\ q_1, q_0 = - |0\rangle\langle 0| \otimes I + |1\rangle\langle 1| \otimes RZ(\lambda) = + CRZ(\theta)\ q_1, q_0 = + |0\rangle\langle 0| \otimes I + |1\rangle\langle 1| \otimes RZ(\theta) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ - 0 & 0 & e^{-i\frac{\lambda}{2}} & 0 \\ - 0 & 0 & 0 & e^{i\frac{\lambda}{2}} + 0 & 0 & e^{-i\frac{\theta}{2}} & 0 \\ + 0 & 0 & 0 & e^{i\frac{\theta}{2}} \end{pmatrix} .. seealso:: diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index e1657bdee8cc..b9bfa694cfeb 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -34,7 +34,7 @@ class U1Gate(Gate): .. math:: - U1(\lambda) = P(\lambda)= U(0,0,\lambda) + U1(\theta) = P(\theta)= U(0,0,\theta) .. code-block:: python @@ -49,32 +49,32 @@ class U1Gate(Gate): .. code-block:: text ┌───────┐ - q_0: ┤ U1(λ) ├ + q_0: ┤ U1(θ) ├ └───────┘ **Matrix Representation:** .. math:: - U1(\lambda) = + U1(\theta) = \begin{pmatrix} 1 & 0 \\ - 0 & e^{i\lambda} + 0 & e^{i\theta} \end{pmatrix} **Examples:** .. math:: - U1(\lambda = \pi) = Z + U1(\theta = \pi) = Z .. math:: - U1(\lambda = \pi/2) = S + U1(\theta = \pi/2) = S .. math:: - U1(\lambda = \pi/4) = T + U1(\theta = \pi/4) = T .. seealso:: @@ -83,7 +83,7 @@ class U1Gate(Gate): .. math:: - U1(\lambda) = e^{i{\lambda}/2} RZ(\lambda) + U1(\theta) = e^{i{\theta}/2} RZ(\theta) :class:`~qiskit.circuit.library.standard_gates.U3Gate`: U3 is a generalization of U2 that covers all single-qubit rotations, @@ -202,7 +202,7 @@ class CU1Gate(ControlledGate): q_0: ─■── - │λ + │θ q_1: ─■── @@ -210,13 +210,13 @@ class CU1Gate(ControlledGate): .. math:: - CU1(\lambda) = + CU1(\theta) = I \otimes |0\rangle\langle 0| + U1 \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ - 0 & 0 & 0 & e^{i\lambda} + 0 & 0 & 0 & e^{i\theta} \end{pmatrix} .. seealso:: diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index c7a8228dd463..8723445cdef4 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -87,6 +87,8 @@ def __init__( self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: symbol} self._name_map = None + self._qpy_replay = [] + self._standalone_param = True def assign(self, parameter, value): if parameter != self: @@ -172,3 +174,5 @@ def __setstate__(self, state): self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: self._symbol_expr} self._name_map = None + self._qpy_replay = [] + self._standalone_param = True diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 16b691480d26..fe786762c096 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -14,6 +14,9 @@ """ from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum from typing import Callable, Union import numbers @@ -30,12 +33,86 @@ ParameterValueType = Union["ParameterExpression", float] +class _OPCode(IntEnum): + ADD = 0 + SUB = 1 + MUL = 2 + DIV = 3 + POW = 4 + SIN = 5 + COS = 6 + TAN = 7 + ASIN = 8 + ACOS = 9 + EXP = 10 + LOG = 11 + SIGN = 12 + GRAD = 13 + CONJ = 14 + SUBSTITUTE = 15 + ABS = 16 + ATAN = 17 + RSUB = 18 + RDIV = 19 + RPOW = 20 + + +_OP_CODE_MAP = ( + "__add__", + "__sub__", + "__mul__", + "__truediv__", + "__pow__", + "sin", + "cos", + "tan", + "arcsin", + "arccos", + "exp", + "log", + "sign", + "gradient", + "conjugate", + "subs", + "abs", + "arctan", + "__rsub__", + "__rtruediv__", + "__rpow__", +) + + +def op_code_to_method(op_code: _OPCode): + """Return the method name for a given op_code.""" + return _OP_CODE_MAP[op_code] + + +@dataclass +class _INSTRUCTION: + op: _OPCode + lhs: ParameterValueType | None + rhs: ParameterValueType | None = None + + +@dataclass +class _SUBS: + binds: dict + op: _OPCode = _OPCode.SUBSTITUTE + + class ParameterExpression: """ParameterExpression class to enable creating expressions of Parameters.""" - __slots__ = ["_parameter_symbols", "_parameter_keys", "_symbol_expr", "_name_map"] + __slots__ = [ + "_parameter_symbols", + "_parameter_keys", + "_symbol_expr", + "_name_map", + "_qpy_replay", + "_standalone_param", + ] - def __init__(self, symbol_map: dict, expr): + def __init__(self, symbol_map: dict, expr, *, _qpy_replay=None): """Create a new :class:`ParameterExpression`. Not intended to be called directly, but to be instantiated via operations @@ -54,6 +131,11 @@ def __init__(self, symbol_map: dict, expr): self._parameter_keys = frozenset(p._hash_key() for p in self._parameter_symbols) self._symbol_expr = expr self._name_map: dict | None = None + self._standalone_param = False + if _qpy_replay is not None: + self._qpy_replay = _qpy_replay + else: + self._qpy_replay = [] @property def parameters(self) -> set: @@ -69,8 +151,14 @@ def _names(self) -> dict: def conjugate(self) -> "ParameterExpression": """Return the conjugate.""" + if self._standalone_param: + new_op = _INSTRUCTION(_OPCode.CONJ, self) + else: + new_op = _INSTRUCTION(_OPCode.CONJ, None) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) conjugated = ParameterExpression( - self._parameter_symbols, symengine.conjugate(self._symbol_expr) + self._parameter_symbols, symengine.conjugate(self._symbol_expr), _qpy_replay=new_replay ) return conjugated @@ -117,6 +205,7 @@ def bind( self._raise_if_passed_unknown_parameters(parameter_values.keys()) self._raise_if_passed_nan(parameter_values) + new_op = _SUBS(parameter_values) symbol_values = {} for parameter, value in parameter_values.items(): if (param_expr := self._parameter_symbols.get(parameter)) is not None: @@ -143,7 +232,12 @@ def bind( f"(Expression: {self}, Bindings: {parameter_values})." ) - return ParameterExpression(free_parameter_symbols, bound_symbol_expr) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) + + return ParameterExpression( + free_parameter_symbols, bound_symbol_expr, _qpy_replay=new_replay + ) def subs( self, parameter_map: dict, allow_unknown_parameters: bool = False @@ -175,6 +269,7 @@ def subs( for p in replacement_expr.parameters } self._raise_if_parameter_names_conflict(inbound_names, parameter_map.keys()) + new_op = _SUBS(parameter_map) # Include existing parameters in self not set to be replaced. new_parameter_symbols = { @@ -192,8 +287,12 @@ def subs( new_parameter_symbols[p] = symbol_type(p.name) substituted_symbol_expr = self._symbol_expr.subs(symbol_map) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) - return ParameterExpression(new_parameter_symbols, substituted_symbol_expr) + return ParameterExpression( + new_parameter_symbols, substituted_symbol_expr, _qpy_replay=new_replay + ) def _raise_if_passed_unknown_parameters(self, parameters): unknown_parameters = parameters - self.parameters @@ -231,7 +330,11 @@ def _raise_if_parameter_names_conflict(self, inbound_parameters, outbound_parame ) def _apply_operation( - self, operation: Callable, other: ParameterValueType, reflected: bool = False + self, + operation: Callable, + other: ParameterValueType, + reflected: bool = False, + op_code: _OPCode = None, ) -> "ParameterExpression": """Base method implementing math operations between Parameters and either a constant or a second ParameterExpression. @@ -253,7 +356,6 @@ def _apply_operation( A new expression describing the result of the operation. """ self_expr = self._symbol_expr - if isinstance(other, ParameterExpression): self._raise_if_parameter_names_conflict(other._names) parameter_symbols = {**self._parameter_symbols, **other._parameter_symbols} @@ -266,10 +368,26 @@ def _apply_operation( if reflected: expr = operation(other_expr, self_expr) + if op_code in {_OPCode.RSUB, _OPCode.RDIV, _OPCode.RPOW}: + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self, other) + else: + new_op = _INSTRUCTION(op_code, None, other) + else: + if self._standalone_param: + new_op = _INSTRUCTION(op_code, other, self) + else: + new_op = _INSTRUCTION(op_code, other, None) else: expr = operation(self_expr, other_expr) - - out_expr = ParameterExpression(parameter_symbols, expr) + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self, other) + else: + new_op = _INSTRUCTION(op_code, None, other) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) + + out_expr = ParameterExpression(parameter_symbols, expr, _qpy_replay=new_replay) out_expr._name_map = self._names.copy() if isinstance(other, ParameterExpression): out_expr._names.update(other._names.copy()) @@ -291,6 +409,13 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: # If it is not contained then return 0 return 0.0 + if self._standalone_param: + new_op = _INSTRUCTION(_OPCode.GRAD, self, param) + else: + new_op = _INSTRUCTION(_OPCode.GRAD, None, param) + qpy_replay = self._qpy_replay.copy() + qpy_replay.append(new_op) + # Compute the gradient of the parameter expression w.r.t. param key = self._parameter_symbols[param] expr_grad = symengine.Derivative(self._symbol_expr, key) @@ -304,7 +429,7 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: parameter_symbols[parameter] = symbol # If the gradient corresponds to a parameter expression then return the new expression. if len(parameter_symbols) > 0: - return ParameterExpression(parameter_symbols, expr=expr_grad) + return ParameterExpression(parameter_symbols, expr=expr_grad, _qpy_replay=qpy_replay) # If no free symbols left, return a complex or float gradient expr_grad_cplx = complex(expr_grad) if expr_grad_cplx.imag != 0: @@ -313,81 +438,89 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: return float(expr_grad) def __add__(self, other): - return self._apply_operation(operator.add, other) + return self._apply_operation(operator.add, other, op_code=_OPCode.ADD) def __radd__(self, other): - return self._apply_operation(operator.add, other, reflected=True) + return self._apply_operation(operator.add, other, reflected=True, op_code=_OPCode.ADD) def __sub__(self, other): - return self._apply_operation(operator.sub, other) + return self._apply_operation(operator.sub, other, op_code=_OPCode.SUB) def __rsub__(self, other): - return self._apply_operation(operator.sub, other, reflected=True) + return self._apply_operation(operator.sub, other, reflected=True, op_code=_OPCode.RSUB) def __mul__(self, other): - return self._apply_operation(operator.mul, other) + return self._apply_operation(operator.mul, other, op_code=_OPCode.MUL) def __pos__(self): - return self._apply_operation(operator.mul, 1) + return self._apply_operation(operator.mul, 1, op_code=_OPCode.MUL) def __neg__(self): - return self._apply_operation(operator.mul, -1) + return self._apply_operation(operator.mul, -1, op_code=_OPCode.MUL) def __rmul__(self, other): - return self._apply_operation(operator.mul, other, reflected=True) + return self._apply_operation(operator.mul, other, reflected=True, op_code=_OPCode.MUL) def __truediv__(self, other): if other == 0: raise ZeroDivisionError("Division of a ParameterExpression by zero.") - return self._apply_operation(operator.truediv, other) + return self._apply_operation(operator.truediv, other, op_code=_OPCode.DIV) def __rtruediv__(self, other): - return self._apply_operation(operator.truediv, other, reflected=True) + return self._apply_operation(operator.truediv, other, reflected=True, op_code=_OPCode.RDIV) def __pow__(self, other): - return self._apply_operation(pow, other) + return self._apply_operation(pow, other, op_code=_OPCode.POW) def __rpow__(self, other): - return self._apply_operation(pow, other, reflected=True) + return self._apply_operation(pow, other, reflected=True, op_code=_OPCode.RPOW) - def _call(self, ufunc): - return ParameterExpression(self._parameter_symbols, ufunc(self._symbol_expr)) + def _call(self, ufunc, op_code): + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self) + else: + new_op = _INSTRUCTION(op_code, None) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) + return ParameterExpression( + self._parameter_symbols, ufunc(self._symbol_expr), _qpy_replay=new_replay + ) def sin(self): """Sine of a ParameterExpression""" - return self._call(symengine.sin) + return self._call(symengine.sin, op_code=_OPCode.SIN) def cos(self): """Cosine of a ParameterExpression""" - return self._call(symengine.cos) + return self._call(symengine.cos, op_code=_OPCode.COS) def tan(self): """Tangent of a ParameterExpression""" - return self._call(symengine.tan) + return self._call(symengine.tan, op_code=_OPCode.TAN) def arcsin(self): """Arcsin of a ParameterExpression""" - return self._call(symengine.asin) + return self._call(symengine.asin, op_code=_OPCode.ASIN) def arccos(self): """Arccos of a ParameterExpression""" - return self._call(symengine.acos) + return self._call(symengine.acos, op_code=_OPCode.ACOS) def arctan(self): """Arctan of a ParameterExpression""" - return self._call(symengine.atan) + return self._call(symengine.atan, op_code=_OPCode.ATAN) def exp(self): """Exponential of a ParameterExpression""" - return self._call(symengine.exp) + return self._call(symengine.exp, op_code=_OPCode.EXP) def log(self): """Logarithm of a ParameterExpression""" - return self._call(symengine.log) + return self._call(symengine.log, op_code=_OPCode.LOG) def sign(self): """Sign of a ParameterExpression""" - return self._call(symengine.sign) + return self._call(symengine.sign, op_code=_OPCode.SIGN) def __repr__(self): return f"{self.__class__.__name__}({str(self)})" @@ -455,7 +588,7 @@ def __deepcopy__(self, memo=None): def __abs__(self): """Absolute of a ParameterExpression""" - return self._call(symengine.Abs) + return self._call(symengine.Abs, _OPCode.ABS) def abs(self): """Absolute of a ParameterExpression""" diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 557940e024c1..dee2f3e72276 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1177,9 +1177,11 @@ def unit(self, value): self._unit = value @classmethod - def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: + def _from_circuit_data( + cls, data: CircuitData, add_regs: bool = False, name: str | None = None + ) -> typing.Self: """A private constructor from rust space circuit data.""" - out = QuantumCircuit() + out = QuantumCircuit(name=name) if data.num_qubits > 0: if add_regs: @@ -2102,14 +2104,14 @@ def map_vars(op): is_control_flow = isinstance(n_op, ControlFlowOp) if ( not is_control_flow - and (condition := getattr(n_op, "condition", None)) is not None + 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) + 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): @@ -2733,7 +2735,10 @@ def has_parameter(self, name_or_param: str | Parameter, /) -> bool: """ if isinstance(name_or_param, str): return self.get_parameter(name_or_param, None) is not None - return self.get_parameter(name_or_param.name) == name_or_param + return ( + isinstance(name_or_param, Parameter) + and self.get_parameter(name_or_param.name, None) == name_or_param + ) @typing.overload def get_var(self, name: str, default: T) -> Union[expr.Var, T]: ... @@ -3520,7 +3525,7 @@ def update_from_expr(objects, node): for instruction in self._data: objects = set(itertools.chain(instruction.qubits, instruction.clbits)) - if (condition := getattr(instruction.operation, "condition", None)) is not None: + 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) @@ -3623,7 +3628,7 @@ def num_connected_components(self, unitary_only: bool = False) -> int: else: args = instruction.qubits + instruction.clbits num_qargs = len(args) + ( - 1 if getattr(instruction.operation, "condition", None) else 0 + 1 if getattr(instruction.operation, "_condition", None) else 0 ) if num_qargs >= 2 and not getattr(instruction.operation, "_directive", False): @@ -3749,39 +3754,13 @@ def copy_empty_like( f"invalid name for a circuit: '{name}'. The name must be a string or 'None'." ) cpy = _copy.copy(self) - # copy registers correctly, in copy.copy they are only copied via reference - cpy.qregs = self.qregs.copy() - cpy.cregs = self.cregs.copy() - cpy._builder_api = _OuterCircuitScopeInterface(cpy) - cpy._ancillas = self._ancillas.copy() - cpy._qubit_indices = self._qubit_indices.copy() - cpy._clbit_indices = self._clbit_indices.copy() - - 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}'") + + _copy_metadata(self, cpy, vars_mode) cpy._data = CircuitData( self._data.qubits, self._data.clbits, global_phase=self._data.global_phase ) - cpy._calibrations = _copy.deepcopy(self._calibrations) - cpy._metadata = _copy.deepcopy(self._metadata) - if name: cpy.name = name return cpy @@ -4392,30 +4371,57 @@ def assign_parameters( # pylint: disable=missing-raises-doc if isinstance(parameters, collections.abc.Mapping): raw_mapping = parameters if flat_input else self._unroll_param_dict(parameters) - our_parameters = self._data.unsorted_parameters() - if strict and (extras := raw_mapping.keys() - our_parameters): + if strict and ( + extras := [ + parameter for parameter in raw_mapping if not self.has_parameter(parameter) + ] + ): raise CircuitError( f"Cannot bind parameters ({', '.join(str(x) for x in extras)}) not present in" " the circuit." ) - parameter_binds = _ParameterBindsDict(raw_mapping, our_parameters) - target._data.assign_parameters_mapping(parameter_binds) + + def create_mapping_view(): + return raw_mapping + + target._data.assign_parameters_mapping(raw_mapping) else: - parameter_binds = _ParameterBindsSequence(target._data.parameters, parameters) + # This should be a cache retrieval, since we warmed the cache. We need to keep hold of + # what the parameters were before anything is assigned, because we assign parameters in + # the calibrations (which aren't tracked in the internal parameter table) after, which + # would change what we create. We don't make the full Python-space mapping object of + # parameters to values eagerly because 99.9% of the time we don't need it, and it's + # relatively expensive to do for large numbers of parameters. + initial_parameters = target._data.parameters + + def create_mapping_view(): + return dict(zip(initial_parameters, parameters)) + target._data.assign_parameters_iterable(parameters) # Finally, assign the parameters inside any of the calibrations. We don't track these in - # the `ParameterTable`, so we manually reconstruct things. + # the `ParameterTable`, so we manually reconstruct things. We lazily construct the mapping + # `{parameter: bound_value}` the first time we encounter a binding (we have to scan for + # this, because calibrations don't use a parameter-table lookup), rather than always paying + # the cost - most circuits don't have parametric calibrations, and it's expensive. + mapping_view = None + def map_calibration(qubits, parameters, schedule): + # All calls to this function should share the same `{Parameter: bound_value}` mapping, + # which we only want to lazily construct a single time. + nonlocal mapping_view + if mapping_view is None: + mapping_view = create_mapping_view() + modified = False new_parameters = list(parameters) for i, parameter in enumerate(new_parameters): if not isinstance(parameter, ParameterExpression): continue - if not (contained := parameter.parameters & parameter_binds.mapping.keys()): + if not (contained := parameter.parameters & mapping_view.keys()): continue for to_bind in contained: - parameter = parameter.assign(to_bind, parameter_binds.mapping[to_bind]) + parameter = parameter.assign(to_bind, mapping_view[to_bind]) if not parameter.parameters: parameter = parameter.numeric() if isinstance(parameter, complex): @@ -4423,7 +4429,7 @@ def map_calibration(qubits, parameters, schedule): new_parameters[i] = parameter modified = True if modified: - schedule.assign_parameters(parameter_binds.mapping) + schedule.assign_parameters(mapping_view) return (qubits, tuple(new_parameters)), schedule target._calibrations = defaultdict( @@ -6762,42 +6768,6 @@ def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> exp return node -class _ParameterBindsDict: - __slots__ = ("mapping", "allowed_keys") - - def __init__(self, mapping, allowed_keys): - self.mapping = mapping - self.allowed_keys = allowed_keys - - def items(self): - """Iterator through all the keys in the mapping that we care about. Wrapping the main - mapping allows us to avoid reconstructing a new 'dict', but just use the given 'mapping' - without any copy / reconstruction.""" - for parameter, value in self.mapping.items(): - if parameter in self.allowed_keys: - yield parameter, value - - -class _ParameterBindsSequence: - __slots__ = ("parameters", "values", "mapping_cache") - - def __init__(self, parameters, values): - self.parameters = parameters - self.values = values - self.mapping_cache = None - - def items(self): - """Iterator through all the keys in the mapping that we care about.""" - return zip(self.parameters, self.values) - - @property - def mapping(self): - """Cached version of a mapping. This is only generated on demand.""" - if self.mapping_cache is None: - self.mapping_cache = dict(zip(self.parameters, self.values)) - return self.mapping_cache - - def _bit_argument_conversion(specifier, bit_sequence, bit_set, type_) -> list[Bit]: """Get the list of bits referred to by the specifier ``specifier``. @@ -6862,3 +6832,34 @@ def _bit_argument_conversion_scalar(specifier, bit_sequence, bit_set, type_): else f"Invalid bit index: '{specifier}' of type '{type(specifier)}'" ) raise CircuitError(message) + + +def _copy_metadata(original, cpy, vars_mode): + # copy registers correctly, in copy.copy they are only copied via reference + cpy.qregs = original.qregs.copy() + cpy.cregs = original.cregs.copy() + cpy._builder_api = _OuterCircuitScopeInterface(cpy) + cpy._ancillas = original._ancillas.copy() + cpy._qubit_indices = original._qubit_indices.copy() + cpy._clbit_indices = original._clbit_indices.copy() + + 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 = original._vars_local.copy() + cpy._vars_input = original._vars_input.copy() + cpy._vars_capture = original._vars_capture.copy() + elif vars_mode == "captures": + cpy._vars_local = {} + cpy._vars_input = {} + cpy._vars_capture = {var.name: var for var in original.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._calibrations = _copy.deepcopy(original._calibrations) + cpy._metadata = _copy.deepcopy(original._metadata) diff --git a/qiskit/circuit/singleton.py b/qiskit/circuit/singleton.py index 874b979ff588..a315f8aaa619 100644 --- a/qiskit/circuit/singleton.py +++ b/qiskit/circuit/singleton.py @@ -251,6 +251,7 @@ class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): import functools +from qiskit.utils import deprecate_func from .instruction import Instruction from .gate import Gate from .controlledgate import ControlledGate, _ctrl_state_to_int @@ -489,6 +490,7 @@ class they are providing overrides for has more lazy attributes or user-exposed instruction._params = _frozenlist(instruction._params) return instruction + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical, val): return self.to_mutable().c_if(classical, val) diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py index 6bbc5439332d..43e81ce61056 100644 --- a/qiskit/circuit/store.py +++ b/qiskit/circuit/store.py @@ -16,6 +16,7 @@ import typing +from qiskit.utils import deprecate_func from .exceptions import CircuitError from .classical import expr, types from .instruction import Instruction @@ -88,6 +89,7 @@ def rvalue(self): """Get the r-value :class:`~.expr.Expr` node that is being written into the l-value.""" return self.params[1] + @deprecate_func(since="1.3.0", removal_timeline="in 2.0.0") def c_if(self, classical, val): """:meta hidden:""" raise NotImplementedError( diff --git a/qiskit/circuit/twirling.py b/qiskit/circuit/twirling.py new file mode 100644 index 000000000000..9cea055ddf48 --- /dev/null +++ b/qiskit/circuit/twirling.py @@ -0,0 +1,145 @@ +# 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. + +"""The twirling module.""" + +from __future__ import annotations +import typing + +from qiskit._accelerate.twirling import twirl_circuit as twirl_rs +from qiskit.circuit.quantumcircuit import QuantumCircuit, _copy_metadata +from qiskit.circuit.gate import Gate +from qiskit.circuit.library.standard_gates import CXGate, ECRGate, CZGate, iSwapGate +from qiskit.exceptions import QiskitError + +if typing.TYPE_CHECKING: + from qiskit.transpiler.target import Target + + +NAME_TO_CLASS = { + "cx": CXGate._standard_gate, + "ecr": ECRGate._standard_gate, + "cz": CZGate._standard_gate, + "iswap": iSwapGate._standard_gate, +} + + +def pauli_twirl_2q_gates( + circuit: QuantumCircuit, + twirling_gate: None | str | Gate | list[str] | list[Gate] = None, + seed: int | None = None, + num_twirls: int | None = None, + target: Target | None = None, +) -> QuantumCircuit | list[QuantumCircuit]: + """Create copies of a given circuit with Pauli twirling applied around specified two qubit + gates. + + If you're running this function with the intent to twirl a circuit to run on hardware this + may not be the most efficient way to perform twirling. Especially if the hardware vendor + has implemented the :mod:`.primitives` execution interface with :class:`.SamplerV2` and + :class:`.EstimatorV2` this most likely is not the best way to apply twirling to your + circuit and you'll want to refer to the implementation of :class:`.SamplerV2` and/or + :class:`.EstimatorV2` for the specified hardware vendor. + + If the intent of this function is to be run after :func:`.transpile` or + :meth:`.PassManager.run` the optional ``target`` argument can be used + so that the inserted 1 qubit Pauli gates are synthesized to be + compatible with the given :class:`.Target` so the output circuit(s) are + still compatible. + + Args: + circuit: The circuit to twirl + twirling_gate: The gate to twirl, defaults to `None` which means twirl all default gates: + :class:`.CXGate`, :class:`.CZGate`, :class:`.ECRGate`, and :class:`.iSwapGate`. + If supplied it can either be a single gate or a list of gates either as either a gate + object or its string name. Currently only the names `"cx"`, `"cz"`, `"ecr"`, and + `"iswap"` are supported. If a gate object is provided outside the default gates it must + have a matrix defined from its :class:`~.Gate.to_matrix` method for the gate to potentially + be twirled. If a valid twirling configuration can't be computed that particular gate will + be silently ignored and not twirled. + seed: An integer seed for the random number generator used internally by this function. + If specified this must be between 0 and 18,446,744,073,709,551,615. + num_twirls: The number of twirling circuits to build. This defaults to ``None`` and will return + a single circuit. If it is an integer a list of circuits with `num_twirls` circuits + will be returned. + target: If specified an :class:`.Target` instance to use for running single qubit decomposition + as part of the Pauli twirling to optimize and map the pauli gates added to the circuit + to the specified target. + + Returns: + A copy of the given circuit with Pauli twirling applied to each + instance of the specified twirling gate. + """ + custom_gates = None + if isinstance(twirling_gate, str): + gate = NAME_TO_CLASS.get(twirling_gate, None) + if gate is None: + raise QiskitError(f"The specified gate name {twirling_gate} is not supported") + twirling_std_gate = [gate] + elif isinstance(twirling_gate, list): + custom_gates = [] + twirling_std_gate = [] + for gate in twirling_gate: + if isinstance(gate, str): + gate = NAME_TO_CLASS.get(gate, None) + if gate is None: + raise QiskitError(f"The specified gate name {twirling_gate} is not supported") + twirling_std_gate.append(gate) + else: + twirling_gate = getattr(gate, "_standard_gate", None) + + if twirling_gate is None: + custom_gates.append(gate) + else: + if twirling_gate in NAME_TO_CLASS.values(): + twirling_std_gate.append(twirling_gate) + else: + custom_gates.append(gate) + if not custom_gates: + custom_gates = None + if not twirling_std_gate: + twirling_std_gate = None + elif twirling_gate is not None: + std_gate = getattr(twirling_gate, "_standard_gate", None) + if std_gate is None: + twirling_std_gate = None + custom_gates = [twirling_gate] + else: + if std_gate in NAME_TO_CLASS.values(): + twirling_std_gate = [std_gate] + else: + twirling_std_gate = None + custom_gates = [twirling_gate] + else: + twirling_std_gate = twirling_gate + out_twirls = num_twirls + if out_twirls is None: + out_twirls = 1 + new_data = twirl_rs( + circuit._data, + twirling_std_gate, + custom_gates, + seed, + out_twirls, + target, + ) + if num_twirls is not None: + out_list = [] + for circ in new_data: + new_circ = QuantumCircuit._from_circuit_data(circ) + _copy_metadata(circuit, new_circ, "alike") + out_list.append(new_circ) + return out_list + else: + out_circ = QuantumCircuit._from_circuit_data(new_data[0]) + _copy_metadata(circuit, out_circ, "alike") + return out_circ diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 2802169b4afb..c42ec0663d90 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -189,6 +189,16 @@ def assemble( ) +# Note for future: this method is used in `BasicSimulator` and may need to be kept past the +# `assemble` removal deadline (2.0). If it is kept (potentially in a different location), +# we will need an alternative for the backend.configuration() access that currently takes +# place in L566 (`parse_circuit_args`) and L351 (`parse_common_args`) +# because backend.configuration() is also set for removal in 2.0. +# The ultimate goal will be to move away from relying on any kind of `assemble` implementation +# because of how tightly coupled it is to these legacy data structures. But as a transition step, +# given that we would only have to support the subcase of `BasicSimulator`, we could probably just +# inline the relevant config values that are already hardcoded in the basic simulator configuration +# generator. def _assemble( experiments: Union[ QuantumCircuit, @@ -229,7 +239,7 @@ def _assemble( experiments = experiments if isinstance(experiments, list) else [experiments] pulse_qobj = any(isinstance(exp, (ScheduleBlock, Schedule, Instruction)) for exp in experiments) with warnings.catch_warnings(): - # The Qobj is deprecated + # The Qobj class is deprecated, the backend.configuration() method is too warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") qobj_id, qobj_header, run_config_common_dict = _parse_common_args( backend, @@ -554,9 +564,12 @@ def _parse_circuit_args( run_config_dict = {"parameter_binds": parameter_binds, **run_config} if parametric_pulses is None: if backend: - run_config_dict["parametric_pulses"] = getattr( - backend.configuration(), "parametric_pulses", [] - ) + with warnings.catch_warnings(): + # TODO (2.0): See comment on L192 regarding backend.configuration removal + warnings.filterwarnings("ignore", category=DeprecationWarning, module="qiskit") + run_config_dict["parametric_pulses"] = getattr( + backend.configuration(), "parametric_pulses", [] + ) else: run_config_dict["parametric_pulses"] = [] else: diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 2b17d8bbae12..27de0a7e1811 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -32,6 +32,7 @@ from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.target import Target +from qiskit.utils import deprecate_arg from qiskit.utils.deprecate_pulse import deprecate_pulse_arg logger = logging.getLogger(__name__) @@ -39,6 +40,32 @@ _CircuitT = TypeVar("_CircuitT", bound=Union[QuantumCircuit, List[QuantumCircuit]]) +@deprecate_arg( + name="instruction_durations", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined instruction durations with " + "`Target.from_configuration(..., instruction_durations=...)`", +) +@deprecate_arg( + name="timing_constraints", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined timing constraints with " + "`Target.from_configuration(..., timing_constraints=...)`", +) +@deprecate_arg( + name="backend_properties", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined properties with Target.from_configuration(..., backend_properties=...)", +) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def transpile( # pylint: disable=too-many-return-statements circuits: _CircuitT, @@ -367,31 +394,57 @@ def callback_func(**kwargs): # 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, - target=target, - backend=backend, - basis_gates=basis_gates, - 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, - 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, - dt=dt, - qubits_initially_zero=qubits_initially_zero, - ) + # Filter instruction_durations, timing_constraints, backend_properties and inst_map deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``inst_map`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``timing_constraints`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``instruction_durations`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``backend_properties`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + pm = generate_preset_pass_manager( + optimization_level, + target=target, + backend=backend, + basis_gates=basis_gates, + 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, + 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, + dt=dt, + qubits_initially_zero=qubits_initially_zero, + ) out_circuits = pm.run(circuits, callback=callback, num_processes=num_processes) diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 66f00a3a9dea..bd7d720a5dd0 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -127,7 +127,7 @@ def fix_condition(op): if (out := operation_map.get(original_id)) is not None: return out - condition = getattr(op, "condition", None) + condition = getattr(op, "_condition", None) if condition: reg, val = condition if isinstance(reg, Clbit): diff --git a/qiskit/dagcircuit/collect_blocks.py b/qiskit/dagcircuit/collect_blocks.py index c5c7b49144f7..99c51d2e3600 100644 --- a/qiskit/dagcircuit/collect_blocks.py +++ b/qiskit/dagcircuit/collect_blocks.py @@ -306,7 +306,7 @@ def split_block_into_layers(block: list[DAGOpNode | DAGDepNode]): cur_bits = set(node.qargs) cur_bits.update(node.cargs) - cond = getattr(node.op, "condition", None) + cond = getattr(node.op, "_condition", None) if cond is not None: cur_bits.update(condition_resources(cond).clbits) @@ -356,7 +356,7 @@ def collapse_to_operation(self, blocks, collapse_fn): for node in block: cur_qubits.update(node.qargs) cur_clbits.update(node.cargs) - cond = getattr(node.op, "condition", None) + cond = getattr(node.op, "_condition", None) if cond is not None: cur_clbits.update(condition_resources(cond).clbits) if isinstance(cond[0], ClassicalRegister): @@ -378,7 +378,7 @@ def collapse_to_operation(self, blocks, collapse_fn): for node in block: instructions = qc.append(CircuitInstruction(node.op, node.qargs, node.cargs)) - cond = getattr(node.op, "condition", None) + cond = getattr(node.op, "_condition", None) if cond is not None: instructions.c_if(*cond) diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index 2658cd731d69..63e9114a6ca1 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -409,13 +409,13 @@ def _create_op_node(self, operation, qargs, cargs): for elem in qargs: qindices_list.append(self.qubits.index(elem)) - if getattr(operation, "condition", None): + if getattr(operation, "_condition", None): # The change to handling operation.condition follows code patterns in quantum_circuit.py. # However: # (1) cindices_list are specific to template optimization and should not be computed # in this place. # (2) Template optimization pass needs currently does not handle general conditions. - cond_bits = condition_resources(operation.condition).clbits + cond_bits = condition_resources(operation._condition).clbits cindices_list = [self.clbits.index(clbit) for clbit in cond_bits] else: cindices_list = [] @@ -609,7 +609,7 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True): for nd in node_block: block_qargs |= set(nd.qargs) block_cargs |= set(nd.cargs) - cond = getattr(nd.op, "condition", None) + cond = getattr(nd.op, "_condition", None) if cond is not None: block_cargs.update(condition_resources(cond).clbits) diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 60e2a7465707..151861d8028c 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -92,8 +92,8 @@ def key(var): def _condition_op_eq(node1, node2, bit_indices1, bit_indices2): - cond1 = node1.op.condition - cond2 = node2.op.condition + cond1 = node1.condition + cond2 = node2.condition if isinstance(cond1, expr.Expr) and isinstance(cond2, expr.Expr): if not expr.structurally_equivalent( cond1, cond2, _make_expr_key(bit_indices1), _make_expr_key(bit_indices2) diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 722ee900ceea..5f9f8d20c75e 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -44,13 +44,15 @@ def _run_circuits( circuits: QuantumCircuit | list[QuantumCircuit], backend: BackendV1 | BackendV2, + clear_metadata: bool = True, **run_options, ) -> tuple[list[Result], list[dict]]: """Remove metadata of circuits and run the circuits on a backend. Args: circuits: The circuits backend: The backend - monitor: Enable job minotor if True + clear_metadata: Clear circuit metadata before passing to backend.run if + True. **run_options: run_options Returns: The result and the metadata of the circuits @@ -60,7 +62,8 @@ def _run_circuits( metadata = [] for circ in circuits: metadata.append(circ.metadata) - circ.metadata = {} + if clear_metadata: + circ.metadata = {} if isinstance(backend, BackendV1): max_circuits = getattr(backend.configuration(), "max_experiments", None) elif isinstance(backend, BackendV2): diff --git a/qiskit/primitives/backend_sampler_v2.py b/qiskit/primitives/backend_sampler_v2.py index bac0bec5eaed..40a6d8560e07 100644 --- a/qiskit/primitives/backend_sampler_v2.py +++ b/qiskit/primitives/backend_sampler_v2.py @@ -17,12 +17,13 @@ import warnings from collections import defaultdict from dataclasses import dataclass -from typing import Iterable +from typing import Any, Iterable, Union import numpy as np from numpy.typing import NDArray from qiskit.circuit import QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.primitives.backend_estimator import _run_circuits from qiskit.primitives.base import BaseSamplerV2 from qiskit.primitives.containers import ( @@ -53,6 +54,11 @@ class Options: Default: None. """ + run_options: dict[str, Any] | None = None + """A dictionary of options to pass to the backend's ``run()`` method. + Default: None (no option passed to backend's ``run`` method) + """ + @dataclass class _MeasureInfo: @@ -62,6 +68,16 @@ class _MeasureInfo: start: int +ResultMemory = Union[list[str], list[list[float]], list[list[list[float]]]] +"""Type alias for possible level 2 and level 1 result memory formats. For level +2, the format is a list of bit strings. For level 1, format can be either a +list of I/Q pairs (list with two floats) for each memory slot if using +``meas_return=avg`` or a list of of lists of I/Q pairs if using +``meas_return=single`` with the outer list indexing shot number and the inner +list indexing memory slot. +""" + + class BackendSamplerV2(BaseSamplerV2): """Evaluates bitstrings for provided quantum circuits @@ -91,6 +107,9 @@ class BackendSamplerV2(BaseSamplerV2): * ``seed_simulator``: The seed to use in the simulator. If None, a random seed will be used. Default: None. + * ``run_options``: A dictionary of options to pass through to the ``run()`` + method of the wrapped :class:`~.BackendV2` instance. + .. note:: This class requires a backend that supports ``memory`` option. @@ -165,19 +184,27 @@ def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult for circuits in bound_circuits: flatten_circuits.extend(np.ravel(circuits).tolist()) + run_opts = self._options.run_options or {} # run circuits results, _ = _run_circuits( flatten_circuits, self._backend, + clear_metadata=False, memory=True, shots=shots, seed_simulator=self._options.seed_simulator, + **run_opts, ) result_memory = _prepare_memory(results) # pack memory to an ndarray of uint8 results = [] start = 0 + meas_level = ( + None + if self._options.run_options is None + else self._options.run_options.get("meas_level") + ) for pub, bound in zip(pubs, bound_circuits): meas_info, max_num_bytes = _analyze_circuit(pub.circuit) end = start + bound.size @@ -189,6 +216,7 @@ def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult meas_info, max_num_bytes, pub.circuit.metadata, + meas_level, ) ) start = end @@ -197,28 +225,43 @@ def _run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult def _postprocess_pub( self, - result_memory: list[list[str]], + result_memory: list[ResultMemory], shots: int, shape: tuple[int, ...], meas_info: list[_MeasureInfo], max_num_bytes: int, circuit_metadata: dict, + meas_level: int | None, ) -> 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) - for item in meas_info - } - memory_array = _memory_array(result_memory, max_num_bytes) - - for samples, index in zip(memory_array, np.ndindex(*shape)): - for item in meas_info: - ary = _samples_to_packed_array(samples, item.num_bits, item.start) - arrays[item.creg_name][index] = ary - - meas = { - item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info - } + """Converts the memory data into a sampler pub result + + For level 2 data, the memory data are stored in an array of bit arrays + with the shape of the pub. For level 1 data, the data are stored in a + complex numpy array. + """ + if meas_level == 2 or meas_level is None: + arrays = { + item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8) + for item in meas_info + } + memory_array = _memory_array(result_memory, max_num_bytes) + + for samples, index in zip(memory_array, np.ndindex(*shape)): + for item in meas_info: + ary = _samples_to_packed_array(samples, item.num_bits, item.start) + arrays[item.creg_name][index] = ary + + meas = { + item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) + for item in meas_info + } + elif meas_level == 1: + raw = np.array(result_memory) + cplx = raw[..., 0] + 1j * raw[..., 1] + cplx = np.reshape(cplx, (*shape, *cplx.shape[1:])) + meas = {item.creg_name: cplx for item in meas_info} + else: + raise QiskitError(f"Unsupported meas_level: {meas_level}") return SamplerPubResult( DataBin(**meas, shape=shape), metadata={"shots": shots, "circuit_metadata": circuit_metadata}, @@ -248,7 +291,7 @@ def _analyze_circuit(circuit: QuantumCircuit) -> tuple[list[_MeasureInfo], int]: return meas_info, _min_num_bytes(max_num_bits) -def _prepare_memory(results: list[Result]) -> list[list[str]]: +def _prepare_memory(results: list[Result]) -> list[ResultMemory]: """Joins splitted results if exceeding max_experiments""" lst = [] for res in results: diff --git a/qiskit/primitives/statevector_sampler.py b/qiskit/primitives/statevector_sampler.py index 0745f1632093..9c12eb7dd437 100644 --- a/qiskit/primitives/statevector_sampler.py +++ b/qiskit/primitives/statevector_sampler.py @@ -289,6 +289,6 @@ def _final_measurement_mapping(circuit: QuantumCircuit) -> dict[tuple[ClassicalR def _has_control_flow(circuit: QuantumCircuit) -> bool: return any( - isinstance((op := instruction.operation), ControlFlowOp) or op.condition + isinstance((op := instruction.operation), ControlFlowOp) or op._condition for instruction in circuit ) diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index 0bc362aa0ef4..50db55b98ae2 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -82,7 +82,7 @@ def init_observable(observable: BaseOperator | str) -> SparsePauliOp: since="1.2", additional_msg="Use ``QuantumCircuit.layout`` and ``SparsePauliOp.apply_layout`` " + "to adjust an operator for a layout. Otherwise, use ``mthree.utils.final_measurement_mapping``. " - + "See https://qiskit-extensions.github.io/mthree/apidocs/utils.html for details.", + + "See for details.", ) def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: """Return the final measurement mapping for the circuit. diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index e3a2f63e6ca0..2bfcd9ee009b 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -160,7 +160,7 @@ interacting with a running job. For a simple example of a provider, see the -`qiskit-aqt-provider `__ +`qiskit-aqt-provider `__ Provider -------- @@ -664,7 +664,7 @@ def status(self): concentrate on higher level applications using these outputs. For example, if your backends were well suited to leverage -`mthree `__ measurement +`mthree `__ measurement mitigation to improve the quality of the results, you could implement a provider-specific :class:`~.Sampler` implementation that leverages the ``M3Mitigation`` class internally to run the circuits and return diff --git a/qiskit/providers/basic_provider/basic_simulator.py b/qiskit/providers/basic_provider/basic_simulator.py index addcd54d0a48..f9534cb4d9aa 100644 --- a/qiskit/providers/basic_provider/basic_simulator.py +++ b/qiskit/providers/basic_provider/basic_simulator.py @@ -50,6 +50,7 @@ from qiskit.qobj import QasmQobj, QasmQobjConfig, QasmQobjExperiment from qiskit.result import Result from qiskit.transpiler import Target +from qiskit.utils.deprecation import deprecate_func from .basic_provider_job import BasicProviderJob from .basic_provider_tools import single_gate_matrix @@ -214,6 +215,14 @@ def _build_basic_target(self) -> Target: ) return target + @deprecate_func( + since="1.3.0", + removal_timeline="in Qiskit 2.0.0", + additional_msg="The `BackendConfiguration` class is part of the deprecated `BackendV1` " + "workflow, and no longer necessary for `BackendV2`. The individual configuration elements " + "can be retrieved directly from the backend or from the contained `Target` instance " + "(`backend.target)`).", + ) def configuration(self) -> BackendConfiguration: """Return the simulator backend configuration. @@ -250,7 +259,7 @@ def configuration(self) -> BackendConfiguration: backend_name=self.name, backend_version=self.backend_version, n_qubits=self.num_qubits, - basis_gates=self.target.operation_names, + basis_gates=list(self.target.operation_names), gates=gates, local=True, simulator=True, @@ -534,7 +543,8 @@ def run( "initial_statevector": np.array([1, 0, 0, 1j]) / math.sqrt(2), } """ - # TODO: replace assemble with new run flow + # TODO: replace assemble with new run flow. If this is not achieved before 2.0, + # see removal note on `def _assemble`, L192 of qiskit/compiler/assembler.py from qiskit.compiler.assembler import _assemble out_options = {} diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index 2815a9897db0..75ce3b8ef755 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -169,12 +169,22 @@ def assert_has( """ instruction = _get_instruction_string(instruction) if not self.has(instruction, _to_tuple(qubits)): - if instruction in self._map: - raise PulseError( - f"Operation '{instruction}' exists, but is only defined for qubits " - f"{self.qubits_with_instruction(instruction)}." + # TODO: PulseError is deprecated, this code will be removed in 2.0. + # In the meantime, we catch the deprecation + # warning not to overload users with non-actionable messages + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*The entire Qiskit Pulse package*", + module="qiskit", ) - raise PulseError(f"Operation '{instruction}' is not defined for this system.") + if instruction in self._map: + raise PulseError( + 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.") def get( self, diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 3cf0d8942553..7c655dfd432b 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -246,12 +246,14 @@ def _instruction_call_site(operation): if 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): + 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 = f"if({operation.condition[0].name}=={operation.condition[1]:d}) " + qasm2_call + qasm2_call = ( + f"if({operation._condition[0].name}=={operation._condition[1]:d}) " + qasm2_call + ) return qasm2_call diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py index 6cdb0f70bba0..39ff388b6b45 100644 --- a/qiskit/qasm2/parse.py +++ b/qiskit/qasm2/parse.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Python-space bytecode interpreter for the output of the main Rust parser logic.""" - +import warnings import dataclasses import math from typing import Iterable, Callable @@ -255,6 +255,11 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): CircuitInstruction(gates[gate_id](*parameters), [qubits[q] for q in op_qubits]) ) elif opcode == OpCode.ConditionedGate: + warnings.warn( + "Conditioned gates in qasm2 will be loaded as an IfElseOp starting in Qiskit 2.0", + FutureWarning, + stacklevel=3, + ) gate_id, parameters, op_qubits, creg, value = op.operands gate = gates[gate_id](*parameters).c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(gate, [qubits[q] for q in op_qubits])) @@ -262,12 +267,22 @@ def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): qubit, clbit = op.operands qc._append(CircuitInstruction(Measure(), (qubits[qubit],), (clbits[clbit],))) elif opcode == OpCode.ConditionedMeasure: + warnings.warn( + "Conditioned measurements in qasm2 will be loaded as an IfElseOp starting in Qiskit 2.0", + FutureWarning, + stacklevel=3, + ) qubit, clbit, creg, value = op.operands measure = Measure().c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(measure, (qubits[qubit],), (clbits[clbit],))) elif opcode == OpCode.Reset: qc._append(CircuitInstruction(Reset(), (qubits[op.operands[0]],))) elif opcode == OpCode.ConditionedReset: + warnings.warn( + "Conditioned resets in qasm2 will be loaded as an IfElseOp starting in Qiskit 2.0", + FutureWarning, + stacklevel=3, + ) qubit, creg, value = op.operands reset = Reset().c_if(qc.cregs[creg], value) qc._append(CircuitInstruction(reset, (qubits[qubit],))) @@ -356,7 +371,7 @@ def __array__(self, dtype=None, copy=None): # to pickle ourselves, we just eagerly create the definition and pickle that. def __getstate__(self): - return (self.name, self.num_qubits, self.params, self.definition, self.condition) + return (self.name, self.num_qubits, self.params, self.definition, self._condition) def __setstate__(self, state): name, num_qubits, params, definition, condition = state diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index eaac830edf65..c723c443fef2 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -15,12 +15,14 @@ """QASM3 AST Nodes""" import enum -from typing import Optional, List, Union, Iterable, Tuple +from typing import Optional, List, Union, Iterable, Tuple, Sequence class ASTNode: """Base abstract class for AST nodes""" + __slots__ = () + class Statement(ASTNode): """ @@ -35,6 +37,8 @@ class Statement(ASTNode): | quantumStatement """ + __slots__ = () + class Pragma(ASTNode): """ @@ -42,6 +46,8 @@ class Pragma(ASTNode): : '#pragma' LBRACE statement* RBRACE // match any valid openqasm statement """ + __slots__ = ("content",) + def __init__(self, content): self.content = content @@ -52,6 +58,8 @@ class CalibrationGrammarDeclaration(Statement): : 'defcalgrammar' calibrationGrammar SEMICOLON """ + __slots__ = ("name",) + def __init__(self, name): self.name = name @@ -62,9 +70,11 @@ class Program(ASTNode): : header (globalStatement | statement)* """ - def __init__(self, header, statements=None): + __slots__ = ("header", "statements") + + def __init__(self, header, statements=()): self.header = header - self.statements = statements or [] + self.statements = statements class Header(ASTNode): @@ -73,6 +83,8 @@ class Header(ASTNode): : version? include* """ + __slots__ = ("version", "includes") + def __init__(self, version, includes): self.version = version self.includes = includes @@ -84,6 +96,8 @@ class Include(ASTNode): : 'include' StringLiteral SEMICOLON """ + __slots__ = ("filename",) + def __init__(self, filename): self.filename = filename @@ -94,6 +108,8 @@ class Version(ASTNode): : 'OPENQASM'(Integer | RealNumber) SEMICOLON """ + __slots__ = ("version_number",) + def __init__(self, version_number): self.version_number = version_number @@ -108,10 +124,14 @@ class QuantumInstruction(ASTNode): | quantumBarrier """ + __slots__ = () + class ClassicalType(ASTNode): """Information about a classical type. This is just an abstract base for inheritance tests.""" + __slots__ = () + class FloatType(ClassicalType, enum.Enum): """Allowed values for the width of floating-point types.""" @@ -126,10 +146,14 @@ class FloatType(ClassicalType, enum.Enum): class BoolType(ClassicalType): """Type information for a Boolean.""" + __slots__ = () + class IntType(ClassicalType): """Type information for a signed integer.""" + __slots__ = ("size",) + def __init__(self, size: Optional[int] = None): self.size = size @@ -137,6 +161,8 @@ def __init__(self, size: Optional[int] = None): class UintType(ClassicalType): """Type information for an unsigned integer.""" + __slots__ = ("size",) + def __init__(self, size: Optional[int] = None): self.size = size @@ -144,19 +170,25 @@ def __init__(self, size: Optional[int] = None): class BitType(ClassicalType): """Type information for a single bit.""" + __slots__ = () + class BitArrayType(ClassicalType): """Type information for a sized number of classical bits.""" + __slots__ = ("size",) + def __init__(self, size: int): self.size = size class Expression(ASTNode): - pass + __slots__ = () class StringifyAndPray(Expression): + __slots__ = ("obj",) + # This is not a real AST node, yet is somehow very common. It's used when there are # `ParameterExpression` instances; instead of actually visiting the Sympy expression tree into # an OQ3 AST, we just convert it to a string, cross our fingers, and hope. @@ -165,6 +197,8 @@ def __init__(self, obj): class Range(Expression): + __slots__ = ("start", "step", "end") + def __init__( self, start: Optional[Expression] = None, @@ -177,6 +211,8 @@ def __init__( class Identifier(Expression): + __slots__ = ("string",) + def __init__(self, string): self.string = string @@ -184,6 +220,8 @@ def __init__(self, string): class SubscriptedIdentifier(Identifier): """An identifier with subscripted access.""" + __slots__ = ("subscript",) + def __init__(self, string: str, subscript: Union[Range, Expression]): super().__init__(string) self.subscript = subscript @@ -198,16 +236,22 @@ class Constant(Expression, enum.Enum): class IntegerLiteral(Expression): + __slots__ = ("value",) + def __init__(self, value): self.value = value class BooleanLiteral(Expression): + __slots__ = ("value",) + def __init__(self, value): self.value = value class BitstringLiteral(Expression): + __slots__ = ("value", "width") + def __init__(self, value, width): self.value = value self.width = width @@ -224,12 +268,16 @@ class DurationUnit(enum.Enum): class DurationLiteral(Expression): + __slots__ = ("value", "unit") + def __init__(self, value: float, unit: DurationUnit): self.value = value self.unit = unit class Unary(Expression): + __slots__ = ("op", "operand") + class Op(enum.Enum): LOGIC_NOT = "!" BIT_NOT = "~" @@ -240,6 +288,8 @@ def __init__(self, op: Op, operand: Expression): class Binary(Expression): + __slots__ = ("op", "left", "right") + class Op(enum.Enum): BIT_AND = "&" BIT_OR = "|" @@ -262,12 +312,16 @@ def __init__(self, op: Op, left: Expression, right: Expression): class Cast(Expression): + __slots__ = ("type", "operand") + def __init__(self, type: ClassicalType, operand: Expression): self.type = type self.operand = operand class Index(Expression): + __slots__ = ("target", "index") + def __init__(self, target: Expression, index: Expression): self.target = target self.index = index @@ -280,6 +334,8 @@ class IndexSet(ASTNode): { Expression (, Expression)* } """ + __slots__ = ("values",) + def __init__(self, values: List[Expression]): self.values = values @@ -290,6 +346,8 @@ class QuantumMeasurement(ASTNode): : 'measure' indexIdentifierList """ + __slots__ = ("identifierList",) + def __init__(self, identifierList: List[Identifier]): self.identifierList = identifierList @@ -301,6 +359,8 @@ class QuantumMeasurementAssignment(Statement): | indexIdentifier EQUALS quantumMeasurement # eg: bits = measure qubits; """ + __slots__ = ("identifier", "quantumMeasurement") + def __init__(self, identifier: Identifier, quantumMeasurement: QuantumMeasurement): self.identifier = identifier self.quantumMeasurement = quantumMeasurement @@ -312,6 +372,8 @@ class Designator(ASTNode): : LBRACKET expression RBRACKET """ + __slots__ = ("expression",) + def __init__(self, expression: Expression): self.expression = expression @@ -319,6 +381,8 @@ def __init__(self, expression: Expression): class ClassicalDeclaration(Statement): """Declaration of a classical type, optionally initializing it to a value.""" + __slots__ = ("type", "identifier", "initializer") + def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=None): self.type = type_ self.identifier = identifier @@ -328,6 +392,8 @@ def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=Non class AssignmentStatement(Statement): """Assignment of an expression to an l-value.""" + __slots__ = ("lvalue", "rvalue") + def __init__(self, lvalue: SubscriptedIdentifier, rvalue: Expression): self.lvalue = lvalue self.rvalue = rvalue @@ -340,6 +406,8 @@ class QuantumDeclaration(ASTNode): 'qubit' designator? Identifier """ + __slots__ = ("identifier", "designator") + def __init__(self, identifier: Identifier, designator=None): self.identifier = identifier self.designator = designator @@ -351,6 +419,8 @@ class AliasStatement(ASTNode): : 'let' Identifier EQUALS indexIdentifier SEMICOLON """ + __slots__ = ("identifier", "value") + def __init__(self, identifier: Identifier, value: Expression): self.identifier = identifier self.value = value @@ -368,6 +438,8 @@ class QuantumGateModifierName(enum.Enum): class QuantumGateModifier(ASTNode): """A modifier of a gate. For example, in ``ctrl @ x $0``, the ``ctrl @`` is the modifier.""" + __slots__ = ("modifier", "argument") + def __init__(self, modifier: QuantumGateModifierName, argument: Optional[Expression] = None): self.modifier = modifier self.argument = argument @@ -379,17 +451,19 @@ class QuantumGateCall(QuantumInstruction): : quantumGateModifier* quantumGateName ( LPAREN expressionList? RPAREN )? indexIdentifierList """ + __slots__ = ("quantumGateName", "indexIdentifierList", "parameters", "modifiers") + def __init__( self, quantumGateName: Identifier, indexIdentifierList: List[Identifier], - parameters: List[Expression] = None, - modifiers: Optional[List[QuantumGateModifier]] = None, + parameters: Sequence[Expression] = (), + modifiers: Sequence[QuantumGateModifier] = (), ): self.quantumGateName = quantumGateName self.indexIdentifierList = indexIdentifierList - self.parameters = parameters or [] - self.modifiers = modifiers or [] + self.parameters = parameters + self.modifiers = modifiers class QuantumBarrier(QuantumInstruction): @@ -398,6 +472,8 @@ class QuantumBarrier(QuantumInstruction): : 'barrier' indexIdentifierList """ + __slots__ = ("indexIdentifierList",) + def __init__(self, indexIdentifierList: List[Identifier]): self.indexIdentifierList = indexIdentifierList @@ -405,6 +481,8 @@ def __init__(self, indexIdentifierList: List[Identifier]): class QuantumReset(QuantumInstruction): """A built-in ``reset q0;`` statement.""" + __slots__ = ("identifier",) + def __init__(self, identifier: Identifier): self.identifier = identifier @@ -412,6 +490,8 @@ def __init__(self, identifier: Identifier): class QuantumDelay(QuantumInstruction): """A built-in ``delay[duration] q0;`` statement.""" + __slots__ = ("duration", "qubits") + def __init__(self, duration: Expression, qubits: List[Identifier]): self.duration = duration self.qubits = qubits @@ -424,6 +504,8 @@ class ProgramBlock(ASTNode): | LBRACE(statement | controlDirective) * RBRACE """ + __slots__ = ("statements",) + def __init__(self, statements: List[Statement]): self.statements = statements @@ -434,6 +516,8 @@ class ReturnStatement(ASTNode): # TODO probably should be a subclass of Control : 'return' ( expression | quantumMeasurement )? SEMICOLON; """ + __slots__ = ("expression",) + def __init__(self, expression=None): self.expression = expression @@ -444,7 +528,7 @@ class QuantumBlock(ProgramBlock): : LBRACE ( quantumStatement | quantumLoop )* RBRACE """ - pass + __slots__ = () class SubroutineBlock(ProgramBlock): @@ -453,7 +537,7 @@ class SubroutineBlock(ProgramBlock): : LBRACE statement* returnStatement? RBRACE """ - pass + __slots__ = () class QuantumGateDefinition(Statement): @@ -462,6 +546,8 @@ class QuantumGateDefinition(Statement): : 'gate' quantumGateSignature quantumBlock """ + __slots__ = ("name", "params", "qubits", "body") + def __init__( self, name: Identifier, @@ -482,14 +568,16 @@ class SubroutineDefinition(Statement): returnSignature? subroutineBlock """ + __slots__ = ("identifier", "arguments", "subroutineBlock") + def __init__( self, identifier: Identifier, subroutineBlock: SubroutineBlock, - arguments=None, # [ClassicalArgument] + arguments=(), # [ClassicalArgument] ): self.identifier = identifier - self.arguments = arguments or [] + self.arguments = arguments self.subroutineBlock = subroutineBlock @@ -499,7 +587,7 @@ class CalibrationArgument(ASTNode): : classicalArgumentList | expressionList """ - pass + __slots__ = () class CalibrationDefinition(Statement): @@ -511,15 +599,17 @@ class CalibrationDefinition(Statement): ; """ + __slots__ = ("name", "identifierList", "calibrationArgumentList") + def __init__( self, name: Identifier, identifierList: List[Identifier], - calibrationArgumentList: Optional[List[CalibrationArgument]] = None, + calibrationArgumentList: Sequence[CalibrationArgument] = (), ): self.name = name self.identifierList = identifierList - self.calibrationArgumentList = calibrationArgumentList or [] + self.calibrationArgumentList = calibrationArgumentList class BranchingStatement(Statement): @@ -528,6 +618,8 @@ class BranchingStatement(Statement): : 'if' LPAREN booleanExpression RPAREN programBlock ( 'else' programBlock )? """ + __slots__ = ("condition", "true_body", "false_body") + def __init__(self, condition: Expression, true_body: ProgramBlock, false_body=None): self.condition = condition self.true_body = true_body @@ -547,6 +639,8 @@ class ForLoopStatement(Statement): | "[" Range "]" """ + __slots__ = ("indexset", "parameter", "body") + def __init__( self, indexset: Union[Identifier, IndexSet, Range], @@ -567,6 +661,8 @@ class WhileLoopStatement(Statement): WhileLoop: "while" "(" Expression ")" ProgramBlock """ + __slots__ = ("condition", "body") + def __init__(self, condition: Expression, body: ProgramBlock): self.condition = condition self.body = body @@ -575,10 +671,14 @@ def __init__(self, condition: Expression, body: ProgramBlock): class BreakStatement(Statement): """AST node for ``break`` statements. Has no associated information.""" + __slots__ = () + class ContinueStatement(Statement): """AST node for ``continue`` statements. Has no associated information.""" + __slots__ = () + class IOModifier(enum.Enum): """IO Modifier object""" @@ -590,6 +690,8 @@ class IOModifier(enum.Enum): class IODeclaration(ClassicalDeclaration): """A declaration of an IO variable.""" + __slots__ = ("modifier",) + def __init__(self, modifier: IOModifier, type_: ClassicalType, identifier: Identifier): super().__init__(type_, identifier) self.modifier = modifier @@ -598,6 +700,8 @@ def __init__(self, modifier: IOModifier, type_: ClassicalType, identifier: Ident class DefaultCase(Expression): """An object representing the `default` special label in switch statements.""" + __slots__ = () + class SwitchStatementPreview(Statement): """AST node for the proposed 'switch-case' extension to OpenQASM 3, before the syntax was @@ -605,6 +709,8 @@ class SwitchStatementPreview(Statement): The stabilized form of the syntax instead uses :class:`.SwitchStatement`.""" + __slots__ = ("target", "cases") + def __init__( self, target: Expression, cases: Iterable[Tuple[Iterable[Expression], ProgramBlock]] ): @@ -619,6 +725,8 @@ class SwitchStatement(Statement): cannot be joined with other cases (even though that's meaningless, the V1 syntax permitted it). """ + __slots__ = ("target", "cases", "default") + def __init__( self, target: Expression, diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index c76e296d6bab..8997770c14e7 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -30,7 +30,6 @@ Barrier, CircuitInstruction, Clbit, - ControlledGate, Gate, Measure, Parameter, @@ -209,46 +208,60 @@ def dump(self, circuit, stream): # comparisons will work. _FIXED_PARAMETERS = (Parameter("p0"), Parameter("p1"), Parameter("p2"), Parameter("p3")) +_CANONICAL_STANDARD_GATES = { + standard: standard.gate_class(*_FIXED_PARAMETERS[: standard.num_params]) + for standard in StandardGate.all_gates() + if not standard.is_controlled_gate +} +_CANONICAL_CONTROLLED_STANDARD_GATES = { + standard: [ + standard.gate_class(*_FIXED_PARAMETERS[: standard.num_params], ctrl_state=ctrl_state) + for ctrl_state in range(1 << standard.num_ctrl_qubits) + ] + for standard in StandardGate.all_gates() + if standard.is_controlled_gate +} + # Mapping of symbols defined by `stdgates.inc` to their gate definition source. _KNOWN_INCLUDES = { "stdgates.inc": { - "p": library.PhaseGate(*_FIXED_PARAMETERS[:1]), - "x": library.XGate(), - "y": library.YGate(), - "z": library.ZGate(), - "h": library.HGate(), - "s": library.SGate(), - "sdg": library.SdgGate(), - "t": library.TGate(), - "tdg": library.TdgGate(), - "sx": library.SXGate(), - "rx": library.RXGate(*_FIXED_PARAMETERS[:1]), - "ry": library.RYGate(*_FIXED_PARAMETERS[:1]), - "rz": library.RZGate(*_FIXED_PARAMETERS[:1]), - "cx": library.CXGate(), - "cy": library.CYGate(), - "cz": library.CZGate(), - "cp": library.CPhaseGate(*_FIXED_PARAMETERS[:1]), - "crx": library.CRXGate(*_FIXED_PARAMETERS[:1]), - "cry": library.CRYGate(*_FIXED_PARAMETERS[:1]), - "crz": library.CRZGate(*_FIXED_PARAMETERS[:1]), - "ch": library.CHGate(), - "swap": library.SwapGate(), - "ccx": library.CCXGate(), - "cswap": library.CSwapGate(), - "cu": library.CUGate(*_FIXED_PARAMETERS[:4]), - "CX": library.CXGate(), - "phase": library.PhaseGate(*_FIXED_PARAMETERS[:1]), - "cphase": library.CPhaseGate(*_FIXED_PARAMETERS[:1]), - "id": library.IGate(), - "u1": library.U1Gate(*_FIXED_PARAMETERS[:1]), - "u2": library.U2Gate(*_FIXED_PARAMETERS[:2]), - "u3": library.U3Gate(*_FIXED_PARAMETERS[:3]), + "p": _CANONICAL_STANDARD_GATES[StandardGate.PhaseGate], + "x": _CANONICAL_STANDARD_GATES[StandardGate.XGate], + "y": _CANONICAL_STANDARD_GATES[StandardGate.YGate], + "z": _CANONICAL_STANDARD_GATES[StandardGate.ZGate], + "h": _CANONICAL_STANDARD_GATES[StandardGate.HGate], + "s": _CANONICAL_STANDARD_GATES[StandardGate.SGate], + "sdg": _CANONICAL_STANDARD_GATES[StandardGate.SdgGate], + "t": _CANONICAL_STANDARD_GATES[StandardGate.TGate], + "tdg": _CANONICAL_STANDARD_GATES[StandardGate.TdgGate], + "sx": _CANONICAL_STANDARD_GATES[StandardGate.SXGate], + "rx": _CANONICAL_STANDARD_GATES[StandardGate.RXGate], + "ry": _CANONICAL_STANDARD_GATES[StandardGate.RYGate], + "rz": _CANONICAL_STANDARD_GATES[StandardGate.RZGate], + "cx": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CXGate][1], + "cy": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CYGate][1], + "cz": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CZGate][1], + "cp": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CPhaseGate][1], + "crx": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CRXGate][1], + "cry": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CRYGate][1], + "crz": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CRZGate][1], + "ch": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CHGate][1], + "swap": _CANONICAL_STANDARD_GATES[StandardGate.SwapGate], + "ccx": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CCXGate][3], + "cswap": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CSwapGate][1], + "cu": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CUGate][1], + "CX": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CXGate][1], + "phase": _CANONICAL_STANDARD_GATES[StandardGate.PhaseGate], + "cphase": _CANONICAL_CONTROLLED_STANDARD_GATES[StandardGate.CPhaseGate][1], + "id": _CANONICAL_STANDARD_GATES[StandardGate.IGate], + "u1": _CANONICAL_STANDARD_GATES[StandardGate.U1Gate], + "u2": _CANONICAL_STANDARD_GATES[StandardGate.U2Gate], + "u3": _CANONICAL_STANDARD_GATES[StandardGate.U3Gate], }, } _BUILTIN_GATES = { - "U": library.UGate(*_FIXED_PARAMETERS[:3]), + "U": _CANONICAL_STANDARD_GATES[StandardGate.UGate], } @@ -479,9 +492,14 @@ def register_gate( def get_gate(self, gate: Gate) -> ast.Identifier | None: """Lookup the identifier for a given `Gate`, if it exists.""" canonical = _gate_canonical_form(gate) - # `our_defn.canonical is None` means a basis gate that we should assume is always valid. if (our_defn := self.gates.get(gate.name)) is not None and ( - our_defn.canonical is None or our_defn.canonical == canonical + # We arrange things such that the known definitions for the vast majority of gates we + # will encounter are the exact same canonical instance, so an `is` check saves time. + our_defn.canonical is canonical + # `our_defn.canonical is None` means a basis gate that we should assume is always valid. + or our_defn.canonical is None + # The last catch, if the canonical form is some custom gate that compares equal to this. + or our_defn.canonical == canonical ): return ast.Identifier(gate.name) if canonical._standard_gate is not None: @@ -506,11 +524,14 @@ def _gate_canonical_form(gate: Gate) -> Gate: # If a gate is part of the Qiskit standard-library gates, we know we can safely produce a # reparameterised gate by passing the parameters positionally to the standard-gate constructor # (and control state, if appropriate). - if gate._standard_gate and not isinstance(gate, ControlledGate): - return gate.base_class(*_FIXED_PARAMETERS[: len(gate.params)]) - elif gate._standard_gate: - return gate.base_class(*_FIXED_PARAMETERS[: len(gate.params)], ctrl_state=gate.ctrl_state) - return gate + standard = gate._standard_gate + if standard is None: + return gate + return ( + _CANONICAL_CONTROLLED_STANDARD_GATES[standard][gate.ctrl_state] + if standard.is_controlled_gate + else _CANONICAL_STANDARD_GATES[standard] + ) @dataclasses.dataclass @@ -597,12 +618,10 @@ def new_context(self, body: QuantumCircuit): self.scope = old_scope self.symbols = old_symbols - def _lookup_variable(self, variable) -> ast.Identifier: - """Lookup a Qiskit object within the current context, and return the name that should be + def _lookup_bit(self, bit) -> ast.Identifier: + """Lookup a Qiskit bit within the current context, and return the name that should be used to represent it in OpenQASM 3 programmes.""" - if isinstance(variable, Bit): - variable = self.scope.bit_map[variable] - return self.symbols.get_variable(variable) + return self.symbols.get_variable(self.scope.bit_map[bit]) def build_program(self): """Builds a Program""" @@ -909,7 +928,7 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen out = [] for register in registers: name = self.symbols.register_variable(register.name, register, allow_rename=True) - elements = [self._lookup_variable(bit) for bit in register] + elements = [self._lookup_bit(bit) for bit in register] for i, bit in enumerate(register): # This might shadow previous definitions, but that's not a problem. self.symbols.set_object_ident( @@ -956,18 +975,17 @@ def build_current_scope(self) -> List[ast.Statement]: if isinstance(instruction.operation, Gate): nodes = [self.build_gate_call(instruction)] elif isinstance(instruction.operation, Barrier): - operands = [self._lookup_variable(operand) for operand in instruction.qubits] + operands = [self._lookup_bit(operand) for operand in instruction.qubits] nodes = [ast.QuantumBarrier(operands)] elif isinstance(instruction.operation, Measure): measurement = ast.QuantumMeasurement( - [self._lookup_variable(operand) for operand in instruction.qubits] + [self._lookup_bit(operand) for operand in instruction.qubits] ) - qubit = self._lookup_variable(instruction.clbits[0]) + qubit = self._lookup_bit(instruction.clbits[0]) nodes = [ast.QuantumMeasurementAssignment(qubit, measurement)] elif isinstance(instruction.operation, Reset): nodes = [ - ast.QuantumReset(self._lookup_variable(operand)) - for operand in instruction.qubits + ast.QuantumReset(self._lookup_bit(operand)) for operand in instruction.qubits ] elif isinstance(instruction.operation, Delay): nodes = [self.build_delay(instruction)] @@ -988,13 +1006,13 @@ def build_current_scope(self) -> List[ast.Statement]: f" but received '{instruction.operation}'" ) - if instruction.operation.condition is None: + if instruction.operation._condition is None: statements.extend(nodes) else: body = ast.ProgramBlock(nodes) statements.append( ast.BranchingStatement( - self.build_expression(_lift_condition(instruction.operation.condition)), + self.build_expression(_lift_condition(instruction.operation._condition)), body, ) ) @@ -1101,9 +1119,14 @@ def build_for_loop(self, instruction: CircuitInstruction) -> ast.ForLoopStatemen body_ast = ast.ProgramBlock(self.build_current_scope()) return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast) + def _lookup_variable_for_expression(self, var): + if isinstance(var, Bit): + return self._lookup_bit(var) + return self.symbols.get_variable(var) + def build_expression(self, node: expr.Expr) -> ast.Expression: """Build an expression.""" - return node.accept(_ExprBuilder(self._lookup_variable)) + return node.accept(_ExprBuilder(self._lookup_variable_for_expression)) def build_delay(self, instruction: CircuitInstruction) -> ast.QuantumDelay: """Build a built-in delay statement.""" @@ -1123,9 +1146,7 @@ def build_delay(self, instruction: CircuitInstruction) -> ast.QuantumDelay: "dt": ast.DurationUnit.SAMPLE, } duration = ast.DurationLiteral(duration_value, unit_map[unit]) - return ast.QuantumDelay( - duration, [self._lookup_variable(qubit) for qubit in instruction.qubits] - ) + return ast.QuantumDelay(duration, [self._lookup_bit(qubit) for qubit in instruction.qubits]) def build_integer(self, value) -> ast.IntegerLiteral: """Build an integer literal, raising a :obj:`.QASM3ExporterError` if the input is not @@ -1145,9 +1166,11 @@ def _rebind_scoped_parameters(self, expression): # missing, pending a new system in Terra to replace it (2022-03-07). if not isinstance(expression, ParameterExpression): return expression + if isinstance(expression, Parameter): + return self.symbols.get_variable(expression).string return expression.subs( { - param: Parameter(self._lookup_variable(param).string) + param: Parameter(self.symbols.get_variable(param).string, uuid=param.uuid) for param in expression.parameters } ) @@ -1160,18 +1183,14 @@ def build_gate_call(self, instruction: CircuitInstruction): ident = self.symbols.get_gate(instruction.operation) if ident is None: ident = self.define_gate(instruction.operation) - qubits = [self._lookup_variable(qubit) for qubit in instruction.qubits] - if self.disable_constants: - parameters = [ - ast.StringifyAndPray(self._rebind_scoped_parameters(param)) - for param in instruction.operation.params - ] - else: - parameters = [ - ast.StringifyAndPray(pi_check(self._rebind_scoped_parameters(param), output="qasm")) - for param in instruction.operation.params - ] - + qubits = [self._lookup_bit(qubit) for qubit in instruction.qubits] + parameters = [ + ast.StringifyAndPray(self._rebind_scoped_parameters(param)) + for param in instruction.operation.params + ] + if not self.disable_constants: + for parameter in parameters: + parameter.obj = pi_check(parameter.obj, output="qasm") return ast.QuantumGateCall(ident, qubits, parameters=parameters) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index f75dbda30d95..ffaaa5f56142 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -125,6 +125,8 @@ def open(*args): will be able to load all released format versions of QPY (up until ``QPY_VERSION``). +.. _qpy_compatibility: + QPY Compatibility ================= @@ -185,6 +187,24 @@ def open(*args): * - Qiskit (qiskit-terra for < 1.0.0) version - :func:`.dump` format(s) output versions - :func:`.load` maximum supported version (older format versions can always be read) + * - 1.3.0 + - 10, 11, 12, 13 + - 13 + * - 1.2.4 + - 10, 11, 12 + - 12 + * - 1.2.3 (yanked) + - 10, 11, 12 + - 12 + * - 1.2.2 + - 10, 11, 12 + - 12 + * - 1.2.1 + - 10, 11, 12 + - 12 + * - 1.2.0 + - 10, 11, 12 + - 12 * - 1.1.0 - 10, 11, 12 - 12 @@ -350,6 +370,141 @@ def open(*args): by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_13: + +Version 13 +---------- + +Version 13 added a native Qiskit serialization representation for :class:`.ParameterExpression`. +Previous QPY versions relied on either ``sympy`` or ``symengine`` to serialize the underlying symbolic +expression. Starting in Version 13, QPY now represents the sequence of API calls used to create the +:class:`.ParameterExpression`. + +The main change in the serialization format is in the :ref:`qpy_param_expr_v3` payload. The +``expr_size`` bytes following the head now contain an array of ``PARAM_EXPR_ELEM_V13`` structs. The +intent is for this array to be read one struct at a time, where each struct describes one of the +calls to make to reconstruct the :class:`.ParameterExpression`. + +PARAM_EXPR_ELEM_V13 +~~~~~~~~~~~~~~~~~~~ + +The struct format is defined as: + +.. code-block:: c + + struct { + unsigned char op_code; + char lhs_type; + char lhs[16]; + char rhs_type; + char rhs[16]; + } PARAM_EXPR_ELEM_V13; + +The ``op_code`` field is used to define the operation added to the :class:`.ParameterExpression`. +The value can be: + +.. list-table:: PARAM_EXPR_ELEM_V13 op code values + :header-rows: 1 + + * - ``op_code`` + - :class:`.ParameterExpression` method + * - 0 + - :meth:`~.ParameterExpression.__add__` + * - 1 + - :meth:`~.ParameterExpression.__sub__` + * - 2 + - :meth:`~.ParameterExpression.__mul__` + * - 3 + - :meth:`~.ParameterExpression.__truediv__` + * - 4 + - :meth:`~.ParameterExpression.__pow__` + * - 5 + - :meth:`~.ParameterExpression.sin` + * - 6 + - :meth:`~.ParameterExpression.cos` + * - 7 + - :meth:`~.ParameterExpression.tan` + * - 8 + - :meth:`~.ParameterExpression.arcsin` + * - 9 + - :meth:`~.ParameterExpression.arccos` + * - 10 + - :meth:`~.ParameterExpression.exp` + * - 11 + - :meth:`~.ParameterExpression.log` + * - 12 + - :meth:`~.ParameterExpression.sign` + * - 13 + - :meth:`~.ParameterExpression.gradient` + * - 14 + - :meth:`~.ParameterExpression.conjugate` + * - 15 + - :meth:`~.ParameterExpression.subs` + * - 16 + - :meth:`~.ParameterExpression.abs` + * - 17 + - :meth:`~.ParameterExpression.arctan` + * - 255 + - NULL + +The ``NULL`` value of 255 is only used to fill the op code field for +entries that are not actual operations but indicate recursive definitions. +Then the ``lhs_type`` and ``rhs_type`` fields are used to describe +the operand types and can be one of the following UTF-8 encoded +characters: + +.. list-table:: PARAM_EXPR_ELEM_V13 operand type values + :header-rows: 1 + + * - Value + - Type + * - ``n`` + - ``None`` + * - ``p`` + - :class:`.Parameter` + * - ``f`` + - ``float`` + * - ``c`` + - ``complex`` + * - ``i`` + - ``int`` + * - ``s`` + - Recursive :class:`.ParameterExpression` definition start + * - ``e`` + - Recursive :class:`.ParameterExpression` definition stop + * - ``u`` + - substitution + +If the type value is ``f`` ,``c`` or ``i``, the corresponding ``lhs`` or `rhs`` +field widths are 128 bits each. In the case of floats, the literal value is encoded as a double +with 0 padding, while complex numbers are encoded as real part followed by imaginary part, +taking up 64 bits each. For ``i`, the value is encoded as a 64 bit signed integer with 0 padding +for the full 128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used +as it indicates an argument that's not used. For ``p`` the data is the UUID for the +:class:`.Parameter` which can be looked up in the symbol map described in the +``map_elements`` outer :ref:`qpy_param_expr_v3` payload. If the type value is +``s`` this marks the start of a a new recursive section for a nested +:class:`.ParameterExpression`. For example, in the following snippet there is an inner ``expr`` +contained in ``final_expr``, constituting a nested expression:: + + from qiskit.circuit import Parameter + + x = Parameter("x") + y = Parameter("y") + z = Parameter("z") + + expr = (x + y) / 2 + final_expr = z**2 + expr + +When ``s`` is encountered, this indicates that until an ``e` struct is reached, the next structs +are used for a recursive definition. For both +``s`` and ``e`` types, the data values are not used, and always set to 0. The type value +of ``u`` is used to represent a substitution call. This is only used for ``lhs_type`` +and is always paired with an ``rhs_type`` of ``n``. The data value is the size in bytes of +a :ref:`qpy_mapping` encoded mapping of :class:`.Parameter` names to their value for the +:meth:`~.ParameterExpression.subs` call. The mapping data is immediately following the +struct, and the next struct starts immediately after the mapping data. + .. _qpy_version_12: Version 12 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 3fe1834db6ef..174acceb59e4 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -318,6 +318,13 @@ def _read_instruction( use_symengine, standalone_vars, ) + if condition is not None: + warnings.warn( + f"The .condition attribute on {gate_name} will be loaded as an IfElseOp " + "starting in Qiskit 2.0", + FutureWarning, + stacklevel=3, + ) inst_obj.condition = condition if instruction.label_size > 0: inst_obj.label = label @@ -414,6 +421,12 @@ def _read_instruction( gate = gate_class(*params) if condition: if not isinstance(gate, ControlFlowOp): + warnings.warn( + f"The .condition attribute on {gate} will be loaded as an " + "IfElseOp starting in Qiskit 2.0", + FutureWarning, + stacklevel=3, + ) gate = gate.c_if(*condition) else: gate.condition = condition @@ -761,13 +774,13 @@ def _write_instruction( condition_type = type_keys.Condition.NONE condition_register = b"" condition_value = 0 - if (op_condition := getattr(instruction.operation, "condition", None)) is not None: + if (op_condition := getattr(instruction.operation, "_condition", None)) is not None: if isinstance(op_condition, expr.Expr): condition_type = type_keys.Condition.EXPRESSION else: condition_type = type_keys.Condition.TWO_TUPLE - condition_register = _dumps_register(instruction.operation.condition[0], index_map) - condition_value = int(instruction.operation.condition[1]) + condition_register = _dumps_register(instruction.operation._condition[0], index_map) + condition_value = int(instruction.operation._condition[1]) gate_class_name = gate_class_name.encode(common.ENCODE) label = getattr(instruction.operation, "label", None) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 5b82e14d15cd..9799fdf3f459 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -15,6 +15,7 @@ from __future__ import annotations import collections.abc +import io import struct import uuid @@ -25,7 +26,12 @@ from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister from qiskit.circuit.classical import expr, types from qiskit.circuit.parameter import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit.parameterexpression import ( + ParameterExpression, + op_code_to_method, + _OPCode, + _SUBS, +) from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.qpy import common, formats, exceptions, type_keys @@ -50,20 +56,132 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _write_parameter_expression(file_obj, obj, use_symengine, *, version): - if use_symengine: - expr_bytes = obj._symbol_expr.__reduce__()[1][0] +def _encode_replay_entry(inst, file_obj, version, r_side=False): + inst_type = None + inst_data = None + if inst is None: + inst_type = "n" + inst_data = b"\x00" + elif isinstance(inst, Parameter): + inst_type = "p" + inst_data = inst.uuid.bytes + elif isinstance(inst, complex): + inst_type = "c" + inst_data = struct.pack("!dd", inst.real, inst.imag) + elif isinstance(inst, float): + inst_type = "f" + inst_data = struct.pack("!Qd", 0, inst) + elif isinstance(inst, int): + inst_type = "i" + inst_data = struct.pack("!Qq", 0, inst) + elif isinstance(inst, ParameterExpression): + if not r_side: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + 255, + "s".encode("utf8"), + b"\x00", + "n".encode("utf8"), + b"\x00", + ) + else: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + 255, + "n".encode("utf8"), + b"\x00", + "s".encode("utf8"), + b"\x00", + ) + file_obj.write(entry) + _write_parameter_expression_v13(file_obj, inst, version) + if not r_side: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + 255, + "e".encode("utf8"), + b"\x00", + "n".encode("utf8"), + b"\x00", + ) + else: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + 255, + "n".encode("utf8"), + b"\x00", + "e".encode("utf8"), + b"\x00", + ) + file_obj.write(entry) + inst_type = "n" + inst_data = b"\x00" else: - from sympy import srepr, sympify + raise exceptions.QpyError("Invalid parameter expression type") + return inst_type, inst_data + + +def _encode_replay_subs(subs, file_obj, version): + with io.BytesIO() as mapping_buf: + subs_dict = {k.name: v for k, v in subs.binds.items()} + common.write_mapping( + mapping_buf, mapping=subs_dict, serializer=dumps_value, version=version + ) + data = mapping_buf.getvalue() + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + subs.op, + "u".encode("utf8"), + struct.pack("!QQ", len(data), 0), + "n".encode("utf8"), + b"\x00", + ) + file_obj.write(entry) + file_obj.write(data) + return subs.binds + + +def _write_parameter_expression_v13(file_obj, obj, version): + symbol_map = {} + for inst in obj._qpy_replay: + if isinstance(inst, _SUBS): + symbol_map.update(_encode_replay_subs(inst, file_obj, version)) + continue + lhs_type, lhs = _encode_replay_entry(inst.lhs, file_obj, version) + rhs_type, rhs = _encode_replay_entry(inst.rhs, file_obj, version, True) + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V13_PACK, + inst.op, + lhs_type.encode("utf8"), + lhs, + rhs_type.encode("utf8"), + rhs, + ) + file_obj.write(entry) + return symbol_map - expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE) +def _write_parameter_expression(file_obj, obj, use_symengine, *, version): + extra_symbols = None + if version < 13: + if use_symengine: + expr_bytes = obj._symbol_expr.__reduce__()[1][0] + else: + from sympy import srepr, sympify + + expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE) + else: + with io.BytesIO() as buf: + extra_symbols = _write_parameter_expression_v13(buf, obj, version) + expr_bytes = buf.getvalue() + symbol_table_len = len(obj._parameter_symbols) + if extra_symbols: + symbol_table_len += 2 * len(extra_symbols) param_expr_header_raw = struct.pack( - formats.PARAMETER_EXPR_PACK, len(obj._parameter_symbols), len(expr_bytes) + formats.PARAMETER_EXPR_PACK, symbol_table_len, len(expr_bytes) ) file_obj.write(param_expr_header_raw) file_obj.write(expr_bytes) - for symbol, value in obj._parameter_symbols.items(): symbol_key = type_keys.Value.assign(symbol) @@ -89,6 +207,49 @@ def _write_parameter_expression(file_obj, obj, use_symengine, *, version): file_obj.write(elem_header) file_obj.write(symbol_data) file_obj.write(value_data) + if extra_symbols: + for symbol in extra_symbols: + symbol_key = type_keys.Value.assign(symbol) + # serialize key + if symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.data_to_binary(symbol, _write_parameter) + # serialize value + value_key, value_data = dumps_value( + symbol, version=version, use_symengine=use_symengine + ) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + symbol_key, + value_key, + len(value_data), + ) + file_obj.write(elem_header) + file_obj.write(symbol_data) + file_obj.write(value_data) + for symbol in extra_symbols.values(): + symbol_key = type_keys.Value.assign(symbol) + # serialize key + if symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.data_to_binary(symbol, _write_parameter) + # serialize value + value_key, value_data = dumps_value( + symbol, version=version, use_symengine=use_symengine + ) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + symbol_key, + value_key, + len(value_data), + ) + file_obj.write(elem_header) + file_obj.write(symbol_data) + file_obj.write(value_data) class _ExprWriter(expr.ExprVisitor[None]): @@ -334,6 +495,141 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): return ParameterExpression(symbol_map, expr_) +def _read_parameter_expression_v13(file_obj, vectors, version): + data = formats.PARAMETER_EXPR( + *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) + ) + + payload = file_obj.read(data.expr_size) + + symbol_map = {} + for _ in range(data.map_elements): + elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( + *struct.unpack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_V3_SIZE), + ) + ) + symbol_key = type_keys.Value(elem_data.symbol_type) + + if symbol_key == type_keys.Value.PARAMETER: + symbol = _read_parameter(file_obj) + elif symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol = _read_parameter_vec(file_obj, vectors) + else: + 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) + if elem_key == type_keys.Value.INTEGER: + value = struct.unpack("!q", binary_data) + elif elem_key == type_keys.Value.FLOAT: + value = struct.unpack("!d", binary_data) + elif elem_key == type_keys.Value.COMPLEX: + value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) + elif elem_key in (type_keys.Value.PARAMETER, type_keys.Value.PARAMETER_VECTOR): + value = symbol._symbol_expr + elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: + value = common.data_from_binary( + binary_data, + _read_parameter_expression_v13, + vectors=vectors, + ) + else: + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") + symbol_map[symbol] = value + with io.BytesIO(payload) as buf: + return _read_parameter_expr_v13(buf, symbol_map, version, vectors) + + +def _read_parameter_expr_v13(buf, symbol_map, version, vectors): + param_uuid_map = {symbol.uuid: symbol for symbol in symbol_map if isinstance(symbol, Parameter)} + name_map = {str(v): k for k, v in symbol_map.items()} + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + stack = [] + while data: + expression_data = formats.PARAM_EXPR_ELEM_V13._make( + struct.unpack(formats.PARAM_EXPR_ELEM_V13_PACK, data) + ) + # LHS + if expression_data.LHS_TYPE == b"p": + stack.append(param_uuid_map[uuid.UUID(bytes=expression_data.LHS)]) + elif expression_data.LHS_TYPE == b"f": + stack.append(struct.unpack("!Qd", expression_data.LHS)[1]) + elif expression_data.LHS_TYPE == b"n": + pass + elif expression_data.LHS_TYPE == b"c": + stack.append(complex(*struct.unpack("!dd", expression_data.LHS))) + elif expression_data.LHS_TYPE == b"i": + stack.append(struct.unpack("!Qq", expression_data.LHS)[1]) + elif expression_data.LHS_TYPE == b"s": + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + continue + elif expression_data.LHS_TYPE == b"e": + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + continue + elif expression_data.LHS_TYPE == b"u": + size = struct.unpack_from("!QQ", expression_data.LHS)[0] + subs_map_data = buf.read(size) + with io.BytesIO(subs_map_data) as mapping_buf: + mapping = common.read_mapping( + mapping_buf, deserializer=loads_value, version=version, vectors=vectors + ) + stack.append({name_map[k]: v for k, v in mapping.items()}) + else: + raise exceptions.QpyError( + "Unknown ParameterExpression operation type {expression_data.LHS_TYPE}" + ) + # RHS + if expression_data.RHS_TYPE == b"p": + stack.append(param_uuid_map[uuid.UUID(bytes=expression_data.RHS)]) + elif expression_data.RHS_TYPE == b"f": + stack.append(struct.unpack("!Qd", expression_data.RHS)[1]) + elif expression_data.RHS_TYPE == b"n": + pass + elif expression_data.RHS_TYPE == b"c": + stack.append(complex(*struct.unpack("!dd", expression_data.RHS))) + elif expression_data.RHS_TYPE == b"i": + stack.append(struct.unpack("!Qq", expression_data.RHS)[1]) + elif expression_data.RHS_TYPE == b"s": + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + continue + elif expression_data.RHS_TYPE == b"e": + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + continue + else: + raise exceptions.QpyError( + f"Unknown ParameterExpression operation type {expression_data.RHS_TYPE}" + ) + if expression_data.OP_CODE == 255: + continue + method_str = op_code_to_method(_OPCode(expression_data.OP_CODE)) + if expression_data.OP_CODE in {0, 1, 2, 3, 4, 13, 15, 18, 19, 20}: + rhs = stack.pop() + lhs = stack.pop() + # Reverse ops for commutative ops, which are add, mul (0 and 2 respectively) + # op codes 13 and 15 can never be reversed and 18, 19, 20 + # are the reversed versions of non-commuative operations + # so 1, 3, 4 and 18, 19, 20 handle this explicitly. + if ( + not isinstance(lhs, ParameterExpression) + and isinstance(rhs, ParameterExpression) + and expression_data.OP_CODE in {0, 2} + ): + if expression_data.OP_CODE == 0: + method_str = "__radd__" + elif expression_data.OP_CODE == 2: + method_str = "__rmul__" + stack.append(getattr(rhs, method_str)(lhs)) + else: + stack.append(getattr(lhs, method_str)(rhs)) + else: + lhs = stack.pop() + stack.append(getattr(lhs, method_str)()) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) + return stack.pop() + + def _read_expr( file_obj, clbits: collections.abc.Sequence[Clbit], @@ -664,13 +960,17 @@ def loads_value( if type_key == type_keys.Value.PARAMETER_EXPRESSION: if version < 3: return common.data_from_binary(binary_data, _read_parameter_expression) - else: + elif version < 13: return common.data_from_binary( binary_data, _read_parameter_expression_v3, vectors=vectors, use_symengine=use_symengine, ) + else: + return common.data_from_binary( + binary_data, _read_parameter_expression_v13, vectors=vectors, version=version + ) if type_key == type_keys.Value.EXPRESSION: return common.data_from_binary( binary_data, diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 8d8a57b7404f..c2585d5be4d3 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 12 +QPY_VERSION = 13 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index a48a9ea777fa..7696cae94e2b 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -259,6 +259,13 @@ PARAMETER_PACK = "!H16s" PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) +# PARAMETEREXPRESSION_ENTRY +PARAM_EXPR_ELEM_V13 = namedtuple( + "PARAM_EXPR_ELEM_V13", ["OP_CODE", "LHS_TYPE", "LHS", "RHS_TYPE", "RHS"] +) +PARAM_EXPR_ELEM_V13_PACK = "!Bc16sc16s" +PARAM_EXPR_ELEM_V13_SIZE = struct.calcsize(PARAM_EXPR_ELEM_V13_PACK) + # COMPLEX COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) COMPLEX_PACK = "!dd" diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index a196874d290c..cab90eb9407f 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -136,9 +136,9 @@ def dump( used as the ``cls`` kwarg on the `json.dump()`` call to JSON serialize that dictionary. use_symengine: If True, all objects containing symbolic expressions will be serialized using symengine's 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. For this reason, the option defaults to False. + but not supported in all platforms. This flag only has an effect if the emitted QPY format + version is 10, 11, or 12. For QPY format version >= 13 (which is the default starting in + Qiskit 1.3.0) this flag is no longer used. version: The QPY format version to emit. By default this defaults to the latest supported format of :attr:`~.qpy.QPY_VERSION`, however for compatibility reasons if you need to load the generated QPY payload with an older diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index adb8d9891940..8479aa5dd991 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -543,13 +543,15 @@ def compose(self, other: Operator, qargs: list | None = None, front: bool = Fals ret._op_shape = new_shape return ret - def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator: + def power( + self, n: float, branch_cut_rotation=cmath.pi * 1e-12, assume_unitary=False + ) -> Operator: """Return the matrix power of the operator. Non-integer powers of operators with an eigenvalue whose complex phase is :math:`\\pi` have a branch cut in the complex plane, which makes the calculation of the principal root around this cut subject to precision / differences in BLAS implementation. For example, the square - root of Pauli Y can return the :math:`\\pi/2` or :math:`\\-pi/2` Y rotation depending on + root of Pauli Y can return the :math:`\\pi/2` or :math:`-\\pi/2` Y rotation depending on whether the -1 eigenvalue is found as ``complex(-1, tiny)`` or ``complex(-1, -tiny)``. Such eigenvalues are really common in quantum information, so this function first phase-rotates the input matrix to shift the branch cut to a far less common point. The underlying @@ -574,6 +576,8 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator: complex plane. This shifts the branch cut away from the common point of :math:`-1`, but can cause a different root to be selected as the principal root. The rotation is anticlockwise, following the standard convention for complex phase. + assume_unitary (bool): if ``True``, the operator is assumed to be unitary. In this case, + for fractional powers we employ a faster implementation based on Schur's decomposition. Returns: Operator: the resulting operator ``O ** n``. @@ -581,6 +585,11 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator: Raises: QiskitError: if the input and output dimensions of the operator are not equal. + + .. note:: + It is only safe to set the argument ``assume_unitary`` to ``True`` when the operator + is unitary (or, more generally, normal). Otherwise, the function will return an + incorrect output. """ if self.input_dims() != self.output_dims(): raise QiskitError("Can only power with input_dims = output_dims.") @@ -590,11 +599,23 @@ def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator: else: import scipy.linalg - ret._data = cmath.rect( - 1, branch_cut_rotation * n - ) * scipy.linalg.fractional_matrix_power( - cmath.rect(1, -branch_cut_rotation) * self.data, n - ) + if assume_unitary: + # Experimentally, for fractional powers this seems to be 3x faster than + # calling scipy.linalg.fractional_matrix_power(self.data, exponent) + decomposition, unitary = scipy.linalg.schur( + cmath.rect(1, -branch_cut_rotation) * self.data, output="complex" + ) + decomposition_diagonal = decomposition.diagonal() + decomposition_power = [pow(element, n) for element in decomposition_diagonal] + unitary_power = unitary @ np.diag(decomposition_power) @ unitary.conj().T + ret._data = cmath.rect(1, branch_cut_rotation * n) * unitary_power + else: + ret._data = cmath.rect( + 1, branch_cut_rotation * n + ) * scipy.linalg.fractional_matrix_power( + cmath.rect(1, -branch_cut_rotation) * self.data, n + ) + return ret def tensor(self, other: Operator) -> Operator: diff --git a/qiskit/quantum_info/operators/symplectic/base_pauli.py b/qiskit/quantum_info/operators/symplectic/base_pauli.py index 1d9e88929b2d..f49127bc32d0 100644 --- a/qiskit/quantum_info/operators/symplectic/base_pauli.py +++ b/qiskit/quantum_info/operators/symplectic/base_pauli.py @@ -33,6 +33,8 @@ # utility for _to_matrix _PARITY = np.array([-1 if bin(i).count("1") % 2 else 1 for i in range(256)], dtype=complex) +# Utility for `_to_label` +_TO_LABEL_CHARS = np.array([ord("I"), ord("X"), ord("Z"), ord("Y")], dtype=np.uint8) class BasePauli(BaseOperator, AdjointMixin, MultiplyMixin): @@ -496,25 +498,15 @@ def _to_label(z, x, phase, group_phase=False, full_group=True, return_phase=Fals the phase ``q`` for the coefficient :math:`(-i)^(q + x.z)` for the label from the full Pauli group. """ - num_qubits = z.size - phase = int(phase) - coeff_labels = {0: "", 1: "-i", 2: "-", 3: "i"} - label = "" - for i in range(num_qubits): - if not z[num_qubits - 1 - i]: - if not x[num_qubits - 1 - i]: - label += "I" - else: - label += "X" - elif not x[num_qubits - 1 - i]: - label += "Z" - else: - label += "Y" - if not group_phase: - phase -= 1 - phase %= 4 + # Map each qubit to the {I: 0, X: 1, Z: 2, Y: 3} integer form, then use Numpy advanced + # indexing to get a new data buffer which is compatible with an ASCII string label. + index = z << 1 + index += x + ascii_label = _TO_LABEL_CHARS[index[::-1]].data.tobytes() + phase = (int(phase) if group_phase else int(phase) - ascii_label.count(b"Y")) % 4 + label = ascii_label.decode("ascii") if phase and full_group: - label = coeff_labels[phase] + label + label = ("", "-i", "-", "i")[phase] + label if return_phase: return label, phase return label diff --git a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py index 283893e59f58..44bcd31e2da5 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py +++ b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py @@ -81,7 +81,7 @@ def _append_operation(clifford, operation, qargs=None): else: # assert isinstance(gate, Instruction) name = gate.name - if getattr(gate, "condition", None) is not None: + if getattr(gate, "_condition", None) is not None: raise QiskitError("Conditional gate is not a valid Clifford operation.") # Apply gate if it is a Clifford basis gate diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 7f1f3405304b..34e524348bf8 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -945,6 +945,14 @@ def to_list(self, array: bool = False): return labels return labels.tolist() + def to_sparse_list(self): + """Convert to a sparse Pauli list format with elements (pauli, qubits, coefficient).""" + pauli_labels = self.paulis.to_labels() + sparse_list = [ + (*sparsify_label(label), coeff) for label, coeff in zip(pauli_labels, self.coeffs) + ] + return sparse_list + def to_matrix(self, sparse: bool = False, force_serial: bool = False) -> np.ndarray: """Convert to a dense or sparse matrix. @@ -1188,5 +1196,13 @@ def apply_layout( return new_op.compose(self, qargs=layout) +def sparsify_label(pauli_string): + """Return a sparse format of a Pauli string, e.g. "XIIIZ" -> ("XZ", [0, 4]).""" + qubits = [i for i, label in enumerate(reversed(pauli_string)) if label != "I"] + sparse_label = "".join(pauli_string[~i] for i in qubits) + + return sparse_label, qubits + + # Update docstrings for API docs generate_apidocs(SparsePauliOp) diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index 4484ffe1ee43..2bbaf4e3c55f 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -476,7 +476,7 @@ def _expectation_value_pauli(self, pauli, qargs=None): pauli_phase = (-1j) ** pauli.phase if pauli.phase else 1 if x_mask + z_mask == 0: - return pauli_phase * np.linalg.norm(self.data) + return pauli_phase * np.linalg.norm(self.data) ** 2 if x_mask == 0: return pauli_phase * expval_pauli_no_x(self.data, self.num_qubits, z_mask) diff --git a/qiskit/result/mitigation/base_readout_mitigator.py b/qiskit/result/mitigation/base_readout_mitigator.py index f4ca398b73cf..296565d47d01 100644 --- a/qiskit/result/mitigation/base_readout_mitigator.py +++ b/qiskit/result/mitigation/base_readout_mitigator.py @@ -21,7 +21,7 @@ class BaseReadoutMitigator(ABC): - """Base readout error mitigator class.""" + """This class is DEPRECATED. Base readout error mitigator class.""" @abstractmethod def quasi_probabilities( diff --git a/qiskit/result/mitigation/correlated_readout_mitigator.py b/qiskit/result/mitigation/correlated_readout_mitigator.py index 99e6f9ae4145..190c0509af0e 100644 --- a/qiskit/result/mitigation/correlated_readout_mitigator.py +++ b/qiskit/result/mitigation/correlated_readout_mitigator.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.exceptions import QiskitError +from qiskit.utils.deprecation import deprecate_func from ..distributions.quasi import QuasiDistribution from ..counts import Counts from .base_readout_mitigator import BaseReadoutMitigator @@ -25,7 +26,7 @@ class CorrelatedReadoutMitigator(BaseReadoutMitigator): - """N-qubit readout error mitigator. + """This class is DEPRECATED. N-qubit readout error mitigator. Mitigates :meth:`expectation_value` and :meth:`quasi_probabilities`. The mitigation_matrix should be calibrated using qiskit experiments. @@ -34,6 +35,13 @@ class CorrelatedReadoutMitigator(BaseReadoutMitigator): :math:`2^N x 2^N` so the mitigation complexity is :math:`O(4^N)`. """ + @deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", + ) def __init__(self, assignment_matrix: np.ndarray, qubits: Optional[Iterable[int]] = None): """Initialize a CorrelatedReadoutMitigator diff --git a/qiskit/result/mitigation/local_readout_mitigator.py b/qiskit/result/mitigation/local_readout_mitigator.py index 197c3f00d9be..ee4b970e085c 100644 --- a/qiskit/result/mitigation/local_readout_mitigator.py +++ b/qiskit/result/mitigation/local_readout_mitigator.py @@ -19,6 +19,7 @@ import numpy as np from qiskit.exceptions import QiskitError +from qiskit.utils.deprecation import deprecate_func from ..distributions.quasi import QuasiDistribution from ..counts import Counts from .base_readout_mitigator import BaseReadoutMitigator @@ -26,7 +27,7 @@ class LocalReadoutMitigator(BaseReadoutMitigator): - """1-qubit tensor product readout error mitigator. + """This class is DEPRECATED. 1-qubit tensor product readout error mitigator. Mitigates :meth:`expectation_value` and :meth:`quasi_probabilities`. The mitigator should either be calibrated using qiskit experiments, @@ -37,6 +38,13 @@ class LocalReadoutMitigator(BaseReadoutMitigator): so it is more efficient than the :class:`CorrelatedReadoutMitigator` class. """ + @deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", + ) def __init__( self, assignment_matrices: Optional[List[np.ndarray]] = None, diff --git a/qiskit/result/mitigation/utils.py b/qiskit/result/mitigation/utils.py index 823e2b69a6ca..8e7ed9e2a6af 100644 --- a/qiskit/result/mitigation/utils.py +++ b/qiskit/result/mitigation/utils.py @@ -19,12 +19,20 @@ import numpy as np from qiskit.exceptions import QiskitError +from qiskit.utils.deprecation import deprecate_func from ..utils import marginal_counts from ..counts import Counts logger = logging.getLogger(__name__) +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def z_diagonal(dim, dtype=float): r"""Return the diagonal for the operator :math:`Z^\otimes n`""" parity = np.zeros(dim, dtype=dtype) @@ -33,6 +41,13 @@ def z_diagonal(dim, dtype=float): return (-1) ** np.mod(parity, 2) +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def expval_with_stddev(coeffs: np.ndarray, probs: np.ndarray, shots: int) -> Tuple[float, float]: """Compute expectation value and standard deviation. Args: @@ -60,6 +75,13 @@ def expval_with_stddev(coeffs: np.ndarray, probs: np.ndarray, shots: int) -> Tup return [expval, calc_stddev] +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def stddev(probs, shots): """Calculate stddev dict""" ret = {} @@ -69,6 +91,13 @@ def stddev(probs, shots): return ret +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def str2diag(string): """Transform diagonal from a string to a numpy array""" chars = { @@ -85,6 +114,13 @@ def str2diag(string): return ret +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def counts_to_vector(counts: Counts, num_qubits: int) -> Tuple[np.ndarray, int]: """Transforms Counts to a probability vector""" vec = np.zeros(2**num_qubits, dtype=float) @@ -96,6 +132,13 @@ def counts_to_vector(counts: Counts, num_qubits: int) -> Tuple[np.ndarray, int]: return vec, shots +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def remap_qubits( vec: np.ndarray, num_qubits: int, qubits: Optional[List[int]] = None ) -> np.ndarray: @@ -108,6 +151,13 @@ def remap_qubits( return vec +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def marganalize_counts( counts: Counts, qubit_index: Dict[int, int], @@ -129,6 +179,13 @@ def marganalize_counts( return counts +@deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `qiskit.result.mitigation` module is deprecated in favor of " + "the https://github.com/Qiskit/qiskit-addon-mthree package.", +) def counts_probability_vector( counts: Counts, qubit_index: Dict[int, int], diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 3d91734277b2..adea95d4260c 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -134,6 +134,22 @@ .. autofunction:: synth_c3x .. autofunction:: synth_c4x +Binary Arithmetic Synthesis +=========================== + +Adders +------ + +.. autofunction:: adder_qft_d00 +.. autofunction:: adder_ripple_c04 +.. autofunction:: adder_ripple_v95 + +Multipliers +----------- + +.. autofunction:: multiplier_cumulative_h18 +.. autofunction:: multiplier_qft_r17 + """ from .evolution import ( @@ -195,3 +211,10 @@ synth_c3x, synth_c4x, ) +from .arithmetic import ( + adder_qft_d00, + adder_ripple_c04, + adder_ripple_v95, + multiplier_cumulative_h18, + multiplier_qft_r17, +) diff --git a/qiskit/synthesis/arithmetic/__init__.py b/qiskit/synthesis/arithmetic/__init__.py new file mode 100644 index 000000000000..f413421d6b4b --- /dev/null +++ b/qiskit/synthesis/arithmetic/__init__.py @@ -0,0 +1,16 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module containing multi-controlled circuits synthesis""" + +from .adders import adder_qft_d00, adder_ripple_c04, adder_ripple_v95 +from .multipliers import multiplier_cumulative_h18, multiplier_qft_r17 diff --git a/qiskit/synthesis/arithmetic/adders/__init__.py b/qiskit/synthesis/arithmetic/adders/__init__.py new file mode 100644 index 000000000000..73be2f449f03 --- /dev/null +++ b/qiskit/synthesis/arithmetic/adders/__init__.py @@ -0,0 +1,17 @@ +# 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. + +"""Module containing multi-controlled circuits synthesis""" + +from .cdkm_ripple_carry_adder import adder_ripple_c04 +from .vbe_ripple_carry_adder import adder_ripple_v95 +from .draper_qft_adder import adder_qft_d00 diff --git a/qiskit/synthesis/arithmetic/adders/cdkm_ripple_carry_adder.py b/qiskit/synthesis/arithmetic/adders/cdkm_ripple_carry_adder.py new file mode 100644 index 000000000000..9e18bfbb9fff --- /dev/null +++ b/qiskit/synthesis/arithmetic/adders/cdkm_ripple_carry_adder.py @@ -0,0 +1,154 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Compute the sum of two qubit registers using ripple-carry approach.""" + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister, AncillaRegister + + +def adder_ripple_c04(num_state_qubits: int, kind: str = "half") -> QuantumCircuit: + r"""A ripple-carry circuit to perform in-place addition on two qubit registers. + + This circuit uses :math:`2n + O(1)` CCX gates and :math:`5n + O(1)` CX gates, + at a depth of :math:`2n + O(1)` [1]. The constant depends on the kind + of adder implemented. + + As an example, a ripple-carry adder circuit that performs addition on two 3-qubit sized + registers with a carry-in bit (``kind="full"``) is as follows: + + .. parsed-literal:: + + ┌──────┐ ┌──────┐ + cin_0: ┤2 ├─────────────────────────────────────┤2 ├ + │ │┌──────┐ ┌──────┐│ │ + a_0: ┤0 ├┤2 ├─────────────────────┤2 ├┤0 ├ + │ ││ │┌──────┐ ┌──────┐│ ││ │ + a_1: ┤ MAJ ├┤0 ├┤2 ├─────┤2 ├┤0 ├┤ UMA ├ + │ ││ ││ │ │ ││ ││ │ + a_2: ┤ ├┤ MAJ ├┤0 ├──■──┤0 ├┤ UMA ├┤ ├ + │ ││ ││ │ │ │ ││ ││ │ + b_0: ┤1 ├┤ ├┤ MAJ ├──┼──┤ UMA ├┤ ├┤1 ├ + └──────┘│ ││ │ │ │ ││ │└──────┘ + b_1: ────────┤1 ├┤ ├──┼──┤ ├┤1 ├──────── + └──────┘│ │ │ │ │└──────┘ + b_2: ────────────────┤1 ├──┼──┤1 ├──────────────── + └──────┘┌─┴─┐└──────┘ + cout_0: ────────────────────────┤ X ├──────────────────────── + └───┘ + + Here *MAJ* and *UMA* gates correspond to the gates introduced in [1]. Note that + in this implementation the input register qubits are ordered as all qubits from + the first input register, followed by all qubits from the second input register. + + Two different kinds of adders are supported. By setting the ``kind`` argument, you can also + choose a half-adder, which doesn't have a carry-in, and a fixed-sized-adder, which has neither + carry-in nor carry-out, and thus acts on fixed register sizes. Unlike the full-adder, + these circuits need one additional helper qubit. + + The circuit diagram for the fixed-point adder (``kind="fixed"``) on 3-qubit sized inputs is + + .. parsed-literal:: + + ┌──────┐┌──────┐ ┌──────┐┌──────┐ + a_0: ┤0 ├┤2 ├────────────────┤2 ├┤0 ├ + │ ││ │┌──────┐┌──────┐│ ││ │ + a_1: ┤ ├┤0 ├┤2 ├┤2 ├┤0 ├┤ ├ + │ ││ ││ ││ ││ ││ │ + a_2: ┤ ├┤ MAJ ├┤0 ├┤0 ├┤ UMA ├┤ ├ + │ ││ ││ ││ ││ ││ │ + b_0: ┤1 MAJ ├┤ ├┤ MAJ ├┤ UMA ├┤ ├┤1 UMA ├ + │ ││ ││ ││ ││ ││ │ + b_1: ┤ ├┤1 ├┤ ├┤ ├┤1 ├┤ ├ + │ │└──────┘│ ││ │└──────┘│ │ + b_2: ┤ ├────────┤1 ├┤1 ├────────┤ ├ + │ │ └──────┘└──────┘ │ │ + help_0: ┤2 ├────────────────────────────────┤2 ├ + └──────┘ └──────┘ + + It has one less qubit than the full-adder since it doesn't have the carry-out, but uses + a helper qubit instead of the carry-in, so it only has one less qubit, not two. + + Args: + num_state_qubits: The number of qubits in either input register for + state :math:`|a\rangle` or :math:`|b\rangle`. The two input + registers must have the same number of qubits. + kind: The kind of adder, can be ``"full"`` for a full adder, ``"half"`` for a half + adder, or ``"fixed"`` for a fixed-sized adder. A full adder includes both carry-in + and carry-out, a half only carry-out, and a fixed-sized adder neither carry-in + nor carry-out. + + Raises: + ValueError: If ``num_state_qubits`` is lower than 1. + + **References:** + + [1] Cuccaro et al., A new quantum ripple-carry addition circuit, 2004. + `arXiv:quant-ph/0410184 `_ + + [2] Vedral et al., Quantum Networks for Elementary Arithmetic Operations, 1995. + `arXiv:quant-ph/9511018 `_ + + """ + if num_state_qubits < 1: + raise ValueError("The number of qubits must be at least 1.") + + circuit = QuantumCircuit() + + if kind == "full": + qr_c = QuantumRegister(1, name="cin") + circuit.add_register(qr_c) + else: + qr_c = AncillaRegister(1, name="help") + + qr_a = QuantumRegister(num_state_qubits, name="a") + qr_b = QuantumRegister(num_state_qubits, name="b") + circuit.add_register(qr_a, qr_b) + + if kind in ["full", "half"]: + qr_z = QuantumRegister(1, name="cout") + circuit.add_register(qr_z) + + if kind != "full": + circuit.add_register(qr_c) + + # build carry circuit for majority of 3 bits in-place + # corresponds to MAJ gate in [1] + qc_maj = QuantumCircuit(3, name="MAJ") + qc_maj.cx(0, 1) + qc_maj.cx(0, 2) + qc_maj.ccx(2, 1, 0) + maj_gate = qc_maj.to_gate() + + # build circuit for reversing carry operation + # corresponds to UMA gate in [1] + qc_uma = QuantumCircuit(3, name="UMA") + qc_uma.ccx(2, 1, 0) + qc_uma.cx(0, 2) + qc_uma.cx(2, 1) + uma_gate = qc_uma.to_gate() + + # build ripple-carry adder circuit + circuit.append(maj_gate, [qr_a[0], qr_b[0], qr_c[0]]) + + for i in range(num_state_qubits - 1): + circuit.append(maj_gate, [qr_a[i + 1], qr_b[i + 1], qr_a[i]]) + + if kind in ["full", "half"]: + circuit.cx(qr_a[-1], qr_z[0]) + + for i in reversed(range(num_state_qubits - 1)): + circuit.append(uma_gate, [qr_a[i + 1], qr_b[i + 1], qr_a[i]]) + + circuit.append(uma_gate, [qr_a[0], qr_b[0], qr_c[0]]) + + return circuit diff --git a/qiskit/synthesis/arithmetic/adders/draper_qft_adder.py b/qiskit/synthesis/arithmetic/adders/draper_qft_adder.py new file mode 100644 index 000000000000..0bf9629b4249 --- /dev/null +++ b/qiskit/synthesis/arithmetic/adders/draper_qft_adder.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Compute the sum of two qubit registers using QFT.""" + +import numpy as np + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.library.basis_change import QFTGate + + +def adder_qft_d00(num_state_qubits: int, kind: str = "half") -> QuantumCircuit: + r"""A circuit that uses QFT to perform in-place addition on two qubit registers. + + For registers with :math:`n` qubits, the QFT adder can perform addition modulo + :math:`2^n` (with ``kind="fixed"``) or ordinary addition by adding a carry qubits (with + ``kind="half"``). The fixed adder uses :math:`(3n^2 - n)/2` :class:`.CPhaseGate` operators, + with an additional :math:`n` for the half adder. + + As an example, a non-fixed_point QFT adder circuit that performs addition on two 2-qubit sized + registers is as follows: + + .. parsed-literal:: + + a_0: ─────────■──────■────────■────────────────────────────────── + │ │ │ + a_1: ─────────┼──────┼────────┼────────■──────■────────────────── + ┌──────┐ │ │ │P(π/4) │ │P(π/2) ┌─────────┐ + b_0: ┤0 ├─┼──────┼────────■────────┼──────■───────┤0 ├ + │ │ │ │P(π/2) │P(π) │ │ + b_1: ┤1 Qft ├─┼──────■─────────────────■──────────────┤1 qft_dg ├ + │ │ │P(π) │ │ + cout: ┤2 ├─■───────────────────────────────────────┤2 ├ + └──────┘ └─────────┘ + + Args: + num_state_qubits: The number of qubits in either input register for + state :math:`|a\rangle` or :math:`|b\rangle`. The two input + registers must have the same number of qubits. + kind: The kind of adder, can be ``"half"`` for a half adder or + ``"fixed"`` for a fixed-sized adder. A half adder contains a carry-out to represent + the most-significant bit, but the fixed-sized adder doesn't and hence performs + addition modulo ``2 ** num_state_qubits``. + + **References:** + + [1] T. G. Draper, Addition on a Quantum Computer, 2000. + `arXiv:quant-ph/0008033 `_ + + [2] Ruiz-Perez et al., Quantum arithmetic with the Quantum Fourier Transform, 2017. + `arXiv:1411.5949 `_ + + [3] Vedral et al., Quantum Networks for Elementary Arithmetic Operations, 1995. + `arXiv:quant-ph/9511018 `_ + + """ + + if kind == "full": + raise ValueError("The DraperQFTAdder only supports 'half' and 'fixed' as ``kind``.") + + if num_state_qubits < 1: + raise ValueError("The number of qubits must be at least 1.") + + qr_a = QuantumRegister(num_state_qubits, name="a") + qr_b = QuantumRegister(num_state_qubits, name="b") + qr_list = [qr_a, qr_b] + + if kind == "half": + qr_z = QuantumRegister(1, name="cout") + qr_list.append(qr_z) + + # add registers + circuit = QuantumCircuit(*qr_list) + + # define register containing the sum and number of qubits for QFT circuit + qr_sum = qr_b[:] if kind == "fixed" else qr_b[:] + qr_z[:] + num_sum = num_state_qubits if kind == "fixed" else num_state_qubits + 1 + + # build QFT adder circuit + qft = QFTGate(num_sum) + circuit.append(qft, qr_sum[:]) + + for j in range(num_state_qubits): + for k in range(num_sum - j): + lam = np.pi / (2**k) + # note: if we were able to remove the final swaps from the QFTGate, we could + # simply use qr_sum[(j + k)] here and avoid synthesizing two swap networks, which + # can be elided and cancelled by the compiler + circuit.cp(lam, qr_a[j], qr_sum[~(j + k)]) + + circuit.append(qft.inverse(), qr_sum[:]) + + return circuit diff --git a/qiskit/synthesis/arithmetic/adders/vbe_ripple_carry_adder.py b/qiskit/synthesis/arithmetic/adders/vbe_ripple_carry_adder.py new file mode 100644 index 000000000000..05d8581db942 --- /dev/null +++ b/qiskit/synthesis/arithmetic/adders/vbe_ripple_carry_adder.py @@ -0,0 +1,161 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Compute the sum of two qubit registers using Classical Addition.""" + +from __future__ import annotations +from qiskit.circuit.bit import Bit + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister, AncillaRegister + + +def adder_ripple_v95(num_state_qubits: int, kind: str = "half") -> QuantumCircuit: + r"""The VBE ripple carry adder [1]. + + This method uses :math:`4n + O(1)` CCX gates and :math:`4n + 1` CX gates at a depth + of :math:`6n - 2` [2]. + + This circuit performs inplace addition of two equally-sized quantum registers. + As an example, a classical adder circuit that performs full addition (i.e. including + a carry-in bit) on two 2-qubit sized registers is as follows: + + .. parsed-literal:: + + ┌────────┐ ┌───────────┐┌──────┐ + cin_0: ┤0 ├───────────────────────┤0 ├┤0 ├ + │ │ │ ││ │ + a_0: ┤1 ├───────────────────────┤1 ├┤1 ├ + │ │┌────────┐ ┌──────┐│ ││ Sum │ + a_1: ┤ ├┤1 ├──■──┤1 ├┤ ├┤ ├ + │ ││ │ │ │ ││ ││ │ + b_0: ┤2 Carry ├┤ ├──┼──┤ ├┤2 Carry_dg ├┤2 ├ + │ ││ │┌─┴─┐│ ││ │└──────┘ + b_1: ┤ ├┤2 Carry ├┤ X ├┤2 Sum ├┤ ├──────── + │ ││ │└───┘│ ││ │ + cout_0: ┤ ├┤3 ├─────┤ ├┤ ├──────── + │ ││ │ │ ││ │ + helper_0: ┤3 ├┤0 ├─────┤0 ├┤3 ├──────── + └────────┘└────────┘ └──────┘└───────────┘ + + + Here *Carry* and *Sum* gates correspond to the gates introduced in [1]. + *Carry_dg* correspond to the inverse of the *Carry* gate. Note that + in this implementation the input register qubits are ordered as all qubits from + the first input register, followed by all qubits from the second input register. + This is different ordering as compared to Figure 2 in [1], which leads to a different + drawing of the circuit. + + Args: + num_state_qubits: The size of the register. + kind: The kind of adder, can be ``"full"`` for a full adder, ``"half"`` for a half + adder, or ``"fixed"`` for a fixed-sized adder. A full adder includes both carry-in + and carry-out, a half only carry-out, and a fixed-sized adder neither carry-in + nor carry-out. + + Raises: + ValueError: If ``num_state_qubits`` is lower than 1. + + **References:** + + [1] Vedral et al., Quantum Networks for Elementary Arithmetic Operations, 1995. + `arXiv:quant-ph/9511018 `_ + + [2] Cuccaro et al., A new quantum ripple-carry addition circuit, 2004. + `arXiv:quant-ph/0410184 `_ + + """ + if num_state_qubits < 1: + raise ValueError("The number of qubits must be at least 1.") + + # define the input registers + registers: list[QuantumRegister | list[Bit]] = [] + if kind == "full": + qr_cin = QuantumRegister(1, name="cin") + registers.append(qr_cin) + else: + qr_cin = QuantumRegister(0) + + qr_a = QuantumRegister(num_state_qubits, name="a") + qr_b = QuantumRegister(num_state_qubits, name="b") + + registers += [qr_a, qr_b] + + if kind in ["half", "full"]: + qr_cout = QuantumRegister(1, name="cout") + registers.append(qr_cout) + else: + qr_cout = QuantumRegister(0) + + if num_state_qubits > 1: + qr_help = AncillaRegister(num_state_qubits - 1, name="helper") + registers.append(qr_help) + else: + qr_help = AncillaRegister(0) + + circuit = QuantumCircuit(*registers) + + # the code is simplified a lot if we create a list of all carries and helpers + carries = qr_cin[:] + qr_help[:] + qr_cout[:] + + # corresponds to Carry gate in [1] + qc_carry = QuantumCircuit(4, name="Carry") + qc_carry.ccx(1, 2, 3) + qc_carry.cx(1, 2) + qc_carry.ccx(0, 2, 3) + carry_gate = qc_carry.to_gate() + carry_gate_dg = carry_gate.inverse() + + # corresponds to Sum gate in [1] + qc_sum = QuantumCircuit(3, name="Sum") + qc_sum.cx(1, 2) + qc_sum.cx(0, 2) + sum_gate = qc_sum.to_gate() + + # handle all cases for the first qubits, depending on whether cin is available + i = 0 + if kind == "half": + i += 1 + circuit.ccx(qr_a[0], qr_b[0], carries[0]) + elif kind == "fixed": + i += 1 + if num_state_qubits == 1: + circuit.cx(qr_a[0], qr_b[0]) + else: + circuit.ccx(qr_a[0], qr_b[0], carries[0]) + + for inp, out in zip(carries[:-1], carries[1:]): + circuit.append(carry_gate, [inp, qr_a[i], qr_b[i], out]) + i += 1 + + if kind in ["full", "half"]: # final CX (cancels for the 'fixed' case) + circuit.cx(qr_a[-1], qr_b[-1]) + + if len(carries) > 1: + circuit.append(sum_gate, [carries[-2], qr_a[-1], qr_b[-1]]) + + i -= 2 + for j, (inp, out) in enumerate(zip(reversed(carries[:-1]), reversed(carries[1:]))): + if j == 0: + if kind == "fixed": + i += 1 + else: + continue + circuit.append(carry_gate_dg, [inp, qr_a[i], qr_b[i], out]) + circuit.append(sum_gate, [inp, qr_a[i], qr_b[i]]) + i -= 1 + + if kind in ["half", "fixed"] and num_state_qubits > 1: + circuit.ccx(qr_a[0], qr_b[0], carries[0]) + circuit.cx(qr_a[0], qr_b[0]) + + return circuit diff --git a/qiskit/synthesis/arithmetic/multipliers/__init__.py b/qiskit/synthesis/arithmetic/multipliers/__init__.py new file mode 100644 index 000000000000..5adcfa65091b --- /dev/null +++ b/qiskit/synthesis/arithmetic/multipliers/__init__.py @@ -0,0 +1,16 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""The multiplier circuit library.""" + +from .hrs_cumulative_multiplier import multiplier_cumulative_h18 +from .rg_qft_multiplier import multiplier_qft_r17 diff --git a/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py b/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py new file mode 100644 index 000000000000..676d57ce4342 --- /dev/null +++ b/qiskit/synthesis/arithmetic/multipliers/hrs_cumulative_multiplier.py @@ -0,0 +1,102 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Compute the product of two qubit registers using classical multiplication approach.""" + +from __future__ import annotations + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister + + +def multiplier_cumulative_h18( + num_state_qubits: int, num_result_qubits: int | None = None +) -> QuantumCircuit: + r"""A multiplication circuit to store product of two input registers out-of-place. + + The circuit uses the approach from Ref. [1]. As an example, a multiplier circuit that + performs a non-modular multiplication on two 3-qubit sized registers is: + + .. plot:: + :include-source: + + from qiskit.synthesis.arithmetic import multiplier_cumulative_h18 + + num_state_qubits = 3 + circuit = multiplier_cumulative_h18(num_state_qubits) + circuit.draw("mpl") + + Multiplication in this circuit is implemented in a classical approach by performing + a series of shifted additions using one of the input registers while the qubits + from the other input register act as control qubits for the adders. + + Args: + num_state_qubits: The number of qubits in either input register for + state :math:`|a\rangle` or :math:`|b\rangle`. The two input + registers must have the same number of qubits. + num_result_qubits: The number of result qubits to limit the output to. + If number of result qubits is :math:`n`, multiplication modulo :math:`2^n` is performed + to limit the output to the specified number of qubits. Default + value is ``2 * num_state_qubits`` to represent any possible + result from the multiplication of the two inputs. + + Raises: + ValueError: If ``num_result_qubits`` is given and not valid, meaning not + in ``[num_state_qubits, 2 * num_state_qubits]``. + + **References:** + + [1] Häner et al., Optimizing Quantum Circuits for Arithmetic, 2018. + `arXiv:1805.12445 `_ + + """ + if num_result_qubits is None: + num_result_qubits = 2 * num_state_qubits + elif num_result_qubits < num_state_qubits or num_result_qubits > 2 * num_state_qubits: + raise ValueError( + f"num_result_qubits ({num_result_qubits}) must be in between num_state_qubits " + f"({num_state_qubits}) and 2 * num_state_qubits ({2 * num_state_qubits})" + ) + + # define the registers + qr_a = QuantumRegister(num_state_qubits, name="a") + qr_b = QuantumRegister(num_state_qubits, name="b") + qr_out = QuantumRegister(num_result_qubits, name="out") + + circuit = QuantumCircuit(qr_a, qr_b, qr_out) + + # prepare adder as controlled gate + # pylint: disable=cyclic-import + from qiskit.circuit.library.arithmetic import HalfAdderGate, ModularAdderGate + + adder = HalfAdderGate(num_state_qubits) + controlled_adder = adder.control(annotated=True) + + # build multiplication circuit + for i in range(num_state_qubits): + excess_qubits = max(0, num_state_qubits + i + 1 - num_result_qubits) + if excess_qubits == 0: + num_adder_qubits = num_state_qubits + this_controlled = controlled_adder + else: + num_adder_qubits = num_state_qubits - excess_qubits + 1 + modular = ModularAdderGate(num_adder_qubits) + this_controlled = modular.control(annotated=True) + + qr_list = ( + [qr_a[i]] + + qr_b[:num_adder_qubits] + + qr_out[i : num_state_qubits + i + 1 - excess_qubits] + ) + circuit.append(this_controlled, qr_list) + + return circuit diff --git a/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py b/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py new file mode 100644 index 000000000000..550fc44694d0 --- /dev/null +++ b/qiskit/synthesis/arithmetic/multipliers/rg_qft_multiplier.py @@ -0,0 +1,99 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2021. +# +# 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. + +"""Compute the product of two qubit registers using QFT.""" + +from __future__ import annotations + +import numpy as np + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.library.standard_gates import PhaseGate +from qiskit.circuit.library.basis_change import QFTGate + + +def multiplier_qft_r17( + num_state_qubits: int, num_result_qubits: int | None = None +) -> QuantumCircuit: + r"""A QFT multiplication circuit to store product of two input registers out-of-place. + + Multiplication in this circuit is implemented using the procedure of Fig. 3 in [1], where + weighted sum rotations are implemented as given in Fig. 5 in [1]. QFT is used on the output + register and is followed by rotations controlled by input registers. The rotations + transform the state into the product of two input registers in QFT base, which is + reverted from QFT base using inverse QFT. + For example, on 3 state qubits, a full multiplier is given by: + + .. plot:: + :include-source: + + from qiskit.synthesis.arithmetic import multiplier_qft_r17 + + num_state_qubits = 3 + circuit = multiplier_qft_r17(num_state_qubits) + circuit.draw("mpl") + + Args: + num_state_qubits: The number of qubits in either input register for + state :math:`|a\rangle` or :math:`|b\rangle`. The two input + registers must have the same number of qubits. + num_result_qubits: The number of result qubits to limit the output to. + If number of result qubits is :math:`n`, multiplication modulo :math:`2^n` is performed + to limit the output to the specified number of qubits. Default + value is ``2 * num_state_qubits`` to represent any possible + result from the multiplication of the two inputs. + + Raises: + ValueError: If ``num_result_qubits`` is given and not valid, meaning not + in ``[num_state_qubits, 2 * num_state_qubits]``. + + **References:** + + [1] Ruiz-Perez et al., Quantum arithmetic with the Quantum Fourier Transform, 2017. + `arXiv:1411.5949 `_ + + """ + # define the registers + if num_result_qubits is None: + num_result_qubits = 2 * num_state_qubits + elif num_result_qubits < num_state_qubits or num_result_qubits > 2 * num_state_qubits: + raise ValueError( + f"num_result_qubits ({num_result_qubits}) must be in between num_state_qubits " + f"({num_state_qubits}) and 2 * num_state_qubits ({2 * num_state_qubits})" + ) + + qr_a = QuantumRegister(num_state_qubits, name="a") + qr_b = QuantumRegister(num_state_qubits, name="b") + qr_out = QuantumRegister(num_result_qubits, name="out") + + # build multiplication circuit + circuit = QuantumCircuit(qr_a, qr_b, qr_out) + qft = QFTGate(num_result_qubits) + + circuit.append(qft, qr_out[:]) + + for j in range(1, num_state_qubits + 1): + for i in range(1, num_state_qubits + 1): + for k in range(1, num_result_qubits + 1): + lam = (2 * np.pi) / (2 ** (i + j + k - 2 * num_state_qubits)) + + # note: if we can synthesize the QFT without swaps, we can implement this circuit + # more efficiently and just apply phase gate on qr_out[(k - 1)] instead + circuit.append( + PhaseGate(lam).control(2), + [qr_a[num_state_qubits - j], qr_b[num_state_qubits - i], qr_out[~(k - 1)]], + ) + + circuit.append(qft.inverse(), qr_out[:]) + + return circuit diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index 5ead6945eba2..7c7857e8dcd1 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -41,7 +41,6 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: `arXiv:2003.09412 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True + synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True, name=str(clifford) ) - circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 9766efe4aa8f..9d5181f3e9a9 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -51,7 +51,8 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: `arXiv:2105.02291 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_greedy_inner(clifford.tableau.astype(bool)), add_regs=True + synth_clifford_greedy_inner(clifford.tableau.astype(bool)), + add_regs=True, + name=str(clifford), ) - circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/evolution/__init__.py b/qiskit/synthesis/evolution/__init__.py index eeb4dc6f8970..4ef6d626d2c2 100644 --- a/qiskit/synthesis/evolution/__init__.py +++ b/qiskit/synthesis/evolution/__init__.py @@ -18,3 +18,4 @@ from .lie_trotter import LieTrotter from .suzuki_trotter import SuzukiTrotter from .qdrift import QDrift +from .pauli_network import synth_pauli_network_rustiq diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 1a01675a6782..db9f4a91f101 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -14,18 +14,15 @@ from __future__ import annotations -import inspect from collections.abc import Callable from typing import Any -import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli -from qiskit.utils.deprecation import deprecate_arg -from .product_formula import ProductFormula +from .suzuki_trotter import SuzukiTrotter -class LieTrotter(ProductFormula): +class LieTrotter(SuzukiTrotter): r"""The Lie-Trotter product formula. The Lie-Trotter formula approximates the exponential of two non-commuting operators @@ -40,7 +37,7 @@ class LieTrotter(ProductFormula): .. math:: - e^{-it(XX + ZZ)} = e^{-it XX}e^{-it ZZ} + \mathcal{O}(t^2). + e^{-it(XI + ZZ)} = e^{-it XI}e^{-it ZZ} + \mathcal{O}(t^2). References: @@ -52,21 +49,6 @@ class LieTrotter(ProductFormula): `arXiv:math-ph/0506007 `_ """ - @deprecate_arg( - name="atomic_evolution", - since="1.2", - predicate=lambda callable: callable is not None - and len(inspect.signature(callable).parameters) == 2, - deprecation_description=( - "The 'Callable[[Pauli | SparsePauliOp, float], QuantumCircuit]' signature of the " - "'atomic_evolution' argument" - ), - additional_msg=( - "Instead you should update your 'atomic_evolution' function to be of the following " - "type: 'Callable[[QuantumCircuit, Pauli | SparsePauliOp, float], None]'." - ), - pending=True, - ) def __init__( self, reps: int = 1, @@ -78,6 +60,7 @@ def __init__( | None ) = None, wrap: bool = False, + preserve_order: bool = True, ) -> None: """ Args: @@ -97,28 +80,19 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + preserve_order: If ``False``, allows reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. """ - super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) - - def synthesize(self, evolution): - # get operators and time to evolve - operators = evolution.operator - time = evolution.time - - # construct the evolution circuit - single_rep = QuantumCircuit(operators[0].num_qubits) - - if not isinstance(operators, list): - pauli_list = [(Pauli(op), np.real(coeff)) for op, coeff in operators.to_list()] - else: - pauli_list = [(op, 1) for op in operators] - - for i, (op, coeff) in enumerate(pauli_list): - self.atomic_evolution(single_rep, op, coeff * time / self.reps) - if self.insert_barriers and i != len(pauli_list) - 1: - single_rep.barrier() - - return single_rep.repeat(self.reps, insert_barriers=self.insert_barriers).decompose() + super().__init__( + 1, + reps, + insert_barriers, + cx_structure, + atomic_evolution, + wrap, + preserve_order=preserve_order, + ) @property def settings(self) -> dict[str, Any]: diff --git a/qiskit/synthesis/evolution/pauli_network.py b/qiskit/synthesis/evolution/pauli_network.py new file mode 100644 index 000000000000..85bc761a6414 --- /dev/null +++ b/qiskit/synthesis/evolution/pauli_network.py @@ -0,0 +1,80 @@ +# 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. + +"""Circuit synthesis for pauli evolution gates.""" + +from __future__ import annotations + +from qiskit.circuit import QuantumCircuit + +from qiskit._accelerate.synthesis.evolution import ( + pauli_network_synthesis as pauli_network_synthesis_inner, +) + + +def synth_pauli_network_rustiq( + num_qubits: int, + pauli_network: list, + optimize_count: bool = True, + preserve_order: bool = True, + upto_clifford: bool = False, + upto_phase: bool = False, + resynth_clifford_method: int = 0, +) -> QuantumCircuit: + """ + Calls Rustiq's pauli network synthesis algorithm. + + The algorithm is described in [1]. The source code (in Rust) is available at + https://github.com/smartiel/rustiq-core. + + Args: + num_qubits: the number of qubits over which the pauli network is defined. + pauli_network: a list of pauli rotations, represented in sparse format: a list of + triples such as `[("XX", [0, 3], theta), ("ZZ", [0, 1], 0.1)]`. + optimize_count: if `True` the synthesis algorithm will try to optimize the 2-qubit + gate count; and if `False` then the 2-qubit depth. + preserve_order: whether the order of paulis should be preserved, up to + commutativity. If the order is not preserved, the returned circuit will + generally not be equivalent to the given pauli network. + upto_clifford: if `True`, the final Clifford operator is not synthesized + and the returned circuit will generally not be equivalent to the given + pauli network. In addition, the argument `upto_phase` would be ignored. + upto_phase: if `True`, the global phase of the returned circuit may differ + from the global phase of the given pauli network. The argument is ignored + when `upto_clifford` is `True`. + resynth_clifford_method: describes the strategy to synthesize the final Clifford + operator. If `0` a naive approach is used, which doubles the number of gates + but preserves the global phase of the circuit. If `1`, the Clifford is + resynthesized using Qiskit's greedy Clifford synthesis algorithm. If `2`, it + is resynthesized by Rustiq itself. If `upto_phase` is `False`, the naive + approach is used, as neither synthesis method preserves the global phase. + + Returns: + A circuit implementation of the pauli network. + + References: + 1. Timothée Goubault de Brugière and Simon Martiel, + *Faster and shorter synthesis of Hamiltonian simulation circuits*, + `arXiv:2404.03280 [quant-ph] `_ + + """ + out = pauli_network_synthesis_inner( + num_qubits=num_qubits, + pauli_network=pauli_network, + optimize_count=optimize_count, + preserve_order=preserve_order, + upto_clifford=upto_clifford, + upto_phase=upto_phase, + resynth_clifford_method=resynth_clifford_method, + ) + circuit = QuantumCircuit._from_circuit_data(out, add_regs=True) + return circuit diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 28bbc5711bf8..b314d5ec5e62 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -15,17 +15,26 @@ from __future__ import annotations import inspect -from collections.abc import Callable -from typing import Any -from functools import partial +import itertools +from collections.abc import Callable, Sequence +from collections import defaultdict +from itertools import combinations +import typing import numpy as np +import rustworkx as rx from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit, ParameterValueType from qiskit.quantum_info import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg +from qiskit._accelerate.circuit_library import pauli_evolution from .evolution_synthesis import EvolutionSynthesis +if typing.TYPE_CHECKING: + from qiskit.circuit.library import PauliEvolutionGate + +SparsePauliLabel = typing.Tuple[str, list[int], ParameterValueType] + class ProductFormula(EvolutionSynthesis): """Product formula base class for the decomposition of non-commuting operator exponentials. @@ -60,6 +69,7 @@ def __init__( | None ) = None, wrap: bool = False, + preserve_order: bool = True, ) -> None: """ Args: @@ -78,24 +88,31 @@ def __init__( Alternatively, the function can also take Pauli operator and evolution time as inputs and returns the circuit that will be appended to the overall circuit being built. - wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes - effect when ``atomic_evolution is None``. + wrap: Whether to wrap the atomic evolutions into custom gate objects. Note that setting + this to ``True`` is slower than ``False``. This only takes effect when + ``atomic_evolution is None``. + preserve_order: If ``False``, allows reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. """ super().__init__() self.order = order self.reps = reps self.insert_barriers = insert_barriers + self.preserve_order = preserve_order # user-provided atomic evolution, stored for serialization self._atomic_evolution = atomic_evolution + + if cx_structure not in ["chain", "fountain"]: + raise ValueError(f"Unsupported CX structure: {cx_structure}") + self._cx_structure = cx_structure self._wrap = wrap # if atomic evolution is not provided, set a default if atomic_evolution is None: - self.atomic_evolution = partial( - _default_atomic_evolution, cx_structure=cx_structure, wrap=wrap - ) + self.atomic_evolution = None elif len(inspect.signature(atomic_evolution).parameters) == 2: @@ -108,8 +125,50 @@ def wrap_atomic_evolution(output, operator, time): else: self.atomic_evolution = atomic_evolution + def expand( + self, evolution: PauliEvolutionGate + ) -> list[tuple[str, tuple[int], ParameterValueType]]: + """Apply the product formula to expand the Hamiltonian in the evolution gate. + + Args: + evolution: The :class:`.PauliEvolutionGate`, whose Hamiltonian we expand. + + Returns: + A list of Pauli rotations in a sparse format, where each element is + ``(paulistring, qubits, coefficient)``. For example, the Lie-Trotter expansion + of ``H = XI + ZZ`` would return ``[("X", [1], 1), ("ZZ", [0, 1], 1)]``. + """ + raise NotImplementedError( + f"The method ``expand`` is not implemented for {self.__class__}. Implement it to " + f"automatically enable the call to {self.__class__}.synthesize." + ) + + def synthesize(self, evolution: PauliEvolutionGate) -> QuantumCircuit: + """Synthesize a :class:`.PauliEvolutionGate`. + + Args: + evolution: The evolution gate to synthesize. + + Returns: + QuantumCircuit: A circuit implementing the evolution. + """ + pauli_rotations = self.expand(evolution) + num_qubits = evolution.num_qubits + + if self._wrap or self._atomic_evolution is not None: + # this is the slow path, where each Pauli evolution is constructed in Rust + # separately and then wrapped into a gate object + circuit = self._custom_evolution(num_qubits, pauli_rotations) + else: + # this is the fast path, where the whole evolution is constructed Rust-side + cx_fountain = self._cx_structure == "fountain" + data = pauli_evolution(num_qubits, pauli_rotations, self.insert_barriers, cx_fountain) + circuit = QuantumCircuit._from_circuit_data(data, add_regs=True) + + return circuit + @property - def settings(self) -> dict[str, Any]: + def settings(self) -> dict[str, typing.Any]: """Return the settings in a dictionary, which can be used to reconstruct the object. Returns: @@ -129,256 +188,124 @@ def settings(self) -> dict[str, Any]: "insert_barriers": self.insert_barriers, "cx_structure": self._cx_structure, "wrap": self._wrap, + "preserve_order": self.preserve_order, } + def _normalize_coefficients( + self, paulis: list[str | list[int], float | complex | ParameterExpression] + ) -> list[str | list[int] | ParameterValueType]: + """Ensure the coefficients are real (or parameter expressions).""" + return [[(op, qubits, real_or_fail(coeff)) for op, qubits, coeff in ops] for ops in paulis] -def evolve_pauli( - output: QuantumCircuit, - pauli: Pauli, - time: float | ParameterExpression = 1.0, - cx_structure: str = "chain", - wrap: bool = False, - label: str | None = None, -) -> None: - r"""Construct a circuit implementing the time evolution of a single Pauli string. - - For a Pauli string :math:`P = \{I, X, Y, Z\}^{\otimes n}` on :math:`n` qubits and an - evolution time :math:`t`, the returned circuit implements the unitary operation - - .. math:: - - U(t) = e^{-itP}. - - Since only a single Pauli string is evolved the circuit decomposition is exact. - - Args: - output: The circuit object to which to append the evolved Pauli. - pauli: The Pauli to evolve. - time: The evolution time. - cx_structure: Determine the structure of CX gates, can be either ``"chain"`` for - next-neighbor connections or ``"fountain"`` to connect directly to the top qubit. - wrap: Whether to wrap the single Pauli evolutions into custom gate objects. - label: A label for the gate. - """ - num_non_identity = len([label for label in pauli.to_label() if label != "I"]) - - # first check, if the Pauli is only the identity, in which case the evolution only - # adds a global phase - if num_non_identity == 0: - output.global_phase -= time - # if we evolve on a single qubit, if yes use the corresponding qubit rotation - elif num_non_identity == 1: - _single_qubit_evolution(output, pauli, time, wrap) - # same for two qubits, use Qiskit's native rotations - elif num_non_identity == 2: - _two_qubit_evolution(output, pauli, time, cx_structure, wrap) - # otherwise do basis transformation and CX chains - else: - _multi_qubit_evolution(output, pauli, time, cx_structure, wrap) - - -def _single_qubit_evolution(output, pauli, time, wrap): - dest = QuantumCircuit(1) if wrap else output - # Note that all phases are removed from the pauli label and are only in the coefficients. - # That's because the operators we evolved have all been translated to a SparsePauliOp. - qubits = [] - label = "" - for i, pauli_i in enumerate(reversed(pauli.to_label())): - idx = 0 if wrap else i - if pauli_i == "X": - dest.rx(2 * time, idx) - qubits.append(i) - label += "X" - elif pauli_i == "Y": - dest.ry(2 * time, idx) - qubits.append(i) - label += "Y" - elif pauli_i == "Z": - dest.rz(2 * time, idx) - qubits.append(i) - label += "Z" - - if wrap: - gate = dest.to_gate(label=f"exp(it {label})") - qubits = [output.qubits[q] for q in qubits] - output.append(gate, qargs=qubits, copy=False) - - -def _two_qubit_evolution(output, pauli, time, cx_structure, wrap): - # Get the Paulis and the qubits they act on. - # Note that all phases are removed from the pauli label and are only in the coefficients. - # That's because the operators we evolved have all been translated to a SparsePauliOp. - labels_as_array = np.array(list(reversed(pauli.to_label()))) - qubits = np.where(labels_as_array != "I")[0] - indices = [0, 1] if wrap else qubits - labels = np.array([labels_as_array[idx] for idx in qubits]) - - dest = QuantumCircuit(2) if wrap else output - - # go through all cases we have implemented in Qiskit - if all(labels == "X"): # RXX - dest.rxx(2 * time, indices[0], indices[1]) - elif all(labels == "Y"): # RYY - dest.ryy(2 * time, indices[0], indices[1]) - elif all(labels == "Z"): # RZZ - dest.rzz(2 * time, indices[0], indices[1]) - elif labels[0] == "Z" and labels[1] == "X": # RZX - dest.rzx(2 * time, indices[0], indices[1]) - elif labels[0] == "X" and labels[1] == "Z": # RXZ - dest.rzx(2 * time, indices[1], indices[0]) - else: # all the others are not native in Qiskit, so use default the decomposition - _multi_qubit_evolution(output, pauli, time, cx_structure, wrap) - return - - if wrap: - gate = dest.to_gate(label=f"exp(it {''.join(labels)})") - qubits = [output.qubits[q] for q in qubits] - output.append(gate, qargs=qubits, copy=False) - - -def _multi_qubit_evolution(output, pauli, time, cx_structure, wrap): - # get diagonalizing clifford - cliff = diagonalizing_clifford(pauli) - - # get CX chain to reduce the evolution to the top qubit - if cx_structure == "chain": - chain = cnot_chain(pauli) - else: - chain = cnot_fountain(pauli) - - # determine qubit to do the rotation on - target = None - # Note that all phases are removed from the pauli label and are only in the coefficients. - # That's because the operators we evolved have all been translated to a SparsePauliOp. - for i, pauli_i in enumerate(reversed(pauli.to_label())): - if pauli_i != "I": - target = i - break - - # build the evolution as: diagonalization, reduction, 1q evolution, followed by inverses - dest = QuantumCircuit(pauli.num_qubits) if wrap else output - dest.compose(cliff, inplace=True) - dest.compose(chain, inplace=True) - dest.rz(2 * time, target) - dest.compose(chain.inverse(), inplace=True) - dest.compose(cliff.inverse(), inplace=True) - - if wrap: - gate = dest.to_gate(label=f"exp(it {pauli.to_label()})") - output.append(gate, qargs=output.qubits, copy=False) - - -def diagonalizing_clifford(pauli: Pauli) -> QuantumCircuit: - """Get the clifford circuit to diagonalize the Pauli operator. + def _custom_evolution(self, num_qubits, pauli_rotations): + """Implement the evolution for the non-standard path. - Args: - pauli: The Pauli to diagonalize. + This is either because a user-defined atomic evolution is given, or because the evolution + of individual Paulis needs to be wrapped in gates. + """ + circuit = QuantumCircuit(num_qubits) + cx_fountain = self._cx_structure == "fountain" - Returns: - A circuit to diagonalize. - """ - cliff = QuantumCircuit(pauli.num_qubits) - for i, pauli_i in enumerate(reversed(pauli.to_label())): - if pauli_i == "Y": - cliff.sdg(i) - if pauli_i in ["X", "Y"]: - cliff.h(i) + num_paulis = len(pauli_rotations) + for i, pauli_rotation in enumerate(pauli_rotations): + if self._atomic_evolution is not None: + # use the user-provided evolution with a global operator + operator = SparsePauliOp.from_sparse_list([pauli_rotation], num_qubits) + self.atomic_evolution(circuit, operator, time=1) # time is inside the Pauli coeff - return cliff + else: # this means self._wrap is True + # we create a local sparse Pauli representation such that the operator + # does not span over all qubits of the circuit + pauli_string, qubits, coeff = pauli_rotation + local_pauli = (pauli_string, list(range(len(qubits))), coeff) + # build the circuit Rust-side + data = pauli_evolution( + len(qubits), + [local_pauli], + False, + cx_fountain, + ) + evo = QuantumCircuit._from_circuit_data(data) -def cnot_chain(pauli: Pauli) -> QuantumCircuit: - """CX chain. + # and append it to the circuit with the correct label + gate = evo.to_gate(label=f"exp(it {pauli_string})") + circuit.append(gate, qubits) - For example, for the Pauli with the label 'XYZIX'. + if self.insert_barriers and i < num_paulis - 1: + circuit.barrier() - .. code-block:: text + return circuit - ┌───┐ - q_0: ──────────┤ X ├ - └─┬─┘ - q_1: ────────────┼── - ┌───┐ │ - q_2: ─────┤ X ├──■── - ┌───┐└─┬─┘ - q_3: ┤ X ├──■─────── - └─┬─┘ - q_4: ──■──────────── - Args: - pauli: The Pauli for which to construct the CX chain. +def real_or_fail(value, tol=100): + """Return real if close, otherwise fail. Unbound parameters are left unchanged. - Returns: - A circuit implementing the CX chain. + Based on NumPy's ``real_if_close``, i.e. ``tol`` is in terms of machine precision for float. """ + if isinstance(value, ParameterExpression): + return value - chain = QuantumCircuit(pauli.num_qubits) - control, target = None, None + abstol = tol * np.finfo(float).eps + if abs(np.imag(value)) < abstol: + return np.real(value) - # iterate over the Pauli's and add CNOTs - for i, pauli_i in enumerate(pauli.to_label()): - i = pauli.num_qubits - i - 1 - if pauli_i != "I": - if control is None: - control = i - else: - target = i + raise ValueError(f"Encountered complex value {value}, but expected real.") - if control is not None and target is not None: - chain.cx(control, target) - control = i - target = None - return chain +def reorder_paulis( + paulis: Sequence[SparsePauliLabel], + strategy: rx.ColoringStrategy = rx.ColoringStrategy.Saturation, +) -> list[SparsePauliLabel]: + r""" + Creates an equivalent operator by reordering terms in order to yield a + shallower circuit after evolution synthesis. The original operator remains + unchanged. + This method works in three steps. First, a graph is constructed, where the + nodes are the terms of the operator and where two nodes are connected if + their terms act on the same qubit (for example, the terms :math:`IXX` and + :math:`IYI` would be connected, but not :math:`IXX` and :math:`YII`). Then, + the graph is colored. Two terms with the same color thus do not act on the + same qubit, and in particular, their evolution subcircuits can be run in + parallel in the greater evolution circuit of ``paulis``. -def cnot_fountain(pauli: Pauli) -> QuantumCircuit: - """CX chain in the fountain shape. - - For example, for the Pauli with the label 'XYZIX'. - - .. code-block:: text - - ┌───┐┌───┐┌───┐ - q_0: ┤ X ├┤ X ├┤ X ├ - └─┬─┘└─┬─┘└─┬─┘ - q_1: ──┼────┼────┼── - │ │ │ - q_2: ──■────┼────┼── - │ │ - q_3: ───────■────┼── - │ - q_4: ────────────■── + This method is deterministic and invariant under permutation of the Pauli + term in ``paulis``. Args: - pauli: The Pauli for which to construct the CX chain. + paulis: The operator whose terms to reorder. + strategy: The coloring heuristic to use, see ``ColoringStrategy`` [#]. + Default is ``ColoringStrategy.Saturation``. + + .. [#] https://www.rustworkx.org/apiref/rustworkx.ColoringStrategy.html#coloringstrategy - Returns: - A circuit implementing the CX chain. """ - chain = QuantumCircuit(pauli.num_qubits) - control, target = None, None - for i, pauli_i in enumerate(reversed(pauli.to_label())): - if pauli_i != "I": - if target is None: - target = i - else: - control = i - - if control is not None and target is not None: - chain.cx(control, target) - control = None - - return chain - - -def _default_atomic_evolution(output, operator, time, cx_structure, wrap): - if isinstance(operator, Pauli): - # single Pauli operator: just exponentiate it - evolve_pauli(output, operator, time, cx_structure, wrap) - else: - # sum of Pauli operators: exponentiate each term (this assumes they commute) - pauli_list = [(Pauli(op), np.real(coeff)) for op, coeff in operator.to_list()] - for pauli, coeff in pauli_list: - evolve_pauli(output, pauli, coeff * time, cx_structure, wrap) + def _term_sort_key(term: SparsePauliLabel) -> typing.Any: + # sort by index, then by pauli + return (term[1], term[0]) + + # Do nothing in trivial cases + if len(paulis) <= 1: + return paulis + + terms = sorted(paulis, key=_term_sort_key) + graph = rx.PyGraph() + graph.add_nodes_from(terms) + indexed_nodes = list(enumerate(graph.nodes())) + for (idx1, (_, ind1, _)), (idx2, (_, ind2, _)) in combinations(indexed_nodes, 2): + # Add an edge between two terms if they touch the same qubit + if len(set(ind1).intersection(ind2)) > 0: + graph.add_edge(idx1, idx2, None) + + # rx.graph_greedy_color is supposed to be deterministic + coloring = rx.graph_greedy_color(graph, strategy=strategy) + terms_by_color = defaultdict(list) + + for term_idx, color in sorted(coloring.items()): + term = graph.nodes()[term_idx] + terms_by_color[color].append(term) + + terms = list(itertools.chain(*terms_by_color.values())) + return terms diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 7c68f66dc8ea..13b6b6dd79e6 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -16,14 +16,19 @@ import inspect import math +import typing +from itertools import chain from collections.abc import Callable import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg +from qiskit.exceptions import QiskitError -from .product_formula import ProductFormula -from .lie_trotter import LieTrotter +from .product_formula import ProductFormula, reorder_paulis + +if typing.TYPE_CHECKING: + from qiskit.circuit.library import PauliEvolutionGate class QDrift(ProductFormula): @@ -63,6 +68,7 @@ def __init__( ) = None, seed: int | None = None, wrap: bool = False, + preserve_order: bool = True, ) -> None: r""" Args: @@ -83,49 +89,50 @@ def __init__( seed: An optional seed for reproducibility of the random sampling process. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + preserve_order: If ``False``, allows reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. """ - super().__init__(1, reps, insert_barriers, cx_structure, atomic_evolution, wrap) + super().__init__( + 1, reps, insert_barriers, cx_structure, atomic_evolution, wrap, preserve_order + ) self.sampled_ops = None self.rng = np.random.default_rng(seed) - def synthesize(self, evolution): - # get operators and time to evolve + def expand(self, evolution: PauliEvolutionGate) -> list[tuple[str, tuple[int], float]]: operators = evolution.operator - time = evolution.time + time = evolution.time # used to determine the number of gates - if not isinstance(operators, list): - pauli_list = [(Pauli(op), coeff) for op, coeff in operators.to_list()] - coeffs = [np.real(coeff) for op, coeff in operators.to_list()] + # QDrift is based on first-order Lie-Trotter, hence we can just concatenate all + # Pauli terms and ignore commutations + if isinstance(operators, list): + paulis = list(chain.from_iterable([op.to_sparse_list() for op in operators])) else: - pauli_list = [(op, 1) for op in operators] - coeffs = [1 for op in operators] + paulis = operators.to_sparse_list() + + try: + coeffs = [float(np.real_if_close(coeff)) for _, _, coeff in paulis] + except TypeError as exc: + raise QiskitError("QDrift requires bound, real coefficients.") from exc # We artificially make the weights positive weights = np.abs(coeffs) lambd = np.sum(weights) num_gates = math.ceil(2 * (lambd**2) * (time**2) * self.reps) + # The protocol calls for the removal of the individual coefficients, # and multiplication by a constant evolution time. - evolution_time = lambd * time / num_gates - - self.sampled_ops = self.rng.choice( - np.array(pauli_list, dtype=object), - size=(num_gates,), - p=weights / lambd, + sampled = self.rng.choice( + np.array(paulis, dtype=object), size=(num_gates,), p=weights / lambd ) - # pylint: disable=cyclic-import - from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate + rescaled_time = 2 * lambd / num_gates * time + sampled_paulis = [ + (pauli[0], pauli[1], np.real(np.sign(pauli[2])) * rescaled_time) for pauli in sampled + ] - # Build the evolution circuit using the LieTrotter synthesis with the sampled operators - lie_trotter = LieTrotter( - insert_barriers=self.insert_barriers, atomic_evolution=self.atomic_evolution - ) - evolution_circuit = PauliEvolutionGate( - sum(SparsePauliOp(np.sign(coeff) * op) for op, coeff in self.sampled_ops), - time=evolution_time, - synthesis=lie_trotter, - ).definition + if not self.preserve_order: + sampled_paulis = reorder_paulis(sampled_paulis) - return evolution_circuit + return sampled_paulis diff --git a/qiskit/synthesis/evolution/suzuki_trotter.py b/qiskit/synthesis/evolution/suzuki_trotter.py index e03fd27e26d4..209f377351a7 100644 --- a/qiskit/synthesis/evolution/suzuki_trotter.py +++ b/qiskit/synthesis/evolution/suzuki_trotter.py @@ -15,16 +15,19 @@ from __future__ import annotations import inspect +import typing from collections.abc import Callable - -import numpy as np +from itertools import chain from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli from qiskit.utils.deprecation import deprecate_arg +from .product_formula import ProductFormula, reorder_paulis -from .product_formula import ProductFormula +if typing.TYPE_CHECKING: + from qiskit.circuit.quantumcircuit import ParameterValueType + from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate class SuzukiTrotter(ProductFormula): @@ -44,7 +47,7 @@ class SuzukiTrotter(ProductFormula): .. math:: - e^{-it(XX + ZZ)} = e^{-it/2 ZZ}e^{-it XX}e^{-it/2 ZZ} + \mathcal{O}(t^3). + e^{-it(XI + ZZ)} = e^{-it/2 XI}e^{-it ZZ}e^{-it/2 XI} + \mathcal{O}(t^3). References: [1]: D. Berry, G. Ahokas, R. Cleve and B. Sanders, @@ -82,6 +85,7 @@ def __init__( | None ) = None, wrap: bool = False, + preserve_order: bool = True, ) -> None: """ Args: @@ -101,55 +105,111 @@ def __init__( built. wrap: Whether to wrap the atomic evolutions into custom gate objects. This only takes effect when ``atomic_evolution is None``. + preserve_order: If ``False``, allows reordering the terms of the operator to + potentially yield a shallower evolution circuit. Not relevant + when synthesizing operator with a single term. Raises: ValueError: If order is not even """ - if order % 2 == 1: + if order > 1 and order % 2 == 1: raise ValueError( "Suzuki product formulae are symmetric and therefore only defined " - "for even orders." + f"for when the order is 1 or even, not {order}." ) - super().__init__(order, reps, insert_barriers, cx_structure, atomic_evolution, wrap) + super().__init__( + order, + reps, + insert_barriers, + cx_structure, + atomic_evolution, + wrap, + preserve_order=preserve_order, + ) + + def expand( + self, evolution: PauliEvolutionGate + ) -> list[tuple[str, list[int], ParameterValueType]]: + """Expand the Hamiltonian into a Suzuki-Trotter sequence of sparse gates. + + For example, the Hamiltonian ``H = IX + ZZ`` for an evolution time ``t`` and + 1 repetition for an order 2 formula would get decomposed into a list of 3-tuples + containing ``(pauli, indices, rz_rotation_angle)``, that is: + + .. code-block:: text + + ("X", [0], t), ("ZZ", [0, 1], 2t), ("X", [0], 2) + + Note that the rotation angle contains a factor of 2, such that that evolution + of a Pauli :math:`P` over time :math:`t`, which is :math:`e^{itP}`, is represented + by ``(P, indices, 2 * t)``. + + For ``N`` repetitions, this sequence would be repeated ``N`` times and the coefficients + divided by ``N``. - def synthesize(self, evolution): - # get operators and time to evolve - operators = evolution.operator + Args: + evolution: The evolution gate to expand. + + Returns: + The Pauli network implementing the Trotter expansion. + """ + operators = evolution.operator # type: SparsePauliOp | list[SparsePauliOp] time = evolution.time - if not isinstance(operators, list): - pauli_list = [(Pauli(op), np.real(coeff)) for op, coeff in operators.to_list()] - else: - pauli_list = [(op, 1) for op in operators] + def to_sparse_list(operator): + paulis = (time * (2 / self.reps) * operator).to_sparse_list() + if not self.preserve_order: + return reorder_paulis(paulis) - ops_to_evolve = self._recurse(self.order, time / self.reps, pauli_list) + return paulis # construct the evolution circuit - single_rep = QuantumCircuit(operators[0].num_qubits) + if isinstance(operators, list): # already sorted into commuting bits + non_commuting = [to_sparse_list(operator) for operator in operators] + else: + # Assume no commutativity here. If we were to group commuting Paulis, + # here would be the location to do so. + non_commuting = [[op] for op in to_sparse_list(operators)] + + # normalize coefficients, i.e. ensure they are float or ParameterExpression + non_commuting = self._normalize_coefficients(non_commuting) - for i, (op, coeff) in enumerate(ops_to_evolve): - self.atomic_evolution(single_rep, op, coeff) - if self.insert_barriers and i != len(ops_to_evolve) - 1: - single_rep.barrier() + # we're already done here since Lie Trotter does not do any operator repetition + product_formula = self._recurse(self.order, non_commuting) + flattened = self.reps * list(chain.from_iterable(product_formula)) - return single_rep.repeat(self.reps, insert_barriers=self.insert_barriers).decompose() + return flattened @staticmethod - def _recurse(order, time, pauli_list): + def _recurse(order, grouped_paulis): if order == 1: - return pauli_list + return grouped_paulis elif order == 2: - halves = [(op, coeff * time / 2) for op, coeff in pauli_list[:-1]] - full = [(pauli_list[-1][0], time * pauli_list[-1][1])] + halves = [ + [(label, qubits, coeff / 2) for label, qubits, coeff in paulis] + for paulis in grouped_paulis[:-1] + ] + full = [grouped_paulis[-1]] return halves + full + list(reversed(halves)) else: reduction = 1 / (4 - 4 ** (1 / (order - 1))) outer = 2 * SuzukiTrotter._recurse( - order - 2, time=reduction * time, pauli_list=pauli_list + order - 2, + [ + [(label, qubits, coeff * reduction) for label, qubits, coeff in paulis] + for paulis in grouped_paulis + ], ) inner = SuzukiTrotter._recurse( - order - 2, time=(1 - 4 * reduction) * time, pauli_list=pauli_list + order - 2, + [ + [ + (label, qubits, coeff * (1 - 4 * reduction)) + for label, qubits, coeff in paulis + ] + for paulis in grouped_paulis + ], ) return outer + inner + outer diff --git a/qiskit/synthesis/qft/qft_decompose_full.py b/qiskit/synthesis/qft/qft_decompose_full.py index 9038ea6589c3..2d3314e650ee 100644 --- a/qiskit/synthesis/qft/qft_decompose_full.py +++ b/qiskit/synthesis/qft/qft_decompose_full.py @@ -14,6 +14,7 @@ """ from __future__ import annotations +import warnings import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -54,7 +55,7 @@ def synth_qft_full( A circuit implementing the QFT operation. """ - + _warn_if_precision_loss(num_qubits - approximation_degree - 1) circuit = QuantumCircuit(num_qubits, name=name) for j in reversed(range(num_qubits)): @@ -77,3 +78,20 @@ def synth_qft_full( circuit = circuit.inverse() return circuit + + +def _warn_if_precision_loss(max_num_entanglements): + """Issue a warning if constructing the circuit will lose precision. + + If we need an angle smaller than ``pi * 2**-1022``, we start to lose precision by going into + the subnormal numbers. We won't lose _all_ precision until an exponent of about 1075, but + beyond 1022 we're using fractional bits to represent leading zeros. + """ + if max_num_entanglements > -np.finfo(float).minexp: # > 1022 for doubles. + warnings.warn( + "precision loss in QFT." + f" The rotation needed to represent {max_num_entanglements} entanglements" + " is smaller than the smallest normal floating-point number.", + category=RuntimeWarning, + stacklevel=4, + ) diff --git a/qiskit/synthesis/qft/qft_decompose_lnn.py b/qiskit/synthesis/qft/qft_decompose_lnn.py index f1a0876a0c3f..3b1ed1f6ddf6 100644 --- a/qiskit/synthesis/qft/qft_decompose_lnn.py +++ b/qiskit/synthesis/qft/qft_decompose_lnn.py @@ -16,6 +16,7 @@ import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.synthesis.permutation.permutation_reverse_lnn import _append_reverse_permutation_lnn_kms +from .qft_decompose_full import _warn_if_precision_loss def synth_qft_line( @@ -51,7 +52,7 @@ def synth_qft_line( Quantum Info. Comput. 4, 4 (July 2004), 237–251. `arXiv:quant-ph/0402196 [quant-ph] `_ """ - + _warn_if_precision_loss(num_qubits - approximation_degree - 1) qc = QuantumCircuit(num_qubits) for i in range(num_qubits): diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index d4c7702da35a..79a444e6220c 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -51,7 +51,6 @@ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.synthesis.one_qubit.one_qubit_decompose import ( - OneQubitEulerDecomposer, DEFAULT_ATOL, ) from qiskit.utils.deprecation import deprecate_func @@ -280,159 +279,27 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ - atol = DEFAULT_ATOL - - scales, test_angles, scale = [], [0.2, 0.3, np.pi / 2], None - - for test_angle in test_angles: - # Check that gate takes a single angle parameter - try: - rxx_equivalent_gate(test_angle, label="foo") - except TypeError as _: - raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") from _ - decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) - - circ = QuantumCircuit(2) - circ.rxx(test_angle, 0, 1) - decomposer_rxx = TwoQubitWeylDecomposition( - Operator(circ).data, - fidelity=None, - _specialization=two_qubit_decompose.Specialization.ControlledEquiv, + if rxx_equivalent_gate._standard_gate is not None: + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate._standard_gate ) - - circ = QuantumCircuit(2) - circ.append(rxx_equivalent_gate(test_angle), qargs=[0, 1]) - decomposer_equiv = TwoQubitWeylDecomposition( - Operator(circ).data, - fidelity=None, - _specialization=two_qubit_decompose.Specialization.ControlledEquiv, - ) - - scale = decomposer_rxx.a / decomposer_equiv.a - - if abs(decomp.a * 2 - test_angle / scale) > atol: - raise QiskitError( - f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." - ) - - scales.append(scale) - - # Check that all three tested angles give the same scale - if not np.allclose(scales, [scale] * len(test_angles)): - raise QiskitError( - f"Cannot initialize {self.__class__.__name__}: with gate {rxx_equivalent_gate}. " - "Inconsistent scaling parameters in checks." + else: + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate ) - - self.scale = scales[0] - self.rxx_equivalent_gate = rxx_equivalent_gate + self.scale = self._inner_decomposition.scale - def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. - - Note: atol ist passed to OneQubitEulerDecomposer. - """ - - # pylint: disable=attribute-defined-outside-init - self.decomposer = TwoQubitWeylDecomposition(unitary) - - oneq_decompose = OneQubitEulerDecomposer("ZYZ") - c1l, c1r, c2l, c2r = ( - oneq_decompose(k, atol=atol) - for k in ( - self.decomposer.K1l, - self.decomposer.K1r, - self.decomposer.K2l, - self.decomposer.K2r, - ) - ) - circ = QuantumCircuit(2, global_phase=self.decomposer.global_phase) - circ.compose(c2r, [0], inplace=True) - circ.compose(c2l, [1], inplace=True) - self._weyl_gate(circ) - circ.compose(c1r, [0], inplace=True) - circ.compose(c1l, [1], inplace=True) - return circ - - def _to_rxx_gate(self, angle: float) -> QuantumCircuit: - """ - Takes an angle and returns the circuit equivalent to an RXXGate with the - RXX equivalent gate as the two-qubit unitary. - Args: - angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) - + unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize. Returns: - Circuit: Circuit equivalent to an RXXGate. - - Raises: - QiskitError: If the circuit is not equivalent to an RXXGate. + QuantumCircuit: Synthesized quantum circuit. + Note: atol is passed to OneQubitEulerDecomposer. """ - - # The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate - # but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl - # parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. - # :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters - # (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - - circ = QuantumCircuit(2) - circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) - decomposer_inv = TwoQubitWeylDecomposition(Operator(circ).data) - - oneq_decompose = OneQubitEulerDecomposer("ZYZ") - - # Express the RXXGate in terms of the user-provided RXXGate equivalent gate. - rxx_circ = QuantumCircuit(2, global_phase=-decomposer_inv.global_phase) - rxx_circ.compose(oneq_decompose(decomposer_inv.K2r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_inv.K2l).inverse(), inplace=True, qubits=[1]) - rxx_circ.compose(circ, inplace=True) - rxx_circ.compose(oneq_decompose(decomposer_inv.K1r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_inv.K1l).inverse(), inplace=True, qubits=[1]) - - return rxx_circ - - def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): - """Appends U_d(a, b, c) to the circuit.""" - - circ_rxx = self._to_rxx_gate(-2 * self.decomposer.a) - circ.compose(circ_rxx, inplace=True) - - # translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. - if abs(self.decomposer.b) > atol: - circ_ryy = QuantumCircuit(2) - circ_ryy.sdg(0) - circ_ryy.sdg(1) - circ_ryy.compose(self._to_rxx_gate(-2 * self.decomposer.b), inplace=True) - circ_ryy.s(0) - circ_ryy.s(1) - circ.compose(circ_ryy, inplace=True) - - # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. - if abs(self.decomposer.c) > atol: - # Since the Weyl chamber is here defined as a > b > |c| we may have - # negative c. This will cause issues in _to_rxx_gate - # as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). - # We therefore produce RZZGate(|c|) and append its inverse to the - # circuit if c < 0. - gamma, invert = -2 * self.decomposer.c, False - if gamma > 0: - gamma *= -1 - invert = True - - circ_rzz = QuantumCircuit(2) - circ_rzz.h(0) - circ_rzz.h(1) - circ_rzz.compose(self._to_rxx_gate(gamma), inplace=True) - circ_rzz.h(0) - circ_rzz.h(1) - - if invert: - circ.compose(circ_rzz.inverse(), inplace=True) - else: - circ.compose(circ_rzz, inplace=True) - - return circ + circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) + return QuantumCircuit._from_circuit_data(circ_data, add_regs=True) class TwoQubitBasisDecomposer: diff --git a/qiskit/synthesis/unitary/qsd.py b/qiskit/synthesis/unitary/qsd.py index 12e8fc0f9f7a..ab0f84ac72a4 100644 --- a/qiskit/synthesis/unitary/qsd.py +++ b/qiskit/synthesis/unitary/qsd.py @@ -251,14 +251,13 @@ def _get_ucry_cz(nqubits, angles): def _apply_a2(circ): - from qiskit.compiler import transpile from qiskit.quantum_info import Operator from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate + from qiskit.transpiler.passes.synthesis import HighLevelSynthesis decomposer = two_qubit_decompose_up_to_diagonal - ccirc = transpile( - circ, basis_gates=["u", "cx", "qsd2q"], optimization_level=0, qubits_initially_zero=False - ) + hls = HighLevelSynthesis(basis_gates=["u", "cx", "qsd2q"], qubits_initially_zero=False) + ccirc = hls(circ) ind2q = [] # collect 2q instrs for i, instruction in enumerate(ccirc.data): diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index ca4e3545a98b..5bc1ae555a5e 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -93,6 +93,7 @@ NormalizeRXAngle OptimizeAnnotated Split2QUnitaries + RemoveIdentityEquivalent Calibration ============= @@ -247,6 +248,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated +from .optimization import RemoveIdentityEquivalent from .optimization import Split2QUnitaries # circuit analysis diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index c75bff1a6ebc..c6a61b8ad49a 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -13,23 +13,12 @@ """Translates gates to a target basis using a given equivalence library.""" -import time import logging -from functools import singledispatchmethod from collections import defaultdict -from qiskit.circuit import ( - ControlFlowOp, - QuantumCircuit, - ParameterExpression, -) -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.basis.basis_translator import basis_search, compose_transforms +from qiskit._accelerate.basis.basis_translator import base_run logger = logging.getLogger(__name__) @@ -136,309 +125,13 @@ def run(self, dag): Returns: DAGCircuit: translated circuit. """ - if self._target_basis is None and self._target is None: - return dag - qarg_indices = {qubit: index for index, qubit in enumerate(dag.qubits)} - - # Names of instructions assumed to supported by any backend. - if self._target is None: - 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", "store"] - target_basis = self._target.keys() - set(self._non_global_operations) - source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) - - target_basis = set(target_basis).union(basic_instrs) - # If the source basis is a subset of the target basis and we have no circuit - # instructions on qargs that have non-global operations there is nothing to - # translate and we can exit early. - source_basis_names = {x[0] for x in source_basis} - if source_basis_names.issubset(target_basis) and not qargs_local_source_basis: - return dag - - logger.info( - "Begin BasisTranslator from source basis %s to target basis %s.", - source_basis, - target_basis, - ) - - # Search for a path from source to target basis. - search_start_time = time.time() - basis_transforms = basis_search(self._equiv_lib, source_basis, target_basis) - - qarg_local_basis_transforms = {} - for qarg, local_source_basis in qargs_local_source_basis.items(): - expanded_target = set(target_basis) - # For any multiqubit operation that contains a subset of qubits that - # has a non-local operation, include that non-local operation in the - # search. This matches with the check we did above to include those - # subset non-local operations in the check here. - if len(qarg) > 1: - for non_local_qarg, local_basis in self._qargs_with_non_global_operation.items(): - if qarg.issuperset(non_local_qarg): - expanded_target |= local_basis - else: - expanded_target |= self._qargs_with_non_global_operation[tuple(qarg)] - - logger.info( - "Performing BasisTranslator search from source basis %s to target " - "basis %s on qarg %s.", - local_source_basis, - expanded_target, - qarg, - ) - local_basis_transforms = basis_search( - self._equiv_lib, local_source_basis, expanded_target - ) - - if local_basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in local_source_basis]} to the backend's (or manually " - f"specified) target basis: {list(expanded_target)}. 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/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - qarg_local_basis_transforms[qarg] = local_basis_transforms - - search_end_time = time.time() - logger.info( - "Basis translation path search completed in %.3fs.", search_end_time - search_start_time - ) - - if basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in source_basis]} to the backend's (or manually specified) target " - 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/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - # Compose found path into a set of instruction substitution rules. - - compose_start_time = time.time() - instr_map = compose_transforms(basis_transforms, source_basis, dag) - extra_instr_map = { - qarg: compose_transforms(transforms, qargs_local_source_basis[qarg], dag) - for qarg, transforms in qarg_local_basis_transforms.items() - } - - compose_end_time = time.time() - logger.info( - "Basis translation paths composed in %.3fs.", compose_end_time - compose_start_time + return base_run( + dag, + self._equiv_lib, + self._qargs_with_non_global_operation, + self._min_qubits, + None if self._target_basis is None else set(self._target_basis), + self._target, + None if self._non_global_operations is None else set(self._non_global_operations), ) - - # Replace source instructions with target translations. - - replace_start_time = time.time() - - def apply_translation(dag, wire_map): - is_updated = False - out_dag = dag.copy_empty_like() - for node in dag.topological_op_nodes(): - node_qargs = tuple(wire_map[bit] for bit in node.qargs) - qubit_set = frozenset(node_qargs) - if node.name in target_basis or len(node.qargs) < self._min_qubits: - if node.name in CONTROL_FLOW_OP_NAMES: - flow_blocks = [] - for block in node.op.blocks: - dag_block = circuit_to_dag(block) - updated_dag, is_updated = apply_translation( - dag_block, - { - inner: wire_map[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - ) - if is_updated: - flow_circ_block = dag_to_circuit(updated_dag) - else: - flow_circ_block = block - flow_blocks.append(flow_circ_block) - node.op = node.op.replace_blocks(flow_blocks) - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if ( - node_qargs in self._qargs_with_non_global_operation - and node.name in self._qargs_with_non_global_operation[node_qargs] - ): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - - if dag._has_calibration_for(node): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if qubit_set in extra_instr_map: - self._replace_node(out_dag, node, extra_instr_map[qubit_set]) - elif (node.name, node.num_qubits) in instr_map: - self._replace_node(out_dag, node, instr_map) - else: - raise TranspilerError(f"BasisTranslator did not map {node.name}.") - is_updated = True - return out_dag, is_updated - - out_dag, _ = apply_translation(dag, qarg_indices) - replace_end_time = time.time() - logger.info( - "Basis translation instructions replaced in %.3fs.", - replace_end_time - replace_start_time, - ) - - return out_dag - - def _replace_node(self, dag, node, instr_map): - target_params, target_dag = instr_map[node.name, node.num_qubits] - if len(node.params) != len(target_params): - raise TranspilerError( - "Translation num_params not equal to op num_params." - f"Op: {node.params} {node.name} Translation: {target_params}\n{target_dag}" - ) - if node.params: - parameter_map = dict(zip(target_params, node.params)) - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - - if not new_node.is_standard_gate(): - new_node.op = new_node.op.copy() - if any(isinstance(x, ParameterExpression) for x in inner_node.params): - new_params = [] - for param in new_node.params: - if not isinstance(param, ParameterExpression): - new_params.append(param) - else: - bind_dict = {x: parameter_map[x] for x in param.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_value = param - for x in bind_dict.items(): - new_value = new_value.assign(*x) - else: - new_value = param.bind(bind_dict) - if not new_value.parameters: - new_value = new_value.numeric() - new_params.append(new_value) - new_node.params = new_params - if not new_node.is_standard_gate(): - new_node.op.params = new_params - dag._apply_op_node_back(new_node) - - if isinstance(target_dag.global_phase, ParameterExpression): - old_phase = target_dag.global_phase - bind_dict = {x: parameter_map[x] for x in old_phase.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_phase = old_phase - for x in bind_dict.items(): - new_phase = new_phase.assign(*x) - else: - new_phase = old_phase.bind(bind_dict) - if not new_phase.parameters: - new_phase = new_phase.numeric() - if isinstance(new_phase, complex): - raise TranspilerError(f"Global phase must be real, but got '{new_phase}'") - dag.global_phase += new_phase - - else: - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - ) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - if not new_node.is_standard_gate: - new_node.op = new_node.op.copy() - # dag_op may be the same instance as other ops in the dag, - # so if there is a condition, need to copy - if getattr(node.op, "condition", None): - new_node_op = new_node.op.to_mutable() - new_node_op.condition = node.op.condition - new_node.op = new_node_op - dag._apply_op_node_back(new_node) - if target_dag.global_phase: - dag.global_phase += target_dag.global_phase - - @singledispatchmethod - def _extract_basis(self, circuit): - return circuit - - @_extract_basis.register - def _(self, dag: DAGCircuit): - for node in dag.op_nodes(): - if not dag._has_calibration_for(node) and len(node.qargs) >= self._min_qubits: - yield (node.name, node.num_qubits) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - yield from self._extract_basis(block) - - @_extract_basis.register - def _(self, circ: QuantumCircuit): - for instruction in circ.data: - operation = instruction.operation - if ( - not circ._has_calibration_for(instruction) - and len(instruction.qubits) >= self._min_qubits - ): - yield (operation.name, operation.num_qubits) - if isinstance(operation, ControlFlowOp): - for block in operation.blocks: - yield from self._extract_basis(block) - - def _extract_basis_target( - self, dag, qarg_indices, source_basis=None, qargs_local_source_basis=None - ): - if source_basis is None: - source_basis = set() - if qargs_local_source_basis is None: - qargs_local_source_basis = defaultdict(set) - for node in dag.op_nodes(): - qargs = tuple(qarg_indices[bit] for bit in node.qargs) - if dag._has_calibration_for(node) or len(node.qargs) < self._min_qubits: - continue - # Treat the instruction as on an incomplete basis if the qargs are in the - # qargs_with_non_global_operation dictionary or if any of the qubits in qargs - # are a superset for a non-local operation. For example, if the qargs - # are (0, 1) and that's a global (ie no non-local operations on (0, 1) - # operation but there is a non-local operation on (1,) we need to - # do an extra non-local search for this op to ensure we include any - # single qubit operation for (1,) as valid. This pattern also holds - # true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, - # and 1q operations in the same manner) - if qargs in self._qargs_with_non_global_operation or any( - frozenset(qargs).issuperset(incomplete_qargs) - for incomplete_qargs in self._qargs_with_non_global_operation - ): - qargs_local_source_basis[frozenset(qargs)].add((node.name, node.num_qubits)) - else: - source_basis.add((node.name, node.num_qubits)) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - block_dag = circuit_to_dag(block) - source_basis, qargs_local_source_basis = self._extract_basis_target( - block_dag, - { - inner: qarg_indices[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - source_basis=source_basis, - qargs_local_source_basis=qargs_local_source_basis, - ) - return source_basis, qargs_local_source_basis diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 78af67ad9118..af17cc226cb7 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -148,7 +148,9 @@ def __init__( (and ``routing_pass`` is not set) then the number of local physical CPUs will be used as the default value. This option is mutually exclusive with the ``routing_pass`` argument and an error - will be raised if both are used. + will be raised if both are used. An additional 3 or 4 trials + depending on the ``coupling_map`` value are run with common layouts + on top of the random trial count specified by this value. skip_routing (bool): If this is set ``True`` and ``routing_pass`` is not used then routing will not be applied to the output circuit. Only the layout will be set in the property set. This is a tradeoff to run custom diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 8e2883b27781..c0e455b2065b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,5 +38,6 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated +from .remove_identity_equiv import RemoveIdentityEquivalent from .split_2q_unitaries import Split2QUnitaries from .collect_and_collapse import CollectAndCollapse diff --git a/qiskit/transpiler/passes/optimization/collect_cliffords.py b/qiskit/transpiler/passes/optimization/collect_cliffords.py index 5ee75af98000..8b26d04045c1 100644 --- a/qiskit/transpiler/passes/optimization/collect_cliffords.py +++ b/qiskit/transpiler/passes/optimization/collect_cliffords.py @@ -83,7 +83,7 @@ def __init__( def _is_clifford_gate(node, matrix_based=False): """Specifies whether a node holds a clifford gate.""" - if getattr(node.op, "condition", None) is not None: + if getattr(node.op, "_condition", None) is not None: return False if node.op.name in clifford_gate_names: return True diff --git a/qiskit/transpiler/passes/optimization/collect_linear_functions.py b/qiskit/transpiler/passes/optimization/collect_linear_functions.py index 158440e6a8a1..25a66e2bf9dc 100644 --- a/qiskit/transpiler/passes/optimization/collect_linear_functions.py +++ b/qiskit/transpiler/passes/optimization/collect_linear_functions.py @@ -71,7 +71,7 @@ def __init__( def _is_linear_gate(node): """Specifies whether a node holds a linear gate.""" - return node.op.name in ("cx", "swap") and getattr(node.op, "condition", None) is None + return node.op.name in ("cx", "swap") and getattr(node, "condition", None) is None def _collapse_to_linear_function(circuit): diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index e0dd61ff6cf4..34d51a17fe4a 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -120,7 +120,7 @@ def collect_key(x): if not isinstance(x, DAGOpNode): return "d" if isinstance(x.op, Gate): - if x.op.is_parameterized() or getattr(x.op, "condition", None) is not None: + if x.op.is_parameterized() or getattr(x.op, "_condition", None) is not None: return "c" return "b" + chr(ord("a") + len(x.qargs)) return "d" @@ -133,7 +133,7 @@ def collect_key(x): # check if the node is a gate and if it is parameterized if ( - getattr(nd.op, "condition", None) is not None + getattr(nd.op, "_condition", None) is not None or nd.op.is_parameterized() or not isinstance(nd.op, Gate) ): diff --git a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py index 97324e2376cd..8c1b67b595a0 100644 --- a/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_inverse_cancellation.py @@ -49,7 +49,7 @@ def _skip_node(self, node): # checking can be extended to cover additional cases. if getattr(node.op, "_directive", False) or node.name in {"measure", "reset", "delay"}: return True - if getattr(node.op, "condition", None): + if getattr(node, "condition", None): return True if node.op.is_parameterized(): return True diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 49f227e8a746..f31401abb6a7 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -12,27 +12,26 @@ """Replace each block of consecutive gates by a single Unitary node.""" from __future__ import annotations +from math import pi -import numpy as np - -from qiskit.circuit.classicalregister import ClassicalRegister -from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.dagcircuit.dagnode import DAGOpNode -from qiskit.quantum_info import Operator from qiskit.synthesis.two_qubit import TwoQubitBasisDecomposer -from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate -from qiskit.circuit.library.standard_gates import CXGate +from qiskit.circuit.library.standard_gates import CXGate, CZGate, iSwapGate, ECRGate, RXXGate + from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.passes.synthesis import unitary_synthesis -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.convert_2q_block_matrix import blocks_to_matrix -from qiskit.exceptions import QiskitError +from qiskit._accelerate.consolidate_blocks import consolidate_blocks from .collect_1q_runs import Collect1qRuns from .collect_2q_blocks import Collect2qBlocks +KAK_GATE_NAMES = { + "cx": CXGate(), + "cz": CZGate(), + "iswap": iSwapGate(), + "ecr": ECRGate(), + "rxx": RXXGate(pi / 2), +} + class ConsolidateBlocks(TransformationPass): """Replace each block of consecutive gates by a single Unitary node. @@ -74,13 +73,20 @@ def __init__( if basis_gates is not None: self.basis_gates = set(basis_gates) self.force_consolidate = force_consolidate - if kak_basis_gate is not None: self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate) elif basis_gates is not None: - self.decomposer = unitary_synthesis._decomposer_2q_from_basis_gates( - basis_gates, approximation_degree=approximation_degree - ) + kak_gates = KAK_GATE_NAMES.keys() & (basis_gates or []) + if kak_gates: + self.decomposer = TwoQubitBasisDecomposer( + KAK_GATE_NAMES[kak_gates.pop()], basis_fidelity=approximation_degree or 1.0 + ) + elif "rzx" in basis_gates: + self.decomposer = TwoQubitBasisDecomposer( + CXGate(), basis_fidelity=approximation_degree or 1.0 + ) + else: + self.decomposer = None else: self.decomposer = TwoQubitBasisDecomposer(CXGate()) @@ -93,89 +99,23 @@ def run(self, dag): if self.decomposer is None: return dag - blocks = self.property_set["block_list"] or [] - basis_gate_name = self.decomposer.gate.name - all_block_gates = set() - for block in blocks: - if len(block) == 1 and self._check_not_in_basis(dag, block[0].name, block[0].qargs): - all_block_gates.add(block[0]) - dag.substitute_node(block[0], UnitaryGate(block[0].op.to_matrix())) - else: - basis_count = 0 - outside_basis = False - block_qargs = set() - block_cargs = set() - for nd in block: - block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and getattr(nd, "condition", None): - block_cargs |= set(getattr(nd, "condition", None)[0]) - all_block_gates.add(nd) - block_index_map = self._block_qargs_to_indices(dag, block_qargs) - for nd in block: - if nd.name == basis_gate_name: - basis_count += 1 - if self._check_not_in_basis(dag, nd.name, nd.qargs): - outside_basis = True - if len(block_qargs) > 2: - q = QuantumRegister(len(block_qargs)) - qc = QuantumCircuit(q) - if block_cargs: - c = ClassicalRegister(len(block_cargs)) - qc.add_register(c) - for nd in block: - qc.append(nd.op, [q[block_index_map[i]] for i in nd.qargs]) - unitary = UnitaryGate(Operator(qc), check_input=False) - else: - try: - matrix = blocks_to_matrix(block, block_index_map) - except QiskitError: - # If building a matrix for the block fails we should not consolidate it - # because there is nothing we can do with it. - continue - unitary = UnitaryGate(matrix, check_input=False) - - max_2q_depth = 20 # If depth > 20, there will be 1q gates to consolidate. - if ( # pylint: disable=too-many-boolean-expressions - self.force_consolidate - or unitary.num_qubits > 2 - or self.decomposer.num_basis_gates(matrix) < basis_count - or len(block) > max_2q_depth - or ((self.basis_gates is not None) and outside_basis) - or ((self.target is not None) and outside_basis) - ): - identity = np.eye(2**unitary.num_qubits) - if np.allclose(identity, unitary.to_matrix()): - for node in block: - dag.remove_op_node(node) - else: - dag.replace_block_with_op( - block, unitary, block_index_map, cycle_check=False - ) - # If 1q runs are collected before consolidate those too - runs = self.property_set["run_list"] or [] - identity_1q = np.eye(2) - for run in runs: - if any(gate in all_block_gates for gate in run): - continue - if len(run) == 1 and not self._check_not_in_basis(dag, run[0].name, run[0].qargs): - dag.substitute_node(run[0], UnitaryGate(run[0].op.to_matrix(), check_input=False)) - else: - qubit = run[0].qargs[0] - operator = run[0].op.to_matrix() - already_in_block = False - for gate in run[1:]: - if gate in all_block_gates: - already_in_block = True - operator = gate.op.to_matrix().dot(operator) - if already_in_block: - continue - unitary = UnitaryGate(operator, check_input=False) - if np.allclose(identity_1q, unitary.to_matrix()): - for node in run: - dag.remove_op_node(node) - else: - dag.replace_block_with_op(run, unitary, {qubit: 0}, cycle_check=False) - + blocks = self.property_set["block_list"] + if blocks is not None: + blocks = [[node._node_id for node in block] for block in blocks] + runs = self.property_set["run_list"] + if runs is not None: + runs = [[node._node_id for node in run] for run in runs] + + consolidate_blocks( + dag, + self.decomposer._inner_decomposer, + self.decomposer.gate.name, + self.force_consolidate, + target=self.target, + basis_gates=self.basis_gates, + blocks=blocks, + runs=runs, + ) dag = self._handle_control_flow_ops(dag) # Clear collected blocks and runs as they are no longer valid after consolidation @@ -195,38 +135,15 @@ def _handle_control_flow_ops(self, dag): pass_manager = PassManager() if "run_list" in self.property_set: pass_manager.append(Collect1qRuns()) - if "block_list" in self.property_set: pass_manager.append(Collect2qBlocks()) pass_manager.append(self) - for node in dag.op_nodes(): - if node.name not in CONTROL_FLOW_OP_NAMES: - continue - dag.substitute_node( - node, - node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), - propagate_condition=False, - ) + control_flow_nodes = dag.control_flow_op_nodes() + if control_flow_nodes is not None: + for node in control_flow_nodes: + dag.substitute_node( + node, + node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), + propagate_condition=False, + ) return dag - - def _check_not_in_basis(self, dag, gate_name, qargs): - if self.target is not None: - return not self.target.instruction_supported( - gate_name, tuple(dag.find_bit(qubit).index for qubit in qargs) - ) - else: - return self.basis_gates and gate_name not in self.basis_gates - - def _block_qargs_to_indices(self, dag, block_qargs): - """Map each qubit in block_qargs to its wire position among the block's wires. - Args: - block_qargs (list): list of qubits that a block acts on - global_index_map (dict): mapping from each qubit in the - circuit to its wire position within that circuit - Returns: - dict: mapping from qarg to position in block - """ - block_indices = [dag.find_bit(q).index for q in block_qargs] - ordered_block_indices = {bit: index for index, bit in enumerate(sorted(block_indices))} - block_positions = {q: ordered_block_indices[dag.find_bit(q).index] for q in block_qargs} - return block_positions diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 4b96c9c86dfb..c926e15800ae 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -21,6 +21,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes.calibration.rzx_builder import _check_calibration_type, CRCalType +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency from qiskit.dagcircuit import DAGCircuit from qiskit.converters import circuit_to_dag @@ -34,6 +35,7 @@ class EchoRZXWeylDecomposition(TransformationPass): Each pair of RZXGates forms an echoed RZXGate. """ + @deprecate_pulse_dependency def __init__(self, instruction_schedule_map=None, target=None): """EchoRZXWeylDecomposition pass. diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py index f8302b9232c0..466bcc4d5fcc 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_gates.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_gates.py @@ -86,7 +86,7 @@ def run(self, dag): for current_node in run: left_name = current_node.name if ( - getattr(current_node.op, "condition", None) is not None + getattr(current_node, "condition", None) is not None or len(current_node.qargs) != 1 or left_name not in ["p", "u1", "u2", "u3", "u", "id"] ): diff --git a/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py b/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py index 6682e7ebbdba..7345a5bc40fd 100644 --- a/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py +++ b/qiskit/transpiler/passes/optimization/optimize_swap_before_measure.py @@ -40,7 +40,7 @@ def run(self, dag): swaps = dag.op_nodes(SwapGate) for swap in swaps[::-1]: - if getattr(swap.op, "condition", None) is not None: + if getattr(swap.op, "_condition", None) is not None: continue final_successor = [] for successor in dag.descendants(swap): diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py new file mode 100644 index 000000000000..fbf132d958a2 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -0,0 +1,69 @@ +# 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. + +"""Transpiler pass to drop gates with negligible effects.""" + +from __future__ import annotations + +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.target import Target +from qiskit.transpiler.basepasses import TransformationPass +from qiskit._accelerate.remove_identity_equiv import remove_identity_equiv + + +class RemoveIdentityEquivalent(TransformationPass): + r"""Remove gates with negligible effects. + + Removes gates whose effect is close to an identity operation, up to the specified + tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered + by this pass. + + For a cutoff fidelity :math:`f`, this pass removes gates whose average + gate fidelity with respect to the identity is below :math:`f`. Concretely, + a gate :math:`G` is removed if :math:`\bar F < f` where + + .. math:: + + \bar{F} = \frac{1 + F_{\text{process}}}{1 + d},\ + + F_{\text{process}} = \frac{|\mathrm{Tr}(G)|^2}{d^2} + + where :math:`d = 2^n` is the dimension of the gate for :math:`n` qubits. + """ + + def __init__( + self, *, approximation_degree: float | None = 1.0, target: None | Target = None + ) -> None: + """Initialize the transpiler pass. + + Args: + approximation_degree: The degree to approximate for the equivalence check. This can be a + floating point value between 0 and 1, or ``None``. If the value is 1 this does not + approximate above floating point precision. For a value < 1 this is used as a scaling + factor for the cutoff fidelity. If the value is ``None`` this approximates up to the + fidelity for the gate specified in ``target``. + + target: If ``approximation_degree`` is set to ``None`` and a :class:`.Target` is provided + for this field the tolerance for determining whether an operation is equivalent to + identity will be set to the reported error rate in the target. If + ``approximation_degree`` (the default) this has no effect, if + ``approximation_degree=None`` it uses the error rate specified in the ``Target`` for + the gate being evaluated, and a numeric value other than 1 with ``target`` set is + used as a scaling factor of the target's error rate. + """ + super().__init__() + self._approximation_degree = approximation_degree + self._target = target + + def run(self, dag: DAGCircuit) -> DAGCircuit: + remove_identity_equiv(dag, self._approximation_degree, self._target) + return dag diff --git a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py index d194d1cbbddf..dc1ff7b3447f 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/backward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/backward_match.py @@ -242,7 +242,7 @@ def _is_same_op(self, node_circuit, node_template): Returns: bool: True if the same, False otherwise. """ - return node_circuit.op == node_template.op + return node_circuit.op.soft_compare(node_template.op) def _is_same_q_conf(self, node_circuit, node_template, qarg_circuit): """ @@ -304,15 +304,15 @@ def _is_same_c_conf(self, node_circuit, node_template, carg_circuit): """ if ( node_circuit.type == "op" - and getattr(node_circuit.op, "condition", None) + and getattr(node_circuit.op, "_condition", None) and node_template.type == "op" - and getattr(node_template.op, "condition", None) + and getattr(node_template.op, "_condition", None) ): if set(carg_circuit) != set(node_template.cindices): return False if ( - getattr(node_circuit.op, "condition", None)[1] - != getattr(node_template.op, "condition", None)[1] + getattr(node_circuit.op, "_condition", None)[1] + != getattr(node_template.op, "_condition", None)[1] ): return False return True diff --git a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py index d8dd5bb2b9a2..b4232d8ff6d2 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/forward_match.py +++ b/qiskit/transpiler/passes/optimization/template_matching/forward_match.py @@ -311,15 +311,15 @@ def _is_same_c_conf(self, node_circuit, node_template): """ if ( node_circuit.type == "op" - and getattr(node_circuit.op, "condition", None) + and getattr(node_circuit.op, "_condition", None) and node_template.type == "op" - and getattr(node_template.op, "condition", None) + and getattr(node_template.op, "_condition", None) ): if set(self.carg_indices) != set(node_template.cindices): return False if ( - getattr(node_circuit.op, "condition", None)[1] - != getattr(node_template.op, "condition", None)[1] + getattr(node_circuit.op, "_condition", None)[1] + != getattr(node_template.op, "_condition", None)[1] ): return False return True diff --git a/qiskit/transpiler/passes/routing/star_prerouting.py b/qiskit/transpiler/passes/routing/star_prerouting.py index 53bc971a268b..259d79ba636b 100644 --- a/qiskit/transpiler/passes/routing/star_prerouting.py +++ b/qiskit/transpiler/passes/routing/star_prerouting.py @@ -223,7 +223,7 @@ def filter_fn(node): return ( len(node.qargs) <= 2 and len(node.cargs) == 0 - and getattr(node.op, "condition", None) is None + and getattr(node, "condition", None) is None and not isinstance(node.op, Barrier) ) @@ -372,7 +372,7 @@ def _extract_nodes(nodes, dag): qubit_indices = [dag.find_bit(qubit).index for qubit in node.qargs] classical_bit_indices = set() - if node.op.condition is not None: + if node.condition is not None: classical_bit_indices.update(condition_resources(node.op.condition).clbits) if isinstance(node.op, SwitchCaseOp): diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 8bdd0a761edf..95adee1d05c7 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -28,7 +28,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence +from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence, Qubit from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap @@ -42,8 +42,8 @@ PowerModifier, ) +from qiskit._accelerate.high_level_synthesis import QubitTracker, QubitContext from .plugin import HighLevelSynthesisPluginManager -from .qubit_tracker import QubitTracker if typing.TYPE_CHECKING: from qiskit.dagcircuit import DAGOpNode @@ -271,96 +271,161 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ qubits = tuple(dag.find_bit(q).index for q in dag.qubits) + context = QubitContext(list(range(len(dag.qubits)))) + tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.qubits_initially_zero: - clean, dirty = set(qubits), set() - else: - clean, dirty = set(), set(qubits) + tracker.set_clean(context.to_globals(qubits)) - tracker = QubitTracker(qubits=qubits, clean=clean, dirty=dirty) - return self._run(dag, tracker) + out_dag = self._run(dag, tracker, context, use_ancillas=True, top_level=True) + return out_dag - def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: - # Check if HighLevelSynthesis can be skipped. - for node in dag.op_nodes(): - qubits = tuple(dag.find_bit(q).index for q in node.qargs) - if not self._definitely_skip_node(node, qubits, dag): - break - else: - # The for-loop terminates without reaching the break statement - return dag + def _run( + self, + dag: DAGCircuit, + tracker: QubitTracker, + context: QubitContext, + use_ancillas: bool, + top_level: bool, + ) -> DAGCircuit: + """ + The main recursive function that synthesizes a DAGCircuit. + + Input: + dag: the DAG to be synthesized. + tracker: the global tracker, tracking the state of original qubits. + context: the correspondence between the dag's qubits and the global qubits. + use_ancillas: if True, synthesis algorithms are allowed to use ancillas. + top_level: specifies if this is the top-level of the recursion. + + The function returns the synthesized DAG. + + Note that by using the auxiliary qubits to synthesize operations present in the input DAG, + the synthesized DAG may be defined over more qubits than the input DAG. In this case, + the function update in-place the global qubits tracker and extends the local-to-global + context. + """ + + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") - # Start by analyzing the nodes in the DAG. This for-loop is a first version of a potentially - # more elaborate approach to find good operation/ancilla allocations. It greedily iterates - # over the nodes, checking whether we can synthesize them, while keeping track of the - # qubit states. It does not trade-off allocations and just gives all available qubits - # to the current operation (a "the-first-takes-all" approach). + # STEP 1: Check if HighLevelSynthesis can be skipped altogether. This is only + # done at the top-level since this does not update the global qubits tracker. + if top_level: + for node in dag.op_nodes(): + qubits = tuple(dag.find_bit(q).index for q in node.qargs) + if not self._definitely_skip_node(node, qubits, dag): + break + else: + # The for-loop terminates without reaching the break statement + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + return dag + + # STEP 2: Analyze the nodes in the DAG. For each node in the DAG that needs + # to be synthesized, we recursively synthesize it and store the result. For + # instance, the result of synthesizing a custom gate is a DAGCircuit corresponding + # to the (recursively synthesized) gate's definition. When the result is a + # DAG, we also store its context (the mapping of its qubits to global qubits). + # In addition, we keep track of the qubit states using the (global) qubits tracker. + # + # Note: This is a first version of a potentially more elaborate approach to find + # good operation/ancilla allocations. The current approach is greedy and just gives + # all available ancilla qubits to the current operation ("the-first-takes-all" approach). + # It does not distribute ancilla qubits between different operations present in the DAG. synthesized_nodes = {} for node in dag.topological_op_nodes(): qubits = tuple(dag.find_bit(q).index for q in node.qargs) + processed = False synthesized = None - used_qubits = None + synthesized_context = None + + # Start by handling special operations. Other cases can also be + # considered: swaps, automatically simplifying control gate (e.g. if + # a control is 0). + if node.op.name in ["id", "delay", "barrier"]: + # tracker not updated, these are no-ops + processed = True + + elif node.op.name == "reset": + # reset qubits to 0 + tracker.set_clean(context.to_globals(qubits)) + processed = True # check if synthesis for the operation can be skipped - if self._definitely_skip_node(node, qubits, dag): - pass + elif self._definitely_skip_node(node, qubits, dag): + tracker.set_dirty(context.to_globals(qubits)) # next check control flow elif node.is_control_flow(): - dag.substitute_node( - node, - control_flow.map_blocks(partial(self._run, tracker=tracker.copy()), node.op), - propagate_condition=False, + inner_context = context.restrict(qubits) + synthesized = control_flow.map_blocks( + partial( + self._run, + tracker=tracker, + context=inner_context, + use_ancillas=False, + top_level=False, + ), + node.op, ) # now we are free to synthesize else: - # this returns the synthesized operation and the qubits it acts on -- note that this - # may be different from the original qubits, since we may use auxiliary qubits - synthesized, used_qubits = self._synthesize_operation(node.op, qubits, tracker) + # This returns the synthesized operation and its context (when the result is + # a DAG, it's the correspondence between its qubits and the global qubits). + # Also note that the DAG may use auxiliary qubits. The qubits tracker and the + # current DAG's context are updated in-place. + synthesized, synthesized_context = self._synthesize_operation( + node.op, qubits, tracker, context, use_ancillas=use_ancillas + ) - # if the synthesis changed the operation (i.e. it is not None), store the result - # and mark the operation qubits as used + # If the synthesis changed the operation (i.e. it is not None), store the result. if synthesized is not None: - synthesized_nodes[node] = (synthesized, used_qubits) - tracker.used(qubits) # assumes that auxiliary are returned in the same state + synthesized_nodes[node._node_id] = (synthesized, synthesized_context) - # if the synthesis did not change anything, just update the qubit tracker - # other cases can be added: swaps, controlled gates (e.g. if control is 0), ... - else: - if node.op.name in ["id", "delay", "barrier"]: - pass # tracker not updated, these are no-ops - elif node.op.name == "reset": - tracker.reset(qubits) # reset qubits to 0 - else: - tracker.used(qubits) # any other op used the clean state up + # If the synthesis did not change anything, just update the qubit tracker. + elif not processed: + tracker.set_dirty(context.to_globals(qubits)) - # we did not change anything just return the input + # We did not change anything just return the input. if len(synthesized_nodes) == 0: + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") return dag - # Otherwise, we will rebuild with the new operations. Note that we could also + # STEP 3. We rebuild the DAG with new operations. Note that we could also # check if no operation changed in size and substitute in-place, but rebuilding is # generally as fast or faster, unless very few operations are changed. out = dag.copy_empty_like() - index_to_qubit = dict(enumerate(dag.qubits)) + num_additional_qubits = context.num_qubits() - out.num_qubits() + + if num_additional_qubits > 0: + out.add_qubits([Qubit() for _ in range(num_additional_qubits)]) + + index_to_qubit = dict(enumerate(out.qubits)) + outer_to_local = context.to_local_mapping() for node in dag.topological_op_nodes(): - if node in synthesized_nodes: - op, qubits = synthesized_nodes[node] - qargs = tuple(index_to_qubit[index] for index in qubits) + + if op_tuple := synthesized_nodes.get(node._node_id, None): + op, op_context = op_tuple + if isinstance(op, Operation): - out.apply_operation_back(op, qargs, cargs=[]) + out.apply_operation_back(op, node.qargs, node.cargs) continue if isinstance(op, QuantumCircuit): op = circuit_to_dag(op, copy_operations=False) + inner_to_global = op_context.to_global_mapping() if isinstance(op, DAGCircuit): qubit_map = { - qubit: index_to_qubit[index] for index, qubit in zip(qubits, op.qubits) + q: index_to_qubit[outer_to_local[inner_to_global[i]]] + for (i, q) in enumerate(op.qubits) } clbit_map = dict(zip(op.clbits, node.cargs)) + for sub_node in op.op_nodes(): out.apply_operation_back( sub_node.op, @@ -368,11 +433,15 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: tuple(clbit_map[carg] for carg in sub_node.cargs), ) out.global_phase += op.global_phase + else: - raise RuntimeError(f"Unexpected synthesized type: {type(op)}") + raise TranspilerError(f"Unexpected synthesized type: {type(op)}") else: out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + if out.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + return out def _synthesize_operation( @@ -380,7 +449,23 @@ def _synthesize_operation( operation: Operation, qubits: tuple[int], tracker: QubitTracker, - ) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, list[int] | None]: + context: QubitContext, + use_ancillas: bool, + ) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, QubitContext | None]: + """ + Synthesizes an operation. The function receives the qubits on which the operation + is defined in the current DAG, the correspondence between the qubits of the current + DAG and the global qubits and the global qubits tracker. The function returns the + result of synthesizing the operation. The value of `None` means that the operation + should remain as it is. When it's a circuit, we also return the context, i.e. the + correspondence of its local qubits and the global qubits. The function changes + in-place the tracker (state of the global qubits), the qubits (when the synthesized + operation is defined over additional ancilla qubits), and the context (to keep track + of where these ancilla qubits maps to). + """ + + synthesized_context = None + # Try to synthesize the operation. We'll go through the following options: # (1) Annotations: if the operator is annotated, synthesize the base operation # and then apply the modifiers. Returns a circuit (e.g. applying a power) @@ -389,31 +474,62 @@ def _synthesize_operation( # if the operation is a Clifford). Returns a circuit. # (3) Unrolling custom definitions: try defining the operation if it is not yet # in the set of supported instructions. Returns a circuit. + # # If any of the above were triggered, we will recurse and go again through these steps # until no further change occurred. At this point, we convert circuits to DAGs (the final # possible return type). If there was no change, we just return ``None``. + num_original_qubits = len(qubits) + qubits = list(qubits) + synthesized = None # Try synthesizing via AnnotatedOperation. This is faster than an isinstance check # but a bit less safe since someone could create operations with a ``modifiers`` attribute. if len(modifiers := getattr(operation, "modifiers", [])) > 0: - # The base operation must be synthesized without using potential control qubits + # Note: the base operation must be synthesized without using potential control qubits # used in the modifiers. num_ctrl = sum( mod.num_ctrl_qubits for mod in modifiers if isinstance(mod, ControlModifier) ) baseop_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones - baseop_tracker = tracker.copy(drop=qubits[:num_ctrl]) # no access to control qubits # get qubits of base operation + control_qubits = qubits[0:num_ctrl] + + # Do not allow access to control qubits + tracker.disable(context.to_globals(control_qubits)) synthesized_base_op, _ = self._synthesize_operation( - operation.base_op, baseop_qubits, baseop_tracker + operation.base_op, + baseop_qubits, + tracker, + context, + use_ancillas=use_ancillas, ) + if synthesized_base_op is None: synthesized_base_op = operation.base_op elif isinstance(synthesized_base_op, DAGCircuit): synthesized_base_op = dag_to_circuit(synthesized_base_op) + # Handle the case that synthesizing the base operation introduced + # additional qubits (e.g. the base operation is a circuit that includes + # an MCX gate). + if synthesized_base_op.num_qubits > len(baseop_qubits): + global_aux_qubits = tracker.borrow( + synthesized_base_op.num_qubits - len(baseop_qubits), + context.to_globals(baseop_qubits), + ) + global_to_local = context.to_local_mapping() + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + # Restore access to control qubits. + tracker.enable(context.to_globals(control_qubits)) + + # This step currently does not introduce ancilla qubits. synthesized = self._apply_annotations(synthesized_base_op, operation.modifiers) # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. @@ -421,57 +537,106 @@ def _synthesize_operation( # Try synthesis via HLS -- which will return ``None`` if unsuccessful. indices = qubits if self._use_qubit_indices else None if len(hls_methods := self._methods_to_try(operation.name)) > 0: + if use_ancillas: + num_clean_available = tracker.num_clean(context.to_globals(qubits)) + num_dirty_available = tracker.num_dirty(context.to_globals(qubits)) + else: + num_clean_available = 0 + num_dirty_available = 0 synthesized = self._synthesize_op_using_plugins( hls_methods, operation, indices, - tracker.num_clean(qubits), - tracker.num_dirty(qubits), + num_clean_available, + num_dirty_available, ) + # It may happen that the plugin synthesis method uses clean/dirty ancilla qubits + if (synthesized is not None) and (synthesized.num_qubits > len(qubits)): + # need to borrow more qubits from tracker + global_aux_qubits = tracker.borrow( + synthesized.num_qubits - len(qubits), context.to_globals(qubits) + ) + global_to_local = context.to_local_mapping() + + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. if synthesized is None and not self._top_level_only: - synthesized = self._unroll_custom_definition(operation, indices) + synthesized = self._get_custom_definition(operation, indices) if synthesized is None: - # if we didn't synthesize, there was nothing to unroll, so just set the used qubits - used_qubits = qubits + # if we didn't synthesize, there was nothing to unroll + # updating the tracker will be handled upstream + pass + + # if it has been synthesized, recurse and finally store the decomposition + elif isinstance(synthesized, Operation): + resynthesized, resynthesized_context = self._synthesize_operation( + synthesized, qubits, tracker, context, use_ancillas=use_ancillas + ) - else: - # if it has been synthesized, recurse and finally store the decomposition - if isinstance(synthesized, Operation): - re_synthesized, qubits = self._synthesize_operation( - synthesized, qubits, tracker.copy() + if resynthesized is not None: + synthesized = resynthesized + else: + tracker.set_dirty(context.to_globals(qubits)) + if isinstance(resynthesized, DAGCircuit): + synthesized_context = resynthesized_context + + elif isinstance(synthesized, QuantumCircuit): + # Synthesized is a quantum circuit which we want to process recursively. + # For example, it's the definition circuit of a custom gate + # or a circuit obtained by calling a synthesis method on a high-level-object. + # In the second case, synthesized may have more qubits than the original node. + + as_dag = circuit_to_dag(synthesized, copy_operations=False) + inner_context = context.restrict(qubits) + + if as_dag.num_qubits() != inner_context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + + # We save the current state of the tracker to be able to return the ancilla + # qubits to the current positions. Note that at this point we do not know + # which ancilla qubits will be allocated. + saved_tracker = tracker.copy() + synthesized = self._run( + as_dag, tracker, inner_context, use_ancillas=use_ancillas, top_level=False + ) + synthesized_context = inner_context + + if (synthesized is not None) and (synthesized.num_qubits() > len(qubits)): + # need to borrow more qubits from tracker + global_aux_qubits = tracker.borrow( + synthesized.num_qubits() - len(qubits), context.to_globals(qubits) + ) + global_to_local = context.to_local_mapping() + + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + + if len(qubits) > num_original_qubits: + tracker.replace_state( + saved_tracker, context.to_globals(qubits[num_original_qubits:]) ) - if re_synthesized is not None: - synthesized = re_synthesized - used_qubits = qubits - - elif isinstance(synthesized, QuantumCircuit): - aux_qubits = tracker.borrow(synthesized.num_qubits - len(qubits), qubits) - used_qubits = qubits + tuple(aux_qubits) - as_dag = circuit_to_dag(synthesized, copy_operations=False) - - # map used qubits to subcircuit - new_qubits = [as_dag.find_bit(q).index for q in as_dag.qubits] - qubit_map = dict(zip(used_qubits, new_qubits)) - - synthesized = self._run(as_dag, tracker.copy(qubit_map)) - if synthesized.num_qubits() != len(used_qubits): - raise RuntimeError( - f"Mismatching number of qubits, using {synthesized.num_qubits()} " - f"but have {len(used_qubits)}." - ) - else: - raise RuntimeError(f"Unexpected synthesized type: {type(synthesized)}") + else: + raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") - if synthesized is not None and used_qubits is None: - raise RuntimeError("Failed to find qubit indices on", synthesized) + if isinstance(synthesized, DAGCircuit) and synthesized_context is None: + raise TranspilerError("HighLevelSynthesis internal error.") - return synthesized, used_qubits + return synthesized, synthesized_context - def _unroll_custom_definition( + def _get_custom_definition( self, inst: Instruction, qubits: list[int] | None ) -> QuantumCircuit | None: # check if the operation is already supported natively @@ -649,6 +814,7 @@ def _definitely_skip_node( dag._has_calibration_for(node) or len(node.qargs) < self._min_qubits or node.is_directive() + or (self._instruction_supported(node.name, qubits) and not node.is_control_flow()) ): return True @@ -666,15 +832,12 @@ def _definitely_skip_node( # If all the above constraints hold, and it's already supported or the basis translator # can handle it, we'll leave it be. and ( - self._instruction_supported(node.name, qubits) # This uses unfortunately private details of `EquivalenceLibrary`, but so does the # `BasisTranslator`, and this is supposed to just be temporary til this is moved # into Rust space. - or ( - self._equiv_lib is not None - and equivalence.Key(name=node.name, num_qubits=node.num_qubits) - in self._equiv_lib.keys() - ) + self._equiv_lib is not None + and equivalence.Key(name=node.name, num_qubits=node.num_qubits) + in self._equiv_lib.keys() ) ) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index fa827a20ed48..6696ff2999d9 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -215,6 +215,7 @@ MCXSynthesis1CleanB95 MCXSynthesisDefault + MCMT Synthesis '''''''''''''' @@ -249,13 +250,165 @@ MCMTSynthesisNoAux MCMTSynthesisDefault + +Pauli Evolution Synthesis +''''''''''''''''''''''''' + +.. list-table:: Plugins for :class:`.PauliEvolutionGate` (key = ``"PauliEvolution"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Description + - Targeted connectivity + * - ``"rustiq"`` + - :class:`~.PauliEvolutionSynthesisRustiq` + - use a diagonalizing Clifford per Pauli term + - all-to-all + * - ``"default"`` + - :class:`~.PauliEvolutionSynthesisDefault` + - use ``rustiq_core`` synthesis library + - all-to-all + +.. autosummary:: + :toctree: ../stubs/ + + PauliEvolutionSynthesisDefault + PauliEvolutionSynthesisRustiq + + +Modular Adder Synthesis +''''''''''''''''''''''' + +.. list-table:: Plugins for :class:`.ModularAdderGate` (key = ``"ModularAdder"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Number of clean ancillas + - Description + * - ``"ripple_cdkm"`` + - :class:`.ModularAdderSynthesisC04` + - 1 + - a ripple-carry adder + * - ``"ripple_vbe"`` + - :class:`.ModularAdderSynthesisV95` + - :math:`n-1`, for :math:`n`-bit numbers + - a ripple-carry adder + * - ``"qft"`` + - :class:`.ModularAdderSynthesisD00` + - 0 + - a QFT-based adder + +.. autosummary:: + :toctree: ../stubs/ + + ModularAdderSynthesisC04 + ModularAdderSynthesisD00 + ModularAdderSynthesisV95 + +Half Adder Synthesis +'''''''''''''''''''' + +.. list-table:: Plugins for :class:`.HalfAdderGate` (key = ``"HalfAdder"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Number of clean ancillas + - Description + * - ``"ripple_cdkm"`` + - :class:`.HalfAdderSynthesisC04` + - 1 + - a ripple-carry adder + * - ``"ripple_vbe"`` + - :class:`.HalfAdderSynthesisV95` + - :math:`n-1`, for :math:`n`-bit numbers + - a ripple-carry adder + * - ``"qft"`` + - :class:`.HalfAdderSynthesisD00` + - 0 + - a QFT-based adder + +.. autosummary:: + :toctree: ../stubs/ + + HalfAdderSynthesisC04 + HalfAdderSynthesisD00 + HalfAdderSynthesisV95 + +Full Adder Synthesis +'''''''''''''''''''' + +.. list-table:: Plugins for :class:`.FullAdderGate` (key = ``"FullAdder"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Number of clean ancillas + - Description + * - ``"ripple_cdkm"`` + - :class:`.FullAdderSynthesisC04` + - 0 + - a ripple-carry adder + * - ``"ripple_vbe"`` + - :class:`.FullAdderSynthesisV95` + - :math:`n-1`, for :math:`n`-bit numbers + - a ripple-carry adder + +.. autosummary:: + :toctree: ../stubs/ + + FullAdderSynthesisC04 + FullAdderSynthesisV95 + + +Multiplier Synthesis +'''''''''''''''''''' + +.. list-table:: Plugins for :class:`.MultiplierGate` (key = ``"Multiplier"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Number of clean ancillas + - Description + * - ``"cumulative"`` + - :class:`.MultiplierSynthesisH18` + - depending on the :class:`.AdderGate` used + - a cumulative adder based on controlled adders + * - ``"qft"`` + - :class:`.MultiplierSynthesisR17` + - 0 + - a QFT-based multiplier + +.. autosummary:: + :toctree: ../stubs/ + + MultiplierSynthesisH18 + MultiplierSynthesisR17 + """ +from __future__ import annotations + +import warnings import numpy as np import rustworkx as rx from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.library import LinearFunction, QFTGate, MCXGate, C3XGate, C4XGate +from qiskit.circuit.library import ( + LinearFunction, + QFTGate, + MCXGate, + C3XGate, + C4XGate, + PauliEvolutionGate, + ModularAdderGate, + HalfAdderGate, + FullAdderGate, + MultiplierGate, +) from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.coupling import CouplingMap @@ -288,8 +441,16 @@ synth_mcx_1_clean_b95, synth_mcx_gray_code, synth_mcx_noaux_v24, + synth_mcmt_vchain, +) +from qiskit.synthesis.evolution import ProductFormula, synth_pauli_network_rustiq +from qiskit.synthesis.arithmetic import ( + adder_ripple_c04, + adder_qft_d00, + adder_ripple_v95, + multiplier_qft_r17, + multiplier_cumulative_h18, ) -from qiskit.synthesis.multi_controlled import synth_mcmt_vchain from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from .plugin import HighLevelSynthesisPlugin @@ -1029,3 +1190,369 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** high_level_object.num_target_qubits, ctrl_state, ) + + +class ModularAdderSynthesisDefault(HighLevelSynthesisPlugin): + """The default modular adder (no carry in, no carry out qubit) synthesis. + + This plugin name is:``ModularAdder.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + If at least one clean auxiliary qubit is available, the :class:`ModularAdderSynthesisC04` + is used, otherwise :class:`ModularAdderSynthesisD00`. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, ModularAdderGate): + return None + + if options.get("num_clean_ancillas", 0) >= 1: + return adder_ripple_c04(high_level_object.num_state_qubits, kind="fixed") + + return adder_qft_d00(high_level_object.num_state_qubits, kind="fixed") + + +class ModularAdderSynthesisC04(HighLevelSynthesisPlugin): + r"""A ripple-carry adder, modulo :math:`2^n`. + + This plugin name is:``ModularAdder.ripple_c04`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + This plugin requires at least one clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, ModularAdderGate): + return None + + # unless we implement the full adder, this implementation needs an ancilla qubit + if options.get("num_clean_ancillas", 0) < 1: + return None + + return adder_ripple_c04(high_level_object.num_state_qubits, kind="fixed") + + +class ModularAdderSynthesisV95(HighLevelSynthesisPlugin): + r"""A ripple-carry adder, modulo :math:`2^n`. + + This plugin name is:``ModularAdder.ripple_v95`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For an adder on 2 registers with :math:`n` qubits each, this plugin requires at + least :math:`n-1` clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, ModularAdderGate): + return None + + num_state_qubits = high_level_object.num_state_qubits + + # for more than 1 state qubit, we need an ancilla + if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + return None + + return adder_ripple_v95(num_state_qubits, kind="fixed") + + +class ModularAdderSynthesisD00(HighLevelSynthesisPlugin): + r"""A QFT-based adder, modulo :math:`2^n`. + + This plugin name is:``ModularAdder.qft_d00`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, ModularAdderGate): + return None + + return adder_qft_d00(high_level_object.num_state_qubits, kind="fixed") + + +class HalfAdderSynthesisDefault(HighLevelSynthesisPlugin): + r"""The default half-adder (no carry in, but a carry out qubit) synthesis. + + If we have an auxiliary qubit available, the Cuccaro ripple-carry adder uses + :math:`O(n)` CX gates and 1 auxiliary qubit, whereas the Vedral ripple-carry uses more CX + and :math:`n-1` auxiliary qubits. The QFT-based adder uses no auxiliary qubits, but + :math:`O(n^2)`, hence it is only used if no auxiliary qubits are available. + + This plugin name is:``HalfAdder.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + If at least one clean auxiliary qubit is available, the :class:`HalfAdderSynthesisC04` + is used, otherwise :class:`HalfAdderSynthesisD00`. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, HalfAdderGate): + return None + + if options.get("num_clean_ancillas", 0) >= 1: + return adder_ripple_c04(high_level_object.num_state_qubits, kind="half") + + return adder_qft_d00(high_level_object.num_state_qubits, kind="half") + + +class HalfAdderSynthesisC04(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-out bit. + + This plugin name is:``HalfAdder.ripple_c04`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + This plugin requires at least one clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, HalfAdderGate): + return None + + # unless we implement the full adder, this implementation needs an ancilla qubit + if options.get("num_clean_ancillas", 0) < 1: + return None + + return adder_ripple_c04(high_level_object.num_state_qubits, kind="half") + + +class HalfAdderSynthesisV95(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-out bit. + + This plugin name is:``HalfAdder.ripple_v95`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For an adder on 2 registers with :math:`n` qubits each, this plugin requires at + least :math:`n-1` clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, HalfAdderGate): + return None + + num_state_qubits = high_level_object.num_state_qubits + + # for more than 1 state qubit, we need an ancilla + if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + return None + + return adder_ripple_v95(num_state_qubits, kind="half") + + +class HalfAdderSynthesisD00(HighLevelSynthesisPlugin): + """A QFT-based adder with a carry-in and a carry-out bit. + + This plugin name is:``HalfAdder.qft_d00`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, HalfAdderGate): + return None + + return adder_qft_d00(high_level_object.num_state_qubits, kind="half") + + +class FullAdderSynthesisC04(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-in and a carry-out bit. + + This plugin name is:``FullAdder.ripple_c04`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + This plugin requires at least one clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, FullAdderGate): + return None + + return adder_ripple_c04(high_level_object.num_state_qubits, kind="full") + + +class FullAdderSynthesisV95(HighLevelSynthesisPlugin): + """A ripple-carry adder with a carry-in and a carry-out bit. + + This plugin name is:``FullAdder.ripple_v95`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + For an adder on 2 registers with :math:`n` qubits each, this plugin requires at + least :math:`n-1` clean auxiliary qubit. + + The plugin supports the following plugin-specific options: + + * ``num_clean_ancillas``: The number of clean auxiliary qubits available. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, FullAdderGate): + return None + + num_state_qubits = high_level_object.num_state_qubits + + # for more than 1 state qubit, we need an ancilla + if num_state_qubits > 1 > options.get("num_clean_ancillas", 1): + return None + + return adder_ripple_v95(num_state_qubits, kind="full") + + +class MultiplierSynthesisH18(HighLevelSynthesisPlugin): + """A cumulative multiplier based on controlled adders. + + This plugin name is:``Multiplier.cumulative_h18`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MultiplierGate): + return None + + return multiplier_cumulative_h18( + high_level_object.num_state_qubits, high_level_object.num_result_qubits + ) + + +class MultiplierSynthesisR17(HighLevelSynthesisPlugin): + """A QFT-based multiplier. + + This plugin name is:``Multiplier.qft_r17`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MultiplierGate): + return None + + return multiplier_qft_r17( + high_level_object.num_state_qubits, high_level_object.num_result_qubits + ) + + +class PauliEvolutionSynthesisDefault(HighLevelSynthesisPlugin): + """Synthesize a :class:`.PauliEvolutionGate` using the default synthesis algorithm. + + This plugin name is:``PauliEvolution.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The following plugin option can be set: + + * preserve_order: If ``False``, allow re-ordering the Pauli terms in the Hamiltonian to + reduce the circuit depth of the decomposition. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, PauliEvolutionGate): + # Don't do anything if a gate is called "evolution" but is not an + # actual PauliEvolutionGate + return None + + algo = high_level_object.synthesis + + if "preserve_order" in options and isinstance(algo, ProductFormula): + algo.preserve_order = options["preserve_order"] + + return algo.synthesize(high_level_object) + + +class PauliEvolutionSynthesisRustiq(HighLevelSynthesisPlugin): + """Synthesize a :class:`.PauliEvolutionGate` using Rustiq. + + This plugin name is :``PauliEvolution.rustiq`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The Rustiq synthesis algorithm is described in [1], and is implemented in + Rust-based quantum circuit synthesis library available at + https://github.com/smartiel/rustiq-core. + + On large circuits the plugin may take a significant runtime. + + The plugin supports the following additional options: + + * optimize_count (bool): if `True` the synthesis algorithm will try to optimize + the 2-qubit gate count; and if `False` then the 2-qubit depth. + * preserve_order (bool): whether the order of paulis should be preserved, up to + commutativity. + * upto_clifford (bool): if `True`, the final Clifford operator is not synthesized. + * upto_phase (bool): if `True`, the global phase of the returned circuit may + differ from the global phase of the given pauli network. + * resynth_clifford_method (int): describes the strategy to synthesize the final + Clifford operator. Allowed values are `0` (naive approach), `1` (qiskit + greedy synthesis), `2` (rustiq isometry synthesis). + + References: + 1. Timothée Goubault de Brugière and Simon Martiel, + *Faster and shorter synthesis of Hamiltonian simulation circuits*, + `arXiv:2404.03280 [quant-ph] `_ + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, PauliEvolutionGate): + # Don't do anything if a gate is called "evolution" but is not an + # actual PauliEvolutionGate + return None + + algo = high_level_object.synthesis + + if not isinstance(algo, ProductFormula): + warnings.warn( + "Cannot apply Rustiq if the evolution synthesis does not implement ``expand``. ", + stacklevel=2, + category=RuntimeWarning, + ) + return None + + if "preserve_order" in options: + algo.preserve_order = options["preserve_order"] + + num_qubits = high_level_object.num_qubits + pauli_network = algo.expand(high_level_object) + + optimize_count = options.get("optimize_count", True) + preserve_order = options.get("preserve_order", True) + upto_clifford = options.get("upto_clifford", False) + upto_phase = options.get("upto_phase", False) + resynth_clifford_method = options.get("resynth_clifford_method", 1) + + return synth_pauli_network_rustiq( + num_qubits=num_qubits, + pauli_network=pauli_network, + optimize_count=optimize_count, + preserve_order=preserve_order, + upto_clifford=upto_clifford, + upto_phase=upto_phase, + resynth_clifford_method=resynth_clifford_method, + ) diff --git a/qiskit/transpiler/passes/synthesis/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py deleted file mode 100644 index f3dd34b7df31..000000000000 --- a/qiskit/transpiler/passes/synthesis/qubit_tracker.py +++ /dev/null @@ -1,132 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""A qubit state tracker for synthesizing operations with auxiliary qubits.""" - -from __future__ import annotations -from collections.abc import Iterable -from dataclasses import dataclass - - -@dataclass -class QubitTracker: - """Track qubits (by global index) and their state. - - The states are distinguished into clean (meaning in state :math:`|0\rangle`) or dirty (an - unknown state). - """ - - # This could in future be extended to track different state types, if necessary. - # However, using sets of integers here is much faster than e.g. storing a dictionary with - # {index: state} entries. - qubits: tuple[int] - clean: set[int] - dirty: set[int] - - def num_clean(self, active_qubits: Iterable[int] | None = None): - """Return the number of clean qubits, not considering the active qubits.""" - # this could be cached if getting the set length becomes a performance bottleneck - return len(self.clean.difference(active_qubits or set())) - - def num_dirty(self, active_qubits: Iterable[int] | None = None): - """Return the number of dirty qubits, not considering the active qubits.""" - return len(self.dirty.difference(active_qubits or set())) - - def borrow(self, num_qubits: int, active_qubits: Iterable[int] | None = None) -> list[int]: - """Get ``num_qubits`` qubits, excluding ``active_qubits``.""" - active_qubits = set(active_qubits or []) - available_qubits = [qubit for qubit in self.qubits if qubit not in active_qubits] - - if num_qubits > (available := len(available_qubits)): - raise RuntimeError(f"Cannot borrow {num_qubits} qubits, only {available} available.") - - # for now, prioritize returning clean qubits - available_clean = [qubit for qubit in available_qubits if qubit in self.clean] - available_dirty = [qubit for qubit in available_qubits if qubit in self.dirty] - - borrowed = available_clean[:num_qubits] - return borrowed + available_dirty[: (num_qubits - len(borrowed))] - - def used(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to used (i.e. False).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean -= qubits - self.dirty |= qubits - - def reset(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to 0 (i.e. True).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean |= qubits - self.dirty -= qubits - - def drop(self, qubits: Iterable[int], check: bool = True) -> None: - """Drop qubits from the tracker, meaning that they are no longer available.""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Dropping untracked qubits: {untracked}. Tracker: {self}") - - self.qubits = tuple(qubit for qubit in self.qubits if qubit not in qubits) - self.clean -= qubits - self.dirty -= qubits - - def copy( - self, qubit_map: dict[int, int] | None = None, drop: Iterable[int] | None = None - ) -> "QubitTracker": - """Copy self. - - Args: - qubit_map: If provided, apply the mapping ``{old_qubit: new_qubit}`` to - the qubits in the tracker. Only those old qubits in the mapping will be - part of the new one. - drop: If provided, drop these qubits in the copied tracker. This argument is ignored - if ``qubit_map`` is given, since the qubits can then just be dropped in the map. - """ - if qubit_map is None and drop is not None: - remaining_qubits = [qubit for qubit in self.qubits if qubit not in drop] - qubit_map = dict(zip(remaining_qubits, remaining_qubits)) - - if qubit_map is None: - clean = self.clean.copy() - dirty = self.dirty.copy() - qubits = self.qubits # tuple is immutable, no need to copy - else: - clean, dirty = set(), set() - for old_index, new_index in qubit_map.items(): - if old_index in self.clean: - clean.add(new_index) - elif old_index in self.dirty: - dirty.add(new_index) - else: - raise ValueError(f"Unknown old qubit index: {old_index}. Tracker: {self}") - - qubits = tuple(qubit_map.values()) - - return QubitTracker(qubits, clean=clean, dirty=dirty) - - def __str__(self) -> str: - return ( - f"QubitTracker({len(self.qubits)}, clean: {self.num_clean()}, dirty: {self.num_dirty()})" - + f"\n\tclean: {self.clean}" - + f"\n\tdirty: {self.dirty}" - ) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 883dbb6c4d68..a2bd044c7341 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -457,37 +457,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: default_kwargs = {} method_list = [(plugin_method, plugin_kwargs), (default_method, default_kwargs)] - for method, kwargs in method_list: - if method.supports_basis_gates: - kwargs["basis_gates"] = self._basis_gates - if method.supports_natural_direction: - kwargs["natural_direction"] = self._natural_direction - if method.supports_pulse_optimize: - kwargs["pulse_optimize"] = self._pulse_optimize - if method.supports_gate_lengths: - _gate_lengths = _gate_lengths or _build_gate_lengths( - self._backend_props, self._target - ) - kwargs["gate_lengths"] = _gate_lengths - if method.supports_gate_errors: - _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) - kwargs["gate_errors"] = _gate_errors - if method.supports_gate_lengths_by_qubit: - _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( - self._backend_props, self._target - ) - kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit - if method.supports_gate_errors_by_qubit: - _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( - self._backend_props, self._target - ) - kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit - supported_bases = method.supported_bases - if supported_bases is not None: - kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) - if method.supports_target: - kwargs["target"] = self._target - # Handle approximation degree as a special case for backwards compatibility, it's # not part of the plugin interface and only something needed for the default # pass. @@ -503,22 +472,55 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and isinstance(kwargs["target"], Target): + if self.method == "default" and self._target is not None: _coupling_edges = ( - list(self._coupling_map.get_edges()) if self._coupling_map is not None else [] + set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) out = run_default_main_loop( dag, list(qubit_indices.values()), self._min_qubits, - kwargs["target"], + self._target, _coupling_edges, self._approximation_degree, - kwargs["natural_direction"], + self._natural_direction, ) return out else: + for method, kwargs in method_list: + if method.supports_basis_gates: + kwargs["basis_gates"] = self._basis_gates + if method.supports_natural_direction: + kwargs["natural_direction"] = self._natural_direction + if method.supports_pulse_optimize: + kwargs["pulse_optimize"] = self._pulse_optimize + if method.supports_gate_lengths: + _gate_lengths = _gate_lengths or _build_gate_lengths( + self._backend_props, self._target + ) + kwargs["gate_lengths"] = _gate_lengths + if method.supports_gate_errors: + _gate_errors = _gate_errors or _build_gate_errors( + self._backend_props, self._target + ) + kwargs["gate_errors"] = _gate_errors + if method.supports_gate_lengths_by_qubit: + _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit + if method.supports_gate_errors_by_qubit: + _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit + supported_bases = method.supported_bases + if supported_bases is not None: + kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) + if method.supports_target: + kwargs["target"] = self._target + out = self._run_main_loop( dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs ) diff --git a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py index a73f9690ae0e..52f56aebbace 100644 --- a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py +++ b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py @@ -22,6 +22,7 @@ ) from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass +from qiskit.utils import deprecate_func class ConvertConditionsToIfOps(TransformationPass): @@ -31,6 +32,10 @@ class ConvertConditionsToIfOps(TransformationPass): This is a simple pass aimed at easing the conversion from the old style of using :meth:`.InstructionSet.c_if` into the new style of using more complex conditional logic.""" + @deprecate_func(since="1.3.0", removal_timeline="in Qiskit 2.0.0") + def __init__(self): + super().__init__() + def _run_inner(self, dag): """Run the pass on one :class:`.DAGCircuit`, mutating it. Returns ``True`` if the circuit was modified and ``False`` if not.""" diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 198eb34c501f..fbdcc9321cbb 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -10,40 +10,17 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Rearrange the direction of the cx nodes to match the directed coupling map.""" - -from math import pi +"""Rearrange the direction of the 2-qubit gate nodes to match the directed coupling map.""" from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError - -from qiskit.converters import dag_to_circuit, circuit_to_dag -from qiskit.circuit import QuantumRegister, ControlFlowOp -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.circuit.library.standard_gates import ( - SGate, - SdgGate, - SXGate, - HGate, - CXGate, - CZGate, - ECRGate, - RXXGate, - RYYGate, - RZZGate, - RZXGate, - SwapGate, -) - - -def _swap_node_qargs(node): - return DAGOpNode(node.op, node.qargs[::-1], node.cargs) +from qiskit._accelerate.gate_direction import fix_gate_direction_coupling, fix_gate_direction_target class GateDirection(TransformationPass): """Modify asymmetric gates to match the hardware coupling direction. - This pass makes use of the following identities:: + This pass supports replacements for the `cx`, `cz`, `ecr`, `swap`, `rzx`, `rxx`, `ryy` and + `rzz` gates, using the following identities:: ┌───┐┌───┐┌───┐ q_0: ──■── q_0: ┤ H ├┤ X ├┤ H ├ @@ -58,6 +35,9 @@ class GateDirection(TransformationPass): │ ECR │ = ┌┴───┴┐├────┤└┬───┬┘│ Ecr │├───┤ q_1: ┤1 ├ q_1: ┤ Sdg ├┤ √X ├─┤ S ├─┤0 ├┤ H ├ └──────┘ └─────┘└────┘ └───┘ └──────┘└───┘ + Note: This is done in terms of less-efficient S/SX/Sdg gates instead of the more natural + `RY(pi /2)` so we have a chance for basis translation to keep things in a discrete basis + during resynthesis, if that's what's being asked for. ┌──────┐ ┌───┐┌──────┐┌───┐ @@ -66,18 +46,18 @@ class GateDirection(TransformationPass): q_1: ┤1 ├ q_1: ┤ H ├┤0 ├┤ H ├ └──────┘ └───┘└──────┘└───┘ + cz, swap, rxx, ryy and rzz directions are fixed by reversing their qargs order. + This pass assumes that the positions of the qubits in the :attr:`.DAGCircuit.qubits` attribute are the physical qubit indices. For example if ``dag.qubits[0]`` is qubit 0 in the :class:`.CouplingMap` or :class:`.Target`. """ - _KNOWN_REPLACEMENTS = frozenset(["cx", "cz", "ecr", "swap", "rzx", "rxx", "ryy", "rzz"]) - def __init__(self, coupling_map, target=None): """GateDirection pass. Args: - coupling_map (CouplingMap): Directed graph represented a coupling map. + coupling_map (CouplingMap): Directed graph representing a coupling map. target (Target): The backend target to use for this pass. If this is specified it will be used instead of the coupling map """ @@ -85,248 +65,6 @@ def __init__(self, coupling_map, target=None): self.coupling_map = coupling_map self.target = target - # Create the replacement dag and associated register. - self._cx_dag = DAGCircuit() - qr = QuantumRegister(2) - self._cx_dag.add_qreg(qr) - self._cx_dag.apply_operation_back(HGate(), [qr[0]], []) - self._cx_dag.apply_operation_back(HGate(), [qr[1]], []) - self._cx_dag.apply_operation_back(CXGate(), [qr[1], qr[0]], []) - self._cx_dag.apply_operation_back(HGate(), [qr[0]], []) - self._cx_dag.apply_operation_back(HGate(), [qr[1]], []) - - # This is done in terms of less-efficient S/SX/Sdg gates instead of the more natural - # `RY(pi /2)` so we have a chance for basis translation to keep things in a discrete basis - # during resynthesis, if that's what's being asked for. - self._ecr_dag = DAGCircuit() - qr = QuantumRegister(2) - self._ecr_dag.global_phase = -pi / 2 - self._ecr_dag.add_qreg(qr) - self._ecr_dag.apply_operation_back(SGate(), [qr[0]], []) - self._ecr_dag.apply_operation_back(SXGate(), [qr[0]], []) - self._ecr_dag.apply_operation_back(SdgGate(), [qr[0]], []) - self._ecr_dag.apply_operation_back(SdgGate(), [qr[1]], []) - self._ecr_dag.apply_operation_back(SXGate(), [qr[1]], []) - self._ecr_dag.apply_operation_back(SGate(), [qr[1]], []) - self._ecr_dag.apply_operation_back(ECRGate(), [qr[1], qr[0]], []) - self._ecr_dag.apply_operation_back(HGate(), [qr[0]], []) - self._ecr_dag.apply_operation_back(HGate(), [qr[1]], []) - - self._cz_dag = DAGCircuit() - qr = QuantumRegister(2) - self._cz_dag.add_qreg(qr) - self._cz_dag.apply_operation_back(CZGate(), [qr[1], qr[0]], []) - - self._swap_dag = DAGCircuit() - qr = QuantumRegister(2) - self._swap_dag.add_qreg(qr) - self._swap_dag.apply_operation_back(SwapGate(), [qr[1], qr[0]], []) - - # If adding more replacements (either static or dynamic), also update the class variable - # `_KNOWN_REPLACMENTS` to include them in the error messages. - self._static_replacements = { - "cx": self._cx_dag, - "cz": self._cz_dag, - "ecr": self._ecr_dag, - "swap": self._swap_dag, - } - - @staticmethod - def _rzx_dag(parameter): - _rzx_dag = DAGCircuit() - qr = QuantumRegister(2) - _rzx_dag.add_qreg(qr) - _rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - _rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - _rzx_dag.apply_operation_back(RZXGate(parameter), [qr[1], qr[0]], []) - _rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - _rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - return _rzx_dag - - @staticmethod - def _rxx_dag(parameter): - _rxx_dag = DAGCircuit() - qr = QuantumRegister(2) - _rxx_dag.add_qreg(qr) - _rxx_dag.apply_operation_back(RXXGate(parameter), [qr[1], qr[0]], []) - return _rxx_dag - - @staticmethod - def _ryy_dag(parameter): - _ryy_dag = DAGCircuit() - qr = QuantumRegister(2) - _ryy_dag.add_qreg(qr) - _ryy_dag.apply_operation_back(RYYGate(parameter), [qr[1], qr[0]], []) - return _ryy_dag - - @staticmethod - def _rzz_dag(parameter): - _rzz_dag = DAGCircuit() - qr = QuantumRegister(2) - _rzz_dag.add_qreg(qr) - _rzz_dag.apply_operation_back(RZZGate(parameter), [qr[1], qr[0]], []) - return _rzz_dag - - def _run_coupling_map(self, dag, wire_map, edges=None): - if edges is None: - edges = set(self.coupling_map.get_edges()) - if not edges: - return dag - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): - new_op = node.op.replace_blocks( - dag_to_circuit( - self._run_coupling_map( - circuit_to_dag(block), - { - inner: wire_map[outer] - for outer, inner in zip(node.qargs, block.qubits) - }, - edges, - ) - ) - for block in node.op.blocks - ) - dag.substitute_node(node, new_op, propagate_condition=False) - continue - if len(node.qargs) != 2: - continue - if dag._has_calibration_for(node): - continue - qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) - if qargs not in edges and (qargs[1], qargs[0]) not in edges: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - ) - if qargs not in edges: - replacement = self._static_replacements.get(node.name) - if replacement is not None: - dag.substitute_node_with_dag(node, replacement) - elif node.name == "rzx": - dag.substitute_node_with_dag(node, self._rzx_dag(*node.op.params)) - elif node.name == "rxx": - dag.substitute_node_with_dag(node, self._rxx_dag(*node.op.params)) - elif node.name == "ryy": - dag.substitute_node_with_dag(node, self._ryy_dag(*node.op.params)) - elif node.name == "rzz": - dag.substitute_node_with_dag(node, self._rzz_dag(*node.op.params)) - else: - raise TranspilerError( - f"'{node.name}' would be supported on '{qargs}' if the direction were" - f" swapped, but no rules are known to do that." - f" {list(self._KNOWN_REPLACEMENTS)} can be automatically flipped." - ) - return dag - - def _run_target(self, dag, wire_map): - # Don't include directives to avoid things like barrier, which are assumed always supported. - for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): - new_op = node.op.replace_blocks( - dag_to_circuit( - self._run_target( - circuit_to_dag(block), - { - inner: wire_map[outer] - for outer, inner in zip(node.qargs, block.qubits) - }, - ) - ) - for block in node.op.blocks - ) - dag.substitute_node(node, new_op, propagate_condition=False) - continue - if len(node.qargs) != 2: - continue - if dag._has_calibration_for(node): - continue - qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) - swapped = (qargs[1], qargs[0]) - if node.name in self._static_replacements: - if self.target.instruction_supported(node.name, qargs): - continue - if self.target.instruction_supported(node.name, swapped): - dag.substitute_node_with_dag(node, self._static_replacements[node.name]) - else: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - f" for {node.name}" - ) - elif node.name == "rzx": - if self.target.instruction_supported( - qargs=qargs, operation_class=RZXGate, parameters=node.op.params - ): - continue - if self.target.instruction_supported( - qargs=swapped, operation_class=RZXGate, parameters=node.op.params - ): - dag.substitute_node_with_dag(node, self._rzx_dag(*node.op.params)) - else: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - f" for {node.name}" - ) - elif node.name == "rxx": - if self.target.instruction_supported( - qargs=qargs, operation_class=RXXGate, parameters=node.op.params - ): - continue - if self.target.instruction_supported( - qargs=swapped, operation_class=RXXGate, parameters=node.op.params - ): - dag.substitute_node_with_dag(node, self._rxx_dag(*node.op.params)) - else: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - f" for {node.name}" - ) - elif node.name == "ryy": - if self.target.instruction_supported( - qargs=qargs, operation_class=RYYGate, parameters=node.op.params - ): - continue - if self.target.instruction_supported( - qargs=swapped, operation_class=RYYGate, parameters=node.op.params - ): - dag.substitute_node_with_dag(node, self._ryy_dag(*node.op.params)) - else: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - f" for {node.name}" - ) - elif node.name == "rzz": - if self.target.instruction_supported( - qargs=qargs, operation_class=RZZGate, parameters=node.op.params - ): - continue - if self.target.instruction_supported( - qargs=swapped, operation_class=RZZGate, parameters=node.op.params - ): - dag.substitute_node_with_dag(node, self._rzz_dag(*node.op.params)) - else: - raise TranspilerError( - f"The circuit requires a connection between physical qubits {qargs}" - f" for {node.name}" - ) - elif self.target.instruction_supported(node.name, qargs): - continue - elif self.target.instruction_supported(node.name, swapped) or dag._has_calibration_for( - _swap_node_qargs(node) - ): - raise TranspilerError( - f"'{node.name}' would be supported on '{qargs}' if the direction were" - f" swapped, but no rules are known to do that." - f" {list(self._KNOWN_REPLACEMENTS)} can be automatically flipped." - ) - else: - raise TranspilerError( - f"'{node.name}' with parameters '{node.op.params}' is not supported on qubits" - f" '{qargs}' in either direction." - ) - - return dag - def run(self, dag): """Run the GateDirection pass on `dag`. @@ -343,7 +81,6 @@ def run(self, dag): TranspilerError: If the circuit cannot be mapped just by flipping the cx nodes. """ - layout_map = {bit: i for i, bit in enumerate(dag.qubits)} if self.target is None: - return self._run_coupling_map(dag, layout_map) - return self._run_target(dag, layout_map) + return fix_gate_direction_coupling(dag, set(self.coupling_map.get_edges())) + return fix_gate_direction_target(dag, self.target) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index baf4482a3b05..52f3d65449e5 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -17,11 +17,13 @@ from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg class PassManagerConfig: """Pass Manager Configuration.""" + @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def __init__( self, initial_layout=None, diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 9301588c0744..85b0c05d5cae 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -40,9 +40,9 @@ from qiskit.transpiler.passes.optimization import ( Optimize1qGatesDecomposition, CommutativeCancellation, - Collect2qBlocks, ConsolidateBlocks, InverseCancellation, + RemoveIdentityEquivalent, ) from qiskit.transpiler.passes import Depth, Size, FixedPoint, MinimumPoint from qiskit.transpiler.passes.utils.gates_basis import GatesInBasis @@ -157,6 +157,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana if pass_manager_config.routing_method != "none": init.append(ElidePermutations()) init.append(RemoveDiagonalGatesBeforeMeasure()) + # Target not set on RemoveIdentityEquivalent because we haven't applied a Layout + # yet so doing anything relative to an error rate in the target is not valid. + init.append( + RemoveIdentityEquivalent( + approximation_degree=pass_manager_config.approximation_degree + ) + ) init.append( InverseCancellation( [ @@ -176,7 +183,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) # If approximation degree is None that indicates a request to approximate up to the # error rates in the target. However, in the init stage we don't yet know the target @@ -582,6 +588,10 @@ def _opt_control(property_set): elif optimization_level == 2: _opt = [ + RemoveIdentityEquivalent( + approximation_degree=pass_manager_config.approximation_degree, + target=pass_manager_config.target, + ), Optimize1qGatesDecomposition( basis=pass_manager_config.basis_gates, target=pass_manager_config.target ), @@ -590,7 +600,6 @@ def _opt_control(property_set): elif optimization_level == 3: # Steps for optimization level 3 _opt = [ - Collect2qBlocks(), ConsolidateBlocks( basis_gates=pass_manager_config.basis_gates, target=pass_manager_config.target, @@ -605,6 +614,10 @@ def _opt_control(property_set): plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, ), + RemoveIdentityEquivalent( + approximation_degree=pass_manager_config.approximation_degree, + target=pass_manager_config.target, + ), Optimize1qGatesDecomposition( basis=pass_manager_config.basis_gates, target=pass_manager_config.target ), @@ -634,7 +647,6 @@ def _unroll_condition(property_set): elif optimization_level == 2: optimization.append( [ - Collect2qBlocks(), ConsolidateBlocks( basis_gates=pass_manager_config.basis_gates, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 25d21880bd23..c9bcc9a7904c 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -24,9 +24,9 @@ from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import BasisTranslator from qiskit.transpiler.passes import Unroll3qOrMore -from qiskit.transpiler.passes import Collect2qBlocks -from qiskit.transpiler.passes import Collect1qRuns from qiskit.transpiler.passes import ConsolidateBlocks +from qiskit.transpiler.passes import Collect1qRuns +from qiskit.transpiler.passes import Collect2qBlocks from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes import HighLevelSynthesis from qiskit.transpiler.passes import CheckMap diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 779a3512faab..96366e14af42 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -17,7 +17,13 @@ import copy import warnings -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit.circuit.controlflow import ( + CONTROL_FLOW_OP_NAMES, + IfElseOp, + WhileLoopOp, + ForLoopOp, + SwitchCaseOp, +) from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping from qiskit.circuit.quantumregister import Qubit from qiskit.providers.backend import Backend @@ -29,6 +35,7 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.target import Target, target_to_backend_properties from qiskit.transpiler.timing_constraints import TimingConstraints +from qiskit.utils import deprecate_arg from qiskit.utils.deprecate_pulse import deprecate_pulse_arg from .level0 import level_0_pass_manager @@ -37,6 +44,32 @@ from .level3 import level_3_pass_manager +@deprecate_arg( + name="instruction_durations", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined instruction durations with " + "`Target.from_configuration(..., instruction_durations=...)`", +) +@deprecate_arg( + name="timing_constraints", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined timing constraints with " + "`Target.from_configuration(..., timing_constraints=...)`", +) +@deprecate_arg( + name="backend_properties", + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The `target` parameter should be used instead. You can build a `Target` instance " + "with defined properties with Target.from_configuration(..., backend_properties=...)", +) @deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_preset_pass_manager( optimization_level=2, @@ -285,6 +318,7 @@ def generate_preset_pass_manager( _skip_target = ( target is None and backend is None + # Note: instruction_durations is deprecated and will be removed in 2.0 (no need for alternative) and (basis_gates is None or coupling_map is None or instruction_durations is not None) ) @@ -309,23 +343,31 @@ def generate_preset_pass_manager( # 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, - ) + with warnings.catch_warnings(): + # TODO: inst_map will be removed in 2.0 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``inst_map`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + # 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. @@ -385,30 +427,41 @@ def generate_preset_pass_manager( "qubits_initially_zero": qubits_initially_zero, } - if backend is not None: - pm_options["_skip_target"] = _skip_target - pm_config = PassManagerConfig.from_backend(backend, **pm_options) - else: - pm_config = PassManagerConfig(**pm_options) - if optimization_level == 0: - pm = level_0_pass_manager(pm_config) - elif optimization_level == 1: - pm = level_1_pass_manager(pm_config) - elif optimization_level == 2: - pm = level_2_pass_manager(pm_config) - elif optimization_level == 3: - pm = level_3_pass_manager(pm_config) - else: - raise ValueError(f"Invalid optimization level {optimization_level}") + with warnings.catch_warnings(): + # inst_map is deprecated in the PassManagerConfig initializer + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*argument ``inst_map`` is deprecated as of Qiskit 1.3", + ) + if backend is not None: + pm_options["_skip_target"] = _skip_target + pm_config = PassManagerConfig.from_backend(backend, **pm_options) + else: + pm_config = PassManagerConfig(**pm_options) + if optimization_level == 0: + pm = level_0_pass_manager(pm_config) + elif optimization_level == 1: + pm = level_1_pass_manager(pm_config) + elif optimization_level == 2: + pm = level_2_pass_manager(pm_config) + elif optimization_level == 3: + pm = level_3_pass_manager(pm_config) + else: + raise ValueError(f"Invalid optimization level {optimization_level}") 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 + # Add control flow gates by default to basis set and name mapping default_gates = {"measure", "delay", "reset"}.union(CONTROL_FLOW_OP_NAMES) - + name_mapping = { + "if_else": IfElseOp, + "while_loop": WhileLoopOp, + "for_loop": ForLoopOp, + "switch_case": SwitchCaseOp, + } try: instructions = set(basis_gates) for name in default_gates: @@ -423,7 +476,16 @@ def _parse_basis_gates(basis_gates, backend, inst_map, skip_target): return None, name_mapping, skip_target for inst in instructions: - if inst not in standard_gates or inst not in default_gates: + if inst not in standard_gates and inst not in default_gates: + warnings.warn( + category=DeprecationWarning, + message=f"Providing non-standard gates ({inst}) through the ``basis_gates`` " + "argument is deprecated for both ``transpile`` and ``generate_preset_pass_manager`` " + "as of Qiskit 1.3.0. " + "It will be removed in Qiskit 2.0. The ``target`` parameter should be used instead. " + "You can build a target instance using ``Target.from_configuration()`` and provide " + "custom gate definitions with the ``custom_name_mapping`` argument.", + ) skip_target = True break @@ -436,7 +498,18 @@ def _parse_basis_gates(basis_gates, backend, inst_map, skip_target): # Check for custom instructions before removing calibrations for inst in instructions: - if inst not in standard_gates or inst not in default_gates: + if inst not in standard_gates and inst not in default_gates: + if inst not in backend.operation_names: + # do not raise warning when the custom instruction comes from the backend + # (common case with BasicSimulator) + warnings.warn( + category=DeprecationWarning, + message="Providing custom gates through the ``basis_gates`` argument is deprecated " + "for both ``transpile`` and ``generate_preset_pass_manager`` as of Qiskit 1.3.0. " + "It will be removed in Qiskit 2.0. The ``target`` parameter should be used instead. " + "You can build a target instance using ``Target.from_configuration()`` and provide" + "custom gate definitions with the ``custom_name_mapping`` argument.", + ) skip_target = True break @@ -461,7 +534,15 @@ def _parse_inst_map(inst_map, backend): 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) + with warnings.catch_warnings(): + # filter target_to_backend_properties warning + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``qiskit.transpiler.target.target_to_backend_properties\\(\\)``.*", + module="qiskit", + ) + backend_properties = target_to_backend_properties(backend.target) return backend_properties diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index 496581698fd4..ac3b2b618cc9 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -145,7 +145,7 @@ def pass_manager(self, pass_manager_config, optimization_level): an ``entry-points`` table in ``pyproject.toml`` for the plugin package with the necessary entry points under the appropriate namespace for the stage your plugin is for. You can see the list of stages, entry points, and expectations from the stage in :ref:`stage_table`. For example, -continuing from the example plugin above:: +continuing from the example plugin above: .. code-block:: toml diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 1edc89013572..fb15a8039d3c 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -1156,9 +1156,16 @@ def from_configuration( if error is None and duration is None and calibration is None: gate_properties[(qubit,)] = None else: - gate_properties[(qubit,)] = InstructionProperties( - duration=duration, error=error, calibration=calibration - ) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``calibration`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + gate_properties[(qubit,)] = InstructionProperties( + duration=duration, error=error, calibration=calibration + ) target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate) edges = list(coupling_map.get_edges()) for gate in two_qubit_gates: @@ -1198,9 +1205,16 @@ def from_configuration( if error is None and duration is None and calibration is None: gate_properties[edge] = None else: - gate_properties[edge] = InstructionProperties( - duration=duration, error=error, calibration=calibration - ) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".*``calibration`` is deprecated as of Qiskit 1.3.*", + module="qiskit", + ) + gate_properties[edge] = InstructionProperties( + duration=duration, error=error, calibration=calibration + ) target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate) for gate in global_ideal_variable_width_gates: target.add_instruction(name_mapping[gate], name=gate) diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index d933d38b5c4a..0aa6e7c10188 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -503,7 +503,7 @@ def _get_gate_span(qubits, node): # type of op must be the only op in the layer if isinstance(node.op, ControlFlowOp): span = qubits - elif node.cargs or getattr(node.op, "condition", None): + elif node.cargs or getattr(node, "condition", None): span = qubits[min_index : len(qubits)] else: span = qubits[min_index : max_index + 1] @@ -582,7 +582,7 @@ def slide_from_left(self, node, index): curr_index = index last_insertable_index = -1 index_stop = -1 - if (condition := getattr(node.op, "condition", None)) is not None: + if (condition := getattr(node, "condition", None)) is not None: index_stop = max( (self.measure_map[bit] for bit in condition_resources(condition).clbits), default=index_stop, diff --git a/releasenotes/notes/add-generate-preset-pm-global-import-efb12f185f3f738b.yaml b/releasenotes/notes/1.2/add-generate-preset-pm-global-import-efb12f185f3f738b.yaml similarity index 100% rename from releasenotes/notes/add-generate-preset-pm-global-import-efb12f185f3f738b.yaml rename to releasenotes/notes/1.2/add-generate-preset-pm-global-import-efb12f185f3f738b.yaml diff --git a/releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml b/releasenotes/notes/1.2/add-qft-gate-fd4e08f6721a9da4.yaml similarity index 100% rename from releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml rename to releasenotes/notes/1.2/add-qft-gate-fd4e08f6721a9da4.yaml diff --git a/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml b/releasenotes/notes/1.2/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml similarity index 100% rename from releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml rename to releasenotes/notes/1.2/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml diff --git a/releasenotes/notes/adjust-neato-settings-3adcc0ae9e245ce9.yaml b/releasenotes/notes/1.2/adjust-neato-settings-3adcc0ae9e245ce9.yaml similarity index 100% rename from releasenotes/notes/adjust-neato-settings-3adcc0ae9e245ce9.yaml rename to releasenotes/notes/1.2/adjust-neato-settings-3adcc0ae9e245ce9.yaml diff --git a/releasenotes/notes/annotated-params-116288d5628f7ee8.yaml b/releasenotes/notes/1.2/annotated-params-116288d5628f7ee8.yaml similarity index 100% rename from releasenotes/notes/annotated-params-116288d5628f7ee8.yaml rename to releasenotes/notes/1.2/annotated-params-116288d5628f7ee8.yaml diff --git a/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml b/releasenotes/notes/1.2/avoid-op-creation-804c0bed6c408911.yaml similarity index 100% rename from releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml rename to releasenotes/notes/1.2/avoid-op-creation-804c0bed6c408911.yaml diff --git a/releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml b/releasenotes/notes/1.2/backendv1-d0d0642ed38fed3c.yaml similarity index 100% rename from releasenotes/notes/backendv1-d0d0642ed38fed3c.yaml rename to releasenotes/notes/1.2/backendv1-d0d0642ed38fed3c.yaml diff --git a/releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml b/releasenotes/notes/1.2/bitarray-postselect-659b8f7801ccaa60.yaml similarity index 100% rename from releasenotes/notes/bitarray-postselect-659b8f7801ccaa60.yaml rename to releasenotes/notes/1.2/bitarray-postselect-659b8f7801ccaa60.yaml diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/1.2/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml similarity index 100% rename from releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml rename to releasenotes/notes/1.2/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml diff --git a/releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml b/releasenotes/notes/1.2/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml similarity index 100% rename from releasenotes/notes/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml rename to releasenotes/notes/1.2/default-level-2-generate-preset-passmanager-ec758ddc896ae2d6.yaml diff --git a/releasenotes/notes/deprecate-circuit-internal-helpers-ee65fbac455de47c.yaml b/releasenotes/notes/1.2/deprecate-circuit-internal-helpers-ee65fbac455de47c.yaml similarity index 100% rename from releasenotes/notes/deprecate-circuit-internal-helpers-ee65fbac455de47c.yaml rename to releasenotes/notes/1.2/deprecate-circuit-internal-helpers-ee65fbac455de47c.yaml diff --git a/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml b/releasenotes/notes/1.2/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml similarity index 100% rename from releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml rename to releasenotes/notes/1.2/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml diff --git a/releasenotes/notes/deprecate-primitives-v1.yaml b/releasenotes/notes/1.2/deprecate-primitives-v1.yaml similarity index 100% rename from releasenotes/notes/deprecate-primitives-v1.yaml rename to releasenotes/notes/1.2/deprecate-primitives-v1.yaml diff --git a/releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml b/releasenotes/notes/1.2/deprecate-visualize_transition-8c1d257b7f37aa58.yaml similarity index 100% rename from releasenotes/notes/deprecate-visualize_transition-8c1d257b7f37aa58.yaml rename to releasenotes/notes/1.2/deprecate-visualize_transition-8c1d257b7f37aa58.yaml diff --git a/releasenotes/notes/deprecate_assemble-67486b4d0a8d4f96.yaml b/releasenotes/notes/1.2/deprecate_assemble-67486b4d0a8d4f96.yaml similarity index 100% rename from releasenotes/notes/deprecate_assemble-67486b4d0a8d4f96.yaml rename to releasenotes/notes/1.2/deprecate_assemble-67486b4d0a8d4f96.yaml diff --git a/releasenotes/notes/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml b/releasenotes/notes/1.2/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml similarity index 100% rename from releasenotes/notes/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml rename to releasenotes/notes/1.2/extract-standard-parametric-controlled-1a495f6f7ce89397.yaml diff --git a/releasenotes/notes/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml b/releasenotes/notes/1.2/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml similarity index 100% rename from releasenotes/notes/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml rename to releasenotes/notes/1.2/fix-2q-basis-decomposer-non-std-kak-gate-edc69ffb5d9ef302.yaml diff --git a/releasenotes/notes/fix-InstructionDurations-b47a9770b424d7a0.yaml b/releasenotes/notes/1.2/fix-InstructionDurations-b47a9770b424d7a0.yaml similarity index 100% rename from releasenotes/notes/fix-InstructionDurations-b47a9770b424d7a0.yaml rename to releasenotes/notes/1.2/fix-InstructionDurations-b47a9770b424d7a0.yaml diff --git a/releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml b/releasenotes/notes/1.2/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml similarity index 100% rename from releasenotes/notes/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml rename to releasenotes/notes/1.2/fix-bitarray-fromcounts-nobits-82958a596b3489ec.yaml diff --git a/releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml b/releasenotes/notes/1.2/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml similarity index 100% rename from releasenotes/notes/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml rename to releasenotes/notes/1.2/fix-bitarray-slice-bits-shots-c9cb7e5d907722f5.yaml diff --git a/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml b/releasenotes/notes/1.2/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml similarity index 100% rename from releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml rename to releasenotes/notes/1.2/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml diff --git a/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml b/releasenotes/notes/1.2/fix-cu-rust-6464b6893ecca1b3.yaml similarity index 100% rename from releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml rename to releasenotes/notes/1.2/fix-cu-rust-6464b6893ecca1b3.yaml diff --git a/releasenotes/notes/fix-dd-misalignment-msg-76fe16e5eb4ae670.yaml b/releasenotes/notes/1.2/fix-dd-misalignment-msg-76fe16e5eb4ae670.yaml similarity index 100% rename from releasenotes/notes/fix-dd-misalignment-msg-76fe16e5eb4ae670.yaml rename to releasenotes/notes/1.2/fix-dd-misalignment-msg-76fe16e5eb4ae670.yaml diff --git a/releasenotes/notes/fix-elide-permutations-1b9e1d10c3abb2a4.yaml b/releasenotes/notes/1.2/fix-elide-permutations-1b9e1d10c3abb2a4.yaml similarity index 100% rename from releasenotes/notes/fix-elide-permutations-1b9e1d10c3abb2a4.yaml rename to releasenotes/notes/1.2/fix-elide-permutations-1b9e1d10c3abb2a4.yaml diff --git a/releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml b/releasenotes/notes/1.2/fix-hoare-opt-56d1ca6a07f07a2d.yaml similarity index 100% rename from releasenotes/notes/fix-hoare-opt-56d1ca6a07f07a2d.yaml rename to releasenotes/notes/1.2/fix-hoare-opt-56d1ca6a07f07a2d.yaml diff --git a/releasenotes/notes/fix-kwarg-validation-BitArray-1bf542a1fb5c15c6.yaml b/releasenotes/notes/1.2/fix-kwarg-validation-BitArray-1bf542a1fb5c15c6.yaml similarity index 100% rename from releasenotes/notes/fix-kwarg-validation-BitArray-1bf542a1fb5c15c6.yaml rename to releasenotes/notes/1.2/fix-kwarg-validation-BitArray-1bf542a1fb5c15c6.yaml diff --git a/releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml b/releasenotes/notes/1.2/fix-mcx-performance-de86bcc9f969b81e.yaml similarity index 100% rename from releasenotes/notes/fix-mcx-performance-de86bcc9f969b81e.yaml rename to releasenotes/notes/1.2/fix-mcx-performance-de86bcc9f969b81e.yaml diff --git a/releasenotes/notes/fix-negative-seed-pm-2813a62a020da115.yaml b/releasenotes/notes/1.2/fix-negative-seed-pm-2813a62a020da115.yaml similarity index 100% rename from releasenotes/notes/fix-negative-seed-pm-2813a62a020da115.yaml rename to releasenotes/notes/1.2/fix-negative-seed-pm-2813a62a020da115.yaml diff --git a/releasenotes/notes/fix-potential-non-determinism-dense-layout-da66de0217121146.yaml b/releasenotes/notes/1.2/fix-potential-non-determinism-dense-layout-da66de0217121146.yaml similarity index 100% rename from releasenotes/notes/fix-potential-non-determinism-dense-layout-da66de0217121146.yaml rename to releasenotes/notes/1.2/fix-potential-non-determinism-dense-layout-da66de0217121146.yaml diff --git a/releasenotes/notes/fix-qft-plugins-7106029d33c44b96.yaml b/releasenotes/notes/1.2/fix-qft-plugins-7106029d33c44b96.yaml similarity index 100% rename from releasenotes/notes/fix-qft-plugins-7106029d33c44b96.yaml rename to releasenotes/notes/1.2/fix-qft-plugins-7106029d33c44b96.yaml diff --git a/releasenotes/notes/fix-qpy-parsing-123-75357c3709e35963.yaml b/releasenotes/notes/1.2/fix-qpy-parsing-123-75357c3709e35963.yaml similarity index 100% rename from releasenotes/notes/fix-qpy-parsing-123-75357c3709e35963.yaml rename to releasenotes/notes/1.2/fix-qpy-parsing-123-75357c3709e35963.yaml diff --git a/releasenotes/notes/fix-qpy-symengine-compat-858970a9a1d6bc14.yaml b/releasenotes/notes/1.2/fix-qpy-symengine-compat-858970a9a1d6bc14.yaml similarity index 100% rename from releasenotes/notes/fix-qpy-symengine-compat-858970a9a1d6bc14.yaml rename to releasenotes/notes/1.2/fix-qpy-symengine-compat-858970a9a1d6bc14.yaml diff --git a/releasenotes/notes/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml b/releasenotes/notes/1.2/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml similarity index 100% rename from releasenotes/notes/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml rename to releasenotes/notes/1.2/fix-sabre-releasevalve-7f9af9bfc0482e04.yaml diff --git a/releasenotes/notes/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml b/releasenotes/notes/1.2/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml similarity index 100% rename from releasenotes/notes/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml rename to releasenotes/notes/1.2/fix-split-2q-unitaries-custom-gate-d10f7670a35548f4.yaml diff --git a/releasenotes/notes/fix-stateprep-normalize-a8057c339ba619bd.yaml b/releasenotes/notes/1.2/fix-stateprep-normalize-a8057c339ba619bd.yaml similarity index 100% rename from releasenotes/notes/fix-stateprep-normalize-a8057c339ba619bd.yaml rename to releasenotes/notes/1.2/fix-stateprep-normalize-a8057c339ba619bd.yaml diff --git a/releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml b/releasenotes/notes/1.2/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml similarity index 100% rename from releasenotes/notes/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml rename to releasenotes/notes/1.2/fix-statevector-sampler-c_if-9753f8f97a3d0ff5.yaml diff --git a/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml b/releasenotes/notes/1.2/fix-synth-qregs-7662681c0ff02511.yaml similarity index 100% rename from releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml rename to releasenotes/notes/1.2/fix-synth-qregs-7662681c0ff02511.yaml diff --git a/releasenotes/notes/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml b/releasenotes/notes/1.2/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml similarity index 100% rename from releasenotes/notes/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml rename to releasenotes/notes/1.2/fix_initialize_gates_to_uncompute-d0dba6a642d07f30.yaml diff --git a/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml b/releasenotes/notes/1.2/improve-quantum-causal-cone-f63eaaa9ab658811.yaml similarity index 100% rename from releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml rename to releasenotes/notes/1.2/improve-quantum-causal-cone-f63eaaa9ab658811.yaml diff --git a/releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml b/releasenotes/notes/1.2/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml similarity index 100% rename from releasenotes/notes/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml rename to releasenotes/notes/1.2/linear-binary-matrix-utils-rust-c48b5577749c34ab.yaml diff --git a/releasenotes/notes/mcx_recursive_clean_ancilla-2b0f6956e0f4cbbd.yaml b/releasenotes/notes/1.2/mcx_recursive_clean_ancilla-2b0f6956e0f4cbbd.yaml similarity index 100% rename from releasenotes/notes/mcx_recursive_clean_ancilla-2b0f6956e0f4cbbd.yaml rename to releasenotes/notes/1.2/mcx_recursive_clean_ancilla-2b0f6956e0f4cbbd.yaml diff --git a/releasenotes/notes/mcxvchain_dirty_auxiliary-5ea4037557209f6e.yaml b/releasenotes/notes/1.2/mcxvchain_dirty_auxiliary-5ea4037557209f6e.yaml similarity index 100% rename from releasenotes/notes/mcxvchain_dirty_auxiliary-5ea4037557209f6e.yaml rename to releasenotes/notes/1.2/mcxvchain_dirty_auxiliary-5ea4037557209f6e.yaml diff --git a/releasenotes/notes/no-elide-routing-none-7c1bebf1283d48c0.yaml b/releasenotes/notes/1.2/no-elide-routing-none-7c1bebf1283d48c0.yaml similarity index 100% rename from releasenotes/notes/no-elide-routing-none-7c1bebf1283d48c0.yaml rename to releasenotes/notes/1.2/no-elide-routing-none-7c1bebf1283d48c0.yaml diff --git a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml b/releasenotes/notes/1.2/oxidize-acg-0294a87c0d5974fa.yaml similarity index 100% rename from releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml rename to releasenotes/notes/1.2/oxidize-acg-0294a87c0d5974fa.yaml diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/1.2/oxidize-permbasic-be27578187ac472f.yaml similarity index 100% rename from releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml rename to releasenotes/notes/1.2/oxidize-permbasic-be27578187ac472f.yaml diff --git a/releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml b/releasenotes/notes/1.2/oxidize-pmh-ec74e4002510eaad.yaml similarity index 100% rename from releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml rename to releasenotes/notes/1.2/oxidize-pmh-ec74e4002510eaad.yaml diff --git a/releasenotes/notes/oxidize-synth-clifford-bm-91d8b974ca0522b7.yaml b/releasenotes/notes/1.2/oxidize-synth-clifford-bm-91d8b974ca0522b7.yaml similarity index 100% rename from releasenotes/notes/oxidize-synth-clifford-bm-91d8b974ca0522b7.yaml rename to releasenotes/notes/1.2/oxidize-synth-clifford-bm-91d8b974ca0522b7.yaml diff --git a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml b/releasenotes/notes/1.2/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml similarity index 100% rename from releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml rename to releasenotes/notes/1.2/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml diff --git a/releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml b/releasenotes/notes/1.2/peephole-before-routing-c3d184b740bb7a8b.yaml similarity index 100% rename from releasenotes/notes/peephole-before-routing-c3d184b740bb7a8b.yaml rename to releasenotes/notes/1.2/peephole-before-routing-c3d184b740bb7a8b.yaml diff --git a/releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml b/releasenotes/notes/1.2/port_star_prerouting-13fae3ff78feb5e3.yaml similarity index 100% rename from releasenotes/notes/port_star_prerouting-13fae3ff78feb5e3.yaml rename to releasenotes/notes/1.2/port_star_prerouting-13fae3ff78feb5e3.yaml diff --git a/releasenotes/notes/qasm2-builtin-gate-d80c2868cdf5f958.yaml b/releasenotes/notes/1.2/qasm2-builtin-gate-d80c2868cdf5f958.yaml similarity index 100% rename from releasenotes/notes/qasm2-builtin-gate-d80c2868cdf5f958.yaml rename to releasenotes/notes/1.2/qasm2-builtin-gate-d80c2868cdf5f958.yaml diff --git a/releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml b/releasenotes/notes/1.2/qasm3-basis-gates-keyword-c5998bff1e178715.yaml similarity index 100% rename from releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml rename to releasenotes/notes/1.2/qasm3-basis-gates-keyword-c5998bff1e178715.yaml diff --git a/releasenotes/notes/qasm3-includes-ceb56f49b8c190ff.yaml b/releasenotes/notes/1.2/qasm3-includes-ceb56f49b8c190ff.yaml similarity index 100% rename from releasenotes/notes/qasm3-includes-ceb56f49b8c190ff.yaml rename to releasenotes/notes/1.2/qasm3-includes-ceb56f49b8c190ff.yaml diff --git a/releasenotes/notes/qasm3-symbol-table-efad35629639c77d.yaml b/releasenotes/notes/1.2/qasm3-symbol-table-efad35629639c77d.yaml similarity index 100% rename from releasenotes/notes/qasm3-symbol-table-efad35629639c77d.yaml rename to releasenotes/notes/1.2/qasm3-symbol-table-efad35629639c77d.yaml diff --git a/releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml b/releasenotes/notes/1.2/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml similarity index 100% rename from releasenotes/notes/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml rename to releasenotes/notes/1.2/replace-initialization-algorithm-by-isometry-41f9ffa58f72ece5.yaml diff --git a/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml b/releasenotes/notes/1.2/restrict-split2q-d51d840cc7a7a482.yaml similarity index 100% rename from releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml rename to releasenotes/notes/1.2/restrict-split2q-d51d840cc7a7a482.yaml diff --git a/releasenotes/notes/sabre_level0-1524f01965257f3f.yaml b/releasenotes/notes/1.2/sabre_level0-1524f01965257f3f.yaml similarity index 100% rename from releasenotes/notes/sabre_level0-1524f01965257f3f.yaml rename to releasenotes/notes/1.2/sabre_level0-1524f01965257f3f.yaml diff --git a/releasenotes/notes/synth_permutation_depth_lnn_kms-c444f3a363f3a903.yaml b/releasenotes/notes/1.2/synth_permutation_depth_lnn_kms-c444f3a363f3a903.yaml similarity index 100% rename from releasenotes/notes/synth_permutation_depth_lnn_kms-c444f3a363f3a903.yaml rename to releasenotes/notes/1.2/synth_permutation_depth_lnn_kms-c444f3a363f3a903.yaml diff --git a/releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml b/releasenotes/notes/1.2/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml similarity index 100% rename from releasenotes/notes/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml rename to releasenotes/notes/1.2/unary_pos_for_parameterexpression-6421421b6dc20fbb.yaml diff --git a/releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml b/releasenotes/notes/1.2/update-primitive-v2-metadata-cf1226e2d6477688.yaml similarity index 100% rename from releasenotes/notes/update-primitive-v2-metadata-cf1226e2d6477688.yaml rename to releasenotes/notes/1.2/update-primitive-v2-metadata-cf1226e2d6477688.yaml diff --git a/releasenotes/notes/update-qasm3-lib-40e15bc24234970d.yaml b/releasenotes/notes/1.2/update-qasm3-lib-40e15bc24234970d.yaml similarity index 100% rename from releasenotes/notes/update-qasm3-lib-40e15bc24234970d.yaml rename to releasenotes/notes/1.2/update-qasm3-lib-40e15bc24234970d.yaml diff --git a/releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml b/releasenotes/notes/1.2/update-rustworkx-min-version-4f07aacfebccae80.yaml similarity index 100% rename from releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml rename to releasenotes/notes/1.2/update-rustworkx-min-version-4f07aacfebccae80.yaml diff --git a/releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml b/releasenotes/notes/1.2/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml similarity index 100% rename from releasenotes/notes/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml rename to releasenotes/notes/1.2/use-target-in-generate-preset-pm-5215e00d22d0205c.yaml diff --git a/releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml b/releasenotes/notes/1.2/workaroud_12361-994d0ac2d2a6ed41.yaml similarity index 100% rename from releasenotes/notes/workaroud_12361-994d0ac2d2a6ed41.yaml rename to releasenotes/notes/1.2/workaroud_12361-994d0ac2d2a6ed41.yaml diff --git a/releasenotes/notes/1.3/Parameterized-commutation-checker-8a78a4715bf78b4e.yaml b/releasenotes/notes/1.3/Parameterized-commutation-checker-8a78a4715bf78b4e.yaml new file mode 100644 index 000000000000..72bd9aefe726 --- /dev/null +++ b/releasenotes/notes/1.3/Parameterized-commutation-checker-8a78a4715bf78b4e.yaml @@ -0,0 +1,14 @@ +--- +features_circuits: + - | + Improved the functionality of :class:`.CommutationChecker` to include + support for the following parameterized gates with free parameters: + :class:`.RXXGate`,:class:`.RYYGate`,:class:`.RZZGate`,:class:`.RZXGate`, + :class:`.RXGate`,:class:`.RYGate`,:class:`.RZGate`,:class:`.PhaseGate`, + :class:`.U1Gate`,:class:`.CRXGate`,:class:`.CRYGate`,:class:`.CRZGate`, + :class:`.CPhaseGate`. + + Before these were only supported with bound parameters. + + + diff --git a/releasenotes/notes/add-gates-to-collect-clifford-af88dd8f7a2a4bf9.yaml b/releasenotes/notes/1.3/add-gates-to-collect-clifford-af88dd8f7a2a4bf9.yaml similarity index 100% rename from releasenotes/notes/add-gates-to-collect-clifford-af88dd8f7a2a4bf9.yaml rename to releasenotes/notes/1.3/add-gates-to-collect-clifford-af88dd8f7a2a4bf9.yaml diff --git a/releasenotes/notes/1.3/add-identity-pass-builtin-2061b29b53b928d3.yaml b/releasenotes/notes/1.3/add-identity-pass-builtin-2061b29b53b928d3.yaml new file mode 100644 index 000000000000..0cf3d503aeea --- /dev/null +++ b/releasenotes/notes/1.3/add-identity-pass-builtin-2061b29b53b928d3.yaml @@ -0,0 +1,8 @@ +--- +features_transpiler: + - | + The :class:`.RemoveIdentityEquivalent` transpiler pass is now run as part + of the preset pass managers at optimization levels 2 and 3. The pass is + run in the ``init`` stage and the ``optimization`` stage, because the + optimizations it applies are valid in both stages and the pass is + fast to execute. diff --git a/releasenotes/notes/add-mcx-plugins-85e5b248692a36db.yaml b/releasenotes/notes/1.3/add-mcx-plugins-85e5b248692a36db.yaml similarity index 100% rename from releasenotes/notes/add-mcx-plugins-85e5b248692a36db.yaml rename to releasenotes/notes/1.3/add-mcx-plugins-85e5b248692a36db.yaml diff --git a/releasenotes/notes/1.3/add-more-sabre-trials-9b421f05d2f48d18.yaml b/releasenotes/notes/1.3/add-more-sabre-trials-9b421f05d2f48d18.yaml new file mode 100644 index 000000000000..c91244c944d6 --- /dev/null +++ b/releasenotes/notes/1.3/add-more-sabre-trials-9b421f05d2f48d18.yaml @@ -0,0 +1,11 @@ +--- +features_transpiler: + - | + The :class:`.SabreLayout` transpiler pass has been updated to run an + additional 2 or 3 layout trials by default independently of the + ``layout_trials`` keyword argument's value. A trivial + layout and its reverse are included for all backends, just like the :class:`.DenseLayout` + trial that was added in 1.2.0. In addition to this, the largest rings on + an IBM backend heavy hex connectivity graph are added if the backends are 127, + 133, or 156 qubits. This can provide a good starting point for some circuits on these commonly run + backends, while for all others it's just an additional "random trial". diff --git a/releasenotes/notes/1.3/add-qpy-v13-3b22ae33045af6c1.yaml b/releasenotes/notes/1.3/add-qpy-v13-3b22ae33045af6c1.yaml new file mode 100644 index 000000000000..3cc80adc0b5c --- /dev/null +++ b/releasenotes/notes/1.3/add-qpy-v13-3b22ae33045af6c1.yaml @@ -0,0 +1,33 @@ +--- +features_qpy: + - | + Added a new QPY format version 13 that adds a Qiskit native representation + of :class:`.ParameterExpression` objects. +issues: + - | + When using QPY formats 10, 11, or 12 there is a dependency on the version + of ``symengine`` installed in the payload for serialized + :class:`.ParamerExpression` if there is mismatched version of the installed + ``symengine`` package between the environment that generated the payload with + :func:`.qpy.dump` and the installed version that is trying to load the payload + with :func:`.qpy.load`. If this is encountered you will need to install the + symengine version from the error message emitted to load the payload. QPY + format version >= 13 (or < 10) will not have this issue and it is recommended + if you're serializing :class:`.ParameterExpression` objects as part of your + circuit or any :class:`.ScheduleBlock` objects you use version 13 to avoid + this issue in the future. +upgrade_qpy: + - | + The :func:`.qpy.dump` function will now emit format version 13 by default. + This means payloads generated with this function by default will only + be compatible with Qiskit >= 1.3.0. If you need for the payload to be + loaded by a older version of Qiskit you can use the ``version`` flag on + :func:`.qpy.dump` to emit a version compatible with earlier releases of + Qiskit. You can refer to :ref:`qpy_compatibility` for more details on this. +# security: +# - | +# Add security notes here, or remove this section. All of the list items in +# this section are combined when the release notes are rendered, so the text +# needs to be worded so that it does not depend on any information only +# available in another section, such as the prelude. This may mean repeating +# some details. diff --git a/releasenotes/notes/add-qv-function-a8990e248d5e7e1a.yaml b/releasenotes/notes/1.3/add-qv-function-a8990e248d5e7e1a.yaml similarity index 100% rename from releasenotes/notes/add-qv-function-a8990e248d5e7e1a.yaml rename to releasenotes/notes/1.3/add-qv-function-a8990e248d5e7e1a.yaml diff --git a/releasenotes/notes/add-random-clifford-util-5358041208729988.yaml b/releasenotes/notes/1.3/add-random-clifford-util-5358041208729988.yaml similarity index 100% rename from releasenotes/notes/add-random-clifford-util-5358041208729988.yaml rename to releasenotes/notes/1.3/add-random-clifford-util-5358041208729988.yaml diff --git a/releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml b/releasenotes/notes/1.3/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml similarity index 100% rename from releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml rename to releasenotes/notes/1.3/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml diff --git a/releasenotes/notes/1.3/add-twirl-circuit-ff4d4437190551bc.yaml b/releasenotes/notes/1.3/add-twirl-circuit-ff4d4437190551bc.yaml new file mode 100644 index 000000000000..46e7701e26ac --- /dev/null +++ b/releasenotes/notes/1.3/add-twirl-circuit-ff4d4437190551bc.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + Added a new circuit manipulation function :func:`.pauli_twirl_2q_gates` that can be used to apply + Pauli twirling to a given circuit. This only works for twirling a fixed set of two-qubit + gates, currently :class:`.CXGate`, :class:`.ECRGate`, :class:`.CZGate`, :class:`.iSwapGate`. + For example: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates + + qc = QuantumCircuit(2) + qc.cx(0, 1) + twirled_circuit = pauli_twirl_2q_gates(qc, seed=123456) + twirled_circuit.draw("mpl") diff --git a/releasenotes/notes/1.3/assign-parameters-perf-regression-fc8c9db134b1763d.yaml b/releasenotes/notes/1.3/assign-parameters-perf-regression-fc8c9db134b1763d.yaml new file mode 100644 index 000000000000..28f29b63ea2a --- /dev/null +++ b/releasenotes/notes/1.3/assign-parameters-perf-regression-fc8c9db134b1763d.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed a performance regression in :meth:`.QuantumCircuit.assign_parameters` introduced in Qiskit + 1.2.0 when calling the method in a tight loop, binding only a small number of parameters out of + a heavily parametric circuit on each iteration. If possible, it is still more performant to + call :meth:`~.QuantumCircuit.assign_parameters` only once, with all assignments at the same + time, as this reduces the proportion of time spent on input normalization and error-checking + overhead. diff --git a/releasenotes/notes/backend-estimator-v2-variance-905c953415ad0e29.yaml b/releasenotes/notes/1.3/backend-estimator-v2-variance-905c953415ad0e29.yaml similarity index 100% rename from releasenotes/notes/backend-estimator-v2-variance-905c953415ad0e29.yaml rename to releasenotes/notes/1.3/backend-estimator-v2-variance-905c953415ad0e29.yaml diff --git a/releasenotes/notes/1.3/backend-sampler-v2-level1-dc13af460cd38454.yaml b/releasenotes/notes/1.3/backend-sampler-v2-level1-dc13af460cd38454.yaml new file mode 100644 index 000000000000..594e610939cf --- /dev/null +++ b/releasenotes/notes/1.3/backend-sampler-v2-level1-dc13af460cd38454.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Support for level 1 data was added to :class:`~.BackendSamplerV2` as was + support for passing options through to the ``run()`` method of the wrapped + :class:`~.BackendV2`. The run options can be specified using a + ``"run_options"`` entry inside of the ``options`` dicitonary passed to + :class:`~.BackendSamplerV2`. The ``"run_options"`` entry should be a + dictionary mapping argument names to values for passing to the backend's + ``run()`` method. When a ``"meas_level"`` option with a value of 1 is set + in the run options, the results from the backend will be treated as level 1 + results rather as bit arrays (the level 2 format). +upgrade: + - | + When using :class:`~.BackendSamplerV2`, circuit metadata is no longer + cleared before passing circuits to the ``run()`` method of the wrapped + :class:`~.BackendV2` instance. diff --git a/releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml b/releasenotes/notes/1.3/barebone-backend-option-675c86df4382a443.yaml similarity index 100% rename from releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml rename to releasenotes/notes/1.3/barebone-backend-option-675c86df4382a443.yaml diff --git a/releasenotes/notes/1.3/basicsimulator-config-copy-5ecdfdf161e488f2.yaml b/releasenotes/notes/1.3/basicsimulator-config-copy-5ecdfdf161e488f2.yaml new file mode 100644 index 000000000000..b5ec0112004e --- /dev/null +++ b/releasenotes/notes/1.3/basicsimulator-config-copy-5ecdfdf161e488f2.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + For :class:`~.BasicSimulator`, the ``basis_gates`` entry in the + configuration instance returned by the ``configuration()`` is now a list + rather than a ``dict_keys`` instance, matching the expected type and + allowing for configuration instance to be deep copied. diff --git a/releasenotes/notes/1.3/binary-arithmetic-gates-6cd2b1c8112febe0.yaml b/releasenotes/notes/1.3/binary-arithmetic-gates-6cd2b1c8112febe0.yaml new file mode 100644 index 000000000000..a082a98389ad --- /dev/null +++ b/releasenotes/notes/1.3/binary-arithmetic-gates-6cd2b1c8112febe0.yaml @@ -0,0 +1,25 @@ +--- +features_circuits: + - | + Added binary arithmetic gates for inplace addition two :math:`n`-qubit registers, that is + :math:`|a\rangle |b\rangle \mapsto |a\rangle |a+b\rangle`. + The :class:`.ModularAdderGate` implements addition modulo :math:`2^n`, the + :class:`.AdderGate` implements standard addition including a carry-out, and the + :class:`.FullAdderGate` includes a carry-in qubit. See the respective documentations for + details and examples. + + In contrast to the existing library circuits, such as :class:`.CDKMRippleCarryAdder`, + handling the abstract gate allows the compiler (or user) to select the optimal gate + synthesis, depending on the circuit's context. + - | + Added the :class:`.MultiplierGate` for multiplication of two :math:`n`-qubit registers, that is + :math:`|a\rangle |b\rangle \mapsto |a\rangle |b\rangle |a \cdot b\rangle`. + See the class documentations for details and examples. +features_synthesis: + - | + Added :func:`.adder_qft_d00`, :func:`.adder_ripple_c04`, and :func:`.adder_ripple_v95` + to synthesize the adder gates, :class:`.ModularAdderGate`, :class:`.AdderGate`, and + :class:`.FullAdderGate`. + - | + Added :func:`.multiplier_cumulative_h18` and :func:`.multiplier_qft_r17` + to synthesize the :class:`.MultiplierGate`. \ No newline at end of file diff --git a/releasenotes/notes/1.3/boolean-logic-gates-40add5cf0b20b5e9.yaml b/releasenotes/notes/1.3/boolean-logic-gates-40add5cf0b20b5e9.yaml new file mode 100644 index 000000000000..d1de403ab4f7 --- /dev/null +++ b/releasenotes/notes/1.3/boolean-logic-gates-40add5cf0b20b5e9.yaml @@ -0,0 +1,14 @@ +--- +features_circuits: + - | + Quantum circuits in :mod:`qiskit.circuit.library.boolean_logic` now have equivalent + representations as :class:`.Gate` objects: + + * :class:`~qiskit.circuit.library.AndGate`, + representing :class:`~qiskit.circuit.library.AND`, + * :class:`~qiskit.circuit.library.OrGate`, + representing :class:`~qiskit.circuit.library.OR`, + * :class:`~qiskit.circuit.library.BitwiseXorGate`, + representing :class:`~qiskit.circuit.library.XOR`, + * :class:`~qiskit.circuit.library.InnerProductGate`, + representing :class:`~qiskit.circuit.library.InnerProduct`. diff --git a/releasenotes/notes/circuit-draw-warn-justify-03434d30cccda452.yaml b/releasenotes/notes/1.3/circuit-draw-warn-justify-03434d30cccda452.yaml similarity index 100% rename from releasenotes/notes/circuit-draw-warn-justify-03434d30cccda452.yaml rename to releasenotes/notes/1.3/circuit-draw-warn-justify-03434d30cccda452.yaml diff --git a/releasenotes/notes/circuit-library-missing-eq-568e7a72008c0ab2.yaml b/releasenotes/notes/1.3/circuit-library-missing-eq-568e7a72008c0ab2.yaml similarity index 100% rename from releasenotes/notes/circuit-library-missing-eq-568e7a72008c0ab2.yaml rename to releasenotes/notes/1.3/circuit-library-missing-eq-568e7a72008c0ab2.yaml diff --git a/releasenotes/notes/1.3/clib-evolved-ops-e91c00964c0209ce.yaml b/releasenotes/notes/1.3/clib-evolved-ops-e91c00964c0209ce.yaml new file mode 100644 index 000000000000..64d81b5e01ed --- /dev/null +++ b/releasenotes/notes/1.3/clib-evolved-ops-e91c00964c0209ce.yaml @@ -0,0 +1,13 @@ +--- +features_circuits: + - | + Added :func:`.evolved_operator_ansatz`, :func:`.hamiltonian_variational_ansatz`, and + :func:`.qaoa_ansatz` to the circuit library to implement variational circuits based on + operator evolutions. :func:`.evolved_operator_ansatz` and :func:`.qaoa_ansatz` are + functionally equivalent to :class:`.EvolvedOperatorAnsatz` and :class:`.QAOAAnsatz`, but + generally more performant. + + The :func:`.hamiltonian_variational_ansatz` is designed to take a single Hamiltonian and + automatically split it into commuting terms to implement a Hamiltonian variational ansatz. + This could already be achieved manually by using the :class:`.EvolvedOperatorAnsatz`, but + is now more convenient to use. diff --git a/releasenotes/notes/1.3/clib-grover-op-cb032144e899ed0d.yaml b/releasenotes/notes/1.3/clib-grover-op-cb032144e899ed0d.yaml new file mode 100644 index 000000000000..93629bb1fa9a --- /dev/null +++ b/releasenotes/notes/1.3/clib-grover-op-cb032144e899ed0d.yaml @@ -0,0 +1,19 @@ +--- +features_circuits: + - | + Added :func:`.grover_operator` to construct a Grover operator circuit, used in e.g. + Grover's algorithm and amplitude estimation/amplification. This function is similar to + :class:`.GroverOperator`, but does not require choosing the implementation of the + multi-controlled X gate a-priori and let's the compiler choose the optimal decomposition + instead. In addition to this, it does not wrap the circuit into an opaque gate and is + faster as less decompositions are required to transpile. + + Example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(2) + oracle.z(0) # good state = first qubit is |1> + grover_op = grover_operator(oracle, insert_barriers=True) + print(grover_op.draw()) diff --git a/releasenotes/notes/control-flow-op-names-c66f38f8a0e15ce7.yaml b/releasenotes/notes/1.3/control-flow-op-names-c66f38f8a0e15ce7.yaml similarity index 100% rename from releasenotes/notes/control-flow-op-names-c66f38f8a0e15ce7.yaml rename to releasenotes/notes/1.3/control-flow-op-names-c66f38f8a0e15ce7.yaml diff --git a/releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml b/releasenotes/notes/1.3/cphase-rzz-equivalence-e8afc37b71a74366.yaml similarity index 100% rename from releasenotes/notes/cphase-rzz-equivalence-e8afc37b71a74366.yaml rename to releasenotes/notes/1.3/cphase-rzz-equivalence-e8afc37b71a74366.yaml diff --git a/releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml b/releasenotes/notes/1.3/dag-oxide-60b3d7219cb21703.yaml similarity index 100% rename from releasenotes/notes/dag-oxide-60b3d7219cb21703.yaml rename to releasenotes/notes/1.3/dag-oxide-60b3d7219cb21703.yaml diff --git a/releasenotes/notes/deprecate-StochasticSwap-451f46b273602b7b.yaml b/releasenotes/notes/1.3/deprecate-StochasticSwap-451f46b273602b7b.yaml similarity index 100% rename from releasenotes/notes/deprecate-StochasticSwap-451f46b273602b7b.yaml rename to releasenotes/notes/1.3/deprecate-StochasticSwap-451f46b273602b7b.yaml diff --git a/releasenotes/notes/1.3/deprecate-basic-simulator-configuration-9d782925196993e9.yaml b/releasenotes/notes/1.3/deprecate-basic-simulator-configuration-9d782925196993e9.yaml new file mode 100644 index 000000000000..59b00df0cf5a --- /dev/null +++ b/releasenotes/notes/1.3/deprecate-basic-simulator-configuration-9d782925196993e9.yaml @@ -0,0 +1,32 @@ +--- +deprecations_providers: + - | + The :meth:`.BasicSimulator.configuration` method is deprecated and will be removed in 2.0.0. + This method returned a legacy ``providers.models.BackendConfiguration`` instance which is part + of the deprecated ``BackendV1`` model. This model has been replaced with :class:`.BackendV2`, + where the constraints are stored directly in the backend instance or the underlying :class:`.Target` + (``backend.target``). + + Here is a quick guide for accessing the most common ``BackendConfiguration`` attributes in the + :class:`BackendV2` model:"" + + BackendV1 model (deprecated) ------------> BackendV2 model + ---------------------------- --------------- + backend.configuration().backend_name backend.name + backend.configuration().backend_version backend.backend_version + backend.configuration().n_qubits backend.num_qubits + backend.configuration().num_qubits backend.num_qubits + backend.configuration().basis_gates backend.target.operation_names (*) + backend.configuration().coupling_map backend.target.build_coupling_map() + backend.configuration().local No representation + backend.configuration().simulator No representation + backend.configuration().conditional No representation + backend.configuration().open_pulse No representation + backend.configuration().memory No representation + backend.configuration().max_shots No representation + + (*) Note that ``backend.target.operation_names`` includes ``basis_gates`` and additional + non-gate instructions, in some implementations it might be necessary to filter the output. + + See `this guide `__ + for more information on migrating to the ``BackendV2`` model. diff --git a/releasenotes/notes/1.3/deprecate-condition_c_if-9548e5522814fe9c.yaml b/releasenotes/notes/1.3/deprecate-condition_c_if-9548e5522814fe9c.yaml new file mode 100644 index 000000000000..e096273f3c8e --- /dev/null +++ b/releasenotes/notes/1.3/deprecate-condition_c_if-9548e5522814fe9c.yaml @@ -0,0 +1,38 @@ +--- +deprecations_circuits: + - | + Deprecated the :attr:`.Instruction.condition` attribute and the + :meth:`.Instruction.c_if` method. They will be removed + in Qiskit 2.0, along with any uses in the Qiskit data + model. This functionality has been superseded by the :class:`.IfElseOp` class + which can be used to describe a classical condition in a circuit. For + example, a circuit using :meth:`.Instruction.c_if` like:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.x(0).c_if(0, 1) + qc.z(1.c_if(1, 0) + qc.measure(0, 0) + qc.measure(1, 1) + + can be rewritten as:: + + qc = QuantumCircuit(2, 2) + qc.h(0) + with expected.if_test((expected.clbits[0], True)): + qc.x(0) + with expected.if_test((expected.clbits[1], False)): + qc.z(1) + qc.measure(0, 0) + qc.measure(1, 1) + + The now deprecated :class:`.ConvertConditionsToIfOps` transpiler pass can + be used to automate this conversion for existing circuits. +deprecations_transpiler: + - | + The transpiler pass :class:`.ConvertConditionsToIfOps` has been deprecated + and will be removed in Qiskit 2.0.0. This class is now deprecated because + the underlying data model for :attr:`.Instruction.condition` which this + pass is converting from has been deprecated and will be removed in 2.0.0. diff --git a/releasenotes/notes/1.3/deprecate-custom-basis-gates-transpile-e4b5893377f23acb.yaml b/releasenotes/notes/1.3/deprecate-custom-basis-gates-transpile-e4b5893377f23acb.yaml new file mode 100644 index 000000000000..e2e794e96708 --- /dev/null +++ b/releasenotes/notes/1.3/deprecate-custom-basis-gates-transpile-e4b5893377f23acb.yaml @@ -0,0 +1,18 @@ +--- +deprecations_transpiler: + - | + Providing custom gates through the ``basis_gates`` argument is deprecated + for both :func:`.transpile` and :func:`generate_preset_pass_manager`, this functionality + will be removed in Qiskit 2.0. Custom gates are still supported in the :class:`.Target` model, + and can be provided through the ``target`` argument. One can build a :class:`.Target` instance + from scratch or use the :meth:`.Target.from_configuration` method with the ``custom_name_mapping`` + argument. For example:: + + from qiskit.circuit.library import XGate + from qiskit.transpiler.target import Target + + basis_gates = ["my_x", "cx"] + custom_name_mapping = {"my_x": XGate()} + target = Target.from_configuration( + basis_gates=basis_gates, num_qubits=2, custom_name_mapping=custom_name_mapping + ) \ No newline at end of file diff --git a/releasenotes/notes/1.3/deprecate-mitigation-f5f6ef3233b3d726.yaml b/releasenotes/notes/1.3/deprecate-mitigation-f5f6ef3233b3d726.yaml new file mode 100644 index 000000000000..850b84f1fb17 --- /dev/null +++ b/releasenotes/notes/1.3/deprecate-mitigation-f5f6ef3233b3d726.yaml @@ -0,0 +1,7 @@ +--- +deprecations_misc: + - | + The ``qiskit.result.mitigation`` module has been deprecated and will be removed in the 2.0 release. + The deprecation includes the ``LocalReadoutMitigator`` and ``CorrelatedReadoutMitigator`` classes + as well as the associated utils. + Their functionality has been superseded by the mthree package, found in https://github.com/Qiskit/qiskit-addon-mthree. diff --git a/releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml b/releasenotes/notes/1.3/deprecate-pulse-package-07a621be1db7fa30.yaml similarity index 94% rename from releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml rename to releasenotes/notes/1.3/deprecate-pulse-package-07a621be1db7fa30.yaml index 65f86d9d299f..479c31556eb5 100644 --- a/releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml +++ b/releasenotes/notes/1.3/deprecate-pulse-package-07a621be1db7fa30.yaml @@ -40,10 +40,11 @@ deprecations_transpiler: * :class:`~qiskit.transpiler.passes.ValidatePulseGates` * :class:`~qiskit.transpiler.passes.RXCalibrationBuilder` * :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` + * :class:`~qiskit.transpiler.passes.EchoRZXWeylDecomposition` - | The `inst_map` argument in :func:`~qiskit.transpiler.generate_preset_pass_manager`, - :meth:`~transpiler.target.Target.from_configuration` and :func:`~qiskit.transpiler.preset_passmanagers.common.generate_scheduling` - is being deprecated. + :meth:`~transpiler.target.Target.from_configuration`, :class:`~qiskit.transpiler.PassManagerConfig` initializer + and :func:`~qiskit.transpiler.preset_passmanagers.common.generate_scheduling` is being deprecated. - | The `calibration` argument in :func:`~qiskit.transpiler.target.InstructionProperties` initializer methods is being deprecated. diff --git a/releasenotes/notes/deprecate-unit-duration-48b76c957bac5691.yaml b/releasenotes/notes/1.3/deprecate-unit-duration-48b76c957bac5691.yaml similarity index 100% rename from releasenotes/notes/deprecate-unit-duration-48b76c957bac5691.yaml rename to releasenotes/notes/1.3/deprecate-unit-duration-48b76c957bac5691.yaml diff --git a/releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml b/releasenotes/notes/1.3/extended-random-circuits-049b67cce39003f4.yaml similarity index 100% rename from releasenotes/notes/extended-random-circuits-049b67cce39003f4.yaml rename to releasenotes/notes/1.3/extended-random-circuits-049b67cce39003f4.yaml diff --git a/releasenotes/notes/1.3/faster-pauli-decomposition-faf2be01a6e75fff.yaml b/releasenotes/notes/1.3/faster-pauli-decomposition-faf2be01a6e75fff.yaml new file mode 100644 index 000000000000..56ad1a725f9a --- /dev/null +++ b/releasenotes/notes/1.3/faster-pauli-decomposition-faf2be01a6e75fff.yaml @@ -0,0 +1,7 @@ +--- +features_quantum_info: + - | + The performance of :meth:`.SparsePauliOp.from_operator` has been optimized on top of the + algorithm improvements methods introduced in Qiskit 1.0. It is now approximately five times + faster than before for fully dense matrices, taking approximately 40ms to decompose a 10q + operator involving all Pauli terms. diff --git a/releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml b/releasenotes/notes/1.3/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml similarity index 100% rename from releasenotes/notes/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml rename to releasenotes/notes/1.3/fix-apply-layout-duplicate-negative-indices-cf5517921fe52706.yaml diff --git a/releasenotes/notes/fix-c3sx-gate-matrix-050cf9f9ac3b2b82.yaml b/releasenotes/notes/1.3/fix-c3sx-gate-matrix-050cf9f9ac3b2b82.yaml similarity index 100% rename from releasenotes/notes/fix-c3sx-gate-matrix-050cf9f9ac3b2b82.yaml rename to releasenotes/notes/1.3/fix-c3sx-gate-matrix-050cf9f9ac3b2b82.yaml diff --git a/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml b/releasenotes/notes/1.3/fix-circular-entanglement-5aadd5adf75c0c13.yaml similarity index 100% rename from releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml rename to releasenotes/notes/1.3/fix-circular-entanglement-5aadd5adf75c0c13.yaml diff --git a/releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml b/releasenotes/notes/1.3/fix-collect-clifford-83af26d98b8c69e8.yaml similarity index 100% rename from releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml rename to releasenotes/notes/1.3/fix-collect-clifford-83af26d98b8c69e8.yaml diff --git a/releasenotes/notes/fix-decompose-hls-5019793177136024.yaml b/releasenotes/notes/1.3/fix-decompose-hls-5019793177136024.yaml similarity index 100% rename from releasenotes/notes/fix-decompose-hls-5019793177136024.yaml rename to releasenotes/notes/1.3/fix-decompose-hls-5019793177136024.yaml diff --git a/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml b/releasenotes/notes/1.3/fix-estimator-reset-9e7539776df4cac4.yaml similarity index 100% rename from releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml rename to releasenotes/notes/1.3/fix-estimator-reset-9e7539776df4cac4.yaml diff --git a/releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml b/releasenotes/notes/1.3/fix-isometry-rust-adf0eed09c6611f1.yaml similarity index 100% rename from releasenotes/notes/fix-isometry-rust-adf0eed09c6611f1.yaml rename to releasenotes/notes/1.3/fix-isometry-rust-adf0eed09c6611f1.yaml diff --git a/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml b/releasenotes/notes/1.3/fix-parameter-cache-05eac2f24477ccb8.yaml similarity index 100% rename from releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml rename to releasenotes/notes/1.3/fix-parameter-cache-05eac2f24477ccb8.yaml diff --git a/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml b/releasenotes/notes/1.3/fix-qc-depth-0q-cdcc9aa14e237e68.yaml similarity index 100% rename from releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml rename to releasenotes/notes/1.3/fix-qc-depth-0q-cdcc9aa14e237e68.yaml diff --git a/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml b/releasenotes/notes/1.3/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml similarity index 100% rename from releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml rename to releasenotes/notes/1.3/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml diff --git a/releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml b/releasenotes/notes/1.3/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml similarity index 100% rename from releasenotes/notes/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml rename to releasenotes/notes/1.3/fix-sparse-pauli-op-apply-layout-zero-43b9e70f0d1536a6.yaml diff --git a/releasenotes/notes/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml b/releasenotes/notes/1.3/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml similarity index 100% rename from releasenotes/notes/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml rename to releasenotes/notes/1.3/fix-sparsepauliop-phase-bug-2b24f4b775ca564f.yaml diff --git a/releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml b/releasenotes/notes/1.3/fix-swap-router-layout-f28cf0a2de7976a8.yaml similarity index 100% rename from releasenotes/notes/fix-swap-router-layout-f28cf0a2de7976a8.yaml rename to releasenotes/notes/1.3/fix-swap-router-layout-f28cf0a2de7976a8.yaml diff --git a/releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml b/releasenotes/notes/1.3/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml similarity index 100% rename from releasenotes/notes/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml rename to releasenotes/notes/1.3/fix-symbolic-unit-scaling-c3eb4d9be674dfd6.yaml diff --git a/releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml b/releasenotes/notes/1.3/fix-v2-pulse-drawer-d05e4e392766909f.yaml similarity index 100% rename from releasenotes/notes/fix-v2-pulse-drawer-d05e4e392766909f.yaml rename to releasenotes/notes/1.3/fix-v2-pulse-drawer-d05e4e392766909f.yaml diff --git a/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml b/releasenotes/notes/1.3/fix-var-wires-4ebc40e0b19df253.yaml similarity index 100% rename from releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml rename to releasenotes/notes/1.3/fix-var-wires-4ebc40e0b19df253.yaml diff --git a/releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml b/releasenotes/notes/1.3/fix-vf2-aer-a7306ce07ea81700.yaml similarity index 100% rename from releasenotes/notes/fix-vf2-aer-a7306ce07ea81700.yaml rename to releasenotes/notes/1.3/fix-vf2-aer-a7306ce07ea81700.yaml diff --git a/releasenotes/notes/fix_11990-8551c7250207fc76.yaml b/releasenotes/notes/1.3/fix_11990-8551c7250207fc76.yaml similarity index 100% rename from releasenotes/notes/fix_11990-8551c7250207fc76.yaml rename to releasenotes/notes/1.3/fix_11990-8551c7250207fc76.yaml diff --git a/releasenotes/notes/1.3/fixes_13306-f9883a733491a72f.yaml b/releasenotes/notes/1.3/fixes_13306-f9883a733491a72f.yaml new file mode 100644 index 000000000000..e46b971b6462 --- /dev/null +++ b/releasenotes/notes/1.3/fixes_13306-f9883a733491a72f.yaml @@ -0,0 +1,14 @@ +--- +deprecations_transpiler: + - | + The following :func:`.transpile` and :func:`.generate_preset_pass_manager` arguments are deprecated in favor of + defining a custom :class:`.Target`: ``instruction_durations``, ``timing_constraints``, and ``backend_properties``. + These arguments can be used to build a target with :meth:`.Target.from_configuration`:: + + Target.from_configuration( + ... + backend_properties = backend_properties, + instruction_durations = instruction_durations, + timing_constraints = timing_constraints + ) + diff --git a/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml b/releasenotes/notes/1.3/fixes_GenericBackendV2-668e40596e1f070d.yaml similarity index 100% rename from releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml rename to releasenotes/notes/1.3/fixes_GenericBackendV2-668e40596e1f070d.yaml diff --git a/releasenotes/notes/followup_12629-8bfcf1a3d4e6cabf.yaml b/releasenotes/notes/1.3/followup_12629-8bfcf1a3d4e6cabf.yaml similarity index 100% rename from releasenotes/notes/followup_12629-8bfcf1a3d4e6cabf.yaml rename to releasenotes/notes/1.3/followup_12629-8bfcf1a3d4e6cabf.yaml diff --git a/releasenotes/notes/hls-with-ancillas-d6792b41dfcf4aac.yaml b/releasenotes/notes/1.3/hls-with-ancillas-d6792b41dfcf4aac.yaml similarity index 100% rename from releasenotes/notes/hls-with-ancillas-d6792b41dfcf4aac.yaml rename to releasenotes/notes/1.3/hls-with-ancillas-d6792b41dfcf4aac.yaml diff --git a/releasenotes/notes/1.3/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml b/releasenotes/notes/1.3/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml new file mode 100644 index 000000000000..f832fed1b72d --- /dev/null +++ b/releasenotes/notes/1.3/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml @@ -0,0 +1,8 @@ +--- +features_transpiler: + - | + Improved handling of ancilla qubits in the :class:`.HighLevelSynthesis` + transpiler pass. For example, a circuit may have custom gates whose + definitions include :class:`.MCXGate`\s. Now the synthesis algorithms + for the inner MCX-gates can use the ancilla qubits available on the + global circuit but outside the custom gates' definitions. diff --git a/releasenotes/notes/1.3/iqp-function-6594f7cf1521499c.yaml b/releasenotes/notes/1.3/iqp-function-6594f7cf1521499c.yaml new file mode 100644 index 000000000000..a33b70c32a33 --- /dev/null +++ b/releasenotes/notes/1.3/iqp-function-6594f7cf1521499c.yaml @@ -0,0 +1,12 @@ +--- +features_circuits: + - | + Added the :func:`~qiskit.circuit.library.iqp` function to construct + Instantaneous Quantum Polynomial time (IQP) circuits. In addition to the + existing :class:`.IQP` class, the function also allows construction of random + IQP circuits:: + + from qiskit.circuit.library import iqp + + random_iqp = iqp(num_qubits=4) + print(random_iqp.draw()) \ No newline at end of file diff --git a/releasenotes/notes/mcmt-gate-a201d516f05c7d56.yaml b/releasenotes/notes/1.3/mcmt-gate-a201d516f05c7d56.yaml similarity index 100% rename from releasenotes/notes/mcmt-gate-a201d516f05c7d56.yaml rename to releasenotes/notes/1.3/mcmt-gate-a201d516f05c7d56.yaml diff --git a/releasenotes/notes/1.3/modernize-circuit-library-6e0be83421fd480b.yaml b/releasenotes/notes/1.3/modernize-circuit-library-6e0be83421fd480b.yaml new file mode 100644 index 000000000000..a1a4bb591860 --- /dev/null +++ b/releasenotes/notes/1.3/modernize-circuit-library-6e0be83421fd480b.yaml @@ -0,0 +1,12 @@ +--- +features_circuits: + - | + As a part of circuit library modernization, each of the following quantum + circuits is either also represented as a :class:`.Gate` object or can be + constructed using a synthesis method: + + * :class:`.GraphState` is represented by :class:`.GraphStateGate`, + * :class:`.FourierChecking` can be constructed using :meth:`.fourier_checking`, + * :class:`.UnitaryOverlap` can be constructed using :meth:`.unitary_overlap`, + * :class:`.HiddenLinearFunction` can be constructed using :meth:`.hidden_linear_function`, + * :class:`.PhaseEstimation` can be constructed using :meth:`.phase_estimation`. diff --git a/releasenotes/notes/1.3/operator-power-assume-unitary-0a2f97ea9de91b49.yaml b/releasenotes/notes/1.3/operator-power-assume-unitary-0a2f97ea9de91b49.yaml new file mode 100644 index 000000000000..780781c212a7 --- /dev/null +++ b/releasenotes/notes/1.3/operator-power-assume-unitary-0a2f97ea9de91b49.yaml @@ -0,0 +1,6 @@ +--- +features_quantum_info: + - | + Added a new argument ``assume_unitary`` to :meth:`qiskit.quantum_info.Operator.power`. + When ``True``, we use a faster method based on Schur's decomposition to raise an + ``Operator`` to a fractional power. diff --git a/releasenotes/notes/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml b/releasenotes/notes/1.3/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml similarity index 100% rename from releasenotes/notes/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml rename to releasenotes/notes/1.3/optimize-1q-gates-decomposition-ce111961b6782ee0.yaml diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/1.3/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml similarity index 100% rename from releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml rename to releasenotes/notes/1.3/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml diff --git a/releasenotes/notes/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml b/releasenotes/notes/1.3/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml similarity index 100% rename from releasenotes/notes/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml rename to releasenotes/notes/1.3/oxidize-commutation-analysis-d2fc81feb6ca80aa.yaml diff --git a/releasenotes/notes/oxidize-random-clifford-934f45876c14c8a0.yaml b/releasenotes/notes/1.3/oxidize-random-clifford-934f45876c14c8a0.yaml similarity index 100% rename from releasenotes/notes/oxidize-random-clifford-934f45876c14c8a0.yaml rename to releasenotes/notes/1.3/oxidize-random-clifford-934f45876c14c8a0.yaml diff --git a/releasenotes/notes/parallel-check-8186a8f074774a1f.yaml b/releasenotes/notes/1.3/parallel-check-8186a8f074774a1f.yaml similarity index 100% rename from releasenotes/notes/parallel-check-8186a8f074774a1f.yaml rename to releasenotes/notes/1.3/parallel-check-8186a8f074774a1f.yaml diff --git a/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml b/releasenotes/notes/1.3/parameterexpression-hash-d2593ab1715aa42c.yaml similarity index 100% rename from releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml rename to releasenotes/notes/1.3/parameterexpression-hash-d2593ab1715aa42c.yaml diff --git a/releasenotes/notes/1.3/pauli-evo-plugins-612850146c3f7d49.yaml b/releasenotes/notes/1.3/pauli-evo-plugins-612850146c3f7d49.yaml new file mode 100644 index 000000000000..c84b4a1646af --- /dev/null +++ b/releasenotes/notes/1.3/pauli-evo-plugins-612850146c3f7d49.yaml @@ -0,0 +1,59 @@ +--- +features_quantum_info: + - | + Added :meth:`.SparsePauliOperator.to_sparse_list` to convert an operator into + a sparse list format. This works inversely to :meth:`.SparsePauliOperator.from_sparse_list`. + For example:: + + from qiskit.quantum_info import SparsePauliOp + + op = SparsePauliOp(["XIII", "IZZI"], coeffs=[1, 2]) + sparse = op.to_sparse_list() # [("X", [3], 1), ("ZZ", [1, 2], 2)] + + other = SparsePauliOp.from_sparse_list(sparse, op.num_qubits) + print(other == op) # True + +features_synthesis: + - | + Added :meth:`.ProductFormula.expand` which allows to view the expansion of a product formula + in a sparse Pauli format. + - | + Added the plugin structure for the :class:`.PauliEvolutionGate`. The default plugin, + :class:`.PauliEvolutionSynthesisDefault`, constructs circuit as before, but faster as it + internally uses Rust. The larger the circuit (e.g. by the Hamiltonian size, the number + of timesteps, or the Suzuki-Trotter order), the higher the speedup. For example, + a 100-qubit Heisenberg Hamiltonian with 10 timesteps and a 4th-order Trotter formula is + now constructed ~9.4x faster. + The new plugin, :class:`.PauliEvolutionSynthesisRustiq`, uses + the synthesis algorithm that is described in the paper "Faster and shorter synthesis of + Hamiltonian simulation circuits" by de Brugière and Martiel (https://arxiv.org/abs/2404.03280) + and is implemented in https://github.com/smartiel/rustiq-core. + For example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.quantum_info import SparsePauliOp + from qiskit.circuit.library import PauliEvolutionGate + from qiskit.compiler import transpile + from qiskit.transpiler.passes import HLSConfig + + op = SparsePauliOp(["XXX", "YYY", "IZZ"]) + qc = QuantumCircuit(4) + qc.append(PauliEvolutionGate(op), [0, 1, 3]) + config = HLSConfig(PauliEvolution=[("rustiq", {"upto_phase": False})]) + tqc = transpile(qc, basis_gates=["cx", "u"], hls_config=config) + tqc.draw(output='mpl') + + This code snippet uses the ``"rustiq"`` plugin to synthesize :class:`.PauliEvolutionGate` + objects in the quantum circuit `qc`. The plugin is called with the additional option + ``"upto_phase" = False`` allowing to obtain smaller circuits at the expense of possibly + not preserving the global phase. For the full list of supported options, see + documentation for :class:`.PauliEvolutionSynthesisRustiq`. + +upgrade: + - | + The following classes now use the :math:`\sqrt{X}` operation to diagonalize the Pauli-Y + operator: :class:`.PauliEvolutionGate`, :class:`.EvolvedOperatorAnsatz`, + :class:`.PauliFeatureMap`. Previously, these classes used either :math:`H S` or + :math:`R_X(-\pi/2)` as basis transformation. Using the :math:`\sqrt{X}` operation, + represented by the :class:`.SXGate` is more efficient as it uses only a single gate + implemented as singleton. diff --git a/releasenotes/notes/1.3/pauli-label-perf-b704cbcc5ef92794.yaml b/releasenotes/notes/1.3/pauli-label-perf-b704cbcc5ef92794.yaml new file mode 100644 index 000000000000..22035371edc2 --- /dev/null +++ b/releasenotes/notes/1.3/pauli-label-perf-b704cbcc5ef92794.yaml @@ -0,0 +1,4 @@ +--- +features_quantum_info: + - | + The performance of :meth:`.Pauli.to_label` has significantly improved for large Paulis. diff --git a/releasenotes/notes/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml b/releasenotes/notes/1.3/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml similarity index 100% rename from releasenotes/notes/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml rename to releasenotes/notes/1.3/paulifeaturemap-takes-dictionary-as-entanglement-02037cb2d46e1c41.yaml diff --git a/releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml b/releasenotes/notes/1.3/plot-circuit-layout-5935646107893c12.yaml similarity index 100% rename from releasenotes/notes/plot-circuit-layout-5935646107893c12.yaml rename to releasenotes/notes/1.3/plot-circuit-layout-5935646107893c12.yaml diff --git a/releasenotes/notes/port-countops-method-3ad362c20b13182c.yaml b/releasenotes/notes/1.3/port-countops-method-3ad362c20b13182c.yaml similarity index 100% rename from releasenotes/notes/port-countops-method-3ad362c20b13182c.yaml rename to releasenotes/notes/1.3/port-countops-method-3ad362c20b13182c.yaml diff --git a/releasenotes/notes/port-elide-permutations-ed91c3d9cef2fec6.yaml b/releasenotes/notes/1.3/port-elide-permutations-ed91c3d9cef2fec6.yaml similarity index 100% rename from releasenotes/notes/port-elide-permutations-ed91c3d9cef2fec6.yaml rename to releasenotes/notes/1.3/port-elide-permutations-ed91c3d9cef2fec6.yaml diff --git a/releasenotes/notes/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml b/releasenotes/notes/1.3/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml similarity index 100% rename from releasenotes/notes/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml rename to releasenotes/notes/1.3/port-synth-cz-depth-line-mr-to-rust-1376d5a41948112a.yaml diff --git a/releasenotes/notes/product-formula-improvements-1bc40650151cf107.yaml b/releasenotes/notes/1.3/product-formula-improvements-1bc40650151cf107.yaml similarity index 100% rename from releasenotes/notes/product-formula-improvements-1bc40650151cf107.yaml rename to releasenotes/notes/1.3/product-formula-improvements-1bc40650151cf107.yaml diff --git a/releasenotes/notes/py3.9-min-now-c9781484a0eb288e.yaml b/releasenotes/notes/1.3/py3.9-min-now-c9781484a0eb288e.yaml similarity index 100% rename from releasenotes/notes/py3.9-min-now-c9781484a0eb288e.yaml rename to releasenotes/notes/1.3/py3.9-min-now-c9781484a0eb288e.yaml diff --git a/releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml b/releasenotes/notes/1.3/qasm2-big-condition-cfd203d53540d4ca.yaml similarity index 100% rename from releasenotes/notes/qasm2-big-condition-cfd203d53540d4ca.yaml rename to releasenotes/notes/1.3/qasm2-big-condition-cfd203d53540d4ca.yaml diff --git a/releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml b/releasenotes/notes/1.3/qasm2-bigint-8eff42acb67903e6.yaml similarity index 100% rename from releasenotes/notes/qasm2-bigint-8eff42acb67903e6.yaml rename to releasenotes/notes/1.3/qasm2-bigint-8eff42acb67903e6.yaml diff --git a/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml b/releasenotes/notes/1.3/qasm2-to-matrix-c707fe1e61b3987f.yaml similarity index 100% rename from releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml rename to releasenotes/notes/1.3/qasm2-to-matrix-c707fe1e61b3987f.yaml diff --git a/releasenotes/notes/qasm3-public-custom-gate-f4b2784f5cfadc30.yaml b/releasenotes/notes/1.3/qasm3-public-custom-gate-f4b2784f5cfadc30.yaml similarity index 100% rename from releasenotes/notes/qasm3-public-custom-gate-f4b2784f5cfadc30.yaml rename to releasenotes/notes/1.3/qasm3-public-custom-gate-f4b2784f5cfadc30.yaml diff --git a/releasenotes/notes/raise-on-illegal-replace-block-50cef8da757a580a.yaml b/releasenotes/notes/1.3/raise-on-illegal-replace-block-50cef8da757a580a.yaml similarity index 100% rename from releasenotes/notes/raise-on-illegal-replace-block-50cef8da757a580a.yaml rename to releasenotes/notes/1.3/raise-on-illegal-replace-block-50cef8da757a580a.yaml diff --git a/releasenotes/notes/1.3/remove_identity_equiv-9c627c8c35b2298a.yaml b/releasenotes/notes/1.3/remove_identity_equiv-9c627c8c35b2298a.yaml new file mode 100644 index 000000000000..90d016803d62 --- /dev/null +++ b/releasenotes/notes/1.3/remove_identity_equiv-9c627c8c35b2298a.yaml @@ -0,0 +1,29 @@ +--- +features_transpiler: + - | + Added a new transpiler pass, :class:`.RemoveIdentityEquivalent` that is used + to remove gates that are equivalent to an identity up to some tolerance. + For example if you had a circuit like: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + qc.draw("mpl") + + running the pass would eliminate the :class:`.CPhaseGate`: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import RemoveIdentityEquivalent + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + + removal_pass = RemoveIdentityEquivalent() + result = removal_pass(qc) + result.draw("mpl") diff --git a/releasenotes/notes/1.3/reorder-trotter-terms-c8a6eb3cdb831f77.yaml b/releasenotes/notes/1.3/reorder-trotter-terms-c8a6eb3cdb831f77.yaml new file mode 100644 index 000000000000..4f77f09e7ab2 --- /dev/null +++ b/releasenotes/notes/1.3/reorder-trotter-terms-c8a6eb3cdb831f77.yaml @@ -0,0 +1,40 @@ +--- +features_synthesis: + - | + Added a new argument ``preserve_order`` to :class:`.ProductFormula`, which allows + re-ordering the Pauli terms in the Hamiltonian before the product formula expansion, + to compress the final circuit depth. By setting this to ``False``, a term of form + + .. math:: + + Z_0 Z_1 + X_1 X_2 + Y_2 Y_3 + + will be re-ordered to + + .. math:: + + Z_0 Z_1 + Y_2 Y_3 + X_1 X_2 + + which will lead to the ``RZZ`` and ``RYY`` rotations being applied in parallel, instead + of three sequential rotations in the first part. + + This option can be set via the plugin interface:: + + from qiskit import QuantumCircuit, transpile + from qiskit.circuit.library import PauliEvolutionGate + from qiskit.quantum_info import SparsePauliOp + from qiskit.synthesis.evolution import SuzukiTrotter + from qiskit.transpiler.passes import HLSConfig + + op = SparsePauliOp(["XXII", "IYYI", "IIZZ"]) + time, reps = 0.1, 1 + + synthesis = SuzukiTrotter(order=2, reps=reps) + hls_config = HLSConfig(PauliEvolution=[("default", {"preserve_order": False})]) + + circuit = QuantumCircuit(op.num_qubits) + circuit.append(PauliEvolutionGate(op, time), circuit.qubits) + + tqc = transpile(circuit, basis_gates=["u", "cx"], hls_config=hls_config) + print(tqc.draw()) + diff --git a/releasenotes/notes/rust-commutation-checker-c738e67efa9d292f.yaml b/releasenotes/notes/1.3/rust-commutation-checker-c738e67efa9d292f.yaml similarity index 100% rename from releasenotes/notes/rust-commutation-checker-c738e67efa9d292f.yaml rename to releasenotes/notes/1.3/rust-commutation-checker-c738e67efa9d292f.yaml diff --git a/releasenotes/notes/1.3/rust-consolidation-a791a00380fc78b8.yaml b/releasenotes/notes/1.3/rust-consolidation-a791a00380fc78b8.yaml new file mode 100644 index 000000000000..c84128888951 --- /dev/null +++ b/releasenotes/notes/1.3/rust-consolidation-a791a00380fc78b8.yaml @@ -0,0 +1,13 @@ +--- +features_transpiler: + - | + The :class:`.ConsolidateGates` pass will now run the equivalent of the + :class:`.Collect2qBlocks` pass internally if it was not run in a pass + manager prior to the pass. Previously it was required that + :class:`.Collect2qBlocks` or :class:`.Collect1qRuns` were run prior to + :class:`.ConsolidateBlocks` for :class:`.ConsolidateBlocks` to do + anything. By doing the collection internally the overhead of the pass + is reduced. If :class:`.Collect2qBlocks` or :class:`.Collect1qRuns` are + run prior to :class:`.ConsolidateBlocks` the collected runs by those + passes from the property set are used and there is no change in behavior + for the pass. diff --git a/releasenotes/notes/rust-paulifm-1dc7b1c2dc756614.yaml b/releasenotes/notes/1.3/rust-paulifm-1dc7b1c2dc756614.yaml similarity index 100% rename from releasenotes/notes/rust-paulifm-1dc7b1c2dc756614.yaml rename to releasenotes/notes/1.3/rust-paulifm-1dc7b1c2dc756614.yaml diff --git a/releasenotes/notes/scipy-1.14-951d1c245473aee9.yaml b/releasenotes/notes/1.3/scipy-1.14-951d1c245473aee9.yaml similarity index 100% rename from releasenotes/notes/scipy-1.14-951d1c245473aee9.yaml rename to releasenotes/notes/1.3/scipy-1.14-951d1c245473aee9.yaml diff --git a/releasenotes/notes/sparse-observable-7de70dcdf6962a64.yaml b/releasenotes/notes/1.3/sparse-observable-7de70dcdf6962a64.yaml similarity index 100% rename from releasenotes/notes/sparse-observable-7de70dcdf6962a64.yaml rename to releasenotes/notes/1.3/sparse-observable-7de70dcdf6962a64.yaml diff --git a/releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml b/releasenotes/notes/1.3/storage-var-a00a33fcf9a71f3f.yaml similarity index 100% rename from releasenotes/notes/storage-var-a00a33fcf9a71f3f.yaml rename to releasenotes/notes/1.3/storage-var-a00a33fcf9a71f3f.yaml diff --git a/releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml b/releasenotes/notes/1.3/target-has-calibration-no-properties-f3be18f2d58f330a.yaml similarity index 100% rename from releasenotes/notes/target-has-calibration-no-properties-f3be18f2d58f330a.yaml rename to releasenotes/notes/1.3/target-has-calibration-no-properties-f3be18f2d58f330a.yaml diff --git a/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml b/releasenotes/notes/1.3/uniform-superposition-gate-3bd95ffdf05ef18c.yaml similarity index 100% rename from releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml rename to releasenotes/notes/1.3/uniform-superposition-gate-3bd95ffdf05ef18c.yaml diff --git a/releasenotes/notes/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml b/releasenotes/notes/1.3/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml similarity index 100% rename from releasenotes/notes/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml rename to releasenotes/notes/1.3/update-remove-diagonal-gates-before-measure-86abe39e46d5dad5.yaml diff --git a/releasenotes/notes/fix-hls-supported-instructions-0d80ea33b3d2257b.yaml b/releasenotes/notes/fix-hls-supported-instructions-0d80ea33b3d2257b.yaml new file mode 100644 index 000000000000..1e9c1a1cebe7 --- /dev/null +++ b/releasenotes/notes/fix-hls-supported-instructions-0d80ea33b3d2257b.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Previously the :class:`.HighLevelSynthesis` transpiler pass synthesized an + instruction for which a synthesis plugin is available, regardless of + whether the instruction is already supported by the target or a part of + the explicitly passed ``basis_gates``. This behavior is now fixed, so that + such already supported instructions are no longer synthesized. diff --git a/releasenotes/notes/fix_identity_operator_9e2ec9770ac046a6.yaml b/releasenotes/notes/fix_identity_operator_9e2ec9770ac046a6.yaml new file mode 100644 index 000000000000..69779a793cc3 --- /dev/null +++ b/releasenotes/notes/fix_identity_operator_9e2ec9770ac046a6.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug that caused :meth:`.Statevector.expectation_value` to yield incorrect results + for the identity operator when the statevector was not normalized. diff --git a/releasenotes/notes/optimize-mux-for_stateprep-ead91cf2a64ad23b.yaml b/releasenotes/notes/optimize-mux-for_stateprep-ead91cf2a64ad23b.yaml new file mode 100644 index 000000000000..b4cfcd09b82a --- /dev/null +++ b/releasenotes/notes/optimize-mux-for_stateprep-ead91cf2a64ad23b.yaml @@ -0,0 +1,8 @@ +--- +features_circuits: + - | + :class:`~.library.UCGate` now includes a ``mux_simp`` boolean attribute that enables the search + for simplifications of Carvalho et al., implemented in :meth:`~.library.UCGate._simplify`. + This optimization, enabled by default, identifies and removes unnecessary controls from the + multiplexer, reducing the number of CX gates and circuit depth, especially in separable + state preparation with :class:`~.library.Initialize`. diff --git a/releasenotes/notes/spo-to-matrix-determinism-554389d6fc98627c.yaml b/releasenotes/notes/spo-to-matrix-determinism-554389d6fc98627c.yaml new file mode 100644 index 000000000000..61961d492e72 --- /dev/null +++ b/releasenotes/notes/spo-to-matrix-determinism-554389d6fc98627c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a per-process based non-determinism in `SparsePauliOp.to_matrix`. The exact order of the + floating-point operations in the summation would previously vary per process, but will now be + identical between different invocations of the same script. See `#13413 `__. diff --git a/requirements.txt b/requirements.txt index 4c13eb6dc60a..6eb5902ae9fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,7 @@ dill>=0.3 python-dateutil>=2.8.0 stevedore>=3.0.0 typing-extensions + +# If updating the version range here, consider updating 'test/qpy_compat/run_tests.sh' to update the +# list of symengine dependencies used in the cross-version tests. symengine>=0.11,<0.14 diff --git a/test/benchmarks/manipulate.py b/test/benchmarks/manipulate.py index 0043c6c59fd4..e5fa10b08836 100644 --- a/test/benchmarks/manipulate.py +++ b/test/benchmarks/manipulate.py @@ -15,124 +15,13 @@ # pylint: disable=unused-wildcard-import,wildcard-import,undefined-variable import os -import numpy as np from qiskit import QuantumCircuit -from qiskit.converters import circuit_to_dag -from qiskit.circuit import CircuitInstruction, Qubit, library -from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit import pauli_twirl_2q_gates from qiskit.passmanager import PropertySet from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from .utils import multi_control_circuit -GATES = { - "id": library.IGate(), - "x": library.XGate(), - "y": library.YGate(), - "z": library.ZGate(), - "cx": library.CXGate(), - "cz": library.CZGate(), -} - -TWIRLING_SETS_NAMES = { - "cx": [ - ["id", "z", "z", "z"], - ["id", "x", "id", "x"], - ["id", "y", "z", "y"], - ["id", "id", "id", "id"], - ["z", "x", "z", "x"], - ["z", "y", "id", "y"], - ["z", "id", "z", "id"], - ["z", "z", "id", "z"], - ["x", "y", "y", "z"], - ["x", "id", "x", "x"], - ["x", "z", "y", "y"], - ["x", "x", "x", "id"], - ["y", "id", "y", "x"], - ["y", "z", "x", "y"], - ["y", "x", "y", "id"], - ["y", "y", "x", "z"], - ], - "cz": [ - ["id", "z", "id", "z"], - ["id", "x", "z", "x"], - ["id", "y", "z", "y"], - ["id", "id", "id", "id"], - ["z", "x", "id", "x"], - ["z", "y", "id", "y"], - ["z", "id", "z", "id"], - ["z", "z", "z", "z"], - ["x", "y", "y", "x"], - ["x", "id", "x", "z"], - ["x", "z", "x", "id"], - ["x", "x", "y", "y"], - ["y", "id", "y", "z"], - ["y", "z", "y", "id"], - ["y", "x", "x", "y"], - ["y", "y", "x", "x"], - ], -} -TWIRLING_SETS = { - key: [[GATES[name] for name in twirl] for twirl in twirls] - for key, twirls in TWIRLING_SETS_NAMES.items() -} - - -def _dag_from_twirl(gate_2q, twirl): - dag = DAGCircuit() - # or use QuantumRegister - doesn't matter - qubits = (Qubit(), Qubit()) - dag.add_qubits(qubits) - dag.apply_operation_back(twirl[0], (qubits[0],), (), check=False) - dag.apply_operation_back(twirl[1], (qubits[1],), (), check=False) - dag.apply_operation_back(gate_2q, qubits, (), check=False) - dag.apply_operation_back(twirl[2], (qubits[0],), (), check=False) - dag.apply_operation_back(twirl[3], (qubits[1],), (), check=False) - return dag - - -def circuit_twirl(qc, twirled_gate="cx", seed=None): - rng = np.random.default_rng(seed) - twirl_set = TWIRLING_SETS.get(twirled_gate, []) - - out = qc.copy_empty_like() - for instruction in qc.data: - if instruction.operation.name != twirled_gate: - out._append(instruction) - else: - # We could also scan through `qc` outside the loop to know how many - # twirled gates we'll be dealing with, and RNG the integers ahead of - # time - that'll be faster depending on what percentage of gates are - # twirled, and how much the Numpy overhead is. - twirls = twirl_set[rng.integers(len(twirl_set))] - control, target = instruction.qubits - out._append(CircuitInstruction(twirls[0], (control,), ())) - out._append(CircuitInstruction(twirls[1], (target,), ())) - out._append(instruction) - out._append(CircuitInstruction(twirls[2], (control,), ())) - out._append(CircuitInstruction(twirls[3], (target,), ())) - return out - - -def dag_twirl(dag, twirled_gate="cx", seed=None): - # This mutates `dag` in place. - rng = np.random.default_rng(seed) - twirl_set = TWIRLING_DAGS.get(twirled_gate, []) - twirled_gate_op = GATES[twirled_gate].base_class - - to_twirl = dag.op_nodes(twirled_gate_op) - twirl_indices = rng.integers(len(twirl_set), size=(len(to_twirl),)) - - for index, op_node in zip(twirl_indices, to_twirl): - dag.substitute_node_with_dag(op_node, twirl_set[index]) - return dag - - -TWIRLING_DAGS = { - key: [_dag_from_twirl(GATES[key], twirl) for twirl in twirls] - for key, twirls in TWIRLING_SETS.items() -} - class TestCircuitManipulate: def setup(self): @@ -149,7 +38,7 @@ def time_DTC100_twirling(self): """Perform Pauli-twirling on a 100Q QV circuit """ - out = circuit_twirl(self.dtc_qc) + out = pauli_twirl_2q_gates(self.dtc_qc, seed=12345678942) return out def time_multi_control_decompose(self): @@ -168,11 +57,3 @@ def time_QV100_basis_change(self): self.translate.property_set = PropertySet() out = self.translate.run(self.qv_qc) return out - - def time_DTC100_twirling_dag(self): - """Perform Pauli-twirling on a 100Q QV - circuit - """ - self.translate.property_set = PropertySet() - out = self.translate.run(self.qv_qc) - return circuit_to_dag(out) diff --git a/test/benchmarks/mapping_passes.py b/test/benchmarks/mapping_passes.py index 4f87323f33a8..07a6b037db5b 100644 --- a/test/benchmarks/mapping_passes.py +++ b/test/benchmarks/mapping_passes.py @@ -35,7 +35,7 @@ def setup(self, n_qubits, depth): n_qubits, depth, measure=True, conditional=True, reset=True, seed=seed, max_operands=2 ) self.fresh_dag = circuit_to_dag(self.circuit) - self.basis_gates = ["u1", "u2", "u3", "cx", "iid"] + self.basis_gates = ["u1", "u2", "u3", "cx", "id"] self.cmap = [ [0, 1], [1, 0], @@ -166,7 +166,7 @@ def setup(self, n_qubits, depth): n_qubits, depth, measure=True, conditional=True, reset=True, seed=seed, max_operands=2 ) self.fresh_dag = circuit_to_dag(self.circuit) - self.basis_gates = ["u1", "u2", "u3", "cx", "iid"] + self.basis_gates = ["u1", "u2", "u3", "cx", "id"] self.cmap = [ [0, 1], [1, 0], diff --git a/test/benchmarks/qasm3_exporter.py b/test/benchmarks/qasm3_exporter.py new file mode 100644 index 000000000000..3860efe8b407 --- /dev/null +++ b/test/benchmarks/qasm3_exporter.py @@ -0,0 +1,83 @@ +# 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. + +# pylint: disable=missing-docstring +# pylint: disable=attribute-defined-outside-init + +from qiskit.circuit import Parameter +from qiskit import QuantumCircuit +from qiskit import qasm3 + +from .utils import random_circuit + + +class RandomBenchmarks: + + params = ([20], [256, 1024], [0, 42]) + + param_names = ["n_qubits", "depth", "seed"] + timeout = 300 + + def setup(self, n_qubits, depth, seed): + self.circuit = random_circuit( + n_qubits, + depth, + measure=True, + conditional=True, + reset=True, + seed=seed, + max_operands=3, + ) + + def time_dumps(self, _, __, ___): + qasm3.dumps(self.circuit) + + +class CustomGateBenchmarks: + + params = ([200], [100]) + + param_names = ["n_qubits", "depth"] + timeout = 300 + + def setup(self, n_qubits, depth): + custom_gate = QuantumCircuit(2, name="custom_gate") + custom_gate.h(0) + custom_gate.x(1) + + qc = QuantumCircuit(n_qubits) + for _ in range(depth): + for i in range(n_qubits - 1): + qc.append(custom_gate.to_gate(), [i, i + 1]) + self.circuit = qc + + def time_dumps(self, _, __): + qasm3.dumps(self.circuit) + + +class ParameterizedBenchmarks: + + params = ([20, 50], [1, 5, 10]) + + param_names = ["n_qubits", "n_params"] + timeout = 300 + + def setup(self, n_qubits, n_params): + qc = QuantumCircuit(n_qubits) + params = [Parameter(f"angle{i}") for i in range(n_params)] + for n in range(n_qubits - 1): + for i in params: + qc.rx(i, n) + self.circuit = qc + + def time_dumps(self, _, __): + qasm3.dumps(self.circuit) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 012697a17dd0..6500e5593ac2 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -26,46 +26,54 @@ def test_lift_legacy_condition(self): clbit = Clbit() inst = Instruction("custom", 1, 0, []) - inst.c_if(cr, 7) - self.assertEqual( - expr.lift_legacy_condition(inst.condition), - expr.Binary( - expr.Binary.Op.EQUAL, - expr.Var(cr, types.Uint(cr.size)), - expr.Value(7, types.Uint(cr.size)), - types.Bool(), - ), - ) + with self.assertWarns(DeprecationWarning): + inst.c_if(cr, 7) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + expr.lift_legacy_condition(inst.condition), + expr.Binary( + expr.Binary.Op.EQUAL, + expr.Var(cr, types.Uint(cr.size)), + expr.Value(7, types.Uint(cr.size)), + types.Bool(), + ), + ) inst = Instruction("custom", 1, 0, []) - inst.c_if(cr, 255) - self.assertEqual( - expr.lift_legacy_condition(inst.condition), - expr.Binary( - expr.Binary.Op.EQUAL, - expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Uint(8), implicit=True), - expr.Value(255, types.Uint(8)), - types.Bool(), - ), - ) + with self.assertWarns(DeprecationWarning): + inst.c_if(cr, 255) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + expr.lift_legacy_condition(inst.condition), + expr.Binary( + expr.Binary.Op.EQUAL, + expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Uint(8), implicit=True), + expr.Value(255, types.Uint(8)), + types.Bool(), + ), + ) inst = Instruction("custom", 1, 0, []) - inst.c_if(clbit, False) - self.assertEqual( - expr.lift_legacy_condition(inst.condition), - expr.Unary( - expr.Unary.Op.LOGIC_NOT, - expr.Var(clbit, types.Bool()), - types.Bool(), - ), - ) + with self.assertWarns(DeprecationWarning): + inst.c_if(clbit, False) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + expr.lift_legacy_condition(inst.condition), + expr.Unary( + expr.Unary.Op.LOGIC_NOT, + expr.Var(clbit, types.Bool()), + types.Bool(), + ), + ) inst = Instruction("custom", 1, 0, []) - inst.c_if(clbit, True) - self.assertEqual( - expr.lift_legacy_condition(inst.condition), - expr.Var(clbit, types.Bool()), - ) + with self.assertWarns(DeprecationWarning): + inst.c_if(clbit, True) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + expr.lift_legacy_condition(inst.condition), + expr.Var(clbit, types.Bool()), + ) def test_value_lifts_qiskit_scalars(self): cr = ClassicalRegister(3, "c") diff --git a/test/python/circuit/library/test_adders.py b/test/python/circuit/library/test_adders.py index 0866cb766710..eacf8057775a 100644 --- a/test/python/circuit/library/test_adders.py +++ b/test/python/circuit/library/test_adders.py @@ -18,9 +18,30 @@ from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import Statevector -from qiskit.circuit.library import CDKMRippleCarryAdder, DraperQFTAdder, VBERippleCarryAdder +from qiskit.circuit.library import ( + CDKMRippleCarryAdder, + DraperQFTAdder, + VBERippleCarryAdder, + ModularAdderGate, + HalfAdderGate, + FullAdderGate, +) +from qiskit.synthesis.arithmetic import adder_ripple_c04, adder_ripple_v95, adder_qft_d00 +from qiskit.transpiler.passes import HLSConfig, HighLevelSynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order +ADDERS = { + "vbe": adder_ripple_v95, + "cdkm": adder_ripple_c04, + "draper": adder_qft_d00, +} + +ADDER_CIRCUITS = { + "vbe": VBERippleCarryAdder, + "cdkm": CDKMRippleCarryAdder, + "draper": DraperQFTAdder, +} + @ddt class TestAdder(QiskitTestCase): @@ -42,7 +63,7 @@ def assertAdditionIsCorrect( inplace: If True, compare against an inplace addition where the result is written into the second register plus carry qubit. If False, assume that the result is written into a third register of appropriate size. - kind: TODO + kind: The kind of adder; "fixed", "half", or "full". """ circuit = QuantumCircuit(*adder.qregs) @@ -100,39 +121,89 @@ def assertAdditionIsCorrect( np.testing.assert_array_almost_equal(expectations, probabilities) @data( - (3, CDKMRippleCarryAdder, True), - (5, CDKMRippleCarryAdder, True), - (3, CDKMRippleCarryAdder, True, "fixed"), - (5, CDKMRippleCarryAdder, True, "fixed"), - (1, CDKMRippleCarryAdder, True, "full"), - (3, CDKMRippleCarryAdder, True, "full"), - (5, CDKMRippleCarryAdder, True, "full"), - (3, DraperQFTAdder, True), - (5, DraperQFTAdder, True), - (3, DraperQFTAdder, True, "fixed"), - (5, DraperQFTAdder, True, "fixed"), - (1, VBERippleCarryAdder, True, "full"), - (3, VBERippleCarryAdder, True, "full"), - (5, VBERippleCarryAdder, True, "full"), - (1, VBERippleCarryAdder, True), - (2, VBERippleCarryAdder, True), - (5, VBERippleCarryAdder, True), - (1, VBERippleCarryAdder, True, "fixed"), - (2, VBERippleCarryAdder, True, "fixed"), - (4, VBERippleCarryAdder, True, "fixed"), + (3, "cdkm", "half"), + (5, "cdkm", "half"), + (3, "cdkm", "fixed"), + (5, "cdkm", "fixed"), + (1, "cdkm", "full"), + (3, "cdkm", "full"), + (5, "cdkm", "full"), + (3, "draper", "half"), + (5, "draper", "half"), + (3, "draper", "fixed"), + (5, "draper", "fixed"), + (1, "vbe", "full"), + (3, "vbe", "full"), + (5, "vbe", "full"), + (1, "vbe", "half"), + (2, "vbe", "half"), + (5, "vbe", "half"), + (1, "vbe", "fixed"), + (2, "vbe", "fixed"), + (4, "vbe", "fixed"), ) @unpack - def test_summation(self, num_state_qubits, adder, inplace, kind="half"): + def test_summation(self, num_state_qubits, adder, kind): """Test summation for all implemented adders.""" - adder = adder(num_state_qubits, kind=kind) - self.assertAdditionIsCorrect(num_state_qubits, adder, inplace, kind) + for use_function in [True, False]: + with self.subTest(use_function=use_function): + if use_function: + circuit = ADDERS[adder](num_state_qubits, kind) + else: + circuit = ADDER_CIRCUITS[adder](num_state_qubits, kind) + + self.assertAdditionIsCorrect(num_state_qubits, circuit, True, kind) - @data(CDKMRippleCarryAdder, DraperQFTAdder, VBERippleCarryAdder) + @data( + CDKMRippleCarryAdder, + DraperQFTAdder, + VBERippleCarryAdder, + adder_ripple_c04, + adder_ripple_v95, + adder_qft_d00, + ) def test_raises_on_wrong_num_bits(self, adder): """Test an error is raised for a bad number of qubits.""" with self.assertRaises(ValueError): _ = adder(-1) + def test_plugins(self): + """Test setting the HLS plugins for the modular adder.""" + + # all gates with the plugins we check + modes = { + "ModularAdder": (ModularAdderGate, ["ripple_c04", "ripple_v95", "qft_d00"]), + "HalfAdder": (HalfAdderGate, ["ripple_c04", "ripple_v95", "qft_d00"]), + "FullAdder": (FullAdderGate, ["ripple_c04", "ripple_v95"]), + } + + # an operation we expect to be in the circuit with given plugin name + expected_ops = { + "ripple_c04": "MAJ", + "ripple_v95": "Carry", + "qft_d00": "cp", + } + + num_state_qubits = 3 + max_auxiliaries = num_state_qubits - 1 # V95 needs these + max_num_qubits = 2 * num_state_qubits + max_auxiliaries + 2 + + for name, (adder_cls, plugins) in modes.items(): + for plugin in plugins: + with self.subTest(name=name, plugin=plugin): + adder = adder_cls(num_state_qubits) + + circuit = QuantumCircuit(max_num_qubits) + circuit.append(adder, range(adder.num_qubits)) + + hls_config = HLSConfig(**{name: [plugin]}) + hls = HighLevelSynthesis(hls_config=hls_config) + + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + + self.assertTrue(expected_ops[plugin] in ops) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_boolean_logic.py b/test/python/circuit/library/test_boolean_logic.py index cdadc9142a97..265f494f4c0c 100644 --- a/test/python/circuit/library/test_boolean_logic.py +++ b/test/python/circuit/library/test_boolean_logic.py @@ -16,9 +16,18 @@ from ddt import ddt, data, unpack import numpy as np -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import XOR, InnerProduct, AND, OR -from qiskit.quantum_info import Statevector +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.circuit.library import ( + XOR, + InnerProduct, + AND, + OR, + AndGate, + OrGate, + BitwiseXorGate, + InnerProductGate, +) +from qiskit.quantum_info import Statevector, Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -26,11 +35,15 @@ class TestBooleanLogicLibrary(QiskitTestCase): """Test library of boolean logic quantum circuits.""" - def assertBooleanFunctionIsCorrect(self, boolean_circuit, reference): - """Assert that ``boolean_circuit`` implements the reference boolean function correctly.""" - circuit = QuantumCircuit(boolean_circuit.num_qubits) - circuit.h(list(range(boolean_circuit.num_variable_qubits))) - circuit.append(boolean_circuit.to_instruction(), list(range(boolean_circuit.num_qubits))) + def assertBooleanFunctionIsCorrect(self, boolean_object, reference): + """Assert that ``boolean_object`` implements the reference boolean function correctly.""" + circuit = QuantumCircuit(boolean_object.num_qubits) + circuit.h(list(range(boolean_object.num_variable_qubits))) + + if isinstance(boolean_object, Gate): + circuit.append(boolean_object, list(range(boolean_object.num_qubits))) + else: + circuit.append(boolean_object.to_instruction(), list(range(boolean_object.num_qubits))) # compute the statevector of the circuit statevector = Statevector.from_label("0" * circuit.num_qubits) @@ -38,18 +51,18 @@ def assertBooleanFunctionIsCorrect(self, boolean_circuit, reference): # trace out ancillas probabilities = statevector.probabilities( - qargs=list(range(boolean_circuit.num_variable_qubits + 1)) + qargs=list(range(boolean_object.num_variable_qubits + 1)) ) # compute the expected outcome by computing the entries of the statevector that should # have a 1 / sqrt(2**n) factor expectations = np.zeros_like(probabilities) - for x in range(2**boolean_circuit.num_variable_qubits): - bits = np.array(list(bin(x)[2:].zfill(boolean_circuit.num_variable_qubits)), dtype=int) + for x in range(2**boolean_object.num_variable_qubits): + bits = np.array(list(bin(x)[2:].zfill(boolean_object.num_variable_qubits)), dtype=int) result = reference(bits[::-1]) - entry = int(str(int(result)) + bin(x)[2:].zfill(boolean_circuit.num_variable_qubits), 2) - expectations[entry] = 1 / 2**boolean_circuit.num_variable_qubits + entry = int(str(int(result)) + bin(x)[2:].zfill(boolean_object.num_variable_qubits), 2) + expectations[entry] = 1 / 2**boolean_object.num_variable_qubits np.testing.assert_array_almost_equal(probabilities, expectations) @@ -63,6 +76,39 @@ def test_xor(self): expected.x(2) self.assertEqual(circuit.decompose(), expected) + def test_xor_gate(self): + """Test XOR-gate.""" + xor_gate = BitwiseXorGate(num_qubits=3, amount=4) + expected = QuantumCircuit(3) + expected.x(2) + self.assertEqual(Operator(xor_gate), Operator(expected)) + + @data( + (5, 12), + (6, 21), + ) + @unpack + def test_xor_equivalence(self, num_qubits, amount): + """Test that XOR-circuit and BitwiseXorGate yield equal operators.""" + xor_gate = BitwiseXorGate(num_qubits, amount) + xor_circuit = XOR(num_qubits, amount) + self.assertEqual(Operator(xor_gate), Operator(xor_circuit)) + + def test_xor_eq(self): + """Test BitwiseXorGate's equality method.""" + xor1 = BitwiseXorGate(num_qubits=5, amount=10) + xor2 = BitwiseXorGate(num_qubits=5, amount=10) + xor3 = BitwiseXorGate(num_qubits=5, amount=11) + self.assertEqual(xor1, xor2) + self.assertNotEqual(xor1, xor3) + + def test_xor_inverse(self): + """Test correctness of the BitwiseXorGate's inverse.""" + xor_gate = BitwiseXorGate(num_qubits=5, amount=10) + xor_gate_inverse = xor_gate.inverse() + self.assertEqual(xor_gate, xor_gate_inverse) + self.assertEqual(Operator(xor_gate), Operator(xor_gate_inverse).adjoint()) + def test_inner_product(self): """Test inner product circuit. @@ -75,6 +121,22 @@ def test_inner_product(self): expected.cz(2, 5) self.assertEqual(circuit.decompose(), expected) + def test_inner_product_gate(self): + """Test inner product gate.""" + inner_product = InnerProductGate(num_qubits=3) + expected = QuantumCircuit(6) + expected.cz(0, 3) + expected.cz(1, 4) + expected.cz(2, 5) + self.assertEqual(Operator(inner_product), Operator(expected)) + + @data(4, 5, 6) + def test_inner_product_equivalence(self, num_qubits): + """Test that XOR-circuit and BitwiseXorGate yield equal operators.""" + inner_product_gate = InnerProductGate(num_qubits) + inner_product_circuit = InnerProduct(num_qubits) + self.assertEqual(Operator(inner_product_gate), Operator(inner_product_circuit)) + @data( (2, None, "noancilla"), (5, None, "noancilla"), @@ -100,6 +162,57 @@ def reference(bits): self.assertBooleanFunctionIsCorrect(or_circuit, reference) + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_or_gate(self, num_variables, flags): + """Test correctness of the OrGate.""" + or_gate = OrGate(num_variables, flags) + flags = flags or [1] * num_variables + + def reference(bits): + flagged = [] + for flag, bit in zip(flags, bits): + if flag < 0: + flagged += [1 - bit] + elif flag > 0: + flagged += [bit] + return np.any(flagged) + + self.assertBooleanFunctionIsCorrect(or_gate, reference) + + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_or_gate_inverse(self, num_variables, flags): + """Test correctness of the OrGate's inverse.""" + or_gate = OrGate(num_variables, flags) + or_gate_inverse = or_gate.inverse() + self.assertEqual(Operator(or_gate), Operator(or_gate_inverse).adjoint()) + + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_or_equivalence(self, num_variables, flags): + """Test that OR-circuit and OrGate yield equal operators + (when not using ancilla qubits). + """ + or_gate = OrGate(num_variables, flags) + or_circuit = OR(num_variables, flags) + self.assertEqual(Operator(or_gate), Operator(or_circuit)) + @data( (2, None, "noancilla"), (2, [-1, 1], "v-chain"), @@ -108,7 +221,7 @@ def reference(bits): ) @unpack def test_and(self, num_variables, flags, mcx_mode): - """Test the and circuit.""" + """Test the AND-circuit.""" and_circuit = AND(num_variables, flags, mcx_mode=mcx_mode) flags = flags or [1] * num_variables @@ -123,6 +236,57 @@ def reference(bits): self.assertBooleanFunctionIsCorrect(and_circuit, reference) + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_and_gate(self, num_variables, flags): + """Test correctness of the AndGate.""" + and_gate = AndGate(num_variables, flags) + flags = flags or [1] * num_variables + + def reference(bits): + flagged = [] + for flag, bit in zip(flags, bits): + if flag < 0: + flagged += [1 - bit] + elif flag > 0: + flagged += [bit] + return np.all(flagged) + + self.assertBooleanFunctionIsCorrect(and_gate, reference) + + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_and_gate_inverse(self, num_variables, flags): + """Test correctness of the AND-gate inverse.""" + and_gate = AndGate(num_variables, flags) + and_gate_inverse = and_gate.inverse() + self.assertEqual(Operator(and_gate), Operator(and_gate_inverse).adjoint()) + + @data( + (2, None), + (2, [-1, 1]), + (5, [0, 0, -1, 1, -1]), + (5, [-1, 0, 0, 1, 1]), + ) + @unpack + def test_and_equivalence(self, num_variables, flags): + """Test that AND-circuit and AND-gate yield equal operators + (when not using ancilla qubits). + """ + and_gate = AndGate(num_variables, flags) + and_circuit = AND(num_variables, flags) + self.assertEqual(Operator(and_gate), Operator(and_circuit)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index a7c9a73abb5f..1b55ea920289 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -12,17 +12,20 @@ """Test the evolution gate.""" +from itertools import permutations + import unittest import numpy as np import scipy from ddt import ddt, data, unpack from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import PauliEvolutionGate +from qiskit.circuit.library import PauliEvolutionGate, HamiltonianGate from qiskit.synthesis import LieTrotter, SuzukiTrotter, MatrixExponential, QDrift -from qiskit.synthesis.evolution.product_formula import cnot_chain, diagonalizing_clifford +from qiskit.synthesis.evolution.product_formula import reorder_paulis from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, Statevector +from qiskit.transpiler.passes import HLSConfig, HighLevelSynthesis from test import QiskitTestCase # pylint: disable=wrong-import-order X = SparsePauliOp("X") @@ -40,6 +43,19 @@ def setUp(self): # fix random seed for reproducibility (used in QDrift) self.seed = 2 + def assertSuzukiTrotterIsCorrect(self, gate): + """Assert the Suzuki Trotter evolution is correct.""" + op = gate.operator + time = gate.time + synthesis = gate.synthesis + + exact_suzuki = SuzukiTrotter( + reps=synthesis.reps, order=synthesis.order, atomic_evolution=exact_atomic_evolution + ) + exact_gate = PauliEvolutionGate(op, time, synthesis=exact_suzuki) + + self.assertTrue(Operator(gate).equiv(exact_gate)) + def test_matrix_decomposition(self): """Test the default decomposition.""" op = (X ^ X ^ X) + (Y ^ Y ^ Y) + (Z ^ Z ^ Z) @@ -52,6 +68,31 @@ def test_matrix_decomposition(self): self.assertTrue(Operator(evo_gate).equiv(evolved)) + def test_reorder_paulis_invariant(self): + """ + Tests that reorder_paulis is deterministic and does not depend on the + order of the terms of the input operator. + """ + terms = [ + (I ^ I ^ X ^ X), + (I ^ I ^ Z ^ Z), + (I ^ Y ^ Y ^ I), + (X ^ I ^ I ^ I), + (X ^ X ^ I ^ I), + (Y ^ I ^ I ^ Y), + ] + results = [] + for seed, tms in enumerate(permutations(terms)): + np.random.seed(seed) + op = reorder_paulis(SparsePauliOp(sum(tms)).to_sparse_list()) + results.append([(t[0], t[1]) for t in op]) + np.random.seed(seed + 42) + op = reorder_paulis(SparsePauliOp(sum(tms)).to_sparse_list()) + results.append([(t[0], t[1]) for t in op]) + + for lst in results[1:]: + self.assertListEqual(lst, results[0]) + def test_lie_trotter(self): """Test constructing the circuit with Lie Trotter decomposition.""" op = (X ^ X ^ X) + (Y ^ Y ^ Y) + (Z ^ Z ^ Z) @@ -59,7 +100,16 @@ def test_lie_trotter(self): reps = 4 evo_gate = PauliEvolutionGate(op, time, synthesis=LieTrotter(reps=reps)) decomposed = evo_gate.definition.decompose() + self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) + self.assertSuzukiTrotterIsCorrect(evo_gate) + + def test_basis_change(self): + """Test the basis change is correctly implemented.""" + op = I ^ Y # use a string for which we do not have a basis gate + time = 0.321 + evo_gate = PauliEvolutionGate(op, time) + self.assertSuzukiTrotterIsCorrect(evo_gate) def test_rzx_order(self): """Test ZX and XZ is mapped onto the correct qubits.""" @@ -105,13 +155,16 @@ def test_suzuki_trotter(self): ) decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], expected_cx) + self.assertSuzukiTrotterIsCorrect(evo_gate) - def test_suzuki_trotter_manual(self): + def test_suzuki_trotter_manual_no_reorder(self): """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" op = X + Y time = 0.1 reps = 1 - evo_gate = PauliEvolutionGate(op, time, synthesis=SuzukiTrotter(order=4, reps=reps)) + evo_gate = PauliEvolutionGate( + op, time, synthesis=SuzukiTrotter(order=4, reps=reps, preserve_order=True) + ) # manually construct expected evolution expected = QuantumCircuit(1) @@ -132,6 +185,40 @@ def test_suzuki_trotter_manual(self): expected.rx(p_4 * time, 0) self.assertEqual(evo_gate.definition, expected) + self.assertSuzukiTrotterIsCorrect(evo_gate) + + @data(True, False) + def test_suzuki_trotter_manual(self, use_plugin): + """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" + op = (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) + time, reps = 0.1, 1 + + synthesis = SuzukiTrotter(order=2, reps=reps) + if use_plugin: + hls_config = HLSConfig(PauliEvolution=[("default", {"preserve_order": False})]) + else: + synthesis.preserve_order = False + hls_config = None + + evo_gate = PauliEvolutionGate(op, time, synthesis=synthesis) + circuit = QuantumCircuit(op.num_qubits) + circuit.append(evo_gate, circuit.qubits) + + if use_plugin: + decomposed = HighLevelSynthesis(hls_config=hls_config)(circuit) + else: + decomposed = circuit.decompose() + + expected = QuantumCircuit(4) + expected.rzz(time, 0, 1) + expected.rxx(time, 2, 3) + expected.ryy(2 * time, 1, 2) + expected.rxx(time, 2, 3) + expected.rzz(time, 0, 1) + self.assertEqual(decomposed, expected) + + def test_suzuki_trotter_plugin(self): + """Test setting options via the plugin.""" @data( (X + Y, 0.5, 1, [(Pauli("X"), 0.5), (Pauli("X"), 0.5)]), @@ -185,6 +272,7 @@ def test_passing_grouped_paulis(self, wrap): decomposed = evo_gate.definition.decompose() else: decomposed = evo_gate.definition + self.assertEqual(decomposed.count_ops()["rz"], 4) self.assertEqual(decomposed.count_ops()["rzz"], 1) self.assertEqual(decomposed.count_ops()["rxx"], 1) @@ -251,6 +339,7 @@ def test_cnot_chain_options(self, option): expected.cx(1, 0) self.assertEqual(expected, evo.definition) + self.assertSuzukiTrotterIsCorrect(evo) @data( Pauli("XI"), @@ -275,6 +364,7 @@ def test_pauliop_coefficients_respected(self): circuit = evo.definition.decompose() rz_angle = circuit.data[0].operation.params[0] self.assertEqual(rz_angle, 10) + self.assertSuzukiTrotterIsCorrect(evo) def test_paulisumop_coefficients_respected(self): """Test that global ``PauliSumOp`` coefficients are being taken care of.""" @@ -286,6 +376,7 @@ def test_paulisumop_coefficients_respected(self): circuit.data[2].operation.params[0], # Z ] self.assertListEqual(rz_angles, [20, 30, -10]) + self.assertSuzukiTrotterIsCorrect(evo) def test_lie_trotter_two_qubit_correct_order(self): """Test that evolutions on two qubit operators are in the right order. @@ -294,10 +385,26 @@ def test_lie_trotter_two_qubit_correct_order(self): """ operator = I ^ Z ^ Z time = 0.5 - exact = scipy.linalg.expm(-1j * time * operator.to_matrix()) lie_trotter = PauliEvolutionGate(operator, time, synthesis=LieTrotter()) - self.assertTrue(Operator(lie_trotter).equiv(exact)) + self.assertSuzukiTrotterIsCorrect(lie_trotter) + + def test_lie_trotter_reordered_manual(self): + """Test the evolution circuit of Lie Trotter against a manually constructed circuit.""" + op = (X ^ I ^ I ^ I) + (X ^ X ^ I ^ I) + (I ^ Y ^ Y ^ I) + (I ^ I ^ Z ^ Z) + time, reps = 0.1, 1 + evo_gate = PauliEvolutionGate( + op, + time, + synthesis=LieTrotter(reps=reps, preserve_order=False), + ) + # manually construct expected evolution + expected = QuantumCircuit(4) + expected.rxx(2 * time, 2, 3) + expected.rzz(2 * time, 0, 1) + expected.rx(2 * time, 3) + expected.ryy(2 * time, 1, 2) + self.assertEqual(evo_gate.definition, expected) def test_complex_op_raises(self): """Test an operator with complex coefficient raises an error.""" @@ -336,6 +443,12 @@ def test_atomic_evolution(self): """Test a custom atomic_evolution.""" def atomic_evolution(pauli, time): + if isinstance(pauli, SparsePauliOp): + if len(pauli.paulis) != 1: + raise ValueError("Unsupported input.") + time *= np.real(pauli.coeffs[0]) + pauli = pauli.paulis[0] + cliff = diagonalizing_clifford(pauli) chain = cnot_chain(pauli) @@ -359,11 +472,73 @@ def atomic_evolution(pauli, time): reps = 4 with self.assertWarns(PendingDeprecationWarning): evo_gate = PauliEvolutionGate( - op, time, synthesis=LieTrotter(reps=reps, atomic_evolution=atomic_evolution) + op, + time, + synthesis=LieTrotter(reps=reps, atomic_evolution=atomic_evolution), ) decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["cx"], reps * 3 * 4) +def exact_atomic_evolution(circuit, pauli, time): + """An exact atomic evolution for Suzuki-Trotter. + + Note that the Pauli has a x2 coefficient already, hence we evolve for time/2. + """ + circuit.append(HamiltonianGate(pauli.to_matrix(), time / 2), circuit.qubits) + + +def diagonalizing_clifford(pauli: Pauli) -> QuantumCircuit: + """Get the clifford circuit to diagonalize the Pauli operator.""" + cliff = QuantumCircuit(pauli.num_qubits) + for i, pauli_i in enumerate(reversed(pauli.to_label())): + if pauli_i == "Y": + cliff.sx(i) + elif pauli_i == "X": + cliff.h(i) + + return cliff + + +def cnot_chain(pauli: Pauli) -> QuantumCircuit: + """CX chain. + + For example, for the Pauli with the label 'XYZIX'. + + .. parsed-literal:: + + ┌───┐ + q_0: ──────────┤ X ├ + └─┬─┘ + q_1: ────────────┼── + ┌───┐ │ + q_2: ─────┤ X ├──■── + ┌───┐└─┬─┘ + q_3: ┤ X ├──■─────── + └─┬─┘ + q_4: ──■──────────── + + """ + + chain = QuantumCircuit(pauli.num_qubits) + control, target = None, None + + # iterate over the Pauli's and add CNOTs + for i, pauli_i in enumerate(pauli.to_label()): + i = pauli.num_qubits - i - 1 + if pauli_i != "I": + if control is None: + control = i + else: + target = i + + if control is not None and target is not None: + chain.cx(control, target) + control = i + target = None + + return chain + + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 8ea66b7012fe..9c923764f408 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -12,25 +12,37 @@ """Test the evolved operator ansatz.""" +from ddt import ddt, data import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import SparsePauliOp, Operator, Pauli from qiskit.circuit.library import HamiltonianGate -from qiskit.circuit.library.n_local import EvolvedOperatorAnsatz +from qiskit.circuit.library.n_local import ( + EvolvedOperatorAnsatz, + evolved_operator_ansatz, + hamiltonian_variational_ansatz, +) from qiskit.synthesis.evolution import MatrixExponential from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestEvolvedOperatorAnsatz(QiskitTestCase): """Test the evolved operator ansatz.""" - def test_evolved_op_ansatz(self): + @data(True, False) + def test_evolved_op_ansatz(self, use_function): """Test the default evolution.""" num_qubits = 3 ops = [Pauli("Z" * num_qubits), Pauli("Y" * num_qubits), Pauli("X" * num_qubits)] - evo = EvolvedOperatorAnsatz(ops, 2) + + if use_function: + evo = evolved_operator_ansatz(ops, 2) + else: + evo = EvolvedOperatorAnsatz(ops, 2) + parameters = evo.parameters reference = QuantumCircuit(num_qubits) @@ -38,22 +50,32 @@ def test_evolved_op_ansatz(self): for string, time in zip(strings, parameters): reference.compose(evolve(string, time), inplace=True) - self.assertEqual(evo.decompose().decompose(), reference) + if not use_function: + evo = evo.decompose().decompose() - def test_custom_evolution(self): + self.assertEqual(evo, reference) + + @data(True, False) + def test_custom_evolution(self, use_function): """Test using another evolution than the default (e.g. matrix evolution).""" op = SparsePauliOp(["ZIX"]) matrix = np.array(op) evolution = MatrixExponential() - evo = EvolvedOperatorAnsatz(op, evolution=evolution) + + if use_function: + evo = evolved_operator_ansatz(op, evolution=evolution) + else: + evo = EvolvedOperatorAnsatz(op, evolution=evolution) + parameters = evo.parameters reference = QuantumCircuit(3) reference.append(HamiltonianGate(matrix, parameters[0]), [0, 1, 2]) - decomposed = evo.decompose().decompose() + if not use_function: + evo = evo.decompose().decompose() - self.assertEqual(decomposed, reference) + self.assertEqual(evo, reference) def test_changing_operators(self): """Test rebuilding after the operators changed.""" @@ -68,12 +90,16 @@ def test_changing_operators(self): self.assertEqual(evo.decompose(reps=2), reference) - def test_invalid_reps(self): + @data(True, False) + def test_invalid_reps(self, use_function): """Test setting an invalid number of reps.""" with self.assertRaises(ValueError): - _ = EvolvedOperatorAnsatz(Pauli("X"), reps=-1) + if use_function: + _ = evolved_operator_ansatz(Pauli("X"), reps=-1) + else: + _ = EvolvedOperatorAnsatz(Pauli("X"), reps=-1) - def test_insert_barriers(self): + def test_insert_barriers_circuit(self): """Test using insert_barriers.""" evo = EvolvedOperatorAnsatz(Pauli("Z"), reps=4, insert_barriers=True) ref = QuantumCircuit(1) @@ -83,21 +109,43 @@ def test_insert_barriers(self): self.assertEqual(evo.decompose(reps=2), ref) + def test_insert_barriers(self): + """Test using insert_barriers.""" + evo = evolved_operator_ansatz(Pauli("Z"), reps=4, insert_barriers=True) + ref = QuantumCircuit(1) + for i, parameter in enumerate(evo.parameters): + ref.rz(2.0 * parameter, 0) + if i < evo.num_parameters - 1: + ref.barrier() + + self.assertEqual(evo, ref) + def test_empty_build_fails(self): """Test setting no operators to evolve raises the appropriate error.""" evo = EvolvedOperatorAnsatz() with self.assertRaises(ValueError): _ = evo.draw() - def test_empty_operator_list(self): + @data(True, False) + def test_empty_operator_list(self, use_function): """Test setting an empty list of operators to be equal to an empty circuit.""" - evo = EvolvedOperatorAnsatz([]) + if use_function: + evo = evolved_operator_ansatz([]) + else: + evo = EvolvedOperatorAnsatz([]) + self.assertEqual(evo, QuantumCircuit()) - def test_matrix_operator(self): + @data(True, False) + def test_matrix_operator(self, use_function): """Test passing a quantum_info.Operator uses the HamiltonianGate.""" unitary = Operator([[0, 1], [1, 0]]) - evo = EvolvedOperatorAnsatz(unitary, reps=3).decompose() + + if use_function: + evo = evolved_operator_ansatz(unitary, reps=3) + else: + evo = EvolvedOperatorAnsatz(unitary, reps=3).decompose() + self.assertEqual(evo.count_ops()["hamiltonian"], 3) def test_flattened(self): @@ -109,6 +157,48 @@ def test_flattened(self): self.assertNotIn("EvolvedOps", evo.count_ops()) self.assertNotIn("PauliEvolution", evo.count_ops()) + def test_flattening(self): + """Test ``flatten`` on the function.""" + operators = [Operator.from_label("X"), SparsePauliOp(["Z"])] + + with self.subTest(flatten=None): + evo = evolved_operator_ansatz(operators, flatten=None) + ops = evo.count_ops() + self.assertIn("hamiltonian", ops) + self.assertNotIn("PauliEvolution", ops) + + with self.subTest(flatten=True): + # check we get a warning when trying to flatten a HamiltonianGate, + # which has an unbound param and cannot be flattened + with self.assertWarnsRegex(UserWarning, "Cannot flatten"): + evo = evolved_operator_ansatz(operators, flatten=True) + + ops = evo.count_ops() + self.assertIn("hamiltonian", ops) + self.assertNotIn("PauliEvolution", ops) + + with self.subTest(flatten=False): + evo = evolved_operator_ansatz(operators, flatten=False) + ops = evo.count_ops() + self.assertIn("hamiltonian", ops) + self.assertIn("PauliEvolution", ops) + + +class TestHamiltonianVariationalAnsatz(QiskitTestCase): + """Test the hamiltonian_variational_ansatz function. + + This is essentially already tested by the evolved_operator_ansatz, we just need + to test the additional commuting functionality. + """ + + def test_detect_commutation(self): + """Test the operator is split into commuting terms.""" + hamiltonian = SparsePauliOp(["XII", "ZZI", "IXI", "IZZ", "IIX"]) + circuit = hamiltonian_variational_ansatz(hamiltonian) + + # this Hamiltonian should be split into 2 commuting groups, hence we get 2 parameters + self.assertEqual(2, circuit.num_parameters) + def evolve(pauli_string, time): """Get the reference evolution circuit for a single Pauli string.""" @@ -119,8 +209,7 @@ def evolve(pauli_string, time): if pauli == "x": forward.h(i) elif pauli == "y": - forward.sdg(i) - forward.h(i) + forward.sx(i) for i in range(1, num_qubits): forward.cx(num_qubits - i, num_qubits - i - 1) diff --git a/test/python/circuit/library/test_fourier_checking.py b/test/python/circuit/library/test_fourier_checking.py index 23595d70d95c..31490584b100 100644 --- a/test/python/circuit/library/test_fourier_checking.py +++ b/test/python/circuit/library/test_fourier_checking.py @@ -16,7 +16,7 @@ from ddt import ddt, data, unpack import numpy as np -from qiskit.circuit.library import FourierChecking +from qiskit.circuit.library import FourierChecking, fourier_checking from qiskit.circuit.exceptions import CircuitError from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -61,6 +61,20 @@ def test_invalid_input_raises(self, f_truth_table, g_truth_table): with self.assertRaises(CircuitError): FourierChecking(f_truth_table, g_truth_table) + @data(([1, -1, -1, -1], [1, 1, -1, -1]), ([1, 1, 1, 1], [1, 1, 1, 1])) + @unpack + def test_fourier_checking_function(self, f_truth_table, g_truth_table): + """Test if the Fourier Checking circuit produces the correct matrix.""" + fc_circuit = fourier_checking(f_truth_table, g_truth_table) + self.assertFourierCheckingIsCorrect(f_truth_table, g_truth_table, fc_circuit) + + @data(([1, -1, -1, -1], [1, 1, -1]), ([1], [-1]), ([1, -1, -1, -1, 1], [1, 1, -1, -1, 1])) + @unpack + def test_invalid_input_raises_function(self, f_truth_table, g_truth_table): + """Test that invalid input truth tables raise an error.""" + with self.assertRaises(CircuitError): + fourier_checking(f_truth_table, g_truth_table) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_graph_state.py b/test/python/circuit/library/test_graph_state.py index dbefa2cebd3d..3e1e3a02967b 100644 --- a/test/python/circuit/library/test_graph_state.py +++ b/test/python/circuit/library/test_graph_state.py @@ -13,10 +13,12 @@ """Test library of graph state circuits.""" import unittest +import numpy as np +from qiskit.circuit import Gate from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.library import GraphState -from qiskit.quantum_info import Clifford +from qiskit.circuit.library import GraphState, GraphStateGate +from qiskit.quantum_info import Clifford, Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -24,11 +26,15 @@ class TestGraphStateLibrary(QiskitTestCase): """Test the graph state circuit.""" def assertGraphStateIsCorrect(self, adjacency_matrix, graph_state): - """Check the stabilizers of the graph state against the expected stabilizers. + """Check the stabilizers of the graph state circuit/gate against the expected stabilizers. Based on https://arxiv.org/pdf/quant-ph/0307130.pdf, Eq. (6). """ + if isinstance(graph_state, Gate): + cliff = Clifford(graph_state.definition) + else: + cliff = Clifford(graph_state) - stabilizers = [stabilizer[1:] for stabilizer in Clifford(graph_state).to_labels(mode="S")] + stabilizers = [stabilizer[1:] for stabilizer in cliff.to_labels(mode="S")] expected_stabilizers = [] # keep track of all expected stabilizers num_vertices = len(adjacency_matrix) @@ -47,7 +53,7 @@ def assertGraphStateIsCorrect(self, adjacency_matrix, graph_state): self.assertListEqual(expected_stabilizers, stabilizers) - def test_graph_state(self): + def test_graph_state_circuit(self): """Verify the GraphState by checking if the circuit has the expected stabilizers.""" adjacency_matrix = [ [0, 1, 0, 0, 1], @@ -59,12 +65,82 @@ def test_graph_state(self): graph_state = GraphState(adjacency_matrix) self.assertGraphStateIsCorrect(adjacency_matrix, graph_state) - def test_non_symmetric_raises(self): + def test_non_symmetric_circuit_raises(self): """Test that adjacency matrix is required to be symmetric.""" adjacency_matrix = [[1, 1, 0], [1, 0, 1], [1, 1, 1]] with self.assertRaises(CircuitError): GraphState(adjacency_matrix) + def test_graph_state_gate(self): + """Verify correctness of GraphStatGate by checking that the gate's definition circuit + has the expected stabilizers. + """ + adjacency_matrix = [ + [0, 1, 0, 0, 1], + [1, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 0], + ] + graph_state = GraphStateGate(adjacency_matrix) + self.assertGraphStateIsCorrect(adjacency_matrix, graph_state) + + def test_non_symmetric_gate_raises(self): + """Test that adjacency matrix is required to be symmetric.""" + adjacency_matrix = [[1, 1, 0], [1, 0, 1], [1, 1, 1]] + with self.assertRaises(CircuitError): + GraphStateGate(adjacency_matrix) + + def test_circuit_and_gate_equivalence(self): + """Test that GraphState-circuit and GraphStateGate yield equal operators.""" + adjacency_matrix = [ + [0, 1, 0, 0, 1], + [1, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 0], + ] + graph_state_gate = GraphStateGate(adjacency_matrix) + graph_state_circuit = GraphState(adjacency_matrix) + self.assertEqual(Operator(graph_state_gate), Operator(graph_state_circuit)) + + def test_adjacency_matrix(self): + """Test GraphStateGate's adjacency_matrix method.""" + adjacency_matrix = [ + [0, 1, 0, 0, 1], + [1, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 0], + ] + graph_state_gate = GraphStateGate(adjacency_matrix) + self.assertTrue(np.all(graph_state_gate.adjacency_matrix == adjacency_matrix)) + + def test_equality(self): + """Test GraphStateGate's equality method.""" + mat1 = [ + [0, 1, 0, 0, 1], + [1, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 0], + ] + mat2 = [ + [0, 1, 0, 0, 1], + [1, 0, 0, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 0], + ] + with self.subTest("equal"): + gate1 = GraphStateGate(mat1) + gate2 = GraphStateGate(mat1) + self.assertEqual(gate1, gate2) + with self.subTest("not equal"): + gate1 = GraphStateGate(mat1) + gate2 = GraphStateGate(mat2) + self.assertNotEqual(gate1, gate2) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_grover_operator.py b/test/python/circuit/library/test_grover_operator.py index 5a4f74f161c9..2c4171b96208 100644 --- a/test/python/circuit/library/test_grover_operator.py +++ b/test/python/circuit/library/test_grover_operator.py @@ -13,15 +13,18 @@ """Test the grover operator.""" import unittest +from ddt import ddt, data import numpy as np -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import GroverOperator +from qiskit import transpile +from qiskit.circuit import QuantumCircuit, Qubit, AncillaQubit +from qiskit.circuit.library import GroverOperator, grover_operator from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, Statevector, DensityMatrix from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestGroverOperator(QiskitTestCase): """Test the Grover operator.""" @@ -43,12 +46,14 @@ def assertGroverOperatorIsCorrect(self, grover_op, oracle, state_in=None, zero_r expected = state_in.dot(zero_reflection).dot(state_in.adjoint()).dot(oracle) self.assertTrue(Operator(grover_op).equiv(expected)) - def test_grover_operator(self): + @data(True, False) + def test_grover_operator(self, use_function): """Test the base case for the Grover operator.""" + grover_constructor = grover_operator if use_function else GroverOperator with self.subTest("single Z oracle"): oracle = QuantumCircuit(3) oracle.z(2) # good state if last qubit is 1 - grover_op = GroverOperator(oracle) + grover_op = grover_constructor(oracle) self.assertGroverOperatorIsCorrect(grover_op, oracle) with self.subTest("target state x0x1"): @@ -57,19 +62,23 @@ def test_grover_operator(self): oracle.z(1) oracle.x(1) oracle.z(3) - grover_op = GroverOperator(oracle) + grover_op = grover_constructor(oracle) self.assertGroverOperatorIsCorrect(grover_op, oracle) - def test_quantum_info_input(self): + @data(True, False) + def test_quantum_info_input(self, use_function): """Test passing quantum_info.Operator and Statevector as input.""" + grover_constructor = grover_operator if use_function else GroverOperator + mark = Statevector.from_label("001") diffuse = 2 * DensityMatrix.from_label("000") - Operator.from_label("III") - grover_op = GroverOperator(oracle=mark, zero_reflection=diffuse) + grover_op = grover_constructor(oracle=mark, zero_reflection=diffuse) self.assertGroverOperatorIsCorrect( grover_op, oracle=np.diag((-1) ** mark.data), zero_reflection=diffuse.data ) - def test_stateprep_contains_instruction(self): + @data(True, False) + def test_stateprep_contains_instruction(self, use_function): """Test wrapping works if the state preparation is not unitary.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -81,18 +90,24 @@ def test_stateprep_contains_instruction(self): stateprep = QuantumCircuit(1) stateprep.append(instr, [0]) - grover_op = GroverOperator(oracle, stateprep) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, stateprep) self.assertEqual(grover_op.num_qubits, 1) - def test_reflection_qubits(self): + @data(True, False) + def test_reflection_qubits(self, use_function): """Test setting idle qubits doesn't apply any operations on these qubits.""" oracle = QuantumCircuit(4) oracle.z(3) - grover_op = GroverOperator(oracle, reflection_qubits=[0, 3]) + + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, reflection_qubits=[0, 3]) + dag = circuit_to_dag(grover_op.decompose()) self.assertEqual(set(dag.idle_wires()), {dag.qubits[1], dag.qubits[2]}) - def test_custom_state_in(self): + @data(True, False) + def test_custom_state_in(self, use_function): """Test passing a custom state_in operator.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -101,10 +116,13 @@ def test_custom_state_in(self): sampling_probability = 0.2 bernoulli.ry(2 * np.arcsin(np.sqrt(sampling_probability)), 0) - grover_op = GroverOperator(oracle, bernoulli) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, bernoulli) + self.assertGroverOperatorIsCorrect(grover_op, oracle, bernoulli) - def test_custom_zero_reflection(self): + @data(True, False) + def test_custom_zero_reflection(self, use_function): """Test passing in a custom zero reflection.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -114,7 +132,8 @@ def test_custom_zero_reflection(self): zero_reflection.rz(np.pi, 0) zero_reflection.x(0) - grover_op = GroverOperator(oracle, zero_reflection=zero_reflection) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, zero_reflection=zero_reflection) with self.subTest("zero reflection up to phase works"): self.assertGroverOperatorIsCorrect(grover_op, oracle) @@ -125,9 +144,10 @@ def test_custom_zero_reflection(self): expected.h(0) # state_in is H expected.compose(zero_reflection, inplace=True) expected.h(0) - self.assertEqual(expected, grover_op.decompose()) + self.assertEqual(expected, grover_op if use_function else grover_op.decompose()) - def test_num_mcx_ancillas(self): + @data(True, False) + def test_num_mcx_ancillas(self, use_function): """Test the number of ancilla bits for the mcx gate in zero_reflection.""" # # q_0: ──■────────────────────── @@ -152,9 +172,50 @@ def test_num_mcx_ancillas(self): oracle.ccx(4, 5, 6) oracle.h(6) oracle.x(6) - grover_op = GroverOperator(oracle, reflection_qubits=[0, 1]) + + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, reflection_qubits=[0, 1]) self.assertEqual(grover_op.width(), 7) + def test_mcx_allocation(self): + """The the automatic allocation of auxiliary qubits for MCX.""" + num_qubits = 10 + oracle = QuantumCircuit(num_qubits) + oracle.z(oracle.qubits) + + grover_op = grover_operator(oracle) + + # without extra qubit space, the MCX gates are synthesized without ancillas + basis_gates = ["u", "cx"] + + is_2q = lambda inst: len(inst.qubits) == 2 + + with self.subTest(msg="no auxiliaries"): + tqc = transpile(grover_op, basis_gates=basis_gates) + depth = tqc.depth(filter_function=is_2q) + self.assertLess(depth, 500) + self.assertGreater(depth, 100) + + # add extra bits that can be used as scratch space + grover_op.add_bits([Qubit() for _ in range(num_qubits)]) + with self.subTest(msg="with auxiliaries"): + tqc = transpile(grover_op, basis_gates=basis_gates) + depth = tqc.depth(filter_function=is_2q) + self.assertLess(depth, 100) + + def test_ancilla_detection(self): + """Test AncillaQubit objects are correctly identified in the oracle.""" + qubits = [AncillaQubit(), Qubit()] + oracle = QuantumCircuit() + oracle.add_bits(qubits) + oracle.z(qubits[1]) # the "good" state is qubit 1 being in state |1> + + grover_op = grover_operator(oracle) + + expected_h = 2 # would be 4 if the ancilla is not detected + + self.assertEqual(expected_h, grover_op.count_ops().get("h", 0)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_hidden_linear_function.py b/test/python/circuit/library/test_hidden_linear_function.py index bc7d9c7545b6..fce0ffb04cd9 100644 --- a/test/python/circuit/library/test_hidden_linear_function.py +++ b/test/python/circuit/library/test_hidden_linear_function.py @@ -17,7 +17,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.library import HiddenLinearFunction +from qiskit.circuit.library import HiddenLinearFunction, hidden_linear_function from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -60,6 +60,17 @@ def test_non_symmetric_raises(self): with self.assertRaises(CircuitError): HiddenLinearFunction([[1, 1, 0], [1, 0, 1], [1, 1, 1]]) + def test_hlf_function(self): + """Test if the HLF matrix produces the right matrix.""" + hidden_function = [[1, 1, 0], [1, 0, 1], [0, 1, 1]] + hlf = hidden_linear_function(hidden_function) + self.assertHLFIsCorrect(hidden_function, hlf) + + def test_non_symmetric_raises_function(self): + """Test that adjacency matrix is required to be symmetric.""" + with self.assertRaises(CircuitError): + hidden_linear_function([[1, 1, 0], [1, 0, 1], [1, 1, 1]]) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_iqp.py b/test/python/circuit/library/test_iqp.py index b371e5546525..ae98b2500120 100644 --- a/test/python/circuit/library/test_iqp.py +++ b/test/python/circuit/library/test_iqp.py @@ -13,29 +13,40 @@ """Test library of IQP circuits.""" import unittest +from ddt import ddt, data import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.library import IQP +from qiskit.circuit.library import IQP, iqp, random_iqp from qiskit.quantum_info import Operator from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestIQPLibrary(QiskitTestCase): """Test library of IQP quantum circuits.""" - def test_iqp(self): - """Test iqp circuit.""" - circuit = IQP(interactions=np.array([[6, 5, 1], [5, 4, 3], [1, 3, 2]])) - - # ┌───┐ ┌─────────┐┌───┐ - # q_0: ┤ H ├─■───────────────────■───────┤ P(3π/4) ├┤ H ├ - # ├───┤ │P(5π/2) │ └┬────────┤├───┤ - # q_1: ┤ H ├─■─────────■─────────┼────────┤ P(π/2) ├┤ H ├ - # ├───┤ │P(3π/2) │P(π/2) ├────────┤├───┤ - # q_2: ┤ H ├───────────■─────────■────────┤ P(π/4) ├┤ H ├ - # └───┘ └────────┘└───┘ + @data(True, False) + def test_iqp(self, use_function): + """Test iqp circuit. + + ┌───┐ ┌─────────┐┌───┐ + q_0: ┤ H ├─■───────────────────■───────┤ P(3π/4) ├┤ H ├ + ├───┤ │P(5π/2) │ └┬────────┤├───┤ + q_1: ┤ H ├─■─────────■─────────┼────────┤ P(π/2) ├┤ H ├ + ├───┤ │P(3π/2) │P(π/2) ├────────┤├───┤ + q_2: ┤ H ├───────────■─────────■────────┤ P(π/4) ├┤ H ├ + └───┘ └────────┘└───┘ + """ + + interactions = np.array([[6, 5, 1], [5, 4, 3], [1, 3, 2]]) + + if use_function: + circuit = iqp(interactions) + else: + circuit = IQP(interactions) + expected = QuantumCircuit(3) expected.h([0, 1, 2]) expected.cp(5 * np.pi / 2, 0, 1) @@ -49,9 +60,34 @@ def test_iqp(self): simulated = Operator(circuit) self.assertTrue(expected.equiv(simulated)) - def test_iqp_bad(self): - """Test that [0,..,n-1] permutation is required (no -1 for last element).""" - self.assertRaises(CircuitError, IQP, [[6, 5], [2, 4]]) + @data(True, False) + def test_iqp_bad(self, use_function): + """Test an error is raised if the interactions matrix is not symmetric.""" + self.assertRaises(CircuitError, iqp if use_function else IQP, [[6, 5], [2, 4]]) + + def test_random_iqp(self): + """Test generating a random IQP circuit.""" + + circuit = random_iqp(num_qubits=4, seed=426) + self.assertEqual(circuit.num_qubits, 4) + + ops = circuit.count_ops() + allowed = {"p", "h", "cp"} + + # we pick a seed where neither the diagonal, nor the off-diagonal is completely 0, + # therefore each gate is expected to be present + self.assertEqual(set(ops.keys()), allowed) + + def test_random_iqp_seed(self): + """Test setting the seed.""" + + seed = 236321 + circuit1 = random_iqp(num_qubits=3, seed=seed) + circuit2 = random_iqp(num_qubits=3, seed=seed) + self.assertEqual(circuit1, circuit2) + + circuit3 = random_iqp(num_qubits=3, seed=seed + 1) + self.assertNotEqual(circuit1, circuit3) if __name__ == "__main__": diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index ead6a07d8b4d..73befb19db46 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -180,7 +180,9 @@ def test_default_plugin(self): gate = XGate() mcmt = MCMTGate(gate, num_controls, num_target) - hls = HighLevelSynthesis() + # make sure MCX-synthesis does not use ancilla qubits + config = HLSConfig(mcx=["noaux_v24"]) + hls = HighLevelSynthesis(hls_config=config) # test a decomposition without sufficient ancillas for MCMT V-chain with self.subTest(msg="insufficient auxiliaries"): diff --git a/test/python/circuit/library/test_multipliers.py b/test/python/circuit/library/test_multipliers.py index fd083e287153..f5665007079b 100644 --- a/test/python/circuit/library/test_multipliers.py +++ b/test/python/circuit/library/test_multipliers.py @@ -13,9 +13,11 @@ """Test multiplier circuits.""" import unittest +import re import numpy as np from ddt import ddt, data, unpack +from qiskit import transpile from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.circuit.library import ( @@ -24,7 +26,10 @@ CDKMRippleCarryAdder, DraperQFTAdder, VBERippleCarryAdder, + MultiplierGate, ) +from qiskit.transpiler.passes import HighLevelSynthesis, HLSConfig +from qiskit.synthesis.arithmetic import multiplier_qft_r17, multiplier_cumulative_h18 from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -53,7 +58,8 @@ def assertMultiplicationIsCorrect( # obtain the statevector and the probabilities, we don't trace out the ancilla qubits # as we verify that all ancilla qubits have been uncomputed to state 0 again - statevector = Statevector(circuit) + tqc = transpile(circuit, basis_gates=["h", "p", "cp", "rz", "cx", "ccx", "swap"]) + statevector = Statevector(tqc) probabilities = statevector.probabilities() pad = "0" * circuit.num_ancillas # state of the ancillas @@ -79,10 +85,18 @@ def assertMultiplicationIsCorrect( (3, RGQFTMultiplier, 5), (3, RGQFTMultiplier, 4), (3, RGQFTMultiplier, 3), + (3, multiplier_qft_r17), + (3, multiplier_qft_r17, 5), + (3, multiplier_qft_r17, 4), + (3, multiplier_qft_r17, 3), (3, HRSCumulativeMultiplier), (3, HRSCumulativeMultiplier, 5), (3, HRSCumulativeMultiplier, 4), (3, HRSCumulativeMultiplier, 3), + (3, multiplier_cumulative_h18), + (3, multiplier_cumulative_h18, 5), + (3, multiplier_cumulative_h18, 4), + (3, multiplier_cumulative_h18, 3), (3, HRSCumulativeMultiplier, None, CDKMRippleCarryAdder), (3, HRSCumulativeMultiplier, None, DraperQFTAdder), (3, HRSCumulativeMultiplier, None, VBERippleCarryAdder), @@ -97,6 +111,7 @@ def test_multiplication(self, num_state_qubits, multiplier, num_result_qubits=No multiplier = multiplier(num_state_qubits, num_result_qubits, adder=adder) else: multiplier = multiplier(num_state_qubits, num_result_qubits) + self.assertMultiplicationIsCorrect(num_state_qubits, num_result_qubits, multiplier) @data( @@ -124,6 +139,29 @@ def test_modular_cumulative_multiplier_custom_adder(self): with self.assertRaises(NotImplementedError): _ = HRSCumulativeMultiplier(3, 3, adder=VBERippleCarryAdder(3)) + def test_plugins(self): + """Test setting the HLS plugins for the modular adder.""" + + # all gates with the plugins we check, including an expected operation + plugins = [("cumulative_h18", "ccircuit-.*"), ("qft_r17", "qft")] + + num_state_qubits = 2 + + for plugin, expected_op in plugins: + with self.subTest(plugin=plugin): + multiplier = MultiplierGate(num_state_qubits) + + circuit = QuantumCircuit(multiplier.num_qubits) + circuit.append(multiplier, range(multiplier.num_qubits)) + + hls_config = HLSConfig(Multiplier=[plugin]) + hls = HighLevelSynthesis(hls_config=hls_config) + + synth = hls(circuit) + ops = set(synth.count_ops().keys()) + + self.assertTrue(any(re.match(expected_op, op) for op in ops)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index dd39aa61ba61..753feff490b1 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -20,12 +20,18 @@ from ddt import ddt, data, unpack from qiskit import transpile -from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression +from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression, Gate from qiskit.circuit.library import ( + n_local, + efficient_su2, + real_amplitudes, + excitation_preserving, + pauli_two_design, NLocal, TwoLocal, RealAmplitudes, ExcitationPreserving, + HGate, XGate, CRXGate, CCXGate, @@ -45,10 +51,23 @@ from qiskit.exceptions import QiskitError from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map +from qiskit._accelerate.circuit_library import Block from test import QiskitTestCase # pylint: disable=wrong-import-order +class Gato(Gate): + """A custom gate.""" + + def __init__(self, x, y): + super().__init__("meow", 1, [x, y]) + + def _define(self): + x, y = self.params + self.definition = QuantumCircuit(1) + self.definition.p(x + y, 0) + + @ddt class TestNLocal(QiskitTestCase): """Test the n-local circuit class.""" @@ -470,6 +489,344 @@ def test_initial_state_as_circuit_object(self): self.assertCircuitEqual(ref, expected) +@ddt +class TestNLocalFunction(QiskitTestCase): + """Test the n_local circuit library function.""" + + def test_empty_blocks(self): + """Test passing no rotation and entanglement blocks.""" + circuit = n_local(2, rotation_blocks=[], entanglement_blocks=[]) + expected = QuantumCircuit(2) + + self.assertEqual(expected, circuit) + + def test_invalid_custom_block(self): + """Test constructing a block from callable but not with a callable.""" + my_block = QuantumCircuit(2) + with self.assertRaises(QiskitError): + _ = Block.from_callable(2, 0, my_block) + + def test_str_blocks(self): + """Test passing blocks as strings.""" + circuit = n_local(2, "h", "ecr", reps=2) + expected = QuantumCircuit(2) + for _ in range(2): + expected.h([0, 1]) + expected.ecr(0, 1) + expected.h([0, 1]) + + self.assertEqual(expected, circuit) + + def test_stdgate_blocks(self): + """Test passing blocks as standard gates.""" + circuit = n_local(2, HGate(), CRXGate(Parameter("x")), reps=2) + + param_iter = iter(circuit.parameters) + expected = QuantumCircuit(2) + for _ in range(2): + expected.h([0, 1]) + expected.crx(next(param_iter), 0, 1) + expected.h([0, 1]) + + self.assertEqual(expected, circuit) + + def test_invalid_str_blocks(self): + """Test passing blocks as invalid string raises.""" + with self.assertRaises(ValueError): + _ = n_local(2, "h", "iamnotanexisting2qgateeventhoughiwanttobe") + + def test_gate_blocks(self): + """Test passing blocks as gates.""" + x = ParameterVector("x", 2) + my_gate = Gato(*x) + + circuit = n_local(4, my_gate, "cx", "linear", reps=3) + + expected_cats = 4 * (3 + 1) # num_qubits * (reps + 1) + expected_cx = 3 * 3 # gates per block * reps + expected_num_params = expected_cats * 2 + + self.assertEqual(expected_cats, circuit.count_ops().get("meow", 0)) + self.assertEqual(expected_cx, circuit.count_ops().get("cx", 0)) + self.assertEqual(expected_num_params, circuit.num_parameters) + + def test_gate_lists(self): + """Test passing a list of strings and gates.""" + reps = 2 + circuit = n_local(4, [XGate(), "ry", SXGate()], ["ryy", CCXGate()], "full", reps) + expected_1q = 4 * (reps + 1) # num_qubits * (reps + 1) + expected_2q = 4 * 3 / 2 * reps # 4 choose 2 * reps + expected_3q = 4 * reps # 4 choose 3 * reps + + ops = circuit.count_ops() + for gate in ["x", "ry", "sx"]: + with self.subTest(gate=gate): + self.assertEqual(expected_1q, ops.get(gate, 0)) + + with self.subTest(gate="ryy"): + self.assertEqual(expected_2q, ops.get("ryy", 0)) + + with self.subTest(gate="ccx"): + self.assertEqual(expected_3q, ops.get("ccx", 0)) + + def test_reps(self): + """Test setting the repetitions.""" + all_reps = [0, 1, 2, 10] + for reps in all_reps: + circuit = n_local(2, rotation_blocks="rx", entanglement_blocks="cz", reps=reps) + expected_rx = (reps + 1) * 2 + expected_cz = reps + + with self.subTest(reps=reps): + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + self.assertEqual(expected_cz, circuit.count_ops().get("cz", 0)) + + def test_negative_reps(self): + """Test negative reps raises.""" + with self.assertRaises(ValueError): + _ = n_local(1, [], [], reps=-1) + + def test_barrier(self): + """Test setting barriers.""" + circuit = n_local(2, "ry", "cx", reps=2, insert_barriers=True) + values = np.ones(circuit.num_parameters) + + expected = QuantumCircuit(2) + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + + self.assertEqual(expected, circuit.assign_parameters(values)) + + def test_parameter_prefix(self): + """Test setting the parameter prefix.""" + circuit = n_local(2, "h", "crx", parameter_prefix="x") + prefixes = [p.name[0] for p in circuit.parameters] + self.assertTrue(all(prefix == "x" for prefix in prefixes)) + + @data(True, False) + def test_overwrite_block_parameters(self, overwrite): + """Test overwriting the block parameters.""" + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + + reps = 3 + circuit = n_local( + 4, [], [block.to_gate()], "linear", reps, overwrite_block_parameters=overwrite + ) + + expected_num_params = reps * 3 if overwrite else 1 + self.assertEqual(expected_num_params, circuit.num_parameters) + + @data(True, False) + def test_skip_final_rotation_layer(self, skip): + """Test skipping the final rotation layer.""" + reps = 5 + num_qubits = 2 + circuit = n_local(num_qubits, "rx", "ch", reps=reps, skip_final_rotation_layer=skip) + expected_rx = num_qubits * (reps + (0 if skip else 1)) + + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + + def test_skip_unentangled_qubits(self): + """Test skipping the unentangled qubits.""" + num_qubits = 6 + entanglement_1 = [[0, 1, 3], [1, 3, 5], [0, 1, 5]] + skipped_1 = [2, 4] + + def entanglement_2(layer): + return entanglement_1 if layer % 2 == 0 else [[0, 1, 2], [2, 3, 5]] + + skipped_2 = [4] + + for entanglement, skipped in zip([entanglement_1, entanglement_2], [skipped_1, skipped_2]): + with self.subTest(entanglement=entanglement, skipped=skipped): + nlocal = n_local( + num_qubits, + rotation_blocks=XGate(), + entanglement_blocks=CCXGate(), + entanglement=entanglement, + reps=3, + skip_unentangled_qubits=True, + ) + + skipped_set = {nlocal.qubits[i] for i in skipped} + dag = circuit_to_dag(nlocal) + idle = set(dag.idle_wires()) + self.assertEqual(skipped_set, idle) + + def test_empty_entanglement(self): + """Test passing an empty list as entanglement.""" + circuit = n_local(3, "h", "cx", entanglement=[], reps=1) + self.assertEqual(6, circuit.count_ops().get("h", 0)) + self.assertEqual(0, circuit.count_ops().get("cx", 0)) + + def test_entanglement_list_of_str(self): + """Test different entanglement strings per entanglement block.""" + circuit = n_local(3, [], ["cx", "cz"], entanglement=["reverse_linear", "full"], reps=1) + self.assertEqual(2, circuit.count_ops().get("cx", 0)) + self.assertEqual(3, circuit.count_ops().get("cz", 0)) + + def test_invalid_entanglement_list(self): + """Test passing an invalid list.""" + with self.assertRaises(TypeError): + _ = n_local(3, "h", "cx", entanglement=[0, 1]) # should be [(0, 1)] + + def test_mismatching_entanglement_blocks_str(self): + """Test an error is raised if the number of entanglements does not match the blocks.""" + entanglement = ["full", "linear", "pairwise"] + blocks = ["ryy", "iswap"] + + with self.assertRaises(QiskitError): + _ = n_local(3, [], blocks, entanglement=entanglement) + + def test_mismatching_entanglement_blocks_indices(self): + """Test an error is raised if the number of entanglements does not match the blocks.""" + ent1 = [(0, 1), (1, 2)] + ent2 = [(0, 2)] + blocks = ["ryy", "iswap"] + + with self.assertRaises(QiskitError): + _ = n_local(3, [], blocks, entanglement=[ent1, ent1, ent2]) + + def test_mismatching_entanglement_indices(self): + """Test an error is raised if the entanglement does not match the blocksize.""" + entanglement = [[0, 1], [2]] + + with self.assertRaises(QiskitError): + _ = n_local(3, "ry", "cx", entanglement) + + def test_entanglement_by_callable(self): + """Test setting the entanglement by callable. + + This is the circuit we test (times 2, with final X layer) + ┌───┐ ┌───┐┌───┐ ┌───┐ + q_0: |0>┤ X ├──■────■───┤ X ├┤ X ├──■─── .. ┤ X ├ + ├───┤ │ │ ├───┤└─┬─┘ │ ├───┤ + q_1: |0>┤ X ├──■────┼───┤ X ├──■────┼─── .. ┤ X ├ + ├───┤┌─┴─┐ │ ├───┤ │ │ x2 ├───┤ + q_2: |0>┤ X ├┤ X ├──■───┤ X ├──■────■─── .. ┤ X ├ + ├───┤└───┘┌─┴─┐ ├───┤ ┌─┴─┐ ├───┤ + q_3: |0>┤ X ├─────┤ X ├─┤ X ├─────┤ X ├─ .. ┤ X ├ + └───┘ └───┘ └───┘ └───┘ └───┘ + """ + circuit = QuantumCircuit(4) + for _ in range(2): + circuit.x([0, 1, 2, 3]) + circuit.barrier() + circuit.ccx(0, 1, 2) + circuit.ccx(0, 2, 3) + circuit.barrier() + circuit.x([0, 1, 2, 3]) + circuit.barrier() + circuit.ccx(2, 1, 0) + circuit.ccx(0, 2, 3) + circuit.barrier() + circuit.x([0, 1, 2, 3]) + + layer_1 = [(0, 1, 2), (0, 2, 3)] + layer_2 = [(2, 1, 0), (0, 2, 3)] + + entanglement = lambda offset: layer_1 if offset % 2 == 0 else layer_2 + + nlocal = QuantumCircuit(4) + nlocal.compose( + n_local( + 4, + rotation_blocks=XGate(), + entanglement_blocks=CCXGate(), + reps=4, + entanglement=entanglement, + insert_barriers=True, + ), + inplace=True, + ) + + self.assertEqual(nlocal, circuit) + + def test_nice_error_if_circuit_passed(self): + """Check the transition-helper error.""" + block = QuantumCircuit(1) + + with self.assertRaisesRegex(ValueError, "but you passed a QuantumCircuit"): + _ = n_local(3, block, "cz") + + +@ddt +class TestNLocalFamily(QiskitTestCase): + """Test the derived circuit functions.""" + + def test_real_amplitudes(self): + """Test the real amplitudes circuit.""" + circuit = real_amplitudes(4) + expected = n_local(4, "ry", "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + def test_efficient_su2(self): + """Test the efficient SU(2) circuit.""" + circuit = efficient_su2(4) + expected = n_local(4, ["ry", "rz"], "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + @data("fsim", "iswap") + def test_excitation_preserving(self, mode): + """Test the excitation preserving circuit.""" + circuit = excitation_preserving(4, mode=mode) + + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + block.ryy(x, 0, 1) + if mode == "fsim": + y = Parameter("y") + block.cp(y, 0, 1) + + expected = n_local(4, "rz", block.to_gate(), "full", reps=3) + self.assertEqual( + expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose() + ) + + def test_excitation_preserving_invalid_mode(self): + """Test an error is raised for an invalid mode.""" + with self.assertRaises(ValueError): + _ = excitation_preserving(2, mode="Fsim") + + with self.assertRaises(ValueError): + _ = excitation_preserving(2, mode="swaip") + + def test_two_design(self): + """Test the Pauli 2-design circuit.""" + circuit = pauli_two_design(3) + expected_ops = {"rx", "ry", "rz", "cz"} + circuit_ops = set(circuit.count_ops().keys()) + + self.assertTrue(circuit_ops.issubset(expected_ops)) + + def test_two_design_seed(self): + """Test the seed""" + seed1 = 123 + seed2 = 321 + + with self.subTest(msg="same circuit with same seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed1) + + self.assertEqual(first.assign_parameters(second.parameters), second) + + with self.subTest(msg="different circuit with different seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed2) + + self.assertNotEqual(first.assign_parameters(second.parameters), second) + + @ddt class TestTwoLocal(QiskitTestCase): """Tests for the TwoLocal circuit.""" @@ -872,6 +1229,14 @@ def test_fsim_circuit(self): self.assertCircuitEqual(library, expected) + def test_excitation_preserving_invalid_mode(self): + """Test an error is raised for an invalid mode.""" + with self.assertRaises(ValueError): + _ = ExcitationPreserving(2, mode="Fsim") + + with self.assertRaises(ValueError): + _ = ExcitationPreserving(2, mode="swaip") + def test_circular_on_same_block_and_circuit_size(self): """Test circular entanglement works correctly if the circuit and block sizes match.""" diff --git a/test/python/circuit/library/test_overlap.py b/test/python/circuit/library/test_overlap.py index 1a95e3ba9155..c103e60fba91 100644 --- a/test/python/circuit/library/test_overlap.py +++ b/test/python/circuit/library/test_overlap.py @@ -12,50 +12,69 @@ """Test unitary overlap function""" import unittest +from ddt import ddt, data import numpy as np from qiskit.circuit import QuantumCircuit, Qubit, Clbit -from qiskit.circuit.library import EfficientSU2, UnitaryOverlap +from qiskit.circuit.library import EfficientSU2, UnitaryOverlap, unitary_overlap from qiskit.quantum_info import Statevector from qiskit.circuit.exceptions import CircuitError from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestUnitaryOverlap(QiskitTestCase): """Test the unitary overlap circuit class.""" - def test_identity(self): + @data(True, False) + def test_identity(self, use_function): """Test identity is returned""" unitary = EfficientSU2(2) unitary.assign_parameters(np.random.random(size=unitary.num_parameters), inplace=True) - - overlap = UnitaryOverlap(unitary, unitary) + if use_function: + overlap = unitary_overlap(unitary, unitary) + else: + overlap = UnitaryOverlap(unitary, unitary) self.assertLess(abs(Statevector.from_instruction(overlap)[0] - 1), 1e-12) - def test_parameterized_identity(self): + @data(True, False) + def test_parameterized_identity(self, use_function): """Test identity is returned""" unitary = EfficientSU2(2) - overlap = UnitaryOverlap(unitary, unitary) + if use_function: + overlap = unitary_overlap(unitary, unitary) + else: + overlap = UnitaryOverlap(unitary, unitary) + rands = np.random.random(size=unitary.num_parameters) double_rands = np.hstack((rands, rands)) overlap.assign_parameters(double_rands, inplace=True) self.assertLess(abs(Statevector.from_instruction(overlap)[0] - 1), 1e-12) - def test_two_parameterized_inputs(self): + @data(True, False) + def test_two_parameterized_inputs(self, use_function): """Test two parameterized inputs""" unitary1 = EfficientSU2(2) unitary2 = EfficientSU2(2) - overlap = UnitaryOverlap(unitary1, unitary2) + if use_function: + overlap = unitary_overlap(unitary1, unitary2) + else: + overlap = UnitaryOverlap(unitary1, unitary2) self.assertEqual(overlap.num_parameters, unitary1.num_parameters + unitary2.num_parameters) - def test_parameter_prefixes(self): + @data(True, False) + def test_parameter_prefixes(self, use_function): """Test two parameterized inputs""" unitary1 = EfficientSU2(2) unitary2 = EfficientSU2(2) - overlap = UnitaryOverlap(unitary1, unitary2, prefix1="a", prefix2="b") + if use_function: + overlap = unitary_overlap(unitary1, unitary2, prefix1="a", prefix2="b") + else: + overlap = UnitaryOverlap(unitary1, unitary2, prefix1="a", prefix2="b") + self.assertEqual(overlap.num_parameters, unitary1.num_parameters + unitary2.num_parameters) expected_names = [f"a[{i}]" for i in range(unitary1.num_parameters)] @@ -63,53 +82,76 @@ def test_parameter_prefixes(self): self.assertListEqual([p.name for p in overlap.parameters], expected_names) - def test_partial_parameterized_inputs(self): + @data(True, False) + def test_partial_parameterized_inputs(self, use_function): """Test one parameterized inputs (1)""" unitary1 = EfficientSU2(2) unitary1.assign_parameters(np.random.random(size=unitary1.num_parameters), inplace=True) unitary2 = EfficientSU2(2, reps=5) - overlap = UnitaryOverlap(unitary1, unitary2) + if use_function: + overlap = unitary_overlap(unitary1, unitary2) + else: + overlap = UnitaryOverlap(unitary1, unitary2) + self.assertEqual(overlap.num_parameters, unitary2.num_parameters) - def test_partial_parameterized_inputs2(self): + @data(True, False) + def test_partial_parameterized_inputs2(self, use_function): """Test one parameterized inputs (2)""" unitary1 = EfficientSU2(2) unitary2 = EfficientSU2(2, reps=5) unitary2.assign_parameters(np.random.random(size=unitary2.num_parameters), inplace=True) - overlap = UnitaryOverlap(unitary1, unitary2) + if use_function: + overlap = unitary_overlap(unitary1, unitary2) + else: + overlap = UnitaryOverlap(unitary1, unitary2) + self.assertEqual(overlap.num_parameters, unitary1.num_parameters) - def test_barrier(self): + @data(True, False) + def test_barrier(self, use_function): """Test that barriers on input circuits are well handled""" unitary1 = EfficientSU2(1, reps=0) unitary1.barrier() unitary2 = EfficientSU2(1, reps=1) unitary2.barrier() - overlap = UnitaryOverlap(unitary1, unitary2) + if use_function: + overlap = unitary_overlap(unitary1, unitary2) + else: + overlap = UnitaryOverlap(unitary1, unitary2) self.assertEqual(overlap.num_parameters, unitary1.num_parameters + unitary2.num_parameters) - def test_measurements(self): + @data(True, False) + def test_measurements(self, use_function): """Test that exception is thrown for measurements""" unitary1 = EfficientSU2(2) unitary1.measure_all() unitary2 = EfficientSU2(2) with self.assertRaises(CircuitError): - _ = UnitaryOverlap(unitary1, unitary2) + if use_function: + _ = unitary_overlap(unitary1, unitary2) + else: + _ = UnitaryOverlap(unitary1, unitary2) - def test_rest(self): + @data(True, False) + def test_rest(self, use_function): """Test that exception is thrown for rest""" unitary1 = EfficientSU2(1, reps=0) unitary1.reset(0) unitary2 = EfficientSU2(1, reps=1) with self.assertRaises(CircuitError): - _ = UnitaryOverlap(unitary1, unitary2) + if use_function: + _ = unitary_overlap(unitary1, unitary2) + else: + _ = UnitaryOverlap(unitary1, unitary2) - def test_controlflow(self): + @data(True, False) + def test_controlflow(self, use_function): """Test that exception is thrown for controlflow""" bit = Clbit() unitary1 = QuantumCircuit([Qubit(), bit]) @@ -121,21 +163,34 @@ def test_controlflow(self): unitary2.rx(0.2, 0) with self.assertRaises(CircuitError): - _ = UnitaryOverlap(unitary1, unitary2) + if use_function: + _ = unitary_overlap(unitary1, unitary2) + else: + _ = UnitaryOverlap(unitary1, unitary2) - def test_mismatching_qubits(self): + @data(True, False) + def test_mismatching_qubits(self, use_function): """Test that exception is thrown for mismatching circuit""" unitary1 = EfficientSU2(2) unitary2 = EfficientSU2(1) with self.assertRaises(CircuitError): - _ = UnitaryOverlap(unitary1, unitary2) + if use_function: + _ = unitary_overlap(unitary1, unitary2) + else: + _ = UnitaryOverlap(unitary1, unitary2) - def test_insert_barrier(self): + @data(True, False) + def test_insert_barrier(self, use_function): """Test inserting barrier between circuits""" unitary1 = EfficientSU2(1, reps=1) unitary2 = EfficientSU2(1, reps=1) - overlap = UnitaryOverlap(unitary1, unitary2, insert_barrier=True) + + if use_function: + overlap = unitary_overlap(unitary1, unitary2, insert_barrier=True) + else: + overlap = UnitaryOverlap(unitary1, unitary2, insert_barrier=True) + self.assertEqual(overlap.count_ops()["barrier"], 1) self.assertEqual( str(overlap.draw(fold=-1, output="text")).strip(), diff --git a/test/python/circuit/library/test_pauli_feature_map.py b/test/python/circuit/library/test_pauli_feature_map.py index 5d61944d4032..d12e89fc556f 100644 --- a/test/python/circuit/library/test_pauli_feature_map.py +++ b/test/python/circuit/library/test_pauli_feature_map.py @@ -70,22 +70,22 @@ def test_pauli_evolution(self): self.assertTrue(Operator(pauli).equiv(evo)) with self.subTest(pauli_string="XYZ"): - # q_0: ─────────────■────────────────────────■────────────── - # ┌─────────┐┌─┴─┐ ┌─┴─┐┌──────────┐ - # q_1: ┤ Rx(π/2) ├┤ X ├──■──────────────■──┤ X ├┤ Rx(-π/2) ├ - # └──┬───┬──┘└───┘┌─┴─┐┌────────┐┌─┴─┐├───┤└──────────┘ - # q_2: ───┤ H ├────────┤ X ├┤ P(2.8) ├┤ X ├┤ H ├──────────── - # └───┘ └───┘└────────┘└───┘└───┘ + # q_0: ────────■────────────────────────■────────── + # ┌────┐┌─┴─┐ ┌─┴─┐┌──────┐ + # q_1: ┤ √X ├┤ X ├──■──────────────■──┤ X ├┤ √Xdg ├ + # └┬───┤└───┘┌─┴─┐┌────────┐┌─┴─┐├───┤└──────┘ + # q_2: ─┤ H ├─────┤ X ├┤ P(2.8) ├┤ X ├┤ H ├──────── + # └───┘ └───┘└────────┘└───┘└───┘ evo = QuantumCircuit(3) # X on the most-significant, bottom qubit, Z on the top evo.h(2) - evo.rx(np.pi / 2, 1) + evo.sx(1) evo.cx(0, 1) evo.cx(1, 2) evo.p(2 * time, 2) evo.cx(1, 2) evo.cx(0, 1) - evo.rx(-np.pi / 2, 1) + evo.sxdg(1) evo.h(2) pauli = encoding.pauli_evolution("XYZ", time) @@ -347,23 +347,23 @@ def test_pauli_xyz(self): params = encoding.parameters - # q_0: ─────────────■────────────────────────■────────────── - # ┌─────────┐┌─┴─┐ ┌─┴─┐┌──────────┐ - # q_1: ┤ Rx(π/2) ├┤ X ├──■──────────────■──┤ X ├┤ Rx(-π/2) ├ - # └──┬───┬──┘└───┘┌─┴─┐┌────────┐┌─┴─┐├───┤└──────────┘ - # q_2: ───┤ H ├────────┤ X ├┤ P(2.8) ├┤ X ├┤ H ├──────────── - # └───┘ └───┘└────────┘└───┘└───┘ + # q_0: ────────■────────────────────────■────────── + # ┌────┐┌─┴─┐ ┌─┴─┐┌──────┐ + # q_1: ┤ √X ├┤ X ├──■──────────────■──┤ X ├┤ √Xdg ├ + # └┬───┤└───┘┌─┴─┐┌────────┐┌─┴─┐├───┤└──────┘ + # q_2: ─┤ H ├─────┤ X ├┤ P(2.8) ├┤ X ├┤ H ├──────── + # └───┘ └───┘└────────┘└───┘└───┘ # X on the most-significant, bottom qubit, Z on the top ref = QuantumCircuit(3) ref.h(range(3)) ref.h(2) - ref.rx(np.pi / 2, 1) + ref.sx(1) ref.cx(0, 1) ref.cx(1, 2) ref.p(2 * np.prod([np.pi - p for p in params]), 2) ref.cx(1, 2) ref.cx(0, 1) - ref.rx(-np.pi / 2, 1) + ref.sxdg(1) ref.h(2) self.assertEqual(ref, encoding) @@ -483,13 +483,13 @@ def test_dict_entanglement(self): ref.cx(1, 2) ref.h([1, 2]) - ref.rx(np.pi / 2, range(3)) + ref.sx(range(3)) ref.cx(0, 1) ref.cx(1, 2) ref.p(2 * np.prod([np.pi - xi for xi in x]), 2) ref.cx(1, 2) ref.cx(0, 1) - ref.rx(-np.pi / 2, range(3)) + ref.sxdg(range(3)) self.assertEqual(ref, circuit) diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py index a2ec59431fe3..380a5f6a0358 100644 --- a/test/python/circuit/library/test_permutation.py +++ b/test/python/circuit/library/test_permutation.py @@ -149,8 +149,10 @@ def test_reverse_ops(self): def test_conditional(self): """Test adding conditional permutations.""" qc = QuantumCircuit(5, 1) - qc.append(PermutationGate([1, 2, 0]), [2, 3, 4]).c_if(0, 1) - self.assertIsNotNone(qc.data[0].operation.condition) + with self.assertWarns(DeprecationWarning): + qc.append(PermutationGate([1, 2, 0]), [2, 3, 4]).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + self.assertIsNotNone(qc.data[0].operation.condition) def test_qasm(self): """Test qasm for circuits with permutations.""" diff --git a/test/python/circuit/library/test_phase_estimation.py b/test/python/circuit/library/test_phase_estimation.py index 28b388a15b2e..5b1da4a0bc7c 100644 --- a/test/python/circuit/library/test_phase_estimation.py +++ b/test/python/circuit/library/test_phase_estimation.py @@ -16,8 +16,9 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import PhaseEstimation, QFT +from qiskit.circuit.library import PhaseEstimation, QFT, phase_estimation from qiskit.quantum_info import Statevector +from qiskit.compiler import transpile from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -149,6 +150,93 @@ def test_phase_estimation_iqft_setting(self): pec = PhaseEstimation(3, unitary, iqft=iqft) self.assertEqual(pec.decompose().data[-1].operation.definition, iqft.decompose()) + def test_phase_estimation_function(self): + """Test the phase estimation function.""" + with self.subTest("U=S, psi=|1>"): + unitary = QuantumCircuit(1) + unitary.s(0) + + eigenstate = QuantumCircuit(1) + eigenstate.x(0) + + # eigenvalue is 1j = exp(2j pi 0.25) thus phi = 0.25 = 0.010 = '010' + # using three digits as 3 evaluation qubits are used + phase_as_binary = "0100" + + pec = phase_estimation(4, unitary) + + self.assertPhaseEstimationIsCorrect(pec, eigenstate, phase_as_binary) + + with self.subTest("U=SZ, psi=|11>"): + unitary = QuantumCircuit(2) + unitary.z(0) + unitary.s(1) + + eigenstate = QuantumCircuit(2) + eigenstate.x([0, 1]) + + # eigenvalue is -1j = exp(2j pi 0.75) thus phi = 0.75 = 0.110 = '110' + # using three digits as 3 evaluation qubits are used + phase_as_binary = "110" + + pec = phase_estimation(3, unitary) + + self.assertPhaseEstimationIsCorrect(pec, eigenstate, phase_as_binary) + + with self.subTest("a 3-q unitary"): + # ┌───┐ + # q_0: ┤ X ├──■────■─────── + # ├───┤ │ │ + # q_1: ┤ X ├──■────■─────── + # ├───┤┌───┐┌─┴─┐┌───┐ + # q_2: ┤ X ├┤ H ├┤ X ├┤ H ├ + # └───┘└───┘└───┘└───┘ + unitary = QuantumCircuit(3) + unitary.x([0, 1, 2]) + unitary.cz(0, 1) + unitary.h(2) + unitary.ccx(0, 1, 2) + unitary.h(2) + + # ┌───┐ + # q_0: ┤ H ├──■────■── + # └───┘┌─┴─┐ │ + # q_1: ─────┤ X ├──┼── + # └───┘┌─┴─┐ + # q_2: ──────────┤ X ├ + # └───┘ + eigenstate = QuantumCircuit(3) + eigenstate.h(0) + eigenstate.cx(0, 1) + eigenstate.cx(0, 2) + + # the unitary acts as identity on the eigenstate, thus the phase is 0 + phase_as_binary = "00" + + pec = phase_estimation(2, unitary) + + self.assertPhaseEstimationIsCorrect(pec, eigenstate, phase_as_binary) + + def test_phase_estimation_function_swaps_get_removed(self): + """Test that transpiling the circuit correctly removes swaps and permutations.""" + unitary = QuantumCircuit(1) + unitary.s(0) + + eigenstate = QuantumCircuit(1) + eigenstate.x(0) + + pec = phase_estimation(4, unitary) + + # transpilation (or more precisely HighLevelSynthesis + ElidePermutations) should + # remove all swap gates (possibly added when synthesizing the QFTGate in the circuit) + # and the final permutation gate + transpiled = transpile(pec) + transpiled_ops = transpiled.count_ops() + + self.assertNotIn("permutation", transpiled_ops) + self.assertNotIn("swap", transpiled_ops) + self.assertNotIn("cx", transpiled_ops) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/library/test_qaoa_ansatz.py b/test/python/circuit/library/test_qaoa_ansatz.py index 799bc9362a0d..bebc7be7b862 100644 --- a/test/python/circuit/library/test_qaoa_ansatz.py +++ b/test/python/circuit/library/test_qaoa_ansatz.py @@ -16,8 +16,7 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import HGate, RXGate, YGate, RYGate, RZGate -from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz +from qiskit.circuit.library import HGate, RXGate, YGate, RYGate, RZGate, QAOAAnsatz, qaoa_ansatz from qiskit.quantum_info import Pauli, SparsePauliOp from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -26,39 +25,65 @@ class TestQAOAAnsatz(QiskitTestCase): """Test QAOAAnsatz.""" - def test_default_qaoa(self): + @data(True, False) + def test_default_qaoa(self, use_function): """Test construction of the default circuit.""" - circuit = QAOAAnsatz(Pauli("I"), 1) + if use_function: + circuit = qaoa_ansatz(Pauli("I"), 1) + else: + circuit = QAOAAnsatz(Pauli("I"), 1).decompose() + parameters = circuit.parameters - circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0].operation, HGate) - self.assertIsInstance(circuit.decompose().data[1].operation, RXGate) - def test_custom_initial_state(self): + if not use_function: + circuit = circuit.decompose() + self.assertIsInstance(circuit.data[1].operation, RXGate) + + @data(True, False) + def test_custom_initial_state(self, use_function): """Test circuit with a custom initial state.""" initial_state = QuantumCircuit(1) initial_state.y(0) - circuit = QAOAAnsatz(initial_state=initial_state, cost_operator=Pauli("I"), reps=1) + + if use_function: + circuit = qaoa_ansatz(initial_state=initial_state, cost_operator=Pauli("I"), reps=1) + else: + circuit = QAOAAnsatz( + initial_state=initial_state, cost_operator=Pauli("I"), reps=1 + ).decompose() + parameters = circuit.parameters - circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0].operation, YGate) - self.assertIsInstance(circuit.decompose().data[1].operation, RXGate) - def test_invalid_reps(self): + if not use_function: + circuit = circuit.decompose() + self.assertIsInstance(circuit.data[1].operation, RXGate) + + @data(True, False) + def test_invalid_reps(self, use_function): """Test negative reps.""" with self.assertRaises(ValueError): - _ = QAOAAnsatz(Pauli("I"), reps=-1) + if use_function: + _ = qaoa_ansatz(Pauli("I"), reps=-1) + else: + _ = QAOAAnsatz(Pauli("I"), reps=-1) - def test_zero_reps(self): + @data(True, False) + def test_zero_reps(self, use_function): """Test zero reps.""" - circuit = QAOAAnsatz(Pauli("IIII"), reps=0) + if use_function: + circuit = qaoa_ansatz(Pauli("IIII"), reps=0) + else: + circuit = QAOAAnsatz(Pauli("IIII"), reps=0).decompose() + reference = QuantumCircuit(4) reference.h(range(4)) - self.assertEqual(circuit.decompose(), reference) + self.assertEqual(circuit, reference) def test_custom_circuit_mixer(self): """Test circuit with a custom mixer as a circuit""" @@ -72,16 +97,23 @@ def test_custom_circuit_mixer(self): self.assertIsInstance(circuit.data[0].operation, HGate) self.assertIsInstance(circuit.data[1].operation, RYGate) - def test_custom_operator_mixer(self): + @data(True, False) + def test_custom_operator_mixer(self, use_function): """Test circuit with a custom mixer as an operator.""" mixer = Pauli("Y") - circuit = QAOAAnsatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer) + if use_function: + circuit = qaoa_ansatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer) + else: + circuit = QAOAAnsatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer).decompose() + parameters = circuit.parameters - circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0].operation, HGate) - self.assertIsInstance(circuit.decompose().data[1].operation, RYGate) + + if not use_function: + circuit = circuit.decompose() + self.assertIsInstance(circuit.data[1].operation, RYGate) def test_parameter_bounds(self): """Test the parameter bounds.""" @@ -97,22 +129,30 @@ def test_parameter_bounds(self): self.assertIsNone(lower) self.assertIsNone(upper) - def test_all_custom_parameters(self): + @data(True, False) + def test_all_custom_parameters(self, use_function): """Test circuit with all custom parameters.""" initial_state = QuantumCircuit(1) initial_state.y(0) mixer = Pauli("Z") - circuit = QAOAAnsatz( - cost_operator=Pauli("I"), reps=2, initial_state=initial_state, mixer_operator=mixer - ) - parameters = circuit.parameters - circuit = circuit.decompose() + if use_function: + circuit = qaoa_ansatz( + cost_operator=Pauli("I"), reps=2, initial_state=initial_state, mixer_operator=mixer + ) + else: + circuit = QAOAAnsatz( + cost_operator=Pauli("I"), reps=2, initial_state=initial_state, mixer_operator=mixer + ).decompose() + parameters = circuit.parameters self.assertEqual(2, len(parameters)) self.assertIsInstance(circuit.data[0].operation, YGate) - self.assertIsInstance(circuit.decompose().data[1].operation, RZGate) - self.assertIsInstance(circuit.decompose().data[2].operation, RZGate) + + if not use_function: + circuit = circuit.decompose() + self.assertIsInstance(circuit.data[1].operation, RZGate) + self.assertIsInstance(circuit.data[2].operation, RZGate) def test_configuration(self): """Test configuration checks.""" @@ -152,14 +192,20 @@ def test_empty_op(self): with self.assertRaises(ValueError): circuit.decompose() - @data(1, 2, 3, 4) - def test_num_qubits(self, num_qubits): - """Test num_qubits with {num_qubits} qubits""" + @data(True, False) + def test_num_qubits(self, use_function): + """Test circuit sizes.""" + for num_qubits in range(1, 5): + with self.subTest(num_qubits=num_qubits): + if use_function: + circuit = qaoa_ansatz(cost_operator=Pauli("I" * num_qubits), reps=5) + else: + circuit = QAOAAnsatz(cost_operator=Pauli("I" * num_qubits), reps=5) - circuit = QAOAAnsatz(cost_operator=Pauli("I" * num_qubits), reps=5) - self.assertEqual(circuit.num_qubits, num_qubits) + self.assertEqual(circuit.num_qubits, num_qubits) - def test_identity(self): + @data(True, False) + def test_identity(self, use_function): """Test construction with identity""" reps = 4 num_qubits = 3 @@ -168,6 +214,9 @@ def test_identity(self): for cost in [pauli_op, pauli_sum_op]: for mixer in [None, pauli_op, pauli_sum_op]: with self.subTest(f"cost: {type(cost)}, mixer:{type(mixer)}"): - circuit = QAOAAnsatz(cost_operator=cost, mixer_operator=mixer, reps=reps) + if use_function: + circuit = qaoa_ansatz(cost_operator=cost, mixer_operator=mixer, reps=reps) + else: + circuit = QAOAAnsatz(cost_operator=cost, mixer_operator=mixer, reps=reps) target = reps if mixer is None else 0 self.assertEqual(circuit.num_parameters, target) diff --git a/test/python/circuit/library/test_qft.py b/test/python/circuit/library/test_qft.py index 85837f0ac80c..4a37b1481476 100644 --- a/test/python/circuit/library/test_qft.py +++ b/test/python/circuit/library/test_qft.py @@ -196,10 +196,12 @@ def __init__(self, *_args, **_kwargs): module=r"qiskit\..*", message=r".*precision loss in QFT.*", ) + warnings.filterwarnings("ignore", category=PendingDeprecationWarning) qft = QFT() # Even with the approximation this will trigger the warning. qft.num_qubits = 1080 qft.approximation_degree = 20 + self.assertFalse(caught_warnings) # Short-circuit the build method so it exits after input validation, but without actually @@ -272,8 +274,10 @@ def test_reverse_ops(self): def test_conditional(self): """Test adding conditional to a QFTGate.""" qc = QuantumCircuit(5, 1) - qc.append(QFTGate(4), [1, 2, 0, 4]).c_if(0, 1) - self.assertIsNotNone(qc.data[0].operation.condition) + with self.assertWarns(DeprecationWarning): + qc.append(QFTGate(4), [1, 2, 0, 4]).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + self.assertIsNotNone(qc.data[0].operation.condition) def test_qasm(self): """Test qasm for circuits with QFTGates.""" diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index ff241baeb61c..9568c1c42084 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. """Test operations on circuit.data.""" +import pickle import ddt from qiskit._accelerate.circuit import CircuitData @@ -110,6 +111,7 @@ def test_copy(self): CircuitInstruction(Measure(), [qr[0]], [cr[1]]), CircuitInstruction(Measure(), [qr[1]], [cr[0]]), ], + global_phase=1, ) qubits = data.qubits clbits = data.clbits @@ -126,6 +128,25 @@ def test_copy(self): self.assertIsNot(data_copy.clbits, clbits) self.assertEqual(data_copy.clbits, clbits) + with self.subTest("global_phase is equal"): + self.assertEqual(data.global_phase, data_copy.global_phase) + + def test_pickle_roundtrip(self): + """Test pickle roundtrip coverage""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + data = CircuitData( + qubits=qr, + clbits=cr, + data=[ + CircuitInstruction(XGate(), [qr[0]], []), + CircuitInstruction(Measure(), [qr[0]], [cr[0]]), + ], + global_phase=1, + ) + + self.assertEqual(data, pickle.loads(pickle.dumps(data))) + @ddt.data( (QuantumRegister(5), ClassicalRegister(5)), ([Qubit() for _ in range(5)], [Clbit() for _ in range(5)]), diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 5e45d0671021..962dd22ac79b 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -138,11 +138,13 @@ def test_qpy_full_path(self): def test_circuit_with_conditional(self): """Test that instructions with conditions are correctly serialized.""" qc = QuantumCircuit(1, 1) - qc.x(0).c_if(qc.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(qc.cregs[0], 1) qpy_file = io.BytesIO() dump(qc, qpy_file) qpy_file.seek(0) - new_circ = load(qpy_file)[0] + with self.assertWarns(DeprecationWarning): + new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertDeprecatedBitProperties(qc, new_circ) @@ -413,13 +415,15 @@ def test_multiple_circuits(self): """Test multiple circuits can be serialized together.""" circuits = [] for i in range(10): - circuits.append( - random_circuit(10, 10, measure=True, conditional=True, reset=True, seed=42 + i) - ) + with self.assertWarns(DeprecationWarning): + circuits.append( + random_circuit(10, 10, measure=True, conditional=True, reset=True, seed=42 + i) + ) qpy_file = io.BytesIO() dump(circuits, qpy_file) qpy_file.seek(0) - new_circs = load(qpy_file) + with self.assertWarns(DeprecationWarning): + new_circs = load(qpy_file) self.assertEqual(circuits, new_circs) for old, new in zip(circuits, new_circs): self.assertDeprecatedBitProperties(old, new) @@ -679,12 +683,14 @@ def test_circuit_with_conditional_with_label(self): """Test that instructions with conditions are correctly serialized.""" qc = QuantumCircuit(1, 1) gate = XGate(label="My conditional x gate") - gate.c_if(qc.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + gate.c_if(qc.cregs[0], 1) qc.append(gate, [0]) qpy_file = io.BytesIO() dump(qc, qpy_file) qpy_file.seek(0) - new_circ = load(qpy_file)[0] + with self.assertWarns(DeprecationWarning): + new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] @@ -760,12 +766,14 @@ def test_single_bit_teleportation(self): qc = QuantumCircuit(qr, cr, name="Reset Test") qc.x(0) qc.measure(0, cr[0]) - qc.x(0).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(cr[0], 1) qc.measure(0, cr[1]) qpy_file = io.BytesIO() dump(qc, qpy_file) qpy_file.seek(0) - new_circ = load(qpy_file)[0] + with self.assertWarns(DeprecationWarning): + new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] @@ -1143,11 +1151,13 @@ def test_qpy_with_for_loop(self): qc.h(0) qc.cx(0, 1) qc.measure(0, 0) - qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) qpy_file = io.BytesIO() dump(qc, qpy_file) qpy_file.seek(0) - new_circuit = load(qpy_file)[0] + with self.assertWarns(DeprecationWarning): + new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) self.assertDeprecatedBitProperties(qc, new_circuit) @@ -1159,11 +1169,13 @@ def test_qpy_with_for_loop_iterator(self): qc.h(0) qc.cx(0, 1) qc.measure(0, 0) - qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) qpy_file = io.BytesIO() dump(qc, qpy_file) qpy_file.seek(0) - new_circuit = load(qpy_file)[0] + with self.assertWarns(DeprecationWarning): + new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) self.assertDeprecatedBitProperties(qc, new_circuit) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 2d1e136d4580..bd35c8d920a5 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1038,18 +1038,21 @@ def test_repeat(self): qc.h(0) qc.cx(0, 1) qc.barrier() - qc.h(0).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(cr, 1) with self.subTest("repeat 0 times"): rep = qc.repeat(0) self.assertEqual(rep, QuantumCircuit(qr, cr)) with self.subTest("repeat 3 times"): - inst = qc.to_instruction() + with self.assertWarns(DeprecationWarning): + inst = qc.to_instruction() ref = QuantumCircuit(qr, cr) for _ in range(3): ref.append(inst, ref.qubits, ref.clbits) - rep = qc.repeat(3) + with self.assertWarns(DeprecationWarning): + rep = qc.repeat(3) self.assertEqual(rep, ref) @data(0, 1, 4) @@ -1295,13 +1298,15 @@ def test_reverse_bits_with_registerless_bits(self): qc = QuantumCircuit([q0, q1], [c0, c1]) qc.h(0) qc.cx(0, 1) - qc.x(0).c_if(1, True) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(1, True) qc.measure(0, 0) expected = QuantumCircuit([c1, c0], [q1, q0]) expected.h(1) expected.cx(1, 0) - expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) expected.measure(1, 1) self.assertEqual(qc.reverse_bits(), expected) @@ -1392,29 +1397,41 @@ def test_compare_circuits_with_single_bit_conditions(self): qreg = QuantumRegister(1, name="q") creg = ClassicalRegister(1, name="c") qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.cregs[0], 1) - qc2.x(0).c_if(qc2.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], True) self.assertEqual(qc1, qc2) # Order of operations transposed. qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.clbits[-1], True) - qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) self.assertNotEqual(qc1, qc2) # Single-bit condition values not the same. qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.cregs[0], 1) - qc2.x(0).c_if(qc2.clbits[-1], False) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], False) self.assertNotEqual(qc1, qc2) def test_compare_a_circuit_with_none(self): diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index d87487639672..14b5364b2168 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -226,8 +226,10 @@ def test_circuit_depth_conditionals1(self): qc.cx(q[2], q[3]) qc.measure(q[0], c[0]) qc.measure(q[1], c[1]) - qc.h(q[2]).c_if(c, 2) - qc.h(q[3]).c_if(c, 4) + with self.assertWarns(DeprecationWarning): + qc.h(q[2]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.h(q[3]).c_if(c, 4) self.assertEqual(qc.depth(), 5) def test_circuit_depth_conditionals2(self): @@ -258,8 +260,10 @@ def test_circuit_depth_conditionals2(self): qc.cx(q[2], q[3]) qc.measure(q[0], c[0]) qc.measure(q[0], c[0]) - qc.h(q[2]).c_if(c, 2) - qc.h(q[3]).c_if(c, 4) + with self.assertWarns(DeprecationWarning): + qc.h(q[2]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.h(q[3]).c_if(c, 4) self.assertEqual(qc.depth(), 6) def test_circuit_depth_conditionals3(self): @@ -287,7 +291,8 @@ def test_circuit_depth_conditionals3(self): qc.h(q[2]) qc.h(q[3]) qc.measure(q[0], c[0]) - qc.cx(q[0], q[3]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.cx(q[0], q[3]).c_if(c, 2) qc.measure(q[1], c[1]) qc.measure(q[2], c[2]) @@ -320,8 +325,10 @@ def test_circuit_depth_bit_conditionals1(self): qc.h(q[3]) qc.measure(q[0], c[0]) qc.measure(q[2], c[2]) - qc.h(q[1]).c_if(c[0], True) - qc.h(q[3]).c_if(c[2], False) + with self.assertWarns(DeprecationWarning): + qc.h(q[1]).c_if(c[0], True) + with self.assertWarns(DeprecationWarning): + qc.h(q[3]).c_if(c[2], False) self.assertEqual(qc.depth(), 3) def test_circuit_depth_bit_conditionals2(self): @@ -362,12 +369,18 @@ def test_circuit_depth_bit_conditionals2(self): qc.h(q[3]) qc.measure(q[0], c[0]) qc.measure(q[2], c[2]) - qc.h(q[1]).c_if(c[1], True) - qc.h(q[3]).c_if(c[3], True) - qc.cx(0, 1).c_if(c[0], False) - qc.cx(2, 3).c_if(c[2], False) - qc.ch(0, 2).c_if(c[1], True) - qc.ch(1, 3).c_if(c[3], True) + with self.assertWarns(DeprecationWarning): + qc.h(q[1]).c_if(c[1], True) + with self.assertWarns(DeprecationWarning): + qc.h(q[3]).c_if(c[3], True) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(c[0], False) + with self.assertWarns(DeprecationWarning): + qc.cx(2, 3).c_if(c[2], False) + with self.assertWarns(DeprecationWarning): + qc.ch(0, 2).c_if(c[1], True) + with self.assertWarns(DeprecationWarning): + qc.ch(1, 3).c_if(c[3], True) self.assertEqual(qc.depth(), 4) def test_circuit_depth_bit_conditionals3(self): @@ -395,9 +408,12 @@ def test_circuit_depth_bit_conditionals3(self): qc.h(q[2]) qc.h(q[3]) qc.measure(q[0], c[0]) - qc.h(1).c_if(c[0], True) - qc.h(q[2]).c_if(c, 2) - qc.h(3).c_if(c[3], True) + with self.assertWarns(DeprecationWarning): + qc.h(1).c_if(c[0], True) + with self.assertWarns(DeprecationWarning): + qc.h(q[2]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.h(3).c_if(c[3], True) qc.measure(q[1], c[1]) qc.measure(q[2], c[2]) qc.measure(q[3], c[3]) @@ -608,11 +624,15 @@ def test_circuit_depth_multiqubit_or_conditional(self): circ.rz(0.1, 1) circ.cz(1, 3) circ.measure(1, 0) - circ.x(0).c_if(0, 1) - self.assertEqual( - circ.depth(lambda x: x.operation.num_qubits >= 2 or x.operation.condition is not None), - 4, - ) + with self.assertWarns(DeprecationWarning): + circ.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + circ.depth( + lambda x: x.operation.num_qubits >= 2 or x.operation.condition is not None + ), + 4, + ) def test_circuit_depth_first_qubit(self): """Test finding depth of gates touching q0 only.""" @@ -765,7 +785,8 @@ def test_circuit_nonlocal_gates(self): qc.cry(0.1, q[2], q[4]) qc.z(q[3:]) qc.cswap(q[1], q[2], q[3]) - qc.iswap(q[0], q[4]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.iswap(q[0], q[4]).c_if(c, 2) result = qc.num_nonlocal_gates() expected = 3 self.assertEqual(expected, result) @@ -813,7 +834,9 @@ def test_circuit_connected_components_multi_reg(self): qc.cx(q1[1], q2[1]) qc.cx(q2[1], q1[2]) qc.cx(q1[2], q2[0]) - self.assertEqual(qc.num_connected_components(), 1) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 1) def test_circuit_connected_components_multi_reg2(self): """Test tensor factors works over multi registers #2.""" @@ -834,7 +857,9 @@ def test_circuit_connected_components_multi_reg2(self): qc.cx(q1[0], q2[1]) qc.cx(q2[0], q1[2]) qc.cx(q1[1], q2[0]) - self.assertEqual(qc.num_connected_components(), 2) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 2) def test_circuit_connected_components_disconnected(self): """Test tensor factors works with 2q subspaces.""" @@ -867,7 +892,9 @@ def test_circuit_connected_components_disconnected(self): qc.cx(q1[2], q2[2]) qc.cx(q1[3], q2[1]) qc.cx(q1[4], q2[0]) - self.assertEqual(qc.num_connected_components(), 5) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 5) def test_circuit_connected_components_with_clbits(self): """Test tensor components with classical register.""" @@ -895,7 +922,9 @@ def test_circuit_connected_components_with_clbits(self): qc.measure(q[1], c[1]) qc.measure(q[2], c[2]) qc.measure(q[3], c[3]) - self.assertEqual(qc.num_connected_components(), 4) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 4) def test_circuit_connected_components_with_cond(self): """Test tensor components with one conditional gate.""" @@ -921,11 +950,14 @@ def test_circuit_connected_components_with_cond(self): qc.h(q[2]) qc.h(q[3]) qc.measure(q[0], c[0]) - qc.cx(q[0], q[3]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.cx(q[0], q[3]).c_if(c, 2) qc.measure(q[1], c[1]) qc.measure(q[2], c[2]) qc.measure(q[3], c[3]) - self.assertEqual(qc.num_connected_components(), 1) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 1) def test_circuit_connected_components_with_cond2(self): """Test tensor components with two conditional gates.""" @@ -949,9 +981,13 @@ def test_circuit_connected_components_with_cond2(self): qc.h(q[1]) qc.h(q[2]) qc.h(q[3]) - qc.h(0).c_if(c, 0) - qc.cx(1, 2).c_if(c, 4) - self.assertEqual(qc.num_connected_components(), 2) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(c, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(1, 2).c_if(c, 4) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 2) def test_circuit_connected_components_with_cond3(self): """Test tensor components with three conditional gates and measurements.""" @@ -977,11 +1013,16 @@ def test_circuit_connected_components_with_cond3(self): qc.h(q[2]) qc.h(q[3]) qc.measure(q[0], c[0]) - qc.h(q[0]).c_if(c, 0) - qc.cx(q[1], q[2]).c_if(c, 1) + with self.assertWarns(DeprecationWarning): + qc.h(q[0]).c_if(c, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(q[1], q[2]).c_if(c, 1) qc.measure(q[2], c[2]) - qc.x(q[3]).c_if(c, 2) - self.assertEqual(qc.num_connected_components(), 1) + with self.assertWarns(DeprecationWarning): + qc.x(q[3]).c_if(c, 2) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 1) def test_circuit_connected_components_with_bit_cond(self): """Test tensor components with one single bit conditional gate.""" @@ -1007,11 +1048,14 @@ def test_circuit_connected_components_with_bit_cond(self): qc.h(q[2]) qc.h(q[3]) qc.measure(q[0], c[0]) - qc.cx(q[0], q[3]).c_if(c[0], True) + with self.assertWarns(DeprecationWarning): + qc.cx(q[0], q[3]).c_if(c[0], True) qc.measure(q[1], c[1]) qc.measure(q[2], c[2]) qc.measure(q[3], c[3]) - self.assertEqual(qc.num_connected_components(), 3) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 3) def test_circuit_connected_components_with_bit_cond2(self): """Test tensor components with two bit conditional gates.""" @@ -1035,10 +1079,15 @@ def test_circuit_connected_components_with_bit_cond2(self): qc.h(q[1]) qc.h(q[2]) qc.h(q[3]) - qc.h(0).c_if(c[1], True) - qc.cx(1, 0).c_if(c[4], False) - qc.cz(2, 3).c_if(c[0], True) - self.assertEqual(qc.num_connected_components(), 5) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(c[1], True) + with self.assertWarns(DeprecationWarning): + qc.cx(1, 0).c_if(c[4], False) + with self.assertWarns(DeprecationWarning): + qc.cz(2, 3).c_if(c[0], True) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 5) def test_circuit_connected_components_with_bit_cond3(self): """Test tensor components with register and bit conditional gates.""" @@ -1063,10 +1112,15 @@ def test_circuit_connected_components_with_bit_cond3(self): qc.h(q[1]) qc.h(q[2]) qc.h(q[3]) - qc.h(q[0]).c_if(c[0], True) - qc.cx(q[1], q[2]).c_if(c, 1) - qc.x(q[3]).c_if(c[2], True) - self.assertEqual(qc.num_connected_components(), 1) + with self.assertWarns(DeprecationWarning): + qc.h(q[0]).c_if(c[0], True) + with self.assertWarns(DeprecationWarning): + qc.cx(q[1], q[2]).c_if(c, 1) + with self.assertWarns(DeprecationWarning): + qc.x(q[3]).c_if(c[2], True) + # Internally calls op.condition_bits + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.num_connected_components(), 1) def test_circuit_unitary_factors1(self): """Test unitary factors empty circuit.""" @@ -1109,7 +1163,8 @@ def test_circuit_unitary_factors3(self): qc.h(q[3]) qc.cx(q[1], q[2]) qc.cx(q[1], q[2]) - qc.cx(q[0], q[3]).c_if(c, 2) + with self.assertWarns(DeprecationWarning): + qc.cx(q[0], q[3]).c_if(c, 2) qc.cx(q[0], q[3]) qc.cx(q[0], q[3]) qc.cx(q[0], q[3]) diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 984851e00388..bbbb494877f6 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -44,9 +44,12 @@ def test_circuit_qasm(self): qc.barrier(qr2) qc.cx(qr2[1], qr1[0]) qc.h(qr2[1]) - qc.x(qr2[1]).c_if(cr, 0) - qc.y(qr1[0]).c_if(cr, 1) - qc.z(qr1[0]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + qc.x(qr2[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.y(qr1[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.z(qr1[0]).c_if(cr, 2) qc.barrier(qr1, qr2) qc.measure(qr1[0], cr[0]) qc.measure(qr2[0], cr[1]) @@ -639,7 +642,8 @@ def test_circuit_raises_on_single_bit_condition(self): """OpenQASM 2 can't represent single-bit conditions, so test that a suitable error is printed if this is attempted.""" qc = QuantumCircuit(1, 1) - qc.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, True) with self.assertRaisesRegex(QasmError, "OpenQASM 2 can only condition on registers"): dumps(qc) diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index a0aeae5ca2c3..9759b5bffd1e 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -13,36 +13,63 @@ """Test commutation checker class .""" import unittest +from test import QiskitTestCase # pylint: disable=wrong-import-order import numpy as np +from ddt import idata, ddt from qiskit import ClassicalRegister from qiskit.circuit import ( - QuantumRegister, - Parameter, - Qubit, AnnotatedOperation, - InverseModifier, ControlModifier, Gate, + InverseModifier, + Parameter, + QuantumRegister, + Qubit, ) from qiskit.circuit.commutation_library import SessionCommutationChecker as scc -from qiskit.dagcircuit import DAGOpNode from qiskit.circuit.library import ( - ZGate, - XGate, - CXGate, + Barrier, CCXGate, + CPhaseGate, + CRXGate, + CRYGate, + CRZGate, + CXGate, + LinearFunction, MCXGate, - RZGate, Measure, - Barrier, + PhaseGate, Reset, - LinearFunction, - SGate, + RXGate, RXXGate, + RYGate, + RYYGate, + RZGate, + RZXGate, + RZZGate, + SGate, + XGate, + ZGate, + HGate, ) -from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.dagcircuit import DAGOpNode + +ROTATION_GATES = [ + RXGate, + RYGate, + RZGate, + PhaseGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, + RXXGate, + RYYGate, + RZZGate, + RZXGate, +] class NewGateCX(Gate): @@ -55,6 +82,7 @@ def to_matrix(self): return np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex) +@ddt class TestCommutationChecker(QiskitTestCase): """Test CommutationChecker class.""" @@ -145,16 +173,10 @@ def test_caching_different_qubit_sets(self): scc.commute(XGate(), [5], [], NewGateCX(), [5, 7], []) self.assertEqual(scc.num_cached_entries(), 1) - def test_cache_with_param_gates(self): + def test_zero_rotations(self): """Check commutativity between (non-parameterized) gates with parameters.""" - scc.clear_cached_commutations() - self.assertTrue(scc.commute(RZGate(0), [0], [], XGate(), [0], [])) - self.assertFalse(scc.commute(RZGate(np.pi / 2), [0], [], XGate(), [0], [])) - self.assertTrue(scc.commute(RZGate(np.pi / 2), [0], [], RZGate(0), [0], [])) - - self.assertFalse(scc.commute(RZGate(np.pi / 2), [1], [], XGate(), [1], [])) - self.assertEqual(scc.num_cached_entries(), 3) + self.assertTrue(scc.commute(XGate(), [0], [], RZGate(0), [0], [])) def test_gates_with_parameters(self): """Check commutativity between (non-parameterized) gates with parameters.""" @@ -172,6 +194,8 @@ def test_parameterized_gates(self): # gate that has parameters and is considered parameterized rz_gate_theta = RZGate(Parameter("Theta")) + rx_gate_theta = RXGate(Parameter("Theta")) + rxx_gate_theta = RXXGate(Parameter("Theta")) rz_gate_phi = RZGate(Parameter("Phi")) self.assertEqual(len(rz_gate_theta.params), 1) self.assertTrue(rz_gate_theta.is_parameterized()) @@ -193,7 +217,6 @@ def test_parameterized_gates(self): # We should detect that parameterized gates over disjoint qubit subsets commute self.assertTrue(scc.commute(rz_gate_theta, [0], [], rz_gate_phi, [1], [])) - # We should detect that parameterized gates over disjoint qubit subsets commute self.assertTrue(scc.commute(rz_gate_theta, [2], [], cx_gate, [1, 3], [])) # However, for now commutativity checker should return False when checking @@ -201,9 +224,14 @@ def test_parameterized_gates(self): # the two gates are over intersecting qubit subsets. # This check should be changed if commutativity checker is extended to # handle parameterized gates better. - self.assertFalse(scc.commute(rz_gate_theta, [0], [], cx_gate, [0, 1], [])) - - self.assertFalse(scc.commute(rz_gate_theta, [0], [], rz_gate, [0], [])) + self.assertFalse(scc.commute(rz_gate_theta, [1], [], cx_gate, [0, 1], [])) + self.assertTrue(scc.commute(rz_gate_theta, [0], [], rz_gate, [0], [])) + self.assertTrue(scc.commute(rz_gate_theta, [0], [], rz_gate_phi, [0], [])) + self.assertTrue(scc.commute(rxx_gate_theta, [0, 1], [], rx_gate_theta, [0], [])) + self.assertTrue(scc.commute(rxx_gate_theta, [0, 1], [], XGate(), [0], [])) + self.assertTrue(scc.commute(XGate(), [0], [], rxx_gate_theta, [0, 1], [])) + self.assertTrue(scc.commute(rx_gate_theta, [0], [], rxx_gate_theta, [0, 1], [])) + self.assertTrue(scc.commute(rz_gate_theta, [0], [], cx_gate, [0, 1], [])) def test_measure(self): """Check commutativity involving measures.""" @@ -252,26 +280,33 @@ def test_conditional_gates(self): # Currently, in all cases commutativity checker should returns False. # This is definitely suboptimal. - self.assertFalse( - scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], []) - ) - self.assertFalse( - scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], []) - ) - self.assertFalse( - scc.commute( - CXGate().c_if(cr[0], 0), - [qr[0], qr[1]], - [], - CXGate().c_if(cr[0], 0), - [qr[0], qr[1]], - [], + with self.assertWarns(DeprecationWarning): + self.assertFalse( + scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], []) + ) + with self.assertWarns(DeprecationWarning): + self.assertFalse( + scc.commute(CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], []) + ) + with self.assertWarns(DeprecationWarning): + self.assertFalse( + scc.commute( + CXGate().c_if(cr[0], 0), + [qr[0], qr[1]], + [], + CXGate().c_if(cr[0], 0), + [qr[0], qr[1]], + [], + ) ) - ) - self.assertFalse( - scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate().c_if(cr[0], 1), [qr[0]], []) - ) - self.assertFalse(scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate(), [qr[0]], [])) + with self.assertWarns(DeprecationWarning): + self.assertFalse( + scc.commute( + XGate().c_if(cr[0], 0), [qr[0]], [], XGate().c_if(cr[0], 1), [qr[0]], [] + ) + ) + with self.assertWarns(DeprecationWarning): + self.assertFalse(scc.commute(XGate().c_if(cr[0], 0), [qr[0]], [], XGate(), [qr[0]], [])) def test_complex_gates(self): """Check commutativity involving more complex gates.""" @@ -354,6 +389,47 @@ def test_serialization(self): cc2.commute_nodes(dop1, dop2) self.assertEqual(cc2.num_cached_entries(), 1) + @idata(ROTATION_GATES) + def test_cutoff_angles(self, gate_cls): + """Check rotations with a small enough angle are cut off.""" + max_power = 30 + from qiskit.circuit.library import DCXGate + + generic_gate = DCXGate() # gate that does not commute with any rotation gate + + cutoff_angle = 1e-5 # this is the cutoff we use in the CommutationChecker + + for i in range(1, max_power + 1): + angle = 2 ** (-i) + gate = gate_cls(angle) + qargs = list(range(gate.num_qubits)) + + if angle < cutoff_angle: + self.assertTrue(scc.commute(generic_gate, [0, 1], [], gate, qargs, [])) + else: + self.assertFalse(scc.commute(generic_gate, [0, 1], [], gate, qargs, [])) + + @idata(ROTATION_GATES) + def test_rotation_mod_2pi(self, gate_cls): + """Test the rotations modulo 2pi commute with any gate.""" + generic_gate = HGate() # does not commute with any rotation gate + even = np.arange(-6, 7, 2) + + with self.subTest(msg="even multiples"): + for multiple in even: + gate = gate_cls(multiple * np.pi) + self.assertTrue( + scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) + ) + + odd = np.arange(-5, 6, 2) + with self.subTest(msg="odd multiples"): + for multiple in odd: + gate = gate_cls(multiple * np.pi) + self.assertFalse( + scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) + ) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 0981b5f3ed79..8221f81ad15e 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -367,10 +367,12 @@ def __init__(self): super().__init__("mygate", 1, []) conditional = QuantumCircuit(1, 1) - conditional.append(Custom(), [0], []).c_if(conditional.clbits[0], True) + with self.assertWarns(DeprecationWarning): + conditional.append(Custom(), [0], []).c_if(conditional.clbits[0], True) test = base.compose(conditional, qubits=[0], clbits=[0], copy=False) self.assertIs(test.data[-1].operation, conditional.data[-1].operation) - self.assertEqual(test.data[-1].operation.condition, (test.clbits[0], True)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(test.data[-1].operation.condition, (test.clbits[0], True)) def test_compose_classical(self): """Composing on classical bits. @@ -463,16 +465,20 @@ def test_compose_conditional(self): creg = ClassicalRegister(2, "rcr") circuit_right = QuantumCircuit(qreg, creg) - circuit_right.x(qreg[1]).c_if(creg, 3) - circuit_right.h(qreg[0]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + circuit_right.x(qreg[1]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + circuit_right.h(qreg[0]).c_if(creg, 3) circuit_right.measure(qreg, creg) # permuted subset of qubits and clbits circuit_composed = self.circuit_left.compose(circuit_right, qubits=[1, 4], clbits=[0, 1]) circuit_expected = self.circuit_left.copy() - circuit_expected.x(self.left_qubit4).c_if(*self.condition) - circuit_expected.h(self.left_qubit1).c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + circuit_expected.x(self.left_qubit4).c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + circuit_expected.h(self.left_qubit1).c_if(*self.condition) circuit_expected.measure(self.left_qubit1, self.left_clbit0) circuit_expected.measure(self.left_qubit4, self.left_clbit1) @@ -489,24 +495,36 @@ def test_compose_conditional_no_match(self): right.cx(0, 1) right.h(0) right.measure([0, 1], [0, 1]) - right.z(2).c_if(right.cregs[0], 1) - right.x(2).c_if(right.cregs[1], 1) + with self.assertWarns(DeprecationWarning): + right.z(2).c_if(right.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + right.x(2).c_if(right.cregs[1], 1) test = QuantumCircuit(3, 3).compose(right, range(3), range(2)) z = next(ins.operation for ins in test.data[::-1] if ins.operation.name == "z") x = next(ins.operation for ins in test.data[::-1] if ins.operation.name == "x") # The registers should have been mapped, including the bits inside them. Unlike the # previous test, there are no matching registers in the destination circuit, so the # composition needs to add new registers (bit groupings) over the existing mapped bits. - self.assertIsNot(z.condition, None) - self.assertIsInstance(z.condition[0], ClassicalRegister) - self.assertEqual(len(z.condition[0]), len(right.cregs[0])) - self.assertIs(z.condition[0][0], test.clbits[0]) - self.assertEqual(z.condition[1], 1) - self.assertIsNot(x.condition, None) - self.assertIsInstance(x.condition[0], ClassicalRegister) - self.assertEqual(len(x.condition[0]), len(right.cregs[1])) - self.assertEqual(z.condition[1], 1) - self.assertIs(x.condition[0][0], test.clbits[1]) + with self.assertWarns(DeprecationWarning): + self.assertIsNot(z.condition, None) + with self.assertWarns(DeprecationWarning): + self.assertIsInstance(z.condition[0], ClassicalRegister) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(z.condition[0]), len(right.cregs[0])) + with self.assertWarns(DeprecationWarning): + self.assertIs(z.condition[0][0], test.clbits[0]) + with self.assertWarns(DeprecationWarning): + self.assertEqual(z.condition[1], 1) + with self.assertWarns(DeprecationWarning): + self.assertIsNot(x.condition, None) + with self.assertWarns(DeprecationWarning): + self.assertIsInstance(x.condition[0], ClassicalRegister) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(x.condition[0]), len(right.cregs[1])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(z.condition[1], 1) + with self.assertWarns(DeprecationWarning): + self.assertIs(x.condition[0][0], test.clbits[1]) def test_compose_switch_match(self): """Test that composition containing a `switch` with a register that matches proceeds @@ -729,11 +747,13 @@ def test_single_bit_condition(self): """Test that compose can correctly handle circuits that contain conditions on single bits. This is a regression test of the bug that broke qiskit-experiments in gh-7653.""" base = QuantumCircuit(1, 1) - base.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(0, True) test = QuantumCircuit(1, 1).compose(base) self.assertIsNot(base.clbits[0], test.clbits[0]) self.assertEqual(base, test) - self.assertIs(test.data[0].operation.condition[0], test.clbits[0]) + with self.assertWarns(DeprecationWarning): + self.assertIs(test.data[0].operation.condition[0], test.clbits[0]) def test_condition_mapping_ifelseop(self): """Test that the condition in an `IfElseOp` is correctly mapped to a new set of bits and diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index 35c57839d753..51733a37ffc9 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -587,7 +587,8 @@ def test_appending_switch_case_op(self, target, labels): self.assertEqual(qc.data[0].operation.name, "switch_case") self.assertEqual(qc.data[0].operation.params, bodies[: len(labels)]) - self.assertEqual(qc.data[0].operation.condition, None) + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.data[0].operation.condition, None) self.assertEqual(qc.data[0].qubits, tuple(qc.qubits[1:4])) self.assertEqual(qc.data[0].clbits, (qc.clbits[1],)) @@ -618,7 +619,7 @@ def test_quantumcircuit_switch(self, target, labels): self.assertEqual(qc.data[0].operation.name, "switch_case") self.assertEqual(qc.data[0].operation.params, bodies[: len(labels)]) - self.assertEqual(qc.data[0].operation.condition, None) + self.assertEqual(qc.data[0].operation._condition, None) self.assertEqual(qc.data[0].qubits, tuple(qc.qubits[1:4])) self.assertEqual(qc.data[0].clbits, (qc.clbits[1],)) @@ -808,15 +809,20 @@ def test_no_c_if_for_while_loop_if_else(self): body = QuantumCircuit(1) with self.assertRaisesRegex(NotImplementedError, r"cannot be classically controlled"): - qc.while_loop((qc.clbits[0], False), body, [qc.qubits[0]], []).c_if(qc.clbits[0], True) + with self.assertWarns(DeprecationWarning): + qc.while_loop((qc.clbits[0], False), body, [qc.qubits[0]], []).c_if( + qc.clbits[0], True + ) with self.assertRaisesRegex(NotImplementedError, r"cannot be classically controlled"): - qc.if_test((qc.clbits[0], False), body, [qc.qubits[0]], []).c_if(qc.clbits[0], True) + with self.assertWarns(DeprecationWarning): + qc.if_test((qc.clbits[0], False), body, [qc.qubits[0]], []).c_if(qc.clbits[0], True) with self.assertRaisesRegex(NotImplementedError, r"cannot be classically controlled"): - qc.if_else((qc.clbits[0], False), body, body, [qc.qubits[0]], []).c_if( - qc.clbits[0], True - ) + with self.assertWarns(DeprecationWarning): + qc.if_else((qc.clbits[0], False), body, body, [qc.qubits[0]], []).c_if( + qc.clbits[0], True + ) def test_nested_parameters_are_recognised(self): """Verify that parameters added inside a control-flow operator get added to the outer diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 7a3a0873ae77..419cfb406d19 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -193,12 +193,16 @@ def test_register_condition_in_nested_block(self): with self.subTest("if/c_if"): test = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) with test.if_test((cr1, 0)): - test.x(0).c_if(cr2, 0) - test.z(0).c_if(cr3, 0) + with self.assertWarns(DeprecationWarning): + test.x(0).c_if(cr2, 0) + with self.assertWarns(DeprecationWarning): + test.z(0).c_if(cr3, 0) true_body = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3) - true_body.x(0).c_if(cr2, 0) - true_body.z(0).c_if(cr3, 0) + with self.assertWarns(DeprecationWarning): + true_body.x(0).c_if(cr2, 0) + with self.assertWarns(DeprecationWarning): + true_body.z(0).c_if(cr3, 0) expected = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) expected.if_test((cr1, 0), true_body, [qr[0]], clbits + list(cr1)) @@ -209,14 +213,18 @@ def test_register_condition_in_nested_block(self): test = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) with test.while_loop((cr1, 0)): with test.if_test((cr2, 0)) as else_: - test.x(0).c_if(cr3, 0) + with self.assertWarns(DeprecationWarning): + test.x(0).c_if(cr3, 0) with else_: - test.z(0).c_if(cr4, 0) + with self.assertWarns(DeprecationWarning): + test.z(0).c_if(cr4, 0) true_body = QuantumCircuit([qr[0]], cr2, cr3, cr4) - true_body.x(0).c_if(cr3, 0) + with self.assertWarns(DeprecationWarning): + true_body.x(0).c_if(cr3, 0) false_body = QuantumCircuit([qr[0]], cr2, cr3, cr4) - false_body.z(0).c_if(cr4, 0) + with self.assertWarns(DeprecationWarning): + false_body.z(0).c_if(cr4, 0) while_body = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) while_body.if_else((cr2, 0), true_body, false_body, [qr[0]], clbits) @@ -647,17 +655,23 @@ def test_if_else_tracks_registers(self): test = QuantumCircuit(qr, *cr) with test.if_test((cr[0], 0)) as else_: - test.h(0).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(cr[1], 0) # Test repetition. - test.h(0).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(cr[1], 0) with else_: - test.h(0).c_if(cr[2], 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(cr[2], 0) true_body = QuantumCircuit([qr[0]], cr[0], cr[1], cr[2]) - true_body.h(qr[0]).c_if(cr[1], 0) - true_body.h(qr[0]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + true_body.h(qr[0]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + true_body.h(qr[0]).c_if(cr[1], 0) false_body = QuantumCircuit([qr[0]], cr[0], cr[1], cr[2]) - false_body.h(qr[0]).c_if(cr[2], 0) + with self.assertWarns(DeprecationWarning): + false_body.h(qr[0]).c_if(cr[2], 0) expected = QuantumCircuit(qr, *cr) expected.if_else( @@ -1036,11 +1050,13 @@ def test_break_continue_accept_c_if(self, loop_operation): test = QuantumCircuit(qubits, clbits) with test.for_loop(range(2)): test.h(0) - loop_operation(test).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + loop_operation(test).c_if(1, 0) body = QuantumCircuit([qubits[0]], [clbits[1]]) body.h(qubits[0]) - loop_operation(body).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + loop_operation(body).c_if(clbits[1], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, body, [qubits[0]], [clbits[1]]) @@ -1052,11 +1068,13 @@ def test_break_continue_accept_c_if(self, loop_operation): test = QuantumCircuit(qubits, clbits) with test.while_loop(cond): test.h(0) - loop_operation(test).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + loop_operation(test).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) body.h(qubits[0]) - loop_operation(body).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + loop_operation(body).c_if(clbits[1], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop(cond, body, [qubits[0]], clbits) @@ -1230,7 +1248,8 @@ def test_break_continue_nested_in_if(self, loop_operation): # full width of the loop do so. with test.if_test(cond_inner): pass - test.h(0).c_if(2, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(2, 0) true_body1 = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) loop_operation(true_body1) @@ -1240,7 +1259,8 @@ def test_break_continue_nested_in_if(self, loop_operation): loop_body = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) loop_body.if_test(cond_inner, true_body1, [qubits[0]], [clbits[0], clbits[2]]) loop_body.if_test(cond_inner, true_body2, [], [clbits[0]]) - loop_body.h(qubits[0]).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[2], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, loop_body, [qubits[0]], [clbits[0], clbits[2]]) @@ -1258,7 +1278,8 @@ def test_break_continue_nested_in_if(self, loop_operation): pass with else_: pass - test.h(0).c_if(2, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(2, 0) true_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) true_body1.h(qubits[1]) @@ -1273,7 +1294,8 @@ def test_break_continue_nested_in_if(self, loop_operation): cond_inner, true_body1, false_body1, [qubits[0], qubits[1]], [clbits[0], clbits[2]] ) loop_body.if_else(cond_inner, true_body2, false_body2, [], [clbits[0]]) - loop_body.h(qubits[0]).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[2], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop( @@ -1289,7 +1311,8 @@ def test_break_continue_nested_in_if(self, loop_operation): loop_operation(test) with test.if_test(cond_inner): pass - test.h(0).c_if(2, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(2, 0) true_body1 = QuantumCircuit([qubits[0], clbits[0], clbits[1], clbits[2]]) loop_operation(true_body1) @@ -1301,7 +1324,8 @@ def test_break_continue_nested_in_if(self, loop_operation): cond_inner, true_body1, [qubits[0]], [clbits[0], clbits[1], clbits[2]] ) loop_body.if_test(cond_inner, true_body2, [], [clbits[0]]) - loop_body.h(qubits[0]).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[2], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop( @@ -1321,7 +1345,8 @@ def test_break_continue_nested_in_if(self, loop_operation): pass with else_: pass - test.h(0).c_if(2, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(2, 0) true_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1], clbits[2]]) true_body1.h(qubits[1]) @@ -1340,7 +1365,8 @@ def test_break_continue_nested_in_if(self, loop_operation): [clbits[0], clbits[1], clbits[2]], ) loop_body.if_else(cond_inner, true_body2, false_body2, [], [clbits[0]]) - loop_body.h(qubits[0]).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[2], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop( @@ -1368,7 +1394,8 @@ def test_break_continue_nested_in_switch(self, loop_operation): with test.switch(clbits[0]) as case: with case(case.DEFAULT): pass - test.h(0).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(clbits[2], 0) body0 = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) loop_operation(body0) @@ -1379,7 +1406,8 @@ def test_break_continue_nested_in_switch(self, loop_operation): loop_body = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) loop_body.switch(clbits[0], [(0, body0), (1, body1)], [qubits[0]], [clbits[0], clbits[2]]) loop_body.switch(clbits[0], [(CASE_DEFAULT, body2)], [], [clbits[0]]) - loop_body.h(qubits[0]).c_if(clbits[2], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[2], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, loop_body, [qubits[0]], [clbits[0], clbits[2]]) @@ -1465,18 +1493,23 @@ def test_break_continue_deeply_nested(self, loop_operation): loop_operation(test) # inner true 2 with test.if_test(cond_inner): - test.h(0).c_if(3, 0) - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) # outer true 2 with test.if_test(cond_outer): - test.h(2).c_if(5, 0) - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) inner_true_body1 = QuantumCircuit(qubits[:4], clbits[:2], clbits[3:7]) loop_operation(inner_true_body1) inner_true_body2 = QuantumCircuit([qubits[0], clbits[0], clbits[3]]) - inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) outer_true_body1 = QuantumCircuit(qubits[:4], clbits[:2], clbits[3:7]) outer_true_body1.if_test( @@ -1485,15 +1518,18 @@ def test_break_continue_deeply_nested(self, loop_operation): outer_true_body1.if_test( cond_inner, inner_true_body2, [qubits[0]], [clbits[0], clbits[3]] ) - outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) outer_true_body2 = QuantumCircuit([qubits[2], clbits[1], clbits[5]]) - outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) loop_body = QuantumCircuit(qubits[:4], clbits[:2] + clbits[3:7]) loop_body.if_test(cond_outer, outer_true_body1, qubits[:4], clbits[:2] + clbits[3:7]) loop_body.if_test(cond_outer, outer_true_body2, [qubits[2]], [clbits[1], clbits[5]]) - loop_body.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[3]).c_if(clbits[6], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, loop_body, qubits[:4], clbits[:2] + clbits[3:7]) @@ -1507,31 +1543,43 @@ def test_break_continue_deeply_nested(self, loop_operation): with test.if_test(cond_outer): # inner 1 with test.if_test(cond_inner) as inner1_else: - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) with inner1_else: - loop_operation(test).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + loop_operation(test).c_if(4, 0) # inner 2 with test.if_test(cond_inner) as inner2_else: - test.h(1).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(5, 0) with inner2_else: - test.h(2).c_if(6, 0) - test.h(3).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(7, 0) # outer 2 with test.if_test(cond_outer) as outer2_else: - test.h(4).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(8, 0) with outer2_else: - test.h(5).c_if(9, 0) - test.h(6).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(10, 0) inner1_true = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) - inner1_true.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + inner1_true.h(qubits[0]).c_if(clbits[3], 0) inner1_false = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) - loop_operation(inner1_false).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + loop_operation(inner1_false).c_if(clbits[4], 0) inner2_true = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) - inner2_true.h(qubits[1]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + inner2_true.h(qubits[1]).c_if(clbits[5], 0) inner2_false = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) - inner2_false.h(qubits[2]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + inner2_false.h(qubits[2]).c_if(clbits[6], 0) outer1_true = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) outer1_true.if_else( @@ -1544,12 +1592,15 @@ def test_break_continue_deeply_nested(self, loop_operation): qubits[1:3], [clbits[0], clbits[5], clbits[6]], ) - outer1_true.h(qubits[3]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + outer1_true.h(qubits[3]).c_if(clbits[7], 0) outer2_true = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) - outer2_true.h(qubits[4]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[4]).c_if(clbits[8], 0) outer2_false = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) - outer2_false.h(qubits[5]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer2_false.h(qubits[5]).c_if(clbits[9], 0) loop_body = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) loop_body.if_test(cond_outer, outer1_true, qubits[:7], clbits[:2] + clbits[3:11]) @@ -1560,7 +1611,8 @@ def test_break_continue_deeply_nested(self, loop_operation): qubits[4:6], [clbits[1], clbits[8], clbits[9]], ) - loop_body.h(qubits[6]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[6]).c_if(clbits[10], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, loop_body, qubits[:7], clbits[:2] + clbits[3:11]) @@ -1574,72 +1626,90 @@ def test_break_continue_deeply_nested(self, loop_operation): test = QuantumCircuit(qubits, clbits) with test.for_loop(range(2)): - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) # outer 1 with test.if_test(cond_outer) as outer1_else: - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) with outer1_else: - test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) # outer 2 (nesting the inner condition in the 'if') with test.if_test(cond_outer) as outer2_else: - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) # inner 21 with test.if_test(cond_inner) as inner21_else: loop_operation(test) with inner21_else: - test.h(4).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(7, 0) # inner 22 with test.if_test(cond_inner) as inner22_else: - test.h(5).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(8, 0) with inner22_else: loop_operation(test) # inner 23 with test.switch(cond_inner[0]) as inner23_case: with inner23_case(True): - test.h(5).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(8, 0) with inner23_case(False): loop_operation(test) - test.h(6).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(9, 0) with outer2_else: - test.h(7).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(7).c_if(10, 0) # inner 24 with test.if_test(cond_inner) as inner24_else: - test.h(8).c_if(11, 0) + with self.assertWarns(DeprecationWarning): + test.h(8).c_if(11, 0) with inner24_else: - test.h(9).c_if(12, 0) + with self.assertWarns(DeprecationWarning): + test.h(9).c_if(12, 0) # outer 3 (nesting the inner condition in an 'else' branch) with test.if_test(cond_outer) as outer3_else: - test.h(10).c_if(13, 0) + with self.assertWarns(DeprecationWarning): + test.h(10).c_if(13, 0) with outer3_else: - test.h(11).c_if(14, 0) + with self.assertWarns(DeprecationWarning): + test.h(11).c_if(14, 0) # inner 31 with test.if_test(cond_inner) as inner31_else: loop_operation(test) with inner31_else: - test.h(12).c_if(15, 0) + with self.assertWarns(DeprecationWarning): + test.h(12).c_if(15, 0) # inner 32 with test.if_test(cond_inner) as inner32_else: - test.h(13).c_if(16, 0) + with self.assertWarns(DeprecationWarning): + test.h(13).c_if(16, 0) with inner32_else: loop_operation(test) # inner 33 with test.if_test(cond_inner) as inner33_else: - test.h(14).c_if(17, 0) + with self.assertWarns(DeprecationWarning): + test.h(14).c_if(17, 0) with inner33_else: - test.h(15).c_if(18, 0) + with self.assertWarns(DeprecationWarning): + test.h(15).c_if(18, 0) - test.h(16).c_if(19, 0) + with self.assertWarns(DeprecationWarning): + test.h(16).c_if(19, 0) # End of test "for" loop. # No `clbits[2]` here because that's only used in `cond_loop`, for while loops. @@ -1648,32 +1718,40 @@ def test_break_continue_deeply_nested(self, loop_operation): loop_bits = loop_qubits + loop_clbits outer1_true = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) - outer1_true.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + outer1_true.h(qubits[1]).c_if(clbits[4], 0) outer1_false = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) - outer1_false.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + outer1_false.h(qubits[2]).c_if(clbits[5], 0) inner21_true = QuantumCircuit(loop_bits) loop_operation(inner21_true) inner21_false = QuantumCircuit(loop_bits) - inner21_false.h(qubits[4]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + inner21_false.h(qubits[4]).c_if(clbits[7], 0) inner22_true = QuantumCircuit(loop_bits) - inner22_true.h(qubits[5]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + inner22_true.h(qubits[5]).c_if(clbits[8], 0) inner22_false = QuantumCircuit(loop_bits) loop_operation(inner22_false) inner23_true = QuantumCircuit(loop_bits) - inner23_true.h(qubits[5]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + inner23_true.h(qubits[5]).c_if(clbits[8], 0) inner23_false = QuantumCircuit(loop_bits) loop_operation(inner23_false) inner24_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner24_true.h(qubits[8]).c_if(clbits[11], 0) + with self.assertWarns(DeprecationWarning): + inner24_true.h(qubits[8]).c_if(clbits[11], 0) inner24_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner24_false.h(qubits[9]).c_if(clbits[12], 0) + with self.assertWarns(DeprecationWarning): + inner24_false.h(qubits[9]).c_if(clbits[12], 0) outer2_true = QuantumCircuit(loop_bits) - outer2_true.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[3]).c_if(clbits[6], 0) outer2_true.if_else(cond_inner, inner21_true, inner21_false, loop_qubits, loop_clbits) outer2_true.if_else(cond_inner, inner22_true, inner22_false, loop_qubits, loop_clbits) outer2_true.switch( @@ -1682,9 +1760,11 @@ def test_break_continue_deeply_nested(self, loop_operation): loop_qubits, loop_clbits, ) - outer2_true.h(qubits[6]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[6]).c_if(clbits[9], 0) outer2_false = QuantumCircuit(loop_bits) - outer2_false.h(qubits[7]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + outer2_false.h(qubits[7]).c_if(clbits[10], 0) outer2_false.if_else( cond_inner, inner24_true, @@ -1696,22 +1776,28 @@ def test_break_continue_deeply_nested(self, loop_operation): inner31_true = QuantumCircuit(loop_bits) loop_operation(inner31_true) inner31_false = QuantumCircuit(loop_bits) - inner31_false.h(qubits[12]).c_if(clbits[15], 0) + with self.assertWarns(DeprecationWarning): + inner31_false.h(qubits[12]).c_if(clbits[15], 0) inner32_true = QuantumCircuit(loop_bits) - inner32_true.h(qubits[13]).c_if(clbits[16], 0) + with self.assertWarns(DeprecationWarning): + inner32_true.h(qubits[13]).c_if(clbits[16], 0) inner32_false = QuantumCircuit(loop_bits) loop_operation(inner32_false) inner33_true = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) - inner33_true.h(qubits[14]).c_if(clbits[17], 0) + with self.assertWarns(DeprecationWarning): + inner33_true.h(qubits[14]).c_if(clbits[17], 0) inner33_false = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) - inner33_false.h(qubits[15]).c_if(clbits[18], 0) + with self.assertWarns(DeprecationWarning): + inner33_false.h(qubits[15]).c_if(clbits[18], 0) outer3_true = QuantumCircuit(loop_bits) - outer3_true.h(qubits[10]).c_if(clbits[13], 0) + with self.assertWarns(DeprecationWarning): + outer3_true.h(qubits[10]).c_if(clbits[13], 0) outer3_false = QuantumCircuit(loop_bits) - outer3_false.h(qubits[11]).c_if(clbits[14], 0) + with self.assertWarns(DeprecationWarning): + outer3_false.h(qubits[11]).c_if(clbits[14], 0) outer3_false.if_else(cond_inner, inner31_true, inner31_false, loop_qubits, loop_clbits) outer3_false.if_else(cond_inner, inner32_true, inner32_false, loop_qubits, loop_clbits) outer3_false.if_else( @@ -1723,7 +1809,8 @@ def test_break_continue_deeply_nested(self, loop_operation): ) loop_body = QuantumCircuit(loop_bits) - loop_body.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[3], 0) loop_body.if_else( cond_outer, outer1_true, @@ -1733,7 +1820,8 @@ def test_break_continue_deeply_nested(self, loop_operation): ) loop_body.if_else(cond_outer, outer2_true, outer2_false, loop_qubits, loop_clbits) loop_body.if_else(cond_outer, outer3_true, outer3_false, loop_qubits, loop_clbits) - loop_body.h(qubits[16]).c_if(clbits[19], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[16]).c_if(clbits[19], 0) expected = QuantumCircuit(qubits, clbits) expected.for_loop(range(2), None, loop_body, loop_qubits, loop_clbits) @@ -1756,33 +1844,41 @@ def test_break_continue_deeply_nested(self, loop_operation): loop_operation(test) # inner true 2 with test.if_test(cond_inner): - test.h(0).c_if(3, 0) - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) # outer true 2 with test.if_test(cond_outer): - test.h(2).c_if(5, 0) - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) inner_true_body1 = QuantumCircuit(qubits[:4], clbits[:7]) loop_operation(inner_true_body1) inner_true_body2 = QuantumCircuit([qubits[0], clbits[0], clbits[3]]) - inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) outer_true_body1 = QuantumCircuit(qubits[:4], clbits[:7]) outer_true_body1.if_test(cond_inner, inner_true_body1, qubits[:4], clbits[:7]) outer_true_body1.if_test( cond_inner, inner_true_body2, [qubits[0]], [clbits[0], clbits[3]] ) - outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) outer_true_body2 = QuantumCircuit([qubits[2], clbits[1], clbits[5]]) - outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) loop_body = QuantumCircuit(qubits[:4], clbits[:7]) loop_body.if_test(cond_outer, outer_true_body1, qubits[:4], clbits[:7]) loop_body.if_test(cond_outer, outer_true_body2, [qubits[2]], [clbits[1], clbits[5]]) - loop_body.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[3]).c_if(clbits[6], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop(cond_loop, loop_body, qubits[:4], clbits[:7]) @@ -1796,31 +1892,43 @@ def test_break_continue_deeply_nested(self, loop_operation): with test.if_test(cond_outer): # inner 1 with test.if_test(cond_inner) as inner1_else: - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) with inner1_else: - loop_operation(test).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + loop_operation(test).c_if(4, 0) # inner 2 with test.if_test(cond_inner) as inner2_else: - test.h(1).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(5, 0) with inner2_else: - test.h(2).c_if(6, 0) - test.h(3).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(7, 0) # outer 2 with test.if_test(cond_outer) as outer2_else: - test.h(4).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(8, 0) with outer2_else: - test.h(5).c_if(9, 0) - test.h(6).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(10, 0) inner1_true = QuantumCircuit(qubits[:7], clbits[:11]) - inner1_true.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + inner1_true.h(qubits[0]).c_if(clbits[3], 0) inner1_false = QuantumCircuit(qubits[:7], clbits[:11]) - loop_operation(inner1_false).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + loop_operation(inner1_false).c_if(clbits[4], 0) inner2_true = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) - inner2_true.h(qubits[1]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + inner2_true.h(qubits[1]).c_if(clbits[5], 0) inner2_false = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) - inner2_false.h(qubits[2]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + inner2_false.h(qubits[2]).c_if(clbits[6], 0) outer1_true = QuantumCircuit(qubits[:7], clbits[:11]) outer1_true.if_else(cond_inner, inner1_true, inner1_false, qubits[:7], clbits[:11]) @@ -1831,12 +1939,15 @@ def test_break_continue_deeply_nested(self, loop_operation): qubits[1:3], [clbits[0], clbits[5], clbits[6]], ) - outer1_true.h(qubits[3]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + outer1_true.h(qubits[3]).c_if(clbits[7], 0) outer2_true = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) - outer2_true.h(qubits[4]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[4]).c_if(clbits[8], 0) outer2_false = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) - outer2_false.h(qubits[5]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer2_false.h(qubits[5]).c_if(clbits[9], 0) loop_body = QuantumCircuit(qubits[:7], clbits[:11]) loop_body.if_test(cond_outer, outer1_true, qubits[:7], clbits[:11]) @@ -1847,7 +1958,8 @@ def test_break_continue_deeply_nested(self, loop_operation): qubits[4:6], [clbits[1], clbits[8], clbits[9]], ) - loop_body.h(qubits[6]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[6]).c_if(clbits[10], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop(cond_loop, loop_body, qubits[:7], clbits[:11]) @@ -1857,65 +1969,81 @@ def test_break_continue_deeply_nested(self, loop_operation): with self.subTest("while/else/else"): test = QuantumCircuit(qubits, clbits) with test.while_loop(cond_loop): - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) # outer 1 with test.if_test(cond_outer) as outer1_else: - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) with outer1_else: - test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) # outer 2 (nesting the inner condition in the 'if') with test.if_test(cond_outer) as outer2_else: - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) # inner 21 with test.if_test(cond_inner) as inner21_else: loop_operation(test) with inner21_else: - test.h(4).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(7, 0) # inner 22 with test.if_test(cond_inner) as inner22_else: - test.h(5).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(8, 0) with inner22_else: loop_operation(test) - test.h(6).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(9, 0) with outer2_else: - test.h(7).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(7).c_if(10, 0) # inner 23 with test.if_test(cond_inner) as inner23_else: - test.h(8).c_if(11, 0) + with self.assertWarns(DeprecationWarning): + test.h(8).c_if(11, 0) with inner23_else: - test.h(9).c_if(12, 0) + with self.assertWarns(DeprecationWarning): + test.h(9).c_if(12, 0) # outer 3 (nesting the inner condition in an 'else' branch) with test.if_test(cond_outer) as outer3_else: - test.h(10).c_if(13, 0) + with self.assertWarns(DeprecationWarning): + test.h(10).c_if(13, 0) with outer3_else: - test.h(11).c_if(14, 0) + with self.assertWarns(DeprecationWarning): + test.h(11).c_if(14, 0) # inner 31 with test.if_test(cond_inner) as inner31_else: loop_operation(test) with inner31_else: - test.h(12).c_if(15, 0) + with self.assertWarns(DeprecationWarning): + test.h(12).c_if(15, 0) # inner 32 with test.if_test(cond_inner) as inner32_else: - test.h(13).c_if(16, 0) + with self.assertWarns(DeprecationWarning): + test.h(13).c_if(16, 0) with inner32_else: loop_operation(test) # inner 33 with test.if_test(cond_inner) as inner33_else: - test.h(14).c_if(17, 0) + with self.assertWarns(DeprecationWarning): + test.h(14).c_if(17, 0) with inner33_else: - test.h(15).c_if(18, 0) - - test.h(16).c_if(19, 0) + with self.assertWarns(DeprecationWarning): + test.h(15).c_if(18, 0) + with self.assertWarns(DeprecationWarning): + test.h(16).c_if(19, 0) # End of test "for" loop. # No `clbits[2]` here because that's only used in `cond_loop`, for while loops. @@ -1924,32 +2052,41 @@ def test_break_continue_deeply_nested(self, loop_operation): loop_bits = loop_qubits + loop_clbits outer1_true = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) - outer1_true.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + outer1_true.h(qubits[1]).c_if(clbits[4], 0) outer1_false = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) - outer1_false.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + outer1_false.h(qubits[2]).c_if(clbits[5], 0) inner21_true = QuantumCircuit(loop_bits) loop_operation(inner21_true) inner21_false = QuantumCircuit(loop_bits) - inner21_false.h(qubits[4]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + inner21_false.h(qubits[4]).c_if(clbits[7], 0) inner22_true = QuantumCircuit(loop_bits) - inner22_true.h(qubits[5]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + inner22_true.h(qubits[5]).c_if(clbits[8], 0) inner22_false = QuantumCircuit(loop_bits) loop_operation(inner22_false) inner23_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner23_true.h(qubits[8]).c_if(clbits[11], 0) + with self.assertWarns(DeprecationWarning): + inner23_true.h(qubits[8]).c_if(clbits[11], 0) inner23_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner23_false.h(qubits[9]).c_if(clbits[12], 0) + with self.assertWarns(DeprecationWarning): + inner23_false.h(qubits[9]).c_if(clbits[12], 0) outer2_true = QuantumCircuit(loop_bits) - outer2_true.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[3]).c_if(clbits[6], 0) outer2_true.if_else(cond_inner, inner21_true, inner21_false, loop_qubits, loop_clbits) outer2_true.if_else(cond_inner, inner22_true, inner22_false, loop_qubits, loop_clbits) - outer2_true.h(qubits[6]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer2_true.h(qubits[6]).c_if(clbits[9], 0) outer2_false = QuantumCircuit(loop_bits) - outer2_false.h(qubits[7]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + outer2_false.h(qubits[7]).c_if(clbits[10], 0) outer2_false.if_else( cond_inner, inner23_true, @@ -1961,22 +2098,28 @@ def test_break_continue_deeply_nested(self, loop_operation): inner31_true = QuantumCircuit(loop_bits) loop_operation(inner31_true) inner31_false = QuantumCircuit(loop_bits) - inner31_false.h(qubits[12]).c_if(clbits[15], 0) + with self.assertWarns(DeprecationWarning): + inner31_false.h(qubits[12]).c_if(clbits[15], 0) inner32_true = QuantumCircuit(loop_bits) - inner32_true.h(qubits[13]).c_if(clbits[16], 0) + with self.assertWarns(DeprecationWarning): + inner32_true.h(qubits[13]).c_if(clbits[16], 0) inner32_false = QuantumCircuit(loop_bits) loop_operation(inner32_false) inner33_true = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) - inner33_true.h(qubits[14]).c_if(clbits[17], 0) + with self.assertWarns(DeprecationWarning): + inner33_true.h(qubits[14]).c_if(clbits[17], 0) inner33_false = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) - inner33_false.h(qubits[15]).c_if(clbits[18], 0) + with self.assertWarns(DeprecationWarning): + inner33_false.h(qubits[15]).c_if(clbits[18], 0) outer3_true = QuantumCircuit(loop_bits) - outer3_true.h(qubits[10]).c_if(clbits[13], 0) + with self.assertWarns(DeprecationWarning): + outer3_true.h(qubits[10]).c_if(clbits[13], 0) outer3_false = QuantumCircuit(loop_bits) - outer3_false.h(qubits[11]).c_if(clbits[14], 0) + with self.assertWarns(DeprecationWarning): + outer3_false.h(qubits[11]).c_if(clbits[14], 0) outer3_false.if_else(cond_inner, inner31_true, inner31_false, loop_qubits, loop_clbits) outer3_false.if_else(cond_inner, inner32_true, inner32_false, loop_qubits, loop_clbits) outer3_false.if_else( @@ -1988,7 +2131,8 @@ def test_break_continue_deeply_nested(self, loop_operation): ) loop_body = QuantumCircuit(loop_bits) - loop_body.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[0]).c_if(clbits[3], 0) loop_body.if_else( cond_outer, outer1_true, @@ -1998,7 +2142,8 @@ def test_break_continue_deeply_nested(self, loop_operation): ) loop_body.if_else(cond_outer, outer2_true, outer2_false, loop_qubits, loop_clbits) loop_body.if_else(cond_outer, outer3_true, outer3_false, loop_qubits, loop_clbits) - loop_body.h(qubits[16]).c_if(clbits[19], 0) + with self.assertWarns(DeprecationWarning): + loop_body.h(qubits[16]).c_if(clbits[19], 0) expected = QuantumCircuit(qubits, clbits) expected.while_loop(cond_loop, loop_body, loop_qubits, loop_clbits) @@ -2008,52 +2153,68 @@ def test_break_continue_deeply_nested(self, loop_operation): with self.subTest("if/while/if/switch"): test = QuantumCircuit(qubits, clbits) with test.if_test(cond_outer): # outer_t - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) with test.while_loop(cond_loop): # loop - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) with test.if_test(cond_inner): # inner_t - test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) with test.switch(5) as case_: with case_(False): # case_f - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) with case_(True): # case_t loop_operation(test) - test.h(4).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(7, 0) # exit inner_t - test.h(5).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(8, 0) # exit loop - test.h(6).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(9, 0) # exit outer_t - test.h(7).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(7).c_if(10, 0) case_f = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) - case_f.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + case_f.h(qubits[3]).c_if(clbits[6], 0) case_t = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) loop_operation(case_t) inner_t = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) - inner_t.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + inner_t.h(qubits[2]).c_if(clbits[5], 0) inner_t.switch( clbits[5], [(False, case_f), (True, case_t)], qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9], ) - inner_t.h(qubits[4]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + inner_t.h(qubits[4]).c_if(clbits[7], 0) loop = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) - loop.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + loop.h(qubits[1]).c_if(clbits[4], 0) loop.if_test(cond_inner, inner_t, qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) - loop.h(qubits[5]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + loop.h(qubits[5]).c_if(clbits[8], 0) outer_t = QuantumCircuit(qubits[:7], clbits[:10]) - outer_t.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + outer_t.h(qubits[0]).c_if(clbits[3], 0) outer_t.while_loop(cond_loop, loop, qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) - outer_t.h(qubits[6]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer_t.h(qubits[6]).c_if(clbits[9], 0) expected = QuantumCircuit(qubits, clbits) expected.if_test(cond_outer, outer_t, qubits[:7], clbits[:10]) - expected.h(qubits[7]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + expected.h(qubits[7]).c_if(clbits[10], 0) self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) @@ -2061,64 +2222,82 @@ def test_break_continue_deeply_nested(self, loop_operation): test = QuantumCircuit(qubits, clbits) with test.switch(0) as case_outer: with case_outer(False): # outer_case_f - test.h(0).c_if(3, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(3, 0) with test.for_loop(range(2)): # loop - test.h(1).c_if(4, 0) + with self.assertWarns(DeprecationWarning): + test.h(1).c_if(4, 0) with test.switch(1) as case_inner: with case_inner(False): # inner_case_f - test.h(2).c_if(5, 0) + with self.assertWarns(DeprecationWarning): + test.h(2).c_if(5, 0) with test.if_test((2, True)) as else_: # if_t - test.h(3).c_if(6, 0) + with self.assertWarns(DeprecationWarning): + test.h(3).c_if(6, 0) with else_: # if_f loop_operation(test) - test.h(4).c_if(7, 0) + with self.assertWarns(DeprecationWarning): + test.h(4).c_if(7, 0) with case_inner(True): # inner_case_t loop_operation(test) - test.h(5).c_if(8, 0) + with self.assertWarns(DeprecationWarning): + test.h(5).c_if(8, 0) # exit loop1 - test.h(6).c_if(9, 0) + with self.assertWarns(DeprecationWarning): + test.h(6).c_if(9, 0) with case_outer(True): # outer_case_t - test.h(7).c_if(10, 0) - test.h(8).c_if(11, 0) + with self.assertWarns(DeprecationWarning): + test.h(7).c_if(10, 0) + with self.assertWarns(DeprecationWarning): + test.h(8).c_if(11, 0) if_t = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) - if_t.h(qubits[3]).c_if(clbits[6], 0) + with self.assertWarns(DeprecationWarning): + if_t.h(qubits[3]).c_if(clbits[6], 0) if_f = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) loop_operation(if_f) inner_case_f = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) - inner_case_f.h(qubits[2]).c_if(clbits[5], 0) + with self.assertWarns(DeprecationWarning): + inner_case_f.h(qubits[2]).c_if(clbits[5], 0) inner_case_f.if_else( (clbits[2], True), if_t, if_f, qubits[1:6], clbits[1:3] + clbits[4:9] ) - inner_case_f.h(qubits[4]).c_if(clbits[7], 0) + with self.assertWarns(DeprecationWarning): + inner_case_f.h(qubits[4]).c_if(clbits[7], 0) inner_case_t = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) loop_operation(inner_case_t) loop = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) - loop.h(qubits[1]).c_if(clbits[4], 0) + with self.assertWarns(DeprecationWarning): + loop.h(qubits[1]).c_if(clbits[4], 0) loop.switch( clbits[1], [(False, inner_case_f), (True, inner_case_t)], qubits[1:6], clbits[1:3] + clbits[4:9], ) - loop.h(qubits[5]).c_if(clbits[8], 0) + with self.assertWarns(DeprecationWarning): + loop.h(qubits[5]).c_if(clbits[8], 0) outer_case_f = QuantumCircuit(qubits[:8], clbits[:11]) - outer_case_f.h(qubits[0]).c_if(clbits[3], 0) + with self.assertWarns(DeprecationWarning): + outer_case_f.h(qubits[0]).c_if(clbits[3], 0) outer_case_f.for_loop(range(2), None, loop, qubits[1:6], clbits[1:3] + clbits[4:9]) - outer_case_f.h(qubits[6]).c_if(clbits[9], 0) + with self.assertWarns(DeprecationWarning): + outer_case_f.h(qubits[6]).c_if(clbits[9], 0) outer_case_t = QuantumCircuit(qubits[:8], clbits[:11]) - outer_case_t.h(qubits[7]).c_if(clbits[10], 0) + with self.assertWarns(DeprecationWarning): + outer_case_t.h(qubits[7]).c_if(clbits[10], 0) expected = QuantumCircuit(qubits, clbits) expected.switch( clbits[0], [(False, outer_case_f), (True, outer_case_t)], qubits[:8], clbits[:11] ) - expected.h(qubits[8]).c_if(clbits[11], 0) + with self.assertWarns(DeprecationWarning): + expected.h(qubits[8]).c_if(clbits[11], 0) self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) @@ -2351,10 +2530,12 @@ def test_access_of_clbit_from_c_if(self): with self.subTest("if"): test = QuantumCircuit(bits) with test.if_test(cond): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) - body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(clbits[1], 0) expected = QuantumCircuit(bits) expected.if_test(cond, body, [qubits[0]], clbits) @@ -2363,41 +2544,49 @@ def test_access_of_clbit_from_c_if(self): with test.if_test(cond) as else_: pass with else_: - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) true_body = QuantumCircuit([qubits[0]], clbits) false_body = QuantumCircuit([qubits[0]], clbits) - false_body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + false_body.h(qubits[0]).c_if(clbits[1], 0) expected = QuantumCircuit(bits) expected.if_else(cond, true_body, false_body, [qubits[0]], clbits) with self.subTest("for"): test = QuantumCircuit(bits) with test.for_loop(range(2)): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) - body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(clbits[1], 0) expected = QuantumCircuit(bits) expected.for_loop(range(2), None, body, [qubits[0]], clbits) with self.subTest("while"): test = QuantumCircuit(bits) with test.while_loop(cond): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) - body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(clbits[1], 0) expected = QuantumCircuit(bits) expected.while_loop(cond, body, [qubits[0]], clbits) with self.subTest("switch"): test = QuantumCircuit(bits) with test.switch(cond[0]) as case, case(False): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) - body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(clbits[1], 0) expected = QuantumCircuit(bits) expected.switch(cond[0], [(False, body)], [qubits[0]], clbits) @@ -2405,10 +2594,12 @@ def test_access_of_clbit_from_c_if(self): test = QuantumCircuit(bits) with test.for_loop(range(2)): with test.if_test(cond): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) true_body = QuantumCircuit([qubits[0]], clbits) - true_body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + true_body.h(qubits[0]).c_if(clbits[1], 0) body = QuantumCircuit([qubits[0]], clbits) body.if_test(cond, body, [qubits[0]], clbits) expected = QuantumCircuit(bits) @@ -2417,10 +2608,12 @@ def test_access_of_clbit_from_c_if(self): with self.subTest("switch inside for"): test = QuantumCircuit(bits) with test.for_loop(range(2)), test.switch(cond[0]) as case, case(False): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits) - body.h(qubits[0]).c_if(clbits[1], 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(clbits[1], 0) body = QuantumCircuit([qubits[0]], clbits) body.switch(cond[0], [(False, body)], [qubits[0]], clbits) expected = QuantumCircuit(bits) @@ -2438,10 +2631,12 @@ def test_access_of_classicalregister_from_c_if(self): with self.subTest("if"): test = QuantumCircuit(qubits, clbits, creg) with test.if_test(cond): - test.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(creg, 0) body = QuantumCircuit([qubits[0]], clbits, creg) - body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(creg, 0) expected = QuantumCircuit(qubits, clbits, creg) expected.if_test(cond, body, [qubits[0]], all_clbits) @@ -2450,41 +2645,49 @@ def test_access_of_classicalregister_from_c_if(self): with test.if_test(cond) as else_: pass with else_: - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) true_body = QuantumCircuit([qubits[0]], clbits, creg) false_body = QuantumCircuit([qubits[0]], clbits, creg) - false_body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + false_body.h(qubits[0]).c_if(creg, 0) expected = QuantumCircuit(qubits, clbits, creg) expected.if_else(cond, true_body, false_body, [qubits[0]], all_clbits) with self.subTest("for"): test = QuantumCircuit(qubits, clbits, creg) with test.for_loop(range(2)): - test.h(0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(1, 0) body = QuantumCircuit([qubits[0]], clbits, creg) - body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(creg, 0) expected = QuantumCircuit(qubits, clbits, creg) expected.for_loop(range(2), None, body, [qubits[0]], all_clbits) with self.subTest("while"): test = QuantumCircuit(qubits, clbits, creg) with test.while_loop(cond): - test.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(creg, 0) body = QuantumCircuit([qubits[0]], clbits, creg) - body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(creg, 0) expected = QuantumCircuit(qubits, clbits, creg) expected.while_loop(cond, body, [qubits[0]], all_clbits) with self.subTest("switch"): test = QuantumCircuit(qubits, clbits, creg) with test.switch(cond[0]) as case, case(False): - test.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(creg, 0) body = QuantumCircuit([qubits[0]], clbits, creg) - body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + body.h(qubits[0]).c_if(creg, 0) expected = QuantumCircuit(qubits, clbits, creg) expected.switch(cond[0], [(False, body)], [qubits[0]], all_clbits) @@ -2492,10 +2695,12 @@ def test_access_of_classicalregister_from_c_if(self): test = QuantumCircuit(qubits, clbits, creg) with test.for_loop(range(2)): with test.if_test(cond): - test.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(creg, 0) true_body = QuantumCircuit([qubits[0]], clbits, creg) - true_body.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + true_body.h(qubits[0]).c_if(creg, 0) body = QuantumCircuit([qubits[0]], clbits, creg) body.if_test(cond, body, [qubits[0]], all_clbits) expected = QuantumCircuit(qubits, clbits, creg) @@ -2505,10 +2710,12 @@ def test_access_of_classicalregister_from_c_if(self): test = QuantumCircuit(qubits, clbits, creg) with test.for_loop(range(2)): with test.switch(cond[0]) as case, case(False): - test.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + test.h(0).c_if(creg, 0) case = QuantumCircuit([qubits[0]], clbits, creg) - case.h(qubits[0]).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + case.h(qubits[0]).c_if(creg, 0) body = QuantumCircuit([qubits[0]], clbits, creg) body.switch(cond[0], [(False, case)], [qubits[0]], all_clbits) expected = QuantumCircuit(qubits, clbits, creg) @@ -3424,7 +3631,8 @@ def test_if_placeholder_rejects_c_if(self): NotImplementedError, r"IfElseOp cannot be classically controlled through Instruction\.c_if", ): - placeholder.c_if(bits[1], 0) + with self.assertWarns(DeprecationWarning): + placeholder.c_if(bits[1], 0) with self.subTest("else"): test = QuantumCircuit(bits) @@ -3441,7 +3649,8 @@ def test_if_placeholder_rejects_c_if(self): NotImplementedError, r"IfElseOp cannot be classically controlled through Instruction\.c_if", ): - placeholder.c_if(bits[1], 0) + with self.assertWarns(DeprecationWarning): + placeholder.c_if(bits[1], 0) def test_switch_rejects_operations_outside_cases(self): """It shouldn't be permissible to try and put instructions inside a switch but outside a @@ -3543,7 +3752,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("else"): test = QuantumCircuit(bits) @@ -3554,7 +3764,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("for"): test = QuantumCircuit(bits) @@ -3563,7 +3774,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("while"): test = QuantumCircuit(bits) @@ -3572,7 +3784,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("switch"): test = QuantumCircuit(bits) @@ -3581,7 +3794,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("if inside for"): # As a side-effect of how the lazy building of 'if' statements works, we actually @@ -3595,7 +3809,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) with self.subTest("switch inside for"): # `switch` has the same lazy building as `if`, so is subject to the same considerations @@ -3608,7 +3823,8 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): - instructions.c_if(*cond) + with self.assertWarns(DeprecationWarning): + instructions.c_if(*cond) def test_raising_inside_context_manager_leave_circuit_usable(self): """Test that if we leave a builder by raising some sort of exception, the circuit is left in diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py index 73e00fa84c90..d9bdbc3fd13f 100644 --- a/test/python/circuit/test_extensions_standard.py +++ b/test/python/circuit/test_extensions_standard.py @@ -76,7 +76,8 @@ def test_barrier_invalid(self): def test_conditional_barrier_invalid(self): qc = self.circuit barrier = qc.barrier(self.qr) - self.assertRaises(QiskitError, barrier.c_if, self.cr, 0) + with self.assertWarns(DeprecationWarning): + self.assertRaises(QiskitError, barrier.c_if, self.cr, 0) def test_barrier_reg(self): self.circuit.barrier(self.qr) @@ -131,16 +132,20 @@ def test_ch_invalid(self): self.assertRaises(CircuitError, qc.ch, "a", self.qr[1]) def test_cif_reg(self): - self.circuit.h(self.qr[0]).c_if(self.cr, 7) + with self.assertWarns(DeprecationWarning): + self.circuit.h(self.qr[0]).c_if(self.cr, 7) self.assertEqual(self.circuit[0].operation.name, "h") self.assertEqual(self.circuit[0].qubits, (self.qr[0],)) - self.assertEqual(self.circuit[0].operation.condition, (self.cr, 7)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.circuit[0].operation.condition, (self.cr, 7)) def test_cif_single_bit(self): - self.circuit.h(self.qr[0]).c_if(self.cr[0], True) + with self.assertWarns(DeprecationWarning): + self.circuit.h(self.qr[0]).c_if(self.cr[0], True) self.assertEqual(self.circuit[0].operation.name, "h") self.assertEqual(self.circuit[0].qubits, (self.qr[0],)) - self.assertEqual(self.circuit[0].operation.condition, (self.cr[0], True)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.circuit[0].operation.condition, (self.cr[0], True)) def test_crz(self): self.circuit.crz(1, self.qr[0], self.qr[1]) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 220478159ee3..3497ef9fe46a 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -311,6 +311,15 @@ class TestGateEquivalenceEqual(QiskitTestCase): "_SingletonGateOverrides", "_SingletonControlledGateOverrides", "QFTGate", + "ModularAdderGate", + "HalfAdderGate", + "FullAdderGate", + "MultiplierGate", + "GraphStateGate", + "AndGate", + "OrGate", + "BitwiseXorGate", + "InnerProductGate", } # Amazingly, Python's scoping rules for class bodies means that this is the closest we can get diff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py index 990df5755d59..c0a8e568e647 100644 --- a/test/python/circuit/test_initializer.py +++ b/test/python/circuit/test_initializer.py @@ -39,6 +39,37 @@ class TestInitialize(QiskitTestCase): _desired_fidelity = 0.99 + def test_disentangled(self): + """test real-valued disentangled state initialization""" + state1 = np.random.rand(8) + state1 = state1 / np.linalg.norm(state1) + state2 = np.random.rand(8) + state2 = state2 / np.linalg.norm(state2) + state3 = np.random.rand(8) + state3 = state3 / np.linalg.norm(state3) + + qc1 = QuantumCircuit(9) + qc1.initialize(state1, [0, 2, 3]) + qc1.initialize(state2, [1, 8, 5]) + qc1.initialize(state3, [7, 6, 4]) + + statevector = Statevector(qc1) + + qc2 = QuantumCircuit(9) + qc2.initialize(statevector) + + qc1 = transpile(qc1, basis_gates=["u", "cx"]) + qc2 = transpile(qc2, basis_gates=["u", "cx"]) + + statevector1 = Statevector(qc1) + statevector2 = Statevector(qc2) + + counts1 = qc1.count_ops()["cx"] + counts2 = qc2.count_ops()["cx"] + + self.assertTrue(counts2 == counts1) + self.assertTrue(np.allclose(statevector1, statevector2)) + def test_uniform_superposition(self): """Initialize a uniform superposition on 2 qubits.""" desired_vector = [0.5, 0.5, 0.5, 0.5] @@ -59,6 +90,7 @@ def test_deterministic_state(self): qr = QuantumRegister(2, "qr") qc = QuantumCircuit(qr) qc.initialize(desired_vector, [qr[0], qr[1]]) + qc = transpile(qc, basis_gates=["u", "cx"]) statevector = Statevector(qc) fidelity = state_fidelity(statevector, desired_vector) self.assertGreater( diff --git a/test/python/circuit/test_instruction_repeat.py b/test/python/circuit/test_instruction_repeat.py index 778e6aad64c2..b2fcda1522e6 100644 --- a/test/python/circuit/test_instruction_repeat.py +++ b/test/python/circuit/test_instruction_repeat.py @@ -55,8 +55,10 @@ def test_standard_1Q_one(self): def test_conditional(self): """Test that repetition works with a condition.""" cr = ClassicalRegister(3, "cr") - gate = SGate().c_if(cr, 7).repeat(5) - self.assertEqual(gate.condition, (cr, 7)) + with self.assertWarns(DeprecationWarning): + gate = SGate().c_if(cr, 7).repeat(5) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate.condition, (cr, 7)) defn = QuantumCircuit(1) for _ in range(5): @@ -98,8 +100,10 @@ def test_standard_2Q_one(self): def test_conditional(self): """Test that repetition works with a condition.""" cr = ClassicalRegister(3, "cr") - gate = CXGate().c_if(cr, 7).repeat(5) - self.assertEqual(gate.condition, (cr, 7)) + with self.assertWarns(DeprecationWarning): + gate = CXGate().c_if(cr, 7).repeat(5) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate.condition, (cr, 7)) defn = QuantumCircuit(2) for _ in range(5): @@ -145,8 +149,10 @@ def test_measure_one(self): def test_measure_conditional(self): """Test conditional measure moves condition to the outside.""" cr = ClassicalRegister(3, "cr") - measure = Measure().c_if(cr, 7).repeat(5) - self.assertEqual(measure.condition, (cr, 7)) + with self.assertWarns(DeprecationWarning): + measure = Measure().c_if(cr, 7).repeat(5) + with self.assertWarns(DeprecationWarning): + self.assertEqual(measure.condition, (cr, 7)) defn = QuantumCircuit(1, 1) for _ in range(5): diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 170b47632c4d..da1d0797870e 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -176,7 +176,8 @@ def circuit_instruction_circuit_roundtrip(self): circ1.u(0.1, 0.2, -0.2, q[0]) circ1.barrier() circ1.measure(q, c) - circ1.rz(0.8, q[0]).c_if(c, 6) + with self.assertWarns(DeprecationWarning): + circ1.rz(0.8, q[0]).c_if(c, 6) inst = circ1.to_instruction() circ2 = QuantumCircuit(q, c, name="circ2") @@ -238,16 +239,20 @@ def test_reverse_instruction(self): circ.u(0.1, 0.2, -0.2, q[0]) circ.barrier() circ.measure(q[0], c[0]) - circ.rz(0.8, q[0]).c_if(c, 6) - inst = circ.to_instruction() + with self.assertWarns(DeprecationWarning): + circ.rz(0.8, q[0]).c_if(c, 6) + with self.assertWarns(DeprecationWarning): + inst = circ.to_instruction() circ = QuantumCircuit(q, c, name="circ") - circ.rz(0.8, q[0]).c_if(c, 6) + with self.assertWarns(DeprecationWarning): + circ.rz(0.8, q[0]).c_if(c, 6) circ.measure(q[0], c[0]) circ.barrier() circ.u(0.1, 0.2, -0.2, q[0]) circ.t(q[1]) - inst_reverse = circ.to_instruction() + with self.assertWarns(DeprecationWarning): + inst_reverse = circ.to_instruction() self.assertEqual(inst.reverse_ops().definition, inst_reverse.definition) @@ -336,8 +341,10 @@ def test_inverse_instruction_with_conditional(self): circ.u(0.1, 0.2, -0.2, q[0]) circ.barrier() circ.measure(q[0], c[0]) - circ.rz(0.8, q[0]).c_if(c, 6) - inst = circ.to_instruction() + with self.assertWarns(DeprecationWarning): + circ.rz(0.8, q[0]).c_if(c, 6) + with self.assertWarns(DeprecationWarning): + inst = circ.to_instruction() self.assertRaises(CircuitError, inst.inverse) def test_inverse_opaque(self): @@ -446,13 +453,18 @@ def key(bit): return body.find_bit(bit).index op = IfElseOp((bits[0], False), body) - self.assertEqual(op.condition_bits, [bits[0]]) + with self.assertWarns(DeprecationWarning): + self.assertEqual(op.condition_bits, [bits[0]]) op = IfElseOp((cr1, 3), body) - self.assertEqual(op.condition_bits, list(cr1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(op.condition_bits, list(cr1)) op = IfElseOp(expr.logic_and(bits[1], expr.equal(cr2, 3)), body) - self.assertEqual(sorted(op.condition_bits, key=key), sorted([bits[1]] + list(cr2), key=key)) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + sorted(op.condition_bits, key=key), sorted([bits[1]] + list(cr2), key=key) + ) def test_instructionset_c_if_direct_resource(self): """Test that using :meth:`.InstructionSet.c_if` with an exact classical resource always @@ -467,8 +479,10 @@ def test_instructionset_c_if_direct_resource(self): def case(resource): qc = QuantumCircuit(cr1, qubits, loose_clbits, cr2, cr3) - qc.x(0).c_if(resource, 0) - c_if_resource = qc.data[0].operation.condition[0] + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(resource, 0) + with self.assertWarns(DeprecationWarning): + c_if_resource = qc.data[0].operation.condition[0] self.assertIs(c_if_resource, resource) with self.subTest("classical register"): @@ -500,9 +514,11 @@ def test_instructionset_c_if_indexing(self): qc = QuantumCircuit(cr1, qubits, loose_clbits, cr2, cr3) for index, clbit in enumerate(qc.clbits): with self.subTest(index=index): - qc.x(0).c_if(index, 0) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(index, 0) qc.measure(0, index) - from_c_if = qc.data[-2].operation.condition[0] + with self.assertWarns(DeprecationWarning): + from_c_if = qc.data[-2].operation.condition[0] from_measure = qc.data[-1].clbits[0] self.assertIs(from_c_if, from_measure) # Sanity check that the bit is also the one we expected. @@ -516,14 +532,20 @@ def test_instructionset_c_if_size_1_classical_register(self): qc = QuantumCircuit(qr, cr) with self.subTest("classical register"): - qc.x(0).c_if(cr, 0) - self.assertIs(qc.data[-1].operation.condition[0], cr) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(qc.data[-1].operation.condition[0], cr) with self.subTest("classical bit by value"): - qc.x(0).c_if(cr[0], 0) - self.assertIs(qc.data[-1].operation.condition[0], cr[0]) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(cr[0], 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(qc.data[-1].operation.condition[0], cr[0]) with self.subTest("classical bit by index"): - qc.x(0).c_if(0, 0) - self.assertIs(qc.data[-1].operation.condition[0], cr[0]) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(qc.data[-1].operation.condition[0], cr[0]) def test_instructionset_c_if_no_classical_registers(self): """Test that using :meth:`.InstructionSet.c_if` works if there are no classical registers @@ -533,11 +555,15 @@ def test_instructionset_c_if_no_classical_registers(self): bits = [Qubit(), Clbit()] qc = QuantumCircuit(bits) with self.subTest("by value"): - qc.x(0).c_if(bits[1], 0) - self.assertIs(qc.data[-1].operation.condition[0], bits[1]) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(bits[1], 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(qc.data[-1].operation.condition[0], bits[1]) with self.subTest("by index"): - qc.x(0).c_if(0, 0) - self.assertIs(qc.data[-1].operation.condition[0], bits[1]) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(qc.data[-1].operation.condition[0], bits[1]) def test_instructionset_c_if_rejects_invalid_specifiers(self): """Test that calling the :meth:`.InstructionSet.c_if` method on instructions added to a @@ -550,7 +576,8 @@ def case(specifier, message): qc = QuantumCircuit(qreg, creg) instruction = qc.x(0) with self.assertRaisesRegex(CircuitError, message): - instruction.c_if(specifier, 0) + with self.assertWarns(DeprecationWarning): + instruction.c_if(specifier, 0) with self.subTest("absent bit"): case(Clbit(), r"Clbit .* is not present in this circuit\.") @@ -574,21 +601,26 @@ def test_instructionset_c_if_with_no_requester(self): instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) - instructions.c_if(register, 0) - self.assertIs(instructions[0].operation.condition[0], register) + with self.assertWarns(DeprecationWarning): + instructions.c_if(register, 0) + with self.assertWarns(DeprecationWarning): + 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(instructions[0].operation.condition[0], bit) + with self.assertWarns(DeprecationWarning): + instructions.c_if(bit, 0) + with self.assertWarns(DeprecationWarning): + self.assertIs(instructions[0].operation.condition[0], bit) with self.subTest("rejects index"): instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) with self.assertRaisesRegex(CircuitError, r"Cannot pass an index as a condition .*"): - instructions.c_if(0, 0) + with self.assertWarns(DeprecationWarning): + instructions.c_if(0, 0) def test_instructionset_c_if_calls_custom_requester(self): """Test that :meth:`.InstructionSet.c_if` calls a custom requester, and uses its output.""" @@ -613,27 +645,33 @@ def dummy_requester(specifier): instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) bit = Clbit() - instructions.c_if(bit, 0) + with self.assertWarns(DeprecationWarning): + instructions.c_if(bit, 0) dummy_requester.assert_called_once_with(bit) - self.assertIs(instructions[0].operation.condition[0], sentinel_bit) + with self.assertWarns(DeprecationWarning): + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) index = 0 - instructions.c_if(index, 0) + with self.assertWarns(DeprecationWarning): + instructions.c_if(index, 0) dummy_requester.assert_called_once_with(index) - self.assertIs(instructions[0].operation.condition[0], sentinel_bit) + with self.assertWarns(DeprecationWarning): + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() instruction = RZGate(0) instructions = InstructionSet(resource_requester=dummy_requester) instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) - instructions.c_if(register, 0) + with self.assertWarns(DeprecationWarning): + instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) - self.assertIs(instructions[0].operation.condition[0], sentinel_register) + with self.assertWarns(DeprecationWarning): + 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)] @@ -641,10 +679,12 @@ def dummy_requester(specifier): for instruction in instruction_list: instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) - instructions.c_if(register, 0) + with self.assertWarns(DeprecationWarning): + instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) for instruction in instruction_list: - self.assertIs(instructions[0].operation.condition[0], sentinel_register) + with self.assertWarns(DeprecationWarning): + 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_random_circuit.py b/test/python/circuit/test_random_circuit.py index ebbdfd28d648..0845fdbe97d6 100644 --- a/test/python/circuit/test_random_circuit.py +++ b/test/python/circuit/test_random_circuit.py @@ -49,18 +49,20 @@ def test_random_circuit_conditional_reset(self): """Test generating random circuits with conditional and reset.""" num_qubits = 1 depth = 100 - circ = random_circuit(num_qubits, depth, conditional=True, reset=True, seed=5) + with self.assertWarns(DeprecationWarning): + circ = random_circuit(num_qubits, depth, conditional=True, reset=True, seed=5) self.assertEqual(circ.width(), 2 * num_qubits) self.assertIn("reset", circ.count_ops()) def test_large_conditional(self): """Test that conditions do not fail with large conditionals. Regression test of gh-6994.""" # The main test is that this call actually returns without raising an exception. - circ = random_circuit(64, 2, conditional=True, seed=0) + with self.assertWarns(DeprecationWarning): + circ = random_circuit(64, 2, conditional=True, seed=0) # Test that at least one instruction had a condition generated. It's possible that this # fails due to very bad luck with the random seed - if so, change the seed to ensure that a # condition _is_ generated, because we need to test that generation doesn't error. - conditions = (getattr(instruction.operation, "condition", None) for instruction in circ) + conditions = (getattr(instruction.operation, "_condition", None) for instruction in circ) conditions = [x for x in conditions if x is not None] self.assertNotEqual(conditions, []) for register, value in conditions: @@ -72,14 +74,15 @@ 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=16) + with self.assertWarns(DeprecationWarning): + 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. self.assertEqual(4, len(op_names)) self.assertEqual(["measure"] * num_qubits, op_names[1 : 1 + num_qubits]) conditions = [ - bool(getattr(instruction.operation, "condition", None)) for instruction in circ + bool(getattr(instruction.operation, "_condition", None)) for instruction in circ ] self.assertEqual([False, False, False, True], conditions) diff --git a/test/python/circuit/test_registerless_circuit.py b/test/python/circuit/test_registerless_circuit.py index 3f77919957da..c3da073e5234 100644 --- a/test/python/circuit/test_registerless_circuit.py +++ b/test/python/circuit/test_registerless_circuit.py @@ -195,10 +195,12 @@ def test_circuit_conditional(self): qreg = QuantumRegister(2) creg = ClassicalRegister(4) circuit = QuantumCircuit(qreg, creg) - circuit.h(0).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(creg, 3) expected = QuantumCircuit(qreg, creg) - expected.h(qreg[0]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + expected.h(qreg[0]).c_if(creg, 3) self.assertEqual(circuit, expected) @@ -333,11 +335,14 @@ def test_circuit_conditional(self): qreg1 = QuantumRegister(2) creg = ClassicalRegister(2) circuit = QuantumCircuit(qreg0, qreg1, creg) - circuit.h(range(1, 3)).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + circuit.h(range(1, 3)).c_if(creg, 3) expected = QuantumCircuit(qreg0, qreg1, creg) - expected.h(qreg0[1]).c_if(creg, 3) - expected.h(qreg1[0]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + expected.h(qreg0[1]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + expected.h(qreg1[0]).c_if(creg, 3) self.assertEqual(circuit, expected) @@ -466,11 +471,14 @@ def test_circuit_conditional(self): qreg1 = QuantumRegister(2) creg = ClassicalRegister(2) circuit = QuantumCircuit(qreg0, qreg1, creg) - circuit.h(slice(1, 3)).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + circuit.h(slice(1, 3)).c_if(creg, 3) expected = QuantumCircuit(qreg0, qreg1, creg) - expected.h(qreg0[1]).c_if(creg, 3) - expected.h(qreg1[0]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + expected.h(qreg0[1]).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + expected.h(qreg1[0]).c_if(creg, 3) self.assertEqual(circuit, expected) @@ -504,10 +512,12 @@ def test_bit_conditional_single_gate(self): qreg = QuantumRegister(1) creg = ClassicalRegister(2) circuit = QuantumCircuit(qreg, creg) - circuit.h(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(0, True) expected = QuantumCircuit(qreg, creg) - expected.h(qreg[0]).c_if(creg[0], True) + with self.assertWarns(DeprecationWarning): + expected.h(qreg[0]).c_if(creg[0], True) self.assertEqual(circuit, expected) def test_bit_conditional_multiple_gates(self): @@ -516,12 +526,18 @@ def test_bit_conditional_multiple_gates(self): creg = ClassicalRegister(2) creg1 = ClassicalRegister(1) circuit = QuantumCircuit(qreg, creg, creg1) - circuit.h(0).c_if(0, True) - circuit.h(1).c_if(1, False) - circuit.cx(1, 0).c_if(2, True) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + circuit.h(1).c_if(1, False) + with self.assertWarns(DeprecationWarning): + circuit.cx(1, 0).c_if(2, True) expected = QuantumCircuit(qreg, creg, creg1) - expected.h(qreg[0]).c_if(creg[0], True) - expected.h(qreg[1]).c_if(creg[1], False) - expected.cx(qreg[1], qreg[0]).c_if(creg1[0], True) + with self.assertWarns(DeprecationWarning): + expected.h(qreg[0]).c_if(creg[0], True) + with self.assertWarns(DeprecationWarning): + expected.h(qreg[1]).c_if(creg[1], False) + with self.assertWarns(DeprecationWarning): + expected.cx(qreg[1], qreg[0]).c_if(creg1[0], True) self.assertEqual(circuit, expected) diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index 6348d31487bd..7ebed694a8db 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -22,6 +22,7 @@ from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 from qiskit.providers.basic_provider import BasicSimulator from qiskit.scheduler import ScheduleConfig +from qiskit.transpiler import InstructionProperties from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -194,12 +195,16 @@ def test_transpile_delay_circuit_without_backend(self): qc.h(0) qc.delay(500, 1) qc.cx(0, 1) - scheduled = transpile( - qc, - scheduling_method="alap", - basis_gates=["h", "cx"], - instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)], - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + scheduled = transpile( + qc, + scheduling_method="alap", + basis_gates=["h", "cx"], + instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)], + ) self.assertEqual(scheduled.duration, 1200) def test_transpile_circuit_with_custom_instruction(self): @@ -210,9 +215,13 @@ def test_transpile_circuit_with_custom_instruction(self): qc = QuantumCircuit(2) qc.delay(500, 1) qc.append(bell.to_instruction(), [0, 1]) - scheduled = transpile( - qc, scheduling_method="alap", instruction_durations=[("bell", [0, 1], 1000)] - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + scheduled = transpile( + qc, scheduling_method="alap", instruction_durations=[("bell", [0, 1], 1000)] + ) self.assertEqual(scheduled.duration, 1500) def test_transpile_delay_circuit_with_dt_but_without_scheduling_method(self): @@ -263,21 +272,29 @@ def test_default_units_for_my_own_duration_users(self): qc.h(0) qc.delay(500, 1) qc.cx(0, 1) - # accept None for qubits - scheduled = transpile( - qc, - basis_gates=["h", "cx", "delay"], - scheduling_method="alap", - instruction_durations=[("h", 0, 200), ("cx", None, 900)], - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + # accept None for qubits + scheduled = transpile( + qc, + basis_gates=["h", "cx", "delay"], + scheduling_method="alap", + instruction_durations=[("h", 0, 200), ("cx", None, 900)], + ) self.assertEqual(scheduled.duration, 1400) - # prioritize specified qubits over None - scheduled = transpile( - qc, - basis_gates=["h", "cx", "delay"], - scheduling_method="alap", - instruction_durations=[("h", 0, 200), ("cx", None, 900), ("cx", [0, 1], 800)], - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + # prioritize specified qubits over None + scheduled = transpile( + qc, + basis_gates=["h", "cx", "delay"], + scheduling_method="alap", + instruction_durations=[("h", 0, 200), ("cx", None, 900), ("cx", [0, 1], 800)], + ) self.assertEqual(scheduled.duration, 1300) def test_unit_seconds_when_using_backend_durations(self): @@ -313,19 +330,23 @@ def test_unit_seconds_when_using_backend_durations(self): ) self.assertEqual(scheduled.duration, 1500) - def test_per_qubit_durations(self): - """See: https://github.com/Qiskit/qiskit-terra/issues/5109""" + def test_per_qubit_durations_loose_constrain(self): + """See Qiskit/5109 and Qiskit/13306""" qc = QuantumCircuit(3) qc.h(0) qc.delay(500, 1) qc.cx(0, 1) qc.h(1) - sc = transpile( - qc, - scheduling_method="alap", - basis_gates=["h", "cx"], - instruction_durations=[("h", None, 200), ("cx", [0, 1], 700)], - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + sc = transpile( + qc, + scheduling_method="alap", + basis_gates=["h", "cx"], + instruction_durations=[("h", None, 200), ("cx", [0, 1], 700)], + ) self.assertEqual(sc.qubit_start_time(0), 300) self.assertEqual(sc.qubit_stop_time(0), 1200) self.assertEqual(sc.qubit_start_time(1), 500) @@ -336,12 +357,21 @@ def test_per_qubit_durations(self): self.assertEqual(sc.qubit_stop_time(0, 1), 1400) qc.measure_all() - sc = transpile( - qc, - scheduling_method="alap", - basis_gates=["h", "cx", "measure"], - instruction_durations=[("h", None, 200), ("cx", [0, 1], 700), ("measure", None, 1000)], - ) + + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + sc = transpile( + qc, + scheduling_method="alap", + basis_gates=["h", "cx", "measure"], + instruction_durations=[ + ("h", None, 200), + ("cx", [0, 1], 700), + ("measure", None, 1000), + ], + ) q = sc.qubits self.assertEqual(sc.qubit_start_time(q[0]), 300) self.assertEqual(sc.qubit_stop_time(q[0]), 2400) @@ -352,6 +382,57 @@ def test_per_qubit_durations(self): self.assertEqual(sc.qubit_start_time(*q), 300) self.assertEqual(sc.qubit_stop_time(*q), 2400) + def test_per_qubit_durations(self): + """Test target with custom instruction_durations""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="argument ``calibrate_instructions`` is deprecated", + ): + target = GenericBackendV2( + 3, + calibrate_instructions=True, + coupling_map=[[0, 1], [1, 2]], + basis_gates=["cx", "h"], + seed=42, + ).target + target.update_instruction_properties("cx", (0, 1), InstructionProperties(0.00001)) + target.update_instruction_properties("cx", (1, 2), InstructionProperties(0.00001)) + target.update_instruction_properties("h", (0,), InstructionProperties(0.000002)) + target.update_instruction_properties("h", (1,), InstructionProperties(0.000002)) + target.update_instruction_properties("h", (2,), InstructionProperties(0.000002)) + + qc = QuantumCircuit(3) + qc.h(0) + qc.delay(500, 1) + qc.cx(0, 1) + qc.h(1) + + sc = transpile(qc, scheduling_method="alap", target=target) + self.assertEqual(sc.qubit_start_time(0), 500) + self.assertEqual(sc.qubit_stop_time(0), 54554) + self.assertEqual(sc.qubit_start_time(1), 9509) + self.assertEqual(sc.qubit_stop_time(1), 63563) + self.assertEqual(sc.qubit_start_time(2), 0) + self.assertEqual(sc.qubit_stop_time(2), 0) + self.assertEqual(sc.qubit_start_time(0, 1), 500) + self.assertEqual(sc.qubit_stop_time(0, 1), 63563) + + qc.measure_all() + + target.update_instruction_properties("measure", (0,), InstructionProperties(0.0001)) + target.update_instruction_properties("measure", (1,), InstructionProperties(0.0001)) + + sc = transpile(qc, scheduling_method="alap", target=target) + q = sc.qubits + self.assertEqual(sc.qubit_start_time(q[0]), 500) + self.assertEqual(sc.qubit_stop_time(q[0]), 514013) + self.assertEqual(sc.qubit_start_time(q[1]), 9509) + self.assertEqual(sc.qubit_stop_time(q[1]), 514013) + self.assertEqual(sc.qubit_start_time(q[2]), 63563) + self.assertEqual(sc.qubit_stop_time(q[2]), 514013) + self.assertEqual(sc.qubit_start_time(*q), 500) + self.assertEqual(sc.qubit_stop_time(*q), 514013) + def test_convert_duration_to_dt(self): """Test that circuit duration unit conversion is applied only when necessary. Tests fix for bug reported in PR #11782.""" diff --git a/test/python/circuit/test_singleton.py b/test/python/circuit/test_singleton.py index 40549dd0ad15..0274242eec8e 100644 --- a/test/python/circuit/test_singleton.py +++ b/test/python/circuit/test_singleton.py @@ -63,7 +63,8 @@ def test_label_not_singleton(self): def test_condition_not_singleton(self): gate = HGate() - condition_gate = HGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + condition_gate = HGate().c_if(Clbit(), 0) self.assertIsNot(gate, condition_gate) def test_raise_on_state_mutation(self): @@ -76,10 +77,12 @@ def test_raise_on_state_mutation(self): def test_labeled_condition(self): singleton_gate = HGate() clbit = Clbit() - gate = HGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = HGate(label="conditionally special").c_if(clbit, 0) self.assertIsNot(singleton_gate, gate) self.assertEqual(gate.label, "conditionally special") - self.assertEqual(gate.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate.condition, (clbit, 0)) def test_default_singleton_copy(self): gate = HGate() @@ -109,19 +112,22 @@ def test_label_copy_new(self): self.assertEqual(copied_label.label, "special") def test_condition_copy(self): - gate = HGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + gate = HGate().c_if(Clbit(), 0) copied = gate.copy() self.assertIsNot(gate, copied) self.assertEqual(gate, copied) def test_condition_label_copy(self): clbit = Clbit() - gate = HGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = HGate(label="conditionally special").c_if(clbit, 0) copied = gate.copy() self.assertIsNot(gate, copied) self.assertEqual(gate, copied) self.assertEqual(copied.label, "conditionally special") - self.assertEqual(copied.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(copied.condition, (clbit, 0)) def test_deepcopy(self): gate = HGate() @@ -136,19 +142,22 @@ def test_deepcopy_with_label(self): self.assertEqual(copied.label, "special") def test_deepcopy_with_condition(self): - gate = HGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + gate = HGate().c_if(Clbit(), 0) copied = copy.deepcopy(gate) self.assertIsNot(gate, copied) self.assertEqual(gate, copied) def test_condition_label_deepcopy(self): clbit = Clbit() - gate = HGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = HGate(label="conditionally special").c_if(clbit, 0) copied = copy.deepcopy(gate) self.assertIsNot(gate, copied) self.assertEqual(gate, copied) self.assertEqual(copied.label, "conditionally special") - self.assertEqual(copied.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(copied.condition, (clbit, 0)) def test_label_deepcopy_new(self): gate = HGate() @@ -193,23 +202,27 @@ def test_round_trip_dag_conversion_with_label(self): def test_round_trip_dag_conversion_with_condition(self): qc = QuantumCircuit(1, 1) - gate = HGate().c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + gate = HGate().c_if(qc.cregs[0], 0) qc.append(gate, [0]) dag = circuit_to_dag(qc) out = dag_to_circuit(dag) self.assertIsNot(qc.data[0].operation, out.data[0].operation) self.assertEqual(qc.data[0].operation, out.data[0].operation) - self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) def test_round_trip_dag_conversion_condition_label(self): qc = QuantumCircuit(1, 1) - gate = HGate(label="conditionally special").c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + gate = HGate(label="conditionally special").c_if(qc.cregs[0], 0) qc.append(gate, [0]) dag = circuit_to_dag(qc) out = dag_to_circuit(dag) self.assertIsNot(qc.data[0].operation, out.data[0].operation) self.assertEqual(qc.data[0].operation, out.data[0].operation) - self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) self.assertEqual(out.data[0].operation.label, "conditionally special") def test_condition_via_instructionset(self): @@ -217,9 +230,11 @@ def test_condition_via_instructionset(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 1) self.assertIsNot(gate, circuit.data[0].operation) - self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) def test_is_mutable(self): gate = HGate() @@ -249,7 +264,8 @@ def test_to_mutable_setter(self): self.assertEqual(mutable_gate.label, "foo") self.assertEqual(mutable_gate.duration, 3) self.assertEqual(mutable_gate.unit, "s") - self.assertEqual(mutable_gate.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(mutable_gate.condition, (clbit, 0)) def test_to_mutable_of_mutable_instance(self): gate = HGate(label="foo") @@ -287,9 +303,11 @@ def test_immutable_pickle(self): def test_mutable_pickle(self): gate = SXGate() clbit = Clbit() - condition_gate = gate.c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + condition_gate = gate.c_if(clbit, 0) self.assertIsNot(gate, condition_gate) - self.assertEqual(condition_gate.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(condition_gate.condition, (clbit, 0)) self.assertTrue(condition_gate.mutable) with io.BytesIO() as fd: pickle.dump(condition_gate, fd) @@ -505,7 +523,8 @@ def test_label_not_singleton(self): def test_condition_not_singleton(self): gate = CZGate() - condition_gate = CZGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + condition_gate = CZGate().c_if(Clbit(), 0) self.assertIsNot(gate, condition_gate) def test_raise_on_state_mutation(self): @@ -518,10 +537,12 @@ def test_raise_on_state_mutation(self): def test_labeled_condition(self): singleton_gate = CSwapGate() clbit = Clbit() - gate = CSwapGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = CSwapGate(label="conditionally special").c_if(clbit, 0) self.assertIsNot(singleton_gate, gate) self.assertEqual(gate.label, "conditionally special") - self.assertEqual(gate.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate.condition, (clbit, 0)) def test_default_singleton_copy(self): gate = CXGate() @@ -551,19 +572,22 @@ def test_label_copy_new(self): self.assertEqual(copied_label.label, "special") def test_condition_copy(self): - gate = CZGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + gate = CZGate().c_if(Clbit(), 0) copied = gate.copy() self.assertIsNot(gate, copied) self.assertEqual(gate, copied) def test_condition_label_copy(self): clbit = Clbit() - gate = CZGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = CZGate(label="conditionally special").c_if(clbit, 0) copied = gate.copy() self.assertIsNot(gate, copied) self.assertEqual(gate, copied) self.assertEqual(copied.label, "conditionally special") - self.assertEqual(copied.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(copied.condition, (clbit, 0)) def test_deepcopy(self): gate = CXGate() @@ -583,19 +607,22 @@ def test_deepcopy_with_label(self): self.assertNotEqual(singleton_gate.label, copied.label) def test_deepcopy_with_condition(self): - gate = CCXGate().c_if(Clbit(), 0) + with self.assertWarns(DeprecationWarning): + gate = CCXGate().c_if(Clbit(), 0) copied = copy.deepcopy(gate) self.assertIsNot(gate, copied) self.assertEqual(gate, copied) def test_condition_label_deepcopy(self): clbit = Clbit() - gate = CHGate(label="conditionally special").c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + gate = CHGate(label="conditionally special").c_if(clbit, 0) copied = copy.deepcopy(gate) self.assertIsNot(gate, copied) self.assertEqual(gate, copied) self.assertEqual(copied.label, "conditionally special") - self.assertEqual(copied.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(copied.condition, (clbit, 0)) def test_label_deepcopy_new(self): gate = CHGate() @@ -640,23 +667,27 @@ def test_round_trip_dag_conversion_with_label(self): def test_round_trip_dag_conversion_with_condition(self): qc = QuantumCircuit(2, 1) - gate = CHGate().c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + gate = CHGate().c_if(qc.cregs[0], 0) qc.append(gate, [0, 1]) dag = circuit_to_dag(qc) out = dag_to_circuit(dag) self.assertIsNot(qc.data[0].operation, out.data[0].operation) self.assertEqual(qc.data[0].operation, out.data[0].operation) - self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) def test_round_trip_dag_conversion_condition_label(self): qc = QuantumCircuit(2, 1) - gate = CHGate(label="conditionally special").c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + gate = CHGate(label="conditionally special").c_if(qc.cregs[0], 0) qc.append(gate, [0, 1]) dag = circuit_to_dag(qc) out = dag_to_circuit(dag) self.assertIsNot(qc.data[0].operation, out.data[0].operation) self.assertEqual(qc.data[0].operation, out.data[0].operation) - self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(out.data[0].operation.condition, (qc.cregs[0], 0)) self.assertEqual(out.data[0].operation.label, "conditionally special") def test_condition_via_instructionset(self): @@ -664,9 +695,10 @@ def test_condition_via_instructionset(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 1) self.assertIsNot(gate, circuit.data[0].operation) - self.assertEqual(circuit.data[0].operation.condition, (cr, 1)) + self.assertEqual(circuit.data[0].operation._condition, (cr, 1)) def test_is_mutable(self): gate = CXGate() @@ -696,7 +728,8 @@ def test_to_mutable_setter(self): self.assertEqual(mutable_gate.label, "foo") self.assertEqual(mutable_gate.duration, 3) self.assertEqual(mutable_gate.unit, "s") - self.assertEqual(mutable_gate.condition, (clbit, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(mutable_gate.condition, (clbit, 0)) def test_to_mutable_of_mutable_instance(self): gate = CZGate(label="foo") @@ -723,27 +756,32 @@ def test_inner_outer_label_with_c_if(self): inner_gate = HGate(label="my h gate") controlled_gate = inner_gate.control(label="foo") clbit = Clbit() - conditonal_controlled_gate = controlled_gate.c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + conditonal_controlled_gate = controlled_gate.c_if(clbit, 0) self.assertTrue(conditonal_controlled_gate.mutable) self.assertEqual("my h gate", conditonal_controlled_gate.base_gate.label) self.assertEqual("foo", conditonal_controlled_gate.label) - self.assertEqual((clbit, 0), conditonal_controlled_gate.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual((clbit, 0), conditonal_controlled_gate.condition) def test_inner_outer_label_with_c_if_deepcopy(self): inner_gate = XGate(label="my h gate") controlled_gate = inner_gate.control(label="foo") clbit = Clbit() - conditonal_controlled_gate = controlled_gate.c_if(clbit, 0) + with self.assertWarns(DeprecationWarning): + conditonal_controlled_gate = controlled_gate.c_if(clbit, 0) self.assertTrue(conditonal_controlled_gate.mutable) self.assertEqual("my h gate", conditonal_controlled_gate.base_gate.label) self.assertEqual("foo", conditonal_controlled_gate.label) - self.assertEqual((clbit, 0), conditonal_controlled_gate.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual((clbit, 0), conditonal_controlled_gate.condition) copied = copy.deepcopy(conditonal_controlled_gate) self.assertIsNot(conditonal_controlled_gate, copied) self.assertTrue(copied.mutable) self.assertEqual("my h gate", copied.base_gate.label) self.assertEqual("foo", copied.label) - self.assertEqual((clbit, 0), copied.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual((clbit, 0), copied.condition) def test_inner_outer_label_pickle(self): inner_gate = XGate(label="my h gate") diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 139192745d2e..ecb98681bbd2 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -73,7 +73,8 @@ def test_rejects_dangerous_cast(self): def test_rejects_c_if(self): instruction = Store(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool())) with self.assertRaises(NotImplementedError): - instruction.c_if(Clbit(), False) + with self.assertWarns(DeprecationWarning): + instruction.c_if(Clbit(), False) class TestStoreCircuit(QiskitTestCase): @@ -241,4 +242,5 @@ def test_rejects_c_if(self): qc = QuantumCircuit([Clbit()], inputs=[a]) instruction_set = qc.store(a, True) with self.assertRaises(NotImplementedError): - instruction_set.c_if(qc.clbits[0], False) + with self.assertWarns(DeprecationWarning): + instruction_set.c_if(qc.clbits[0], False) diff --git a/test/python/circuit/test_twirling.py b/test/python/circuit/test_twirling.py new file mode 100644 index 000000000000..59e6b0c41fe2 --- /dev/null +++ b/test/python/circuit/test_twirling.py @@ -0,0 +1,212 @@ +# 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 Qiskit's AnnotatedOperation class.""" + +import ddt +import numpy as np + +from qiskit.circuit import QuantumCircuit, pauli_twirl_2q_gates, Gate +from qiskit.circuit.library import ( + CXGate, + ECRGate, + CZGate, + iSwapGate, + SwapGate, + PermutationGate, + XGate, + CCXGate, + RZXGate, +) +from qiskit.circuit.random import random_circuit +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Operator +from qiskit.transpiler.target import Target +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +@ddt.ddt +class TestTwirling(QiskitTestCase): + """Testing qiskit.circuit.twirl_circuit""" + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_twirl_circuit_equiv(self, gate): + """Test the twirled circuit is equivalent.""" + qc = QuantumCircuit(2) + qc.append(gate(), (0, 1)) + for i in range(100): + with self.subTest(i): + res = pauli_twirl_2q_gates(qc, gate, i) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + # Assert we have more than just a 2q gate in the circuit + self.assertGreater(len(res.count_ops()), 1) + + def test_twirl_circuit_None(self): + """Test the default twirl all gates.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + res = pauli_twirl_2q_gates(qc, seed=12345) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"{qc}\nnot equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + self.assertEqual(sum(res.count_ops().values()), 20) + + def test_twirl_circuit_list(self): + """Test twirling for a circuit list of gates to twirl.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=["cx", iSwapGate()], seed=12345) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"{qc}\nnot equiv to\n{res}" + ) + self.assertNotEqual(res, qc) + self.assertEqual(sum(res.count_ops().values()), 12) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_many_twirls_equiv(self, gate): + """Test the twirled circuits are equivalent if num_twirls>1.""" + qc = QuantumCircuit(2) + qc.append(gate(), (0, 1)) + res = pauli_twirl_2q_gates(qc, gate, seed=424242, num_twirls=1000) + for twirled_circuit in res: + np.testing.assert_allclose( + Operator(qc), Operator(twirled_circuit), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + self.assertNotEqual(twirled_circuit, qc) + + def test_invalid_gate(self): + """Test an error is raised with a non-standard gate.""" + + class MyGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__("custom", num_qubits=2, params=[]) + + qc = QuantumCircuit(2) + qc.append(MyGate(), (0, 1)) + + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate=MyGate()) + + def test_custom_standard_gate(self): + """Test an error is raised with an unsupported standard gate.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=SwapGate()) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {qc} not equiv to\n{res}" + ) + self.assertNotEqual(qc, res) + + def test_invalid_string(self): + """Test an error is raised with an unsupported standard gate.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate="swap") + + def test_invalid_str_entry_in_list(self): + """Test an error is raised with an unsupported string gate in list.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, twirling_gate=[CXGate, "swap"]) + + def test_invalid_class_entry_in_list(self): + """Test an error is raised with an unsupported string gate in list.""" + qc = QuantumCircuit(2) + qc.swap(0, 1) + res = pauli_twirl_2q_gates(qc, twirling_gate=[SwapGate(), "cx"]) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {qc} not equiv to\n{res}" + ) + self.assertNotEqual(qc, res) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_full_circuit(self, gate): + """Test a circuit with a random assortment of gates.""" + qc = random_circuit(5, 25, seed=12345678942) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2]) + res = pauli_twirl_2q_gates(qc) + np.testing.assert_allclose( + Operator(qc), Operator(res), err_msg=f"gate: {gate} not equiv to\n{res}" + ) + + @ddt.data(CXGate, ECRGate, CZGate, iSwapGate) + def test_control_flow(self, gate): + """Test we twirl inside control flow blocks.""" + qc = QuantumCircuit(2, 1) + with qc.if_test((qc.clbits[0], 0)): + qc.append(gate(), [0, 1]) + res = pauli_twirl_2q_gates(qc) + np.testing.assert_allclose( + Operator(res.data[0].operation.blocks[0]), + Operator(gate()), + err_msg=f"gate: {gate} not equiv to\n{res}", + ) + + def test_metadata_is_preserved(self): + """Test we preserve circuit metadata after twirling.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.ecr(0, 1) + qc.iswap(0, 1) + qc.cz(0, 1) + qc.metadata = {"is_this_circuit_twirled?": True} + res = pauli_twirl_2q_gates(qc, twirling_gate=CZGate, num_twirls=5) + for out_circ in res: + self.assertEqual(out_circ.metadata, qc.metadata) + + def test_random_circuit_optimized(self): + """Test we run 1q gate optimization if specified.""" + qc = random_circuit(5, 25, seed=1234567842) + qc.barrier() + qc = qc.decompose() + target = Target.from_configuration(basis_gates=["cx", "iswap", "cz", "ecr", "r"]) + res = pauli_twirl_2q_gates(qc, seed=12345678, num_twirls=5, target=target) + for out_circ in res: + self.assertEqual( + Operator(out_circ), + Operator(qc), + f"{qc}\nnot equiv to\n{out_circ}", + ) + count_ops = out_circ.count_ops() + self.assertNotIn("x", count_ops) + self.assertNotIn("y", count_ops) + self.assertNotIn("z", count_ops) + self.assertNotIn("id", count_ops) + self.assertIn("r", count_ops) + + def test_error_on_invalid_qubit_count(self): + """Test an error is raised on non-2q gates.""" + qc = QuantumCircuit(5) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [CCXGate()]) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [XGate()]) + + def test_error_on_parameterized_gate(self): + """Test an error is raised on parameterized 2q gates.""" + qc = QuantumCircuit(5) + with self.assertRaises(QiskitError): + pauli_twirl_2q_gates(qc, [RZXGate(3.24)]) diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index 0277c4afb43d..7f68c1eca296 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -31,6 +31,8 @@ _id = np.eye(2, 2) _not = np.matrix([[0, 1], [1, 0]]) +_had = 1 / np.sqrt(2) * np.matrix([[1, 1], [1, -1]]) +_rand = random_unitary(2, seed=541234).data @ddt @@ -44,29 +46,36 @@ class TestUCGate(QiskitTestCase): [_id, _id], [_id, 1j * _id], [_id, _not, _id, _not], + [_rand, _had, _rand, _had, _rand, _had, _rand, _had], + [_had, _had, _had, _had, _had, _had, _had, _had], [random_unitary(2, seed=541234).data for _ in range(2**2)], [random_unitary(2, seed=975163).data for _ in range(2**3)], [random_unitary(2, seed=629462).data for _ in range(2**4)], ], up_to_diagonal=[True, False], + mux_simp=[True, False], ) - def test_ucg(self, squs, up_to_diagonal): + def test_ucg(self, squs, up_to_diagonal, mux_simp): """Test uniformly controlled gates.""" num_con = int(np.log2(len(squs))) q = QuantumRegister(num_con + 1) qc = QuantumCircuit(q) - uc = UCGate(squs, up_to_diagonal=up_to_diagonal) + uc = UCGate(squs, up_to_diagonal=up_to_diagonal, mux_simp=mux_simp) qc.append(uc, q) # Decompose the gate qc = transpile(qc, basis_gates=["u1", "u3", "u2", "cx", "id"]) + # Simulate the decomposed gate unitary = Operator(qc).data if up_to_diagonal: - ucg = UCGate(squs, up_to_diagonal=up_to_diagonal) - unitary = np.dot(np.diagflat(ucg._get_diagonal()), unitary) + ucg = UCGate(squs, up_to_diagonal=up_to_diagonal, mux_simp=mux_simp) + diag = np.diagflat(ucg._get_diagonal()) + unitary = np.dot(diag, unitary) + unitary_desired = _get_ucg_matrix(squs) + self.assertTrue(matrix_equal(unitary_desired, unitary, ignore_phase=True)) def test_global_phase_ucg(self): @@ -100,6 +109,21 @@ def test_inverse_ucg(self): self.assertTrue(np.allclose(unitary_desired, unitary)) + def test_ucge(self): + """test ucg simplification""" + gate_list = [_had, _had, _had, _had, _had, _had, _had, _had] + + qc1 = QuantumCircuit(4) + uc1 = UCGate(gate_list, up_to_diagonal=False, mux_simp=False) + qc1.append(uc1, range(4)) + op1 = Operator(qc1).data + + qc2 = QuantumCircuit(4) + uc2 = UCGate(gate_list, up_to_diagonal=False, mux_simp=True) + qc2.append(uc2, range(4)) + op2 = Operator(qc2).data + self.assertTrue(np.allclose(op1, op2)) + def test_repeat(self): """test repeat operation""" gates = [random_unitary(2, seed=seed).data for seed in [124435, 876345, 687462, 928365]] diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index b384d8b9267f..6a954d871cc9 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -285,7 +285,8 @@ def test_measure_to_registers_when_conditionals(self): qc.measure(qr[0], cr1) # Measure not required for a later conditional qc.measure(qr[1], cr2[1]) # Measure required for a later conditional - qc.h(qr[1]).c_if(cr2, 3) + with self.assertWarns(DeprecationWarning): + qc.h(qr[1]).c_if(cr2, 3) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) @@ -305,7 +306,8 @@ def test_convert_to_bfunc_plus_conditional(self): cr = ClassicalRegister(1) qc = QuantumCircuit(qr, cr) - qc.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr, 1) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) @@ -326,7 +328,8 @@ def test_convert_to_bfunc_plus_conditional_onebit(self): cr = ClassicalRegister(3) qc = QuantumCircuit(qr, cr) - qc.h(qr[0]).c_if(cr[2], 1) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr[2], 1) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) @@ -352,7 +355,8 @@ def test_resize_value_to_register(self): cr3 = ClassicalRegister(1) qc = QuantumCircuit(qr, cr1, cr2, cr3) - qc.h(qr[0]).c_if(cr2, 2) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr2, 2) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) diff --git a/test/python/compiler/test_disassembler.py b/test/python/compiler/test_disassembler.py index 805fac1178a1..4556550422b2 100644 --- a/test/python/compiler/test_disassembler.py +++ b/test/python/compiler/test_disassembler.py @@ -191,7 +191,8 @@ def test_circuit_with_conditionals(self): qc = QuantumCircuit(qr, cr1, cr2) qc.measure(qr[0], cr1) # Measure not required for a later conditional qc.measure(qr[1], cr2[1]) # Measure required for a later conditional - qc.h(qr[1]).c_if(cr2, 3) + with self.assertWarns(DeprecationWarning): + qc.h(qr[1]).c_if(cr2, 3) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) circuits, run_config_out, header = disassemble(qobj) @@ -207,7 +208,8 @@ def test_circuit_with_simple_conditional(self): qr = QuantumRegister(1) cr = ClassicalRegister(1) qc = QuantumCircuit(qr, cr) - qc.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr, 1) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) circuits, run_config_out, header = disassemble(qobj) @@ -228,7 +230,8 @@ def test_circuit_with_single_bit_conditions(self): qr = QuantumRegister(1) cr = ClassicalRegister(2) qc = QuantumCircuit(qr, cr) - qc.h(qr[0]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr[0], 1) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) @@ -267,9 +270,12 @@ def test_multiple_conditionals_multiple_registers(self): qc = QuantumCircuit(qr, cr1, cr2, cr3, cr4) qc.x(qr[1]) qc.h(qr) - qc.cx(qr[1], qr[0]).c_if(cr3, 14) - qc.ccx(qr[0], qr[2], qr[1]).c_if(cr4, 1) - qc.h(qr).c_if(cr1, 3) + with self.assertWarns(DeprecationWarning): + qc.cx(qr[1], qr[0]).c_if(cr3, 14) + with self.assertWarns(DeprecationWarning): + qc.ccx(qr[0], qr[2], qr[1]).c_if(cr4, 1) + with self.assertWarns(DeprecationWarning): + qc.h(qr).c_if(cr1, 3) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) circuits, run_config_out, header = disassemble(qobj) @@ -285,7 +291,8 @@ def test_circuit_with_bit_conditional_1(self): qr = QuantumRegister(2) cr = ClassicalRegister(2) qc = QuantumCircuit(qr, cr) - qc.h(qr[0]).c_if(cr[1], True) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr[1], True) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) circuits, run_config_out, header = disassemble(qobj) @@ -302,9 +309,12 @@ def test_circuit_with_bit_conditional_2(self): cr = ClassicalRegister(2) cr1 = ClassicalRegister(2) qc = QuantumCircuit(qr, cr, cr1) - qc.h(qr[0]).c_if(cr1[1], False) - qc.h(qr[1]).c_if(cr[0], True) - qc.cx(qr[0], qr[1]).c_if(cr1[0], False) + with self.assertWarns(DeprecationWarning): + qc.h(qr[0]).c_if(cr1[1], False) + with self.assertWarns(DeprecationWarning): + qc.h(qr[1]).c_if(cr[0], True) + with self.assertWarns(DeprecationWarning): + qc.cx(qr[0], qr[1]).c_if(cr1[0], False) with self.assertWarns(DeprecationWarning): qobj = assemble(qc) circuits, run_config_out, header = disassemble(qobj) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 38991b63a63e..851c6817d82f 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1522,14 +1522,18 @@ def test_circuit_with_delay(self, optimization_level): qc.delay(500, 1) qc.cx(0, 1) - out = transpile( - qc, - scheduling_method="alap", - basis_gates=["h", "cx"], - instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)], - optimization_level=optimization_level, - seed_transpiler=42, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + out = transpile( + qc, + scheduling_method="alap", + basis_gates=["h", "cx"], + instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)], + optimization_level=optimization_level, + seed_transpiler=42, + ) self.assertEqual(out.duration, 1200) @@ -1610,7 +1614,7 @@ def test_scheduling_timing_constraints(self): timing_constraints=timing_constraints, ) - def test_scheduling_instruction_constraints(self): + def test_scheduling_instruction_constraints_backend(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" @@ -1638,14 +1642,47 @@ def test_scheduling_instruction_constraints(self): ) self.assertEqual(scheduled.duration, 1500) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + scheduled = transpile( + qc, + backend=backend_v2, + scheduling_method="alap", + instruction_durations=durations, + layout_method="trivial", + ) + self.assertEqual(scheduled.duration, 1500) + + def test_scheduling_instruction_constraints(self): + """Test that scheduling-related loose transpile constraints work with target.""" + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="argument ``calibrate_instructions`` is deprecated", + ): + target = GenericBackendV2( + 2, + calibrate_instructions=True, + coupling_map=[[0, 1]], + basis_gates=["cx", "h"], + seed=42, + ).target + qc = QuantumCircuit(2) + qc.h(0) + qc.delay(0.000001, 1, "s") + qc.cx(0, 1) + + # update cx to 2 seconds + target.update_instruction_properties("cx", (0, 1), InstructionProperties(0.000001)) + scheduled = transpile( qc, - backend=backend_v2, + target=target, scheduling_method="alap", - instruction_durations=durations, layout_method="trivial", ) - self.assertEqual(scheduled.duration, 1500) + self.assertEqual(scheduled.duration, 9010) def test_scheduling_dt_constraints(self): """Test that scheduling-related loose transpile constraints @@ -1675,8 +1712,7 @@ def test_scheduling_dt_constraints(self): self.assertEqual(scheduled.duration, original_duration * 2) def test_backend_props_constraints(self): - """Test that loose transpile constraints - work with both BackendV1 and BackendV2.""" + """Test that loose transpile constraints work with both BackendV1 and BackendV2.""" with self.assertWarns(DeprecationWarning): backend_v1 = Fake20QV1() @@ -1733,13 +1769,17 @@ def test_backend_props_constraints(self): ) self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) - result = transpile( - qc, - backend=backend_v2, - backend_properties=custom_backend_properties, - optimization_level=2, - seed_transpiler=42, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + result = transpile( + qc, + backend=backend_v2, + backend_properties=custom_backend_properties, + optimization_level=2, + seed_transpiler=42, + ) self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) @@ -1756,21 +1796,29 @@ def test_no_infinite_loop(self, optimization_level): seed_transpiler=42, ) - # Expect a -pi/2 global phase for the U3 to RZ/SX conversion, and - # a -0.5 * theta phase for RZ to P twice, once at theta, and once at 3 pi - # for the second and third RZ gates in the U3 decomposition. - expected = QuantumCircuit( - 1, global_phase=-np.pi / 2 - 0.5 * (-0.2 + np.pi) - 0.5 * 3 * np.pi - ) - expected.p(-np.pi, 0) - expected.sx(0) - expected.p(np.pi - 0.2, 0) - expected.sx(0) + if optimization_level == 1: + # Expect a -pi/2 global phase for the U3 to RZ/SX conversion, and + # a -0.5 * theta phase for RZ to P twice, once at theta, and once at 3 pi + # for the second and third RZ gates in the U3 decomposition. + expected = QuantumCircuit( + 1, global_phase=-np.pi / 2 - 0.5 * (-0.2 + np.pi) - 0.5 * 3 * np.pi + ) + expected.p(-np.pi, 0) + expected.sx(0) + expected.p(np.pi - 0.2, 0) + expected.sx(0) + else: + expected = QuantumCircuit(1, global_phase=(15 * np.pi - 1) / 10) + expected.sx(0) + expected.p(1.0 / 5.0 + np.pi, 0) + expected.sx(0) + expected.p(3 * np.pi, 0) error_message = ( f"\nOutput circuit:\n{out!s}\n{Operator(out).data}\n" f"Expected circuit:\n{expected!s}\n{Operator(expected).data}" ) + self.assertEqual(Operator(qc), Operator(out)) self.assertEqual(out, expected, error_message) @data(0, 1, 2, 3) @@ -2228,7 +2276,8 @@ def _regular_circuit(self): base.append(CustomCX(), [3, 6]) base.append(CustomCX(), [5, 4]) base.append(CustomCX(), [5, 3]) - base.append(CustomCX(), [2, 4]).c_if(base.cregs[0], 3) + with self.assertWarns(DeprecationWarning): + base.append(CustomCX(), [2, 4]).c_if(base.cregs[0], 3) base.ry(a, 4) base.measure(4, 2) return base @@ -2842,12 +2891,14 @@ def test_parallel_singleton_conditional_gate(self, opt_level): circ = QuantumCircuit(2, 1) circ.h(0) circ.measure(0, circ.clbits[0]) - circ.z(1).c_if(circ.clbits[0], 1) + with self.assertWarns(DeprecationWarning): + circ.z(1).c_if(circ.clbits[0], 1) res = transpile( [circ, circ], backend, optimization_level=opt_level, seed_transpiler=123456769 ) self.assertTrue(res[0].data[-1].operation.mutable) - self.assertEqual(res[0].data[-1].operation.condition, (res[0].clbits[0], 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(res[0].data[-1].operation.condition, (res[0].clbits[0], 1)) @data(0, 1, 2, 3) def test_backendv2_and_basis_gates(self, opt_level): @@ -3292,7 +3343,8 @@ def test_shared_classical_between_components_condition(self, opt_level): for i in range(18): qc.measure(i, creg[i]) - qc.ecr(20, 21).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.ecr(20, 21).c_if(creg, 0) tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) def _visit_block(circuit, qubit_mapping=None): @@ -3328,9 +3380,11 @@ def test_shared_classical_between_components_condition_large_to_small(self, opt_ qc.measure(24, creg[0]) qc.measure(23, creg[1]) # Component 1 - qc.h(0).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(creg, 0) for i in range(18): - qc.ecr(0, i + 1).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.ecr(0, i + 1).c_if(creg, 0) tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=123456789) def _visit_block(circuit, qubit_mapping=None): @@ -3402,9 +3456,11 @@ def test_shared_classical_between_components_condition_large_to_small_reverse_in qc.measure(0, creg[0]) qc.measure(1, creg[1]) # Component 1 - qc.h(24).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.h(24).c_if(creg, 0) for i in range(23, 5, -1): - qc.ecr(24, i).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.ecr(24, i).c_if(creg, 0) tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) def _visit_block(circuit, qubit_mapping=None): @@ -3475,15 +3531,19 @@ def test_chained_data_dependency(self, opt_level): measure_op = Measure() qc.append(measure_op, [9], [creg[0]]) # Component 1 - qc.h(10).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.h(10).c_if(creg, 0) for i in range(11, 20): - qc.ecr(10, i).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.ecr(10, i).c_if(creg, 0) measure_op = Measure() qc.append(measure_op, [19], [creg[0]]) # Component 2 - qc.h(20).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.h(20).c_if(creg, 0) for i in range(21, 30): - qc.cz(20, i).c_if(creg, 0) + with self.assertWarns(DeprecationWarning): + qc.cz(20, i).c_if(creg, 0) measure_op = Measure() qc.append(measure_op, [29], [creg[0]]) tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) @@ -3739,14 +3799,32 @@ def test_triple_circuit_invalid_layout(self, routing_method): qc.cy(20, 28) qc.cy(20, 29) qc.measure_all() + with self.assertRaises(TranspilerError): - transpile( - qc, - self.backend, - layout_method="trivial", - routing_method=routing_method, - seed_transpiler=42, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + if routing_method == "stochastic": + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The StochasticSwap transpilation pass is a suboptimal", + ): + transpile( + qc, + self.backend, + layout_method="trivial", + routing_method=routing_method, + seed_transpiler=42, + ) + else: + transpile( + qc, + self.backend, + layout_method="trivial", + routing_method=routing_method, + seed_transpiler=42, + ) @data("stochastic") def test_triple_circuit_invalid_layout_stochastic(self, routing_method): @@ -3784,8 +3862,20 @@ def test_triple_circuit_invalid_layout_stochastic(self, routing_method): qc.cy(20, 28) qc.cy(20, 29) qc.measure_all() - with self.assertWarns(DeprecationWarning): - with self.assertRaises(TranspilerError): + with self.assertRaises(TranspilerError): + if routing_method == "stochastic": + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The StochasticSwap transpilation pass is a suboptimal", + ): + transpile( + qc, + self.backend, + layout_method="trivial", + routing_method=routing_method, + seed_transpiler=42, + ) + else: transpile( qc, self.backend, diff --git a/test/python/converters/test_circuit_to_dag.py b/test/python/converters/test_circuit_to_dag.py index 852cb324aa79..06d3dec654e4 100644 --- a/test/python/converters/test_circuit_to_dag.py +++ b/test/python/converters/test_circuit_to_dag.py @@ -34,7 +34,8 @@ def test_circuit_and_dag(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) diff --git a/test/python/converters/test_circuit_to_dagdependency.py b/test/python/converters/test_circuit_to_dagdependency.py index 65221e9cf2c1..7d3bbfd2c49c 100644 --- a/test/python/converters/test_circuit_to_dagdependency.py +++ b/test/python/converters/test_circuit_to_dagdependency.py @@ -33,7 +33,8 @@ def test_circuit_and_dag_canonical(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) @@ -51,7 +52,8 @@ def test_circuit_and_dag_canonical2(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) diff --git a/test/python/converters/test_circuit_to_dagdependency_v2.py b/test/python/converters/test_circuit_to_dagdependency_v2.py index 3323ca0e6768..c7a31dc5767c 100644 --- a/test/python/converters/test_circuit_to_dagdependency_v2.py +++ b/test/python/converters/test_circuit_to_dagdependency_v2.py @@ -33,7 +33,8 @@ def test_circuit_and_dag_canonical(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) diff --git a/test/python/converters/test_circuit_to_instruction.py b/test/python/converters/test_circuit_to_instruction.py index e3239d4b5ff4..1b225831bb23 100644 --- a/test/python/converters/test_circuit_to_instruction.py +++ b/test/python/converters/test_circuit_to_instruction.py @@ -56,22 +56,28 @@ def test_flatten_registers_of_circuit_single_bit_cond(self): cr1 = ClassicalRegister(3, "cr1") cr2 = ClassicalRegister(3, "cr2") circ = QuantumCircuit(qr1, qr2, cr1, cr2) - circ.h(qr1[0]).c_if(cr1[1], True) - circ.h(qr2[1]).c_if(cr2[0], False) - circ.cx(qr1[1], qr2[2]).c_if(cr2[2], True) + with self.assertWarns(DeprecationWarning): + circ.h(qr1[0]).c_if(cr1[1], True) + with self.assertWarns(DeprecationWarning): + circ.h(qr2[1]).c_if(cr2[0], False) + with self.assertWarns(DeprecationWarning): + circ.cx(qr1[1], qr2[2]).c_if(cr2[2], True) circ.measure(qr2[2], cr2[0]) - inst = circuit_to_instruction(circ) + with self.assertWarns(DeprecationWarning): + inst = circuit_to_instruction(circ) q = QuantumRegister(5, "q") c = ClassicalRegister(6, "c") self.assertEqual(inst.definition[0].qubits, (q[0],)) self.assertEqual(inst.definition[1].qubits, (q[3],)) self.assertEqual(inst.definition[2].qubits, (q[1], q[4])) - - self.assertEqual(inst.definition[0].operation.condition, (c[1], True)) - self.assertEqual(inst.definition[1].operation.condition, (c[3], False)) - self.assertEqual(inst.definition[2].operation.condition, (c[5], True)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(inst.definition[0].operation.condition, (c[1], True)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(inst.definition[1].operation.condition, (c[3], False)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(inst.definition[2].operation.condition, (c[5], True)) def test_flatten_circuit_registerless(self): """Test that the conversion works when the given circuit has bits that are not contained in @@ -196,8 +202,10 @@ def test_registerless_classical_bits(self): Regression test of gh-7394.""" expected = QuantumCircuit([Qubit(), Clbit()]) - expected.h(0).c_if(expected.clbits[0], 0) - test = circuit_to_instruction(expected) + with self.assertWarns(DeprecationWarning): + expected.h(0).c_if(expected.clbits[0], 0) + with self.assertWarns(DeprecationWarning): + test = circuit_to_instruction(expected) self.assertIsInstance(test, Instruction) self.assertIsInstance(test.definition, QuantumCircuit) @@ -206,7 +214,8 @@ def test_registerless_classical_bits(self): test_instruction = test.definition.data[0] expected_instruction = expected.data[0] self.assertIs(type(test_instruction.operation), type(expected_instruction.operation)) - self.assertEqual(test_instruction.operation.condition, (test.definition.clbits[0], 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(test_instruction.operation.condition, (test.definition.clbits[0], 0)) def test_zero_operands(self): """Test that an instruction can be created, even if it has zero operands.""" diff --git a/test/python/converters/test_dag_to_dagdependency.py b/test/python/converters/test_dag_to_dagdependency.py index 6b52652e6ace..b4a66659245b 100644 --- a/test/python/converters/test_dag_to_dagdependency.py +++ b/test/python/converters/test_dag_to_dagdependency.py @@ -34,7 +34,8 @@ def test_circuit_and_dag_dependency(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) @@ -55,7 +56,8 @@ def test_circuit_and_dag_dependency2(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) diff --git a/test/python/converters/test_dag_to_dagdependency_v2.py b/test/python/converters/test_dag_to_dagdependency_v2.py index 925bf442f477..951e31835fe9 100644 --- a/test/python/converters/test_dag_to_dagdependency_v2.py +++ b/test/python/converters/test_dag_to_dagdependency_v2.py @@ -34,7 +34,8 @@ def test_circuit_and_dag_dependency(self): circuit_in.h(qr[1]) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) - circuit_in.x(qr[0]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_in.x(qr[0]).c_if(cr, 0x3) circuit_in.measure(qr[0], cr[0]) circuit_in.measure(qr[1], cr[1]) circuit_in.measure(qr[2], cr[2]) diff --git a/test/python/dagcircuit/test_collect_blocks.py b/test/python/dagcircuit/test_collect_blocks.py index b2715078d7f5..d8178fdb3a54 100644 --- a/test/python/dagcircuit/test_collect_blocks.py +++ b/test/python/dagcircuit/test_collect_blocks.py @@ -243,7 +243,8 @@ def test_circuit_has_conditional_gates(self): qc.x(0) qc.x(1) qc.cx(1, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.x(0) qc.x(1) qc.cx(0, 1) @@ -263,11 +264,13 @@ def test_circuit_has_conditional_gates(self): # conditional gate (note that x(1) following the measure is collected into the first # block). block_collector = BlockCollector(circuit_to_dag(qc)) - blocks = block_collector.collect_all_matching_blocks( - lambda node: node.op.name in ["x", "cx"] and not getattr(node.op, "condition", None), - split_blocks=False, - min_block_size=1, - ) + with self.assertWarns(DeprecationWarning): + blocks = block_collector.collect_all_matching_blocks( + lambda node: node.op.name in ["x", "cx"] + and not getattr(node.op, "condition", None), + split_blocks=False, + min_block_size=1, + ) self.assertEqual(len(blocks), 2) self.assertEqual(len(blocks[0]), 4) self.assertEqual(len(blocks[1]), 2) @@ -280,7 +283,8 @@ def test_circuit_has_conditional_gates_dagdependency(self): qc.x(0) qc.x(1) qc.cx(1, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.x(0) qc.x(1) qc.cx(0, 1) @@ -300,11 +304,13 @@ def test_circuit_has_conditional_gates_dagdependency(self): # conditional gate (note that x(1) following the measure is collected into the first # block). block_collector = BlockCollector(circuit_to_dag(qc)) - blocks = block_collector.collect_all_matching_blocks( - lambda node: node.op.name in ["x", "cx"] and not getattr(node.op, "condition", None), - split_blocks=False, - min_block_size=1, - ) + with self.assertWarns(DeprecationWarning): + blocks = block_collector.collect_all_matching_blocks( + lambda node: node.op.name in ["x", "cx"] + and not getattr(node.op, "condition", None), + split_blocks=False, + min_block_size=1, + ) self.assertEqual(len(blocks), 2) self.assertEqual(len(blocks[0]), 4) self.assertEqual(len(blocks[1]), 2) @@ -544,11 +550,13 @@ def test_collect_blocks_with_clbits(self): condition.""" qc = QuantumCircuit(4, 3) - qc.cx(0, 1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 1) qc.cx(2, 3) qc.cx(1, 2) qc.cx(0, 1) - qc.cx(2, 3).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(2, 3).c_if(1, 0) dag = circuit_to_dag(qc) @@ -567,7 +575,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dag_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -580,11 +589,13 @@ def test_collect_blocks_with_clbits_dagdependency(self): under conditions, using DAGDependency.""" qc = QuantumCircuit(4, 3) - qc.cx(0, 1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 1) qc.cx(2, 3) qc.cx(1, 2) qc.cx(0, 1) - qc.cx(2, 3).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(2, 3).c_if(1, 0) dag = circuit_to_dagdependency(qc) @@ -603,7 +614,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dagdependency_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -620,10 +632,13 @@ def test_collect_blocks_with_clbits2(self): cbit = Clbit() qc = QuantumCircuit(qreg, creg, [cbit]) - qc.cx(0, 1).c_if(creg[1], 1) - qc.cx(2, 3).c_if(cbit, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[1], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(2, 3).c_if(cbit, 0) qc.cx(1, 2) - qc.cx(0, 1).c_if(creg[2], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[2], 1) dag = circuit_to_dag(qc) @@ -642,7 +657,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dag_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -659,10 +675,13 @@ def test_collect_blocks_with_clbits2_dagdependency(self): cbit = Clbit() qc = QuantumCircuit(qreg, creg, [cbit]) - qc.cx(0, 1).c_if(creg[1], 1) - qc.cx(2, 3).c_if(cbit, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[1], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(2, 3).c_if(cbit, 0) qc.cx(1, 2) - qc.cx(0, 1).c_if(creg[2], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[2], 1) dag = circuit_to_dag(qc) @@ -681,7 +700,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dag_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -698,9 +718,11 @@ def test_collect_blocks_with_cregs(self): creg2 = ClassicalRegister(2, "cr2") qc = QuantumCircuit(qreg, creg, creg2) - qc.cx(0, 1).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg, 3) qc.cx(1, 2) - qc.cx(0, 1).c_if(creg[2], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[2], 1) dag = circuit_to_dag(qc) @@ -719,7 +741,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dag_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -737,9 +760,11 @@ def test_collect_blocks_with_cregs_dagdependency(self): creg2 = ClassicalRegister(2, "cr2") qc = QuantumCircuit(qreg, creg, creg2) - qc.cx(0, 1).c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg, 3) qc.cx(1, 2) - qc.cx(0, 1).c_if(creg[2], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(creg[2], 1) dag = circuit_to_dagdependency(qc) @@ -758,7 +783,8 @@ def _collapse_fn(circuit): return op # Collapse block with measures into a single "COLLAPSED" block - dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) collapsed_qc = dagdependency_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) @@ -917,14 +943,19 @@ def test_split_layers_dagdependency(self): def test_block_collapser_register_condition(self): """Test that BlockCollapser can handle a register being used more than once.""" qc = QuantumCircuit(1, 2) - qc.x(0).c_if(qc.cregs[0], 0) - qc.y(0).c_if(qc.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + qc.y(0).c_if(qc.cregs[0], 1) dag = circuit_to_dag(qc) blocks = BlockCollector(dag).collect_all_matching_blocks( lambda _: True, split_blocks=False, min_block_size=1 ) - dag = BlockCollapser(dag).collapse_to_operation(blocks, lambda circ: circ.to_instruction()) + with self.assertWarns(DeprecationWarning): + dag = BlockCollapser(dag).collapse_to_operation( + blocks, lambda circ: circ.to_instruction() + ) collapsed_qc = dag_to_circuit(dag) self.assertEqual(len(collapsed_qc.data), 1) diff --git a/test/python/dagcircuit/test_compose.py b/test/python/dagcircuit/test_compose.py index 27404cec05c2..1bbd3031d310 100644 --- a/test/python/dagcircuit/test_compose.py +++ b/test/python/dagcircuit/test_compose.py @@ -321,8 +321,10 @@ def test_compose_conditional(self): creg = ClassicalRegister(2, "rcr") circuit_right = QuantumCircuit(qreg, creg) - circuit_right.x(qreg[1]).c_if(creg, 2) - circuit_right.h(qreg[0]).c_if(creg, 1) + with self.assertWarns(DeprecationWarning): + circuit_right.x(qreg[1]).c_if(creg, 2) + with self.assertWarns(DeprecationWarning): + circuit_right.h(qreg[0]).c_if(creg, 1) circuit_right.measure(qreg, creg) # permuted subset of qubits and clbits @@ -330,16 +332,19 @@ def test_compose_conditional(self): dag_right = circuit_to_dag(circuit_right) # permuted subset of qubits and clbits - dag_left.compose( - dag_right, - qubits=[self.left_qubit1, self.left_qubit4], - clbits=[self.left_clbit1, self.left_clbit0], - ) + with self.assertWarns(DeprecationWarning): + dag_left.compose( + dag_right, + qubits=[self.left_qubit1, self.left_qubit4], + clbits=[self.left_clbit1, self.left_clbit0], + ) circuit_composed = dag_to_circuit(dag_left) circuit_expected = self.circuit_left.copy() - circuit_expected.x(self.left_qubit4).c_if(*self.condition1) - circuit_expected.h(self.left_qubit1).c_if(*self.condition2) + with self.assertWarns(DeprecationWarning): + circuit_expected.x(self.left_qubit4).c_if(*self.condition1) + with self.assertWarns(DeprecationWarning): + circuit_expected.h(self.left_qubit1).c_if(*self.condition2) circuit_expected.measure(self.left_qubit4, self.left_clbit0) circuit_expected.measure(self.left_qubit1, self.left_clbit1) @@ -423,12 +428,13 @@ def test_compose_condition_multiple_classical(self): circuit_left = QuantumCircuit(qreg, creg1, creg2) circuit_right = QuantumCircuit(qreg, creg1, creg2) - circuit_right.h(0).c_if(creg1, 1) + with self.assertWarns(DeprecationWarning): + circuit_right.h(0).c_if(creg1, 1) dag_left = circuit_to_dag(circuit_left) dag_right = circuit_to_dag(circuit_right) - - dag_composed = dag_left.compose(dag_right, qubits=[0], clbits=[0, 1], inplace=False) + with self.assertWarns(DeprecationWarning): + dag_composed = dag_left.compose(dag_right, qubits=[0], clbits=[0, 1], inplace=False) dag_expected = circuit_to_dag(circuit_right.copy()) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index e2881cf4a3d9..96f307ea8548 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -118,7 +118,7 @@ def raise_if_dagcircuit_invalid(dag): out_wires = set(dag._out_wires(node._node_id)) node_cond_bits = set( - node.op.condition[0][:] if getattr(node.op, "condition", None) is not None else [] + node.condition[0][:] if getattr(node, "condition", None) is not None else [] ) node_qubits = set(node.qargs) node_clbits = set(node.cargs) @@ -561,7 +561,8 @@ def setUp(self): def test_apply_operation_back(self): """The apply_operation_back() method.""" - x_gate = XGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) @@ -573,7 +574,8 @@ def test_apply_operation_back(self): def test_edges(self): """Test that DAGCircuit.edges() behaves as expected with ops.""" - x_gate = XGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + x_gate = XGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0], []) self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) self.dag.apply_operation_back(Measure(), [self.qubit1], [self.clbit1]) @@ -590,13 +592,14 @@ def test_apply_operation_back_conditional(self): """Test consistency of apply_operation_back with condition set.""" # Single qubit gate conditional: qc.h(qr[2]).c_if(cr, 3) - - h_gate = HGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + h_gate = HGate().c_if(*self.condition) h_node = self.dag.apply_operation_back(h_gate, [self.qubit2], []) self.assertEqual(h_node.qargs, (self.qubit2,)) self.assertEqual(h_node.cargs, ()) - self.assertEqual(h_node.op.condition, h_gate.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual(h_node.op.condition, h_gate.condition) self.assertEqual( sorted(self.dag._in_edges(h_node._node_id)), @@ -630,13 +633,14 @@ def test_apply_operation_back_conditional_measure(self): new_creg = ClassicalRegister(1, "cr2") self.dag.add_creg(new_creg) - - meas_gate = Measure().c_if(new_creg, 0) + with self.assertWarns(DeprecationWarning): + meas_gate = Measure().c_if(new_creg, 0) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit0], [self.clbit0]) self.assertEqual(meas_node.qargs, (self.qubit0,)) self.assertEqual(meas_node.cargs, (self.clbit0,)) - self.assertEqual(meas_node.op.condition, meas_gate.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( sorted(self.dag._in_edges(meas_node._node_id)), @@ -675,13 +679,14 @@ def test_apply_operation_back_conditional_measure_to_self(self): # Measure targeting a clbit which _is_ a member of the conditional # register. qc.measure(qr[0], cr[0]).c_if(cr, 3) - - meas_gate = Measure().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + meas_gate = Measure().c_if(*self.condition) meas_node = self.dag.apply_operation_back(meas_gate, [self.qubit1], [self.clbit1]) self.assertEqual(meas_node.qargs, (self.qubit1,)) self.assertEqual(meas_node.cargs, (self.clbit1,)) - self.assertEqual(meas_node.op.condition, meas_gate.condition) + with self.assertWarns(DeprecationWarning): + self.assertEqual(meas_node.op.condition, meas_gate.condition) self.assertEqual( sorted(self.dag._in_edges(meas_node._node_id)), @@ -1239,7 +1244,8 @@ def test_dag_collect_runs(self): def test_dag_collect_runs_start_with_conditional(self): """Test collect runs with a conditional at the start of the run.""" - h_gate = HGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1252,7 +1258,8 @@ def test_dag_collect_runs_start_with_conditional(self): def test_dag_collect_runs_conditional_in_middle(self): """Test collect_runs with a conditional in the middle of a run.""" - h_gate = HGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1294,7 +1301,8 @@ def test_dag_collect_1q_runs_start_with_conditional(self): """Test collect 1q runs with a conditional at the start of the run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1309,7 +1317,8 @@ def test_dag_collect_1q_runs_conditional_in_middle(self): """Test collect_1q_runs with a conditional in the middle of a run.""" self.dag.apply_operation_back(Reset(), [self.qubit0]) self.dag.apply_operation_back(Delay(100), [self.qubit0]) - h_gate = HGate().c_if(*self.condition) + with self.assertWarns(DeprecationWarning): + h_gate = HGate().c_if(*self.condition) self.dag.apply_operation_back(HGate(), [self.qubit0]) self.dag.apply_operation_back(h_gate, [self.qubit0]) self.dag.apply_operation_back(HGate(), [self.qubit0]) @@ -1387,7 +1396,8 @@ def test_layers_basic(self): qubit1 = qreg[1] clbit0 = creg[0] clbit1 = creg[1] - x_gate = XGate().c_if(creg, 3) + with self.assertWarns(DeprecationWarning): + x_gate = XGate().c_if(creg, 3) dag = DAGCircuit() dag.add_qreg(qreg) dag.add_creg(creg) @@ -1856,29 +1866,41 @@ def test_semantic_conditions(self): qreg = QuantumRegister(1, name="q") creg = ClassicalRegister(1, name="c") qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.cregs[0], 1) - qc2.x(0).c_if(qc2.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], True) self.assertEqual(circuit_to_dag(qc1), circuit_to_dag(qc2)) # Order of operations transposed. qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.clbits[-1], True) - qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) self.assertNotEqual(circuit_to_dag(qc1), circuit_to_dag(qc2)) # Single-bit condition values not the same. qc1 = QuantumCircuit(qreg, creg, [Clbit()]) - qc1.x(0).c_if(qc1.cregs[0], 1) - qc1.x(0).c_if(qc1.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc1.x(0).c_if(qc1.clbits[-1], True) qc2 = QuantumCircuit(qreg, creg, [Clbit()]) - qc2.x(0).c_if(qc2.cregs[0], 1) - qc2.x(0).c_if(qc2.clbits[-1], False) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0).c_if(qc2.clbits[-1], False) self.assertNotEqual(circuit_to_dag(qc1), circuit_to_dag(qc2)) def test_semantic_expr(self): @@ -2489,7 +2511,8 @@ def test_substitute_without_propagating_bit_conditional(self): sub = QuantumCircuit(2, 1) sub.h(0) - sub.cx(0, 1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + sub.cx(0, 1).c_if(0, True) sub.h(0) expected = DAGCircuit() @@ -2522,7 +2545,8 @@ def test_substitute_without_propagating_register_conditional(self): sub = QuantumCircuit(QuantumRegister(2), ClassicalRegister(2)) sub.h(0) - sub.cx(0, 1).c_if(sub.cregs[0], 3) + with self.assertWarns(DeprecationWarning): + sub.cx(0, 1).c_if(sub.cregs[0], 3) sub.h(0) expected = DAGCircuit() @@ -2559,8 +2583,10 @@ def test_substitute_with_provided_wire_map_propagate_condition(self): sub.cx(0, 1) sub.h(0) - conditioned_h = HGate().c_if(*conditioned_cz.condition) - conditioned_cx = CXGate().c_if(*conditioned_cz.condition) + with self.assertWarns(DeprecationWarning): + conditioned_h = HGate().c_if(*conditioned_cz.condition) + with self.assertWarns(DeprecationWarning): + conditioned_cx = CXGate().c_if(*conditioned_cz.condition) expected = DAGCircuit() expected.add_qubits(base_qubits) @@ -2593,11 +2619,13 @@ def test_substitute_with_provided_wire_map_no_propagate_condition(self): sub = QuantumCircuit(2, 1) sub.h(0) - sub.cx(0, 1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + sub.cx(0, 1).c_if(0, True) sub.h(0) conditioned_cx = CXGate().to_mutable() - conditioned_cx.condition = conditioned_cz.condition + with self.assertWarns(DeprecationWarning): + conditioned_cx.condition = conditioned_cz.condition expected = DAGCircuit() expected.add_qubits(base_qubits) @@ -2626,7 +2654,8 @@ def test_creates_additional_alias_register(self): target = base.apply_operation_back(Instruction("dummy", 2, 2, []), base_qreg, base_creg[:2]) sub = QuantumCircuit(QuantumRegister(2), ClassicalRegister(2)) - sub.cx(0, 1).c_if(sub.cregs[0], 3) + with self.assertWarns(DeprecationWarning): + sub.cx(0, 1).c_if(sub.cregs[0], 3) base.substitute_node_with_dag(target, circuit_to_dag(sub)) @@ -2694,7 +2723,8 @@ def test_substituting_node_preserves_args_condition(self, inplace): self.assertEqual(replacement_node.op.name, "cz") self.assertEqual(replacement_node.qargs, (qr[1], qr[0])) self.assertEqual(replacement_node.cargs, ()) - self.assertEqual(replacement_node.op.condition, (cr, 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(replacement_node.op.condition, (cr, 1)) self.assertEqual(replacement_node is node_to_be_replaced, inplace) @data(True, False) @@ -2731,12 +2761,14 @@ def test_refuses_to_overwrite_condition(self, inplace): dag = DAGCircuit() dag.add_qreg(qr) dag.add_creg(cr) - node = dag.apply_operation_back(XGate().c_if(cr, 2), qr, []) + with self.assertWarns(DeprecationWarning): + node = dag.apply_operation_back(XGate().c_if(cr, 2), qr, []) with self.assertRaisesRegex(DAGCircuitError, "Cannot propagate a condition"): - dag.substitute_node( - node, XGate().c_if(cr, 1), inplace=inplace, propagate_condition=True - ) + with self.assertWarns(DeprecationWarning): + dag.substitute_node( + node, XGate().c_if(cr, 1), inplace=inplace, propagate_condition=True + ) @data(True, False) def test_replace_if_else_op_with_another(self, inplace): @@ -3194,13 +3226,15 @@ def setUp(self): def test_creg_conditional(self): """Test consistency of conditional on classical register.""" - self.circuit.h(self.qreg[0]).c_if(self.creg, 1) + with self.assertWarns(DeprecationWarning): + self.circuit.h(self.qreg[0]).c_if(self.creg, 1) self.dag = circuit_to_dag(self.circuit) gate_node = self.dag.gate_nodes()[0] self.assertEqual(gate_node.op, HGate()) self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) - self.assertEqual(gate_node.op.condition, (self.creg, 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate_node.op.condition, (self.creg, 1)) gate_node_preds = list(self.dag.predecessors(gate_node)) gate_node_in_edges = [ @@ -3235,14 +3269,15 @@ def test_creg_conditional(self): def test_clbit_conditional(self): """Test consistency of conditional on single classical bit.""" - - self.circuit.h(self.qreg[0]).c_if(self.creg[0], 1) + with self.assertWarns(DeprecationWarning): + self.circuit.h(self.qreg[0]).c_if(self.creg[0], 1) self.dag = circuit_to_dag(self.circuit) gate_node = self.dag.gate_nodes()[0] self.assertEqual(gate_node.op, HGate()) self.assertEqual(gate_node.qargs, (self.qreg[0],)) self.assertEqual(gate_node.cargs, ()) - self.assertEqual(gate_node.op.condition, (self.creg[0], 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(gate_node.op.condition, (self.creg[0], 1)) gate_node_preds = list(self.dag.predecessors(gate_node)) gate_node_in_edges = [ diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 80b471b66063..62845461dacf 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -493,7 +493,8 @@ def test_dynamic_circuit(self): qc.h(0) qc.cx(0, 1) qc.measure(1, 0) - qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) observable = SparsePauliOp("IZ") diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index eb3b79f1b911..dc77a3c47ce6 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -397,7 +397,8 @@ def test_circuit_with_dynamic_circuit(self): qc.h(0) qc.cx(0, 1) qc.measure(0, 0) - qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) with self.assertWarns(DeprecationWarning): backend = Aer.get_backend("aer_simulator") diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index e66b594cc738..77830d5c9224 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -31,8 +31,10 @@ from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.providers import JobStatus from qiskit.providers.backend_compat import BackendV2Converter -from qiskit.providers.basic_provider import BasicSimulator +from qiskit.providers.basic_provider import BasicProviderJob, BasicSimulator from qiskit.providers.fake_provider import Fake7QPulseV1, GenericBackendV2 +from qiskit.result import Result +from qiskit.qobj.utils import MeasReturnType, MeasLevel from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from ..legacy_cmaps import LAGOS_CMAP @@ -50,6 +52,64 @@ BACKENDS = BACKENDS_V1 + BACKENDS_V2 +class Level1BackendV2(GenericBackendV2): + """Wrapper around GenericBackendV2 adding level 1 data support for testing + + GenericBackendV2 is used to run the simulation. Then level 1 data (a + complex number per classical register per shot) is generated by mapping 0 + to -1 and 1 to 1 with a random number added to each shot drawn from a + normal distribution with a standard deviation of ``level1_sigma``. Each + data point has ``1j * idx`` added to it where ``idx`` is the index of the + classical register. For ``meas_return="avg"``, the individual shot results + are still calculated and then averaged. + """ + + level1_sigma = 0.1 + + def run(self, run_input, **options): + # Validate level 1 options + if "meas_level" not in options or "meas_return" not in options: + raise ValueError(f"{type(self)} requires 'meas_level' and 'meas_return' run options!") + meas_level = options.pop("meas_level") + if meas_level != 1: + raise ValueError(f"'meas_level' must be 1, not {meas_level}") + meas_return = options.pop("meas_return") + if meas_return not in ("single", "avg"): + raise ValueError(f"Unexpected value for 'meas_return': {meas_return}") + + options["memory"] = True + + rng = np.random.default_rng(seed=options.get("seed_simulator")) + + inner_job = super().run(run_input, **options) + result_dict = inner_job.result().to_dict() + for circ, exp_result in zip(run_input, result_dict["results"]): + num_clbits = sum(cr.size for cr in circ.cregs) + bitstrings = [ + format(int(x, 16), f"0{num_clbits}b") for x in exp_result["data"]["memory"] + ] + new_data = [ + [ + [2 * int(d) - 1 + rng.normal(scale=self.level1_sigma), i] + for i, d in enumerate(reversed(bs)) + ] + for bs in bitstrings + ] + if meas_return == "avg": + new_data = [ + [sum(shot[idx][0] for shot in new_data) / len(new_data), idx] + for idx in range(num_clbits) + ] + exp_result["meas_return"] = MeasReturnType.AVERAGE + else: + exp_result["meas_return"] = MeasReturnType.SINGLE + exp_result["data"] = {"memory": new_data} + exp_result["meas_level"] = MeasLevel.KERNELED + + result = Result.from_dict(result_dict) + return BasicProviderJob(self, inner_job.job_id(), result) + + @ddt class TestBackendSamplerV2(QiskitTestCase): """Test for BackendSamplerV2""" @@ -942,6 +1002,55 @@ def test_run_shots_result_size_v1(self, backend): self.assertLessEqual(result[0].data.meas.num_shots, self._shots) self.assertEqual(sum(result[0].data.meas.get_counts().values()), self._shots) + def test_run_level1(self): + """Test running with meas_level=1""" + nq = 2 + shots = 100 + + backend = Level1BackendV2(nq) + qc = QuantumCircuit(nq) + qc.x(1) + qc.measure_all() + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + options = { + "default_shots": shots, + "seed_simulator": self._seed, + "run_options": { + "meas_level": 1, + "meas_return": "single", + }, + } + sampler = BackendSamplerV2(backend=backend, options=options) + result_single = sampler.run([qc]).result() + + options = { + "default_shots": shots, + "seed_simulator": self._seed, + "run_options": { + "meas_level": 1, + "meas_return": "avg", + }, + } + sampler = BackendSamplerV2(backend=backend, options=options) + result_avg = sampler.run([qc]).result() + + # Check that averaging the meas_return="single" data matches the + # meas_return="avg" data. + averaged_singles = np.average(result_single[0].join_data(), axis=0) + average_data = result_avg[0].join_data() + self.assertLessEqual( + max(abs(averaged_singles - average_data)), + backend.level1_sigma / np.sqrt(shots) * 6, + ) + + # Check that average data matches expected form for the circuit + expected_average = np.array([-1, 1 + 1j]) + self.assertLessEqual( + max(abs(expected_average - average_data)), + backend.level1_sigma / np.sqrt(shots) * 6, + ) + @combine(backend=BACKENDS_V2) def test_primitive_job_status_done(self, backend): """test primitive job's status""" @@ -1215,8 +1324,10 @@ def test_circuit_with_aliased_cregs(self, backend): qc.h(0) qc.measure(0, c1) qc.measure(1, c2) - qc.z(2).c_if(c1, 1) - qc.x(2).c_if(c2, 1) + with self.assertWarns(DeprecationWarning): + qc.z(2).c_if(c1, 1) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(c2, 1) qc2 = QuantumCircuit(5, 5) qc2.compose(qc, [0, 2, 3], [2, 4], inplace=True) cregs = [creg.name for creg in qc2.cregs] @@ -1251,8 +1362,10 @@ def test_circuit_with_aliased_cregs_v1(self, backend): qc.h(0) qc.measure(0, c1) qc.measure(1, c2) - qc.z(2).c_if(c1, 1) - qc.x(2).c_if(c2, 1) + with self.assertWarns(DeprecationWarning): + qc.z(2).c_if(c1, 1) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(c2, 1) qc2 = QuantumCircuit(5, 5) qc2.compose(qc, [0, 2, 3], [2, 4], inplace=True) cregs = [creg.name for creg in qc2.cregs] diff --git a/test/python/primitives/test_primitive.py b/test/python/primitives/test_primitive.py index fc0118564f3d..78bcff5ff405 100644 --- a/test/python/primitives/test_primitive.py +++ b/test/python/primitives/test_primitive.py @@ -163,7 +163,8 @@ def test_circuit_key_controlflow(self): qc.h(0) qc.cx(0, 1) qc.measure(0, 0) - qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) self.assertIsInstance(hash(_circuit_key(qc)), int) self.assertIsInstance(json.dumps(_circuit_key(qc)), str) diff --git a/test/python/primitives/test_statevector_sampler.py b/test/python/primitives/test_statevector_sampler.py index a782aafaeaf5..3af4e8d9f688 100644 --- a/test/python/primitives/test_statevector_sampler.py +++ b/test/python/primitives/test_statevector_sampler.py @@ -283,7 +283,8 @@ def test_run_errors(self): qc4 = QuantumCircuit(2, 2) qc4.h(0) qc4.measure(1, 1) - qc4.x(0).c_if(1, 1) + with self.assertWarns(DeprecationWarning): + qc4.x(0).c_if(1, 1) qc4.measure(0, 0) sampler = StatevectorSampler() @@ -592,8 +593,10 @@ def test_circuit_with_aliased_cregs(self): c2 = ClassicalRegister(1, "c2") qc = QuantumCircuit(q, c1, c2) - qc.z(2).c_if(c1, 1) - qc.x(2).c_if(c2, 1) + with self.assertWarns(DeprecationWarning): + qc.z(2).c_if(c1, 1) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(c2, 1) qc2 = QuantumCircuit(5, 5) qc2.compose(qc, [0, 2, 3], [2, 4], inplace=True) # Note: qc2 has aliased cregs, c0 -> c[2] and c1 -> c[4]. diff --git a/test/python/providers/basic_provider/__init__.py b/test/python/providers/basic_provider/__init__.py index 0c8b92822219..c84f6a74b9d4 100644 --- a/test/python/providers/basic_provider/__init__.py +++ b/test/python/providers/basic_provider/__init__.py @@ -20,7 +20,9 @@ class BasicProviderBackendTestMixin: def test_configuration(self): """Test backend.configuration().""" - configuration = self.backend.configuration() + with self.assertWarns(DeprecationWarning): + # The method is deprecated + configuration = self.backend.configuration() return configuration def test_run_circuit(self): diff --git a/test/python/providers/basic_provider/test_basic_simulator.py b/test/python/providers/basic_provider/test_basic_simulator.py index 57dd67dfd3c3..925823b49300 100644 --- a/test/python/providers/basic_provider/test_basic_simulator.py +++ b/test/python/providers/basic_provider/test_basic_simulator.py @@ -174,7 +174,8 @@ def test_if_statement(self): circuit_if_true.x(qr[1]) circuit_if_true.measure(qr[0], cr[0]) circuit_if_true.measure(qr[1], cr[1]) - circuit_if_true.x(qr[2]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_if_true.x(qr[2]).c_if(cr, 0x3) circuit_if_true.measure(qr[0], cr[0]) circuit_if_true.measure(qr[1], cr[1]) circuit_if_true.measure(qr[2], cr[2]) @@ -193,7 +194,8 @@ def test_if_statement(self): circuit_if_false.x(qr[0]) circuit_if_false.measure(qr[0], cr[0]) circuit_if_false.measure(qr[1], cr[1]) - circuit_if_false.x(qr[2]).c_if(cr, 0x3) + with self.assertWarns(DeprecationWarning): + circuit_if_false.x(qr[2]).c_if(cr, 0x3) circuit_if_false.measure(qr[0], cr[0]) circuit_if_false.measure(qr[1], cr[1]) circuit_if_false.measure(qr[2], cr[2]) @@ -230,7 +232,8 @@ def test_bit_cif_crossaffect(self): circuit.x([qr[1], qr[2]]) circuit.measure(qr[1], cr[1]) circuit.measure(qr[2], cr[2]) - circuit.h(qr[0]).c_if(cr[0], True) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[0], True) circuit.measure(qr[0], cr1[0]) job = self.backend.run(circuit, shots=shots, seed_simulator=self.seed) result = job.result().get_counts() @@ -269,8 +272,10 @@ def test_teleport(self): circuit.barrier(qr) circuit.measure(qr[0], cr0[0]) circuit.measure(qr[1], cr1[0]) - circuit.z(qr[2]).c_if(cr0, 1) - circuit.x(qr[2]).c_if(cr1, 1) + with self.assertWarns(DeprecationWarning): + circuit.z(qr[2]).c_if(cr0, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[2]).c_if(cr1, 1) circuit.measure(qr[2], cr2[0]) job = self.backend.run( transpile(circuit, self.backend), shots=shots, seed_simulator=self.seed diff --git a/test/python/qasm2/test_arxiv_examples.py b/test/python/qasm2/test_arxiv_examples.py index 197b842697e1..85ed23e0ed19 100644 --- a/test/python/qasm2/test_arxiv_examples.py +++ b/test/python/qasm2/test_arxiv_examples.py @@ -72,7 +72,8 @@ def test_teleportation(self, parser): if(c1==1) x q[2]; post q[2]; measure q[2] -> c2[0];""" - parsed = parser(example) + with self.assertWarns(DeprecationWarning): + parsed = parser(example) post = gate_builder("post", [], QuantumCircuit([Qubit()])) @@ -90,8 +91,10 @@ def test_teleportation(self, parser): qc.h(q[0]) qc.measure(q[0], c0[0]) qc.measure(q[1], c1[0]) - qc.z(q[2]).c_if(c0, 1) - qc.x(q[2]).c_if(c1, 1) + with self.assertWarns(DeprecationWarning): + qc.z(q[2]).c_if(c0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(q[2]).c_if(c1, 1) qc.append(post(), [q[2]], []) qc.measure(q[2], c2[0]) @@ -168,7 +171,8 @@ def test_inverse_qft_1(self, parser): if(c==7) u1(pi/2+pi/4+pi/8) q[3]; h q[3]; measure q[3] -> c[3];""" - parsed = parser(example) + with self.assertWarns(DeprecationWarning): + parsed = parser(example) q = QuantumRegister(4, "q") c = ClassicalRegister(4, "c") @@ -177,21 +181,32 @@ def test_inverse_qft_1(self, parser): qc.barrier(q) qc.h(q[0]) qc.measure(q[0], c[0]) - qc.append(U1Gate(math.pi / 2).c_if(c, 1), [q[1]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c, 1), [q[1]]) qc.h(q[1]) qc.measure(q[1], c[1]) - qc.append(U1Gate(math.pi / 4).c_if(c, 1), [q[2]]) - qc.append(U1Gate(math.pi / 2).c_if(c, 2), [q[2]]) - qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 3), [q[2]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4).c_if(c, 1), [q[2]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c, 2), [q[2]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 3), [q[2]]) qc.h(q[2]) qc.measure(q[2], c[2]) - qc.append(U1Gate(math.pi / 8).c_if(c, 1), [q[3]]) - qc.append(U1Gate(math.pi / 4).c_if(c, 2), [q[3]]) - qc.append(U1Gate(math.pi / 8 + math.pi / 4).c_if(c, 3), [q[3]]) - qc.append(U1Gate(math.pi / 2).c_if(c, 4), [q[3]]) - qc.append(U1Gate(math.pi / 8 + math.pi / 2).c_if(c, 5), [q[3]]) - qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 6), [q[3]]) - qc.append(U1Gate(math.pi / 8 + math.pi / 4 + math.pi / 2).c_if(c, 7), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 8).c_if(c, 1), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4).c_if(c, 2), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 8 + math.pi / 4).c_if(c, 3), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c, 4), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 8 + math.pi / 2).c_if(c, 5), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 6), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 8 + math.pi / 4 + math.pi / 2).c_if(c, 7), [q[3]]) qc.h(q[3]) qc.measure(q[3], c[3]) @@ -224,7 +239,8 @@ def test_inverse_qft_2(self, parser): if(c2==1) u1(pi/2) q[3]; h q[3]; measure q[3] -> c3[0];""" - parsed = parser(example) + with self.assertWarns(DeprecationWarning): + parsed = parser(example) q = QuantumRegister(4, "q") c0 = ClassicalRegister(1, "c0") @@ -236,16 +252,22 @@ def test_inverse_qft_2(self, parser): qc.barrier(q) qc.h(q[0]) qc.measure(q[0], c0[0]) - qc.append(U1Gate(math.pi / 2).c_if(c0, 1), [q[1]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c0, 1), [q[1]]) qc.h(q[1]) qc.measure(q[1], c1[0]) - qc.append(U1Gate(math.pi / 4).c_if(c0, 1), [q[2]]) - qc.append(U1Gate(math.pi / 2).c_if(c1, 1), [q[2]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4).c_if(c0, 1), [q[2]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c1, 1), [q[2]]) qc.h(q[2]) qc.measure(q[2], c2[0]) - qc.append(U1Gate(math.pi / 8).c_if(c0, 1), [q[3]]) - qc.append(U1Gate(math.pi / 4).c_if(c1, 1), [q[3]]) - qc.append(U1Gate(math.pi / 2).c_if(c2, 1), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 8).c_if(c0, 1), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 4).c_if(c1, 1), [q[3]]) + with self.assertWarns(DeprecationWarning): + qc.append(U1Gate(math.pi / 2).c_if(c2, 1), [q[3]]) qc.h(q[3]) qc.measure(q[3], c3[0]) @@ -423,7 +445,8 @@ def test_error_correction(self, parser): if(syn==2) x q[2]; if(syn==3) x q[1]; measure q -> c;""" - parsed = parser(example) + with self.assertWarns(DeprecationWarning): + parsed = parser(example) syndrome_definition = QuantumCircuit([Qubit() for _ in [None] * 5]) syndrome_definition.cx(0, 3) @@ -442,9 +465,12 @@ def test_error_correction(self, parser): qc.barrier(q) qc.append(syndrome(), [q[0], q[1], q[2], a[0], a[1]]) qc.measure(a, syn) - qc.x(q[0]).c_if(syn, 1) - qc.x(q[2]).c_if(syn, 2) - qc.x(q[1]).c_if(syn, 3) + with self.assertWarns(DeprecationWarning): + qc.x(q[0]).c_if(syn, 1) + with self.assertWarns(DeprecationWarning): + qc.x(q[2]).c_if(syn, 2) + with self.assertWarns(DeprecationWarning): + qc.x(q[1]).c_if(syn, 3) qc.measure(q, c) self.assertEqual(parsed, qc) diff --git a/test/python/qasm2/test_circuit_methods.py b/test/python/qasm2/test_circuit_methods.py index 1ba9689ab3ef..d5fa814f7c52 100644 --- a/test/python/qasm2/test_circuit_methods.py +++ b/test/python/qasm2/test_circuit_methods.py @@ -180,14 +180,16 @@ def test_qasm_text_conditional(self): ) + "\n" ) - q_circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + q_circuit = QuantumCircuit.from_qasm_str(qasm_string) qr = QuantumRegister(1, "q") cr0 = ClassicalRegister(4, "c0") cr1 = ClassicalRegister(4, "c1") ref = QuantumCircuit(qr, cr0, cr1) ref.x(qr[0]) - ref.x(qr[0]).c_if(cr1, 4) + with self.assertWarns(DeprecationWarning): + ref.x(qr[0]).c_if(cr1, 4) self.assertEqual(len(q_circuit.cregs), 2) self.assertEqual(len(q_circuit.qregs), 1) diff --git a/test/python/qasm2/test_export.py b/test/python/qasm2/test_export.py index 8de4bb8eb34f..ef6ab8076ef6 100644 --- a/test/python/qasm2/test_export.py +++ b/test/python/qasm2/test_export.py @@ -44,9 +44,12 @@ def test_basic_output(self): qc.barrier(qr2) qc.cx(qr2[1], qr1[0]) qc.h(qr2[1]) - qc.x(qr2[1]).c_if(cr, 0) - qc.y(qr1[0]).c_if(cr, 1) - qc.z(qr1[0]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + qc.x(qr2[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.y(qr1[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.z(qr1[0]).c_if(cr, 2) qc.barrier(qr1, qr2) qc.measure(qr1[0], cr[0]) qc.measure(qr2[0], cr[1]) @@ -616,7 +619,8 @@ def test_rotation_angles_close_to_pi(self): def test_raises_on_single_bit_condition(self): qc = QuantumCircuit(1, 1) - qc.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, True) with self.assertRaisesRegex( qasm2.QASM2ExportError, "OpenQASM 2 can only condition on registers" diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index a5e5bbd77329..8f3c6aaabd75 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -256,11 +256,14 @@ def test_conditioned(self): if (cond == 0) U(0, 0, 0) q[0]; if (cond == 1) CX q[1], q[0]; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) cond = ClassicalRegister(1, "cond") qc = QuantumCircuit(QuantumRegister(2, "q"), cond) - qc.u(0, 0, 0, 0).c_if(cond, 0) - qc.cx(1, 0).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, 0).c_if(cond, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(1, 0).c_if(cond, 1) self.assertEqual(parsed, qc) def test_conditioned_broadcast(self): @@ -271,15 +274,20 @@ def test_conditioned_broadcast(self): if (cond == 0) U(0, 0, 0) q1; if (cond == 1) CX q1[0], q2; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) cond = ClassicalRegister(1, "cond") q1 = QuantumRegister(2, "q1") q2 = QuantumRegister(2, "q2") qc = QuantumCircuit(q1, q2, cond) - qc.u(0, 0, 0, q1[0]).c_if(cond, 0) - qc.u(0, 0, 0, q1[1]).c_if(cond, 0) - qc.cx(q1[0], q2[0]).c_if(cond, 1) - qc.cx(q1[0], q2[1]).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, q1[0]).c_if(cond, 0) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, q1[1]).c_if(cond, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(q1[0], q2[0]).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(q1[0], q2[1]).c_if(cond, 1) self.assertEqual(parsed, qc) def test_constant_folding(self): @@ -338,19 +346,29 @@ def test_huge_conditions(self): if (cond=={bigint}) measure qr[0] -> cr[0]; if (cond=={bigint}) measure qr -> cr; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) qr, cr = QuantumRegister(2, "qr"), ClassicalRegister(2, "cr") cond = ClassicalRegister(500, "cond") qc = QuantumCircuit(qr, cr, cond) - qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) - qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) - qc.u(0, 0, 0, qr[1]).c_if(cond, bigint) - qc.reset(qr[0]).c_if(cond, bigint) - qc.reset(qr[0]).c_if(cond, bigint) - qc.reset(qr[1]).c_if(cond, bigint) - qc.measure(qr[0], cr[0]).c_if(cond, bigint) - qc.measure(qr[0], cr[0]).c_if(cond, bigint) - qc.measure(qr[1], cr[1]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, qr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.u(0, 0, 0, qr[1]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.reset(qr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.reset(qr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.reset(qr[1]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.measure(qr[0], cr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.measure(qr[0], cr[0]).c_if(cond, bigint) + with self.assertWarns(DeprecationWarning): + qc.measure(qr[1], cr[1]).c_if(cond, bigint) self.assertEqual(parsed, qc) @@ -383,14 +401,16 @@ def test_conditioned(self): creg cond[1]; if (cond == 0) not_bell q[0], q[1]; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) not_bell_def = QuantumCircuit([Qubit(), Qubit()]) not_bell_def.u(0, 0, 0, 0) not_bell_def.cx(0, 1) not_bell = gate_builder("not_bell", [], not_bell_def) cond = ClassicalRegister(1, "cond") qc = QuantumCircuit(QuantumRegister(2, "q"), cond) - qc.append(not_bell().c_if(cond, 0), [0, 1]) + with self.assertWarns(DeprecationWarning): + qc.append(not_bell().c_if(cond, 0), [0, 1]) self.assertEqual(parsed, qc) def test_constant_folding_in_definition(self): @@ -735,21 +755,25 @@ def test_deepcopy_conditioned_defined_gate(self): creg c[1]; if (c == 1) my_gate q[0]; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) my_gate = parsed.data[0].operation self.assertEqual(my_gate.name, "my_gate") - self.assertEqual(my_gate.condition, (parsed.cregs[0], 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(my_gate.condition, (parsed.cregs[0], 1)) copied = copy.deepcopy(parsed) copied_gate = copied.data[0].operation self.assertEqual(copied_gate.name, "my_gate") - self.assertEqual(copied_gate.condition, (copied.cregs[0], 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(copied_gate.condition, (copied.cregs[0], 1)) pickled = pickle.loads(pickle.dumps(parsed)) pickled_gate = pickled.data[0].operation self.assertEqual(pickled_gate.name, "my_gate") - self.assertEqual(pickled_gate.condition, (pickled.cregs[0], 1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pickled_gate.condition, (pickled.cregs[0], 1)) class TestOpaque(QiskitTestCase): @@ -904,12 +928,16 @@ def test_conditioned(self): if (cond == 0) measure q[0] -> c[0]; if (cond == 1) measure q -> c; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) cond = ClassicalRegister(1, "cond") qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c"), cond) - qc.measure(0, 0).c_if(cond, 0) - qc.measure(0, 0).c_if(cond, 1) - qc.measure(1, 1).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.measure(0, 0).c_if(cond, 0) + with self.assertWarns(DeprecationWarning): + qc.measure(0, 0).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.measure(1, 1).c_if(cond, 1) self.assertEqual(parsed, qc) def test_broadcast_against_empty_register(self): @@ -991,12 +1019,16 @@ def test_conditioned(self): if (cond == 0) reset q[0]; if (cond == 1) reset q; """ - parsed = qiskit.qasm2.loads(program) + with self.assertWarns(DeprecationWarning): + parsed = qiskit.qasm2.loads(program) cond = ClassicalRegister(1, "cond") qc = QuantumCircuit(QuantumRegister(2, "q"), cond) - qc.reset(0).c_if(cond, 0) - qc.reset(0).c_if(cond, 1) - qc.reset(1).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.reset(0).c_if(cond, 0) + with self.assertWarns(DeprecationWarning): + qc.reset(0).c_if(cond, 1) + with self.assertWarns(DeprecationWarning): + qc.reset(1).c_if(cond, 1) self.assertEqual(parsed, qc) def test_broadcast_against_empty_register(self): diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index eefc0a2cc53c..acae1d7d3cd7 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -104,9 +104,12 @@ def test_regs_conds_qasm(self): qc.measure(qr1[0], cr[0]) qc.measure(qr2[0], cr[1]) qc.measure(qr2[1], cr[2]) - qc.x(qr2[1]).c_if(cr, 0) - qc.y(qr1[0]).c_if(cr, 1) - qc.z(qr1[0]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + qc.x(qr2[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.y(qr1[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.z(qr1[0]).c_if(cr, 2) expected_qasm = "\n".join( [ "OPENQASM 3.0;", @@ -723,8 +726,10 @@ def test_teleportation(self): qc.barrier() qc.measure([0, 1], [0, 1]) qc.barrier() - qc.x(2).c_if(qc.clbits[1], 1) - qc.z(2).c_if(qc.clbits[0], 1) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(qc.clbits[1], 1) + with self.assertWarns(DeprecationWarning): + qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) expected_qasm = """\ @@ -780,8 +785,10 @@ def test_basis_gates(self): qc.barrier() qc.measure([0, 1], [0, 1]) qc.barrier() - qc.x(2).c_if(qc.clbits[1], 1) - qc.z(2).c_if(qc.clbits[0], 1) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(qc.clbits[1], 1) + with self.assertWarns(DeprecationWarning): + qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) expected_qasm = """\ @@ -2046,11 +2053,15 @@ def test_unusual_conditions(self): qc = QuantumCircuit(3, 2) qc.h(0) qc.measure(0, 0) - qc.measure(1, 1).c_if(0, True) - qc.reset([0, 1]).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.measure(1, 1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.reset([0, 1]).c_if(0, True) with qc.while_loop((qc.clbits[0], True)): - qc.break_loop().c_if(0, True) - qc.continue_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.continue_loop().c_if(0, True) # Terra forbids delay and barrier from being conditioned through `c_if`, but in theory they # should work fine in a dynamic-circuits sense (although what a conditional barrier _means_ # is a whole other kettle of fish). diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index e909f7ced455..8890a45ffe9e 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -306,6 +306,71 @@ def test_compatibility_version_roundtrip(self): qc.measure_all() self.assert_roundtrip_equal(qc, version=QPY_COMPATIBILITY_VERSION) + def test_nested_params_subs(self): + """Test substitution works.""" + qc = QuantumCircuit(1) + a = Parameter("a") + b = Parameter("b") + expr = a + b + expr = expr.subs({b: a}) + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + + def test_all_the_expression_ops(self): + """Test a circuit with an expression that uses all the ops available.""" + qc = QuantumCircuit(1) + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + d = Parameter("d") + + expression = (a + b.sin() / 4) * c**2 + final_expr = ( + (expression.cos() + d.arccos() - d.arcsin() + d.arctan() + d.tan()) / d.exp() + + expression.gradient(a) + + expression.log() + - a.sin() + - b.conjugate() + ) + final_expr = final_expr.abs() + final_expr = final_expr.subs({c: a}) + + qc.rx(final_expr, 0) + self.assert_roundtrip_equal(qc) + + def test_rpow(self): + """Test rpow works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159**a + expr = expr**b + expr = 1.2345**expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + + def test_rsub(self): + """Test rsub works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159 - a + expr = expr - b + expr = 1.2345 - expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + + def test_rdiv(self): + """Test rdiv works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159 / a + expr = expr / b + expr = 1.2345 / expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + class TestUseSymengineFlag(QpyCircuitTestCase): """Test that the symengine flag works correctly.""" diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index e78feaf3b78f..253bc15852b6 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -387,7 +387,8 @@ def test_barrier_delay_sim(self): def test_from_circuit_with_conditional_gate(self): """Test initialization from circuit with conditional gate.""" qc = QuantumCircuit(2, 1) - qc.h(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(0, 0) qc.cx(0, 1) with self.assertRaises(QiskitError): 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 65f19eb8e44c..3f96cd32e15f 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 @@ -19,7 +19,7 @@ import numpy as np import rustworkx as rx import scipy.sparse -from ddt import ddt +import ddt from qiskit import QiskitError from qiskit.circuit import Parameter, ParameterExpression, ParameterVector @@ -141,19 +141,49 @@ def test_sparse_pauli_op_init(self): self.assertEqual(spp_op, ref_op) -@ddt +@ddt.ddt class TestSparsePauliOpConversions(QiskitTestCase): """Tests SparsePauliOp representation conversions.""" - def test_from_operator(self): + @ddt.data(1, 2, 4) + def test_from_operator_single(self, num_qubits): """Test from_operator methods.""" - for tup in it.product(["I", "X", "Y", "Z"], repeat=2): + for tup in it.product(["I", "X", "Y", "Z"], repeat=num_qubits): label = "".join(tup) with self.subTest(msg=label): spp_op = SparsePauliOp.from_operator(Operator(pauli_mat(label))) np.testing.assert_array_equal(spp_op.coeffs, [1]) self.assertEqual(spp_op.paulis, PauliList(label)) + @ddt.data( + SparsePauliOp.from_sparse_list([("", (), 1.0), ("X", (0,), -2.0j)], num_qubits=1), + SparsePauliOp.from_sparse_list([("", (), 1.0), ("Y", (0,), -2.0j)], num_qubits=1), + SparsePauliOp.from_sparse_list([("Y", (0,), 1.0), ("Z", (0,), -2.0j)], num_qubits=1), + SparsePauliOp.from_sparse_list( + [("Y", (0,), 1.0), ("YY", (1, 0), -0.5), ("YYY", (2, 1, 0), 1j)], num_qubits=3 + ), + SparsePauliOp.from_sparse_list( + [("XZ", (2, 0), 1.0), ("YZ", (1, 0), -0.5), ("ZZ", (2, 1), 1j)], num_qubits=3 + ), + ) + def test_from_operator_roundtrip(self, op): + """Test `SparsePauliOp.from_operator` roundtrips things correctly.""" + # Ensure canonical order of the input. Part of this test is ensuring that the output is + # given in canonical order too. The coefficients in the inputs are chosen to be simple + # multiples of powers of two, so there are no floating-point rounding or associativity + # concerns. + op = op.simplify().sort() + roundtrip = SparsePauliOp.from_operator(op.to_matrix()) + self.assertEqual(roundtrip, op) + + def test_from_operator_tolerance(self): + """Test that terms whose coefficient falls below the tolerance are removed.""" + operator = SparsePauliOp.from_list( + [("IIXI", 0.25), ("IIZI", -0.25j), ("IXYI", 0.5j)] + ).to_matrix() + expected = SparsePauliOp.from_list([("IXYI", 0.5j)]) + self.assertEqual(SparsePauliOp.from_operator(operator, 0.26), expected) + def test_from_list(self): """Test from_list method.""" labels = ["XXZ", "IXI", "YZZ", "III"] @@ -416,7 +446,7 @@ def bind_one(a): return np.vectorize(bind_one, otypes=[complex])(array) -@ddt +@ddt.ddt class TestSparsePauliOpMethods(QiskitTestCase): """Tests for SparsePauliOp operator methods.""" diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index d9423d0ec141..7bb0db110ac1 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -522,8 +522,16 @@ def test_power_of_nonunitary(self): expected = Operator([[1.0 + 0.0j, 0.5 - 0.5j], [0.0 + 0.0j, 0.0 + 1.0j]]) assert_allclose(powered.data, expected.data) - @ddt.data(0.5, 1.0 / 3.0, 0.25) - def test_root_stability(self, root): + @ddt.data( + (0.5, False), + (1.0 / 3.0, False), + (0.25, False), + (0.5, True), + (1.0 / 3.0, True), + (0.25, True), + ) + @ddt.unpack + def test_root_stability(self, root, assume_unitary): """Test that the root of operators that have eigenvalues that are -1 up to floating-point imprecision stably choose the positive side of the principal-root branch cut.""" rng = np.random.default_rng(2024_10_22) @@ -540,17 +548,24 @@ def test_root_stability(self, root): matrices = [basis.conj().T @ np.diag(eigenvalues) @ basis for basis in bases] expected = [basis.conj().T @ np.diag(root_eigenvalues) @ basis for basis in bases] self.assertEqual( - [Operator(matrix).power(root) for matrix in matrices], + [Operator(matrix).power(root, assume_unitary=assume_unitary) for matrix in matrices], [Operator(single) for single in expected], ) - def test_root_branch_cut(self): + @ddt.data(True, False) + def test_root_branch_cut(self, assume_unitary): """Test that we can choose where the branch cut appears in the root.""" z_op = Operator(library.ZGate()) # Depending on the direction we move the branch cut, we should be able to select the root to # be either of the two valid options. - self.assertEqual(z_op.power(0.5, branch_cut_rotation=1e-3), Operator(library.SGate())) - self.assertEqual(z_op.power(0.5, branch_cut_rotation=-1e-3), Operator(library.SdgGate())) + self.assertEqual( + z_op.power(0.5, branch_cut_rotation=1e-3, assume_unitary=assume_unitary), + Operator(library.SGate()), + ) + self.assertEqual( + z_op.power(0.5, branch_cut_rotation=-1e-3, assume_unitary=assume_unitary), + Operator(library.SdgGate()), + ) def test_expand(self): """Test expand method.""" diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index 29d5d42f3783..d16b3b3453ec 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -1152,6 +1152,31 @@ def test_expval_pauli_qargs(self, qubits): expval = state.expectation_value(op, qubits) self.assertAlmostEqual(expval, target) + def test_expval_identity(self): + """Test whether the calculation for identity operator has been fixed""" + + # 1 qubit case test + state_1 = Statevector.from_label("0") + state_1_n1 = 2 * state_1 # test the same state with different norms + state_1_n2 = (1 + 2j) * state_1 + identity_op_1 = SparsePauliOp.from_list([("I", 1)]) + expval_state_1 = state_1.expectation_value(identity_op_1) + expval_state_1_n1 = state_1_n1.expectation_value(identity_op_1) + expval_state_1_n2 = state_1_n2.expectation_value(identity_op_1) + self.assertAlmostEqual(expval_state_1, 1.0 + 0j) + self.assertAlmostEqual(expval_state_1_n1, 4 + 0j) + self.assertAlmostEqual(expval_state_1_n2, 5 + 0j) + + # Let's try a multi-qubit case + n_qubits = 3 + state_coeff = 3 - 4j + op_coeff = 2 - 2j + state_test = state_coeff * Statevector.from_label("0" * n_qubits) + op_test = SparsePauliOp.from_list([("I" * n_qubits, op_coeff)]) + expval = state_test.expectation_value(op_test) + target = op_coeff * np.abs(state_coeff) ** 2 + self.assertAlmostEqual(expval, target) + @data(*(qargs for i in range(4) for qargs in permutations(range(4), r=i + 1))) def test_probabilities_qargs(self, qargs): """Test probabilities method with qargs""" diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index 656d60020bc7..dd6fc5800fcb 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -13,16 +13,64 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring import copy +import itertools import pickle +import random import unittest import ddt import numpy as np -from qiskit.circuit import Parameter -from qiskit.quantum_info import SparseObservable, SparsePauliOp, Pauli +from qiskit import transpile +from qiskit.circuit import Measure, Parameter, library, QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import SparseObservable, SparsePauliOp, Pauli, PauliList +from qiskit.transpiler import Target -from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import QiskitTestCase, combine # pylint: disable=wrong-import-order + + +def single_cases(): + return [ + SparseObservable.zero(0), + SparseObservable.zero(10), + SparseObservable.identity(0), + SparseObservable.identity(1_000), + SparseObservable.from_label("IIXIZI"), + SparseObservable.from_list([("YIXZII", -0.25), ("01rl+-", 0.25 + 0.5j)]), + # Includes a duplicate entry. + SparseObservable.from_list([("IXZ", -0.25), ("01I", 0.25 + 0.5j), ("IXZ", 0.75)]), + ] + + +def lnn_target(num_qubits): + """Create a simple `Target` object with an arbitrary basis-gate set, and open-path + connectivity.""" + out = Target() + out.add_instruction(library.RZGate(Parameter("a")), {(q,): None for q in range(num_qubits)}) + out.add_instruction(library.SXGate(), {(q,): None for q in range(num_qubits)}) + out.add_instruction(Measure(), {(q,): None for q in range(num_qubits)}) + out.add_instruction( + library.CXGate(), + { + pair: None + for lower in range(num_qubits - 1) + for pair in [(lower, lower + 1), (lower + 1, lower)] + }, + ) + return out + + +class AllowRightArithmetic: + """Some type that implements only the right-hand-sided arithmatic operations, and allows + `SparseObservable` to pass through them. + + The purpose of this is to detect that `SparseObservable` is correctly delegating binary + operators to the other type if given an object it cannot coerce because of its type.""" + + SENTINEL = object() + + __radd__ = __rsub__ = __rmul__ = __rtruediv__ = __rxor__ = lambda self, other: self.SENTINEL @ddt.ddt @@ -107,6 +155,17 @@ def test_default_constructor_copy(self): with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"): SparseObservable(base, num_qubits=base.num_qubits + 1) + def test_default_constructor_term(self): + expected = SparseObservable.from_list([("IIZXII+-", 2j)]) + self.assertEqual(SparseObservable(expected[0]), expected) + + def test_default_constructor_term_iterable(self): + expected = SparseObservable.from_list([("IIZXII+-", 2j), ("rlIIIIII", 0.5)]) + terms = [expected[0], expected[1]] + self.assertEqual(SparseObservable(list(terms)), expected) + self.assertEqual(SparseObservable(tuple(terms)), expected) + self.assertEqual(SparseObservable(term for term in terms), expected) + def test_default_constructor_failed_inference(self): with self.assertRaises(TypeError): # Mixed dense/sparse list. @@ -128,6 +187,13 @@ def test_num_terms(self): SparseObservable.from_list([("IIIXIZ", 1.0), ("YY+-II", 0.5j)]).num_terms, 2 ) + def test_len(self): + self.assertEqual(len(SparseObservable.zero(0)), 0) + self.assertEqual(len(SparseObservable.zero(10)), 0) + self.assertEqual(len(SparseObservable.identity(0)), 1) + self.assertEqual(len(SparseObservable.identity(1_000_000)), 1) + self.assertEqual(len(SparseObservable.from_list([("IIIXIZ", 1.0), ("YY+-II", 0.5j)])), 2) + def test_bit_term_enum(self): # These are very explicit tests that effectively just duplicate magic numbers, but the point # is that those magic numbers are required to be constant as their values are part of the @@ -176,14 +242,7 @@ def test_bit_term_enum(self): } self.assertEqual({label: SparseObservable.BitTerm[label] for label in labels}, labels) - @ddt.data( - SparseObservable.zero(0), - SparseObservable.zero(10), - SparseObservable.identity(0), - SparseObservable.identity(1_000), - SparseObservable.from_label("IIXIZI"), - SparseObservable.from_list([("YIXZII", -0.25), ("01rl+-", 0.25 + 0.5j)]), - ) + @ddt.idata(single_cases()) def test_pickle(self, observable): self.assertEqual(observable, copy.copy(observable)) self.assertIsNot(observable, copy.copy(observable)) @@ -547,6 +606,41 @@ def test_from_sparse_pauli_op_failures(self): with self.assertRaisesRegex(TypeError, "complex-typed coefficients"): SparseObservable.from_sparse_pauli_op(parametric) + def test_from_terms(self): + self.assertEqual(SparseObservable.from_terms([], num_qubits=5), SparseObservable.zero(5)) + self.assertEqual(SparseObservable.from_terms((), num_qubits=0), SparseObservable.zero(0)) + self.assertEqual( + SparseObservable.from_terms((None for _ in []), num_qubits=3), SparseObservable.zero(3) + ) + + expected = SparseObservable.from_sparse_list( + [ + ("XYZ", (4, 2, 1), 1j), + ("+-rl", (8, 5, 3, 2), 0.5), + ("01", (5, 0), 2.0), + ], + num_qubits=10, + ) + self.assertEqual(SparseObservable.from_terms(list(expected)), expected) + self.assertEqual(SparseObservable.from_terms(tuple(expected)), expected) + self.assertEqual(SparseObservable.from_terms(term for term in expected), expected) + self.assertEqual( + SparseObservable.from_terms( + (term for term in expected), num_qubits=expected.num_qubits + ), + expected, + ) + + def test_from_terms_failures(self): + with self.assertRaisesRegex(ValueError, "cannot construct.*without knowing `num_qubits`"): + SparseObservable.from_terms([]) + + left, right = SparseObservable("IIXYI")[0], SparseObservable("IIIIIIIIX")[0] + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): + SparseObservable.from_terms([left, right]) + with self.assertRaisesRegex(ValueError, "mismatched numbers of qubits"): + SparseObservable.from_terms([left], num_qubits=100) + def test_zero(self): zero_5 = SparseObservable.zero(5) self.assertEqual(zero_5.num_qubits, 5) @@ -577,13 +671,7 @@ def test_identity(self): np.testing.assert_equal(id_0.indices, np.array([], dtype=np.uint32)) np.testing.assert_equal(id_0.boundaries, np.array([0, 0], dtype=np.uintp)) - @ddt.data( - SparseObservable.zero(0), - SparseObservable.zero(5), - SparseObservable.identity(0), - SparseObservable.identity(1_000_000), - SparseObservable.from_list([("+-rl01", -0.5), ("IXZYZI", 1.0j)]), - ) + @ddt.idata(single_cases()) def test_copy(self, obs): self.assertEqual(obs, obs.copy()) self.assertIsNot(obs, obs.copy()) @@ -930,3 +1018,993 @@ def test_attributes_repr(self): self.assertIn("bit_terms", repr(obs.bit_terms)) self.assertIn("indices", repr(obs.indices)) self.assertIn("boundaries", repr(obs.boundaries)) + + @combine( + obs=single_cases(), + # This includes some elements that aren't native `complex`, but still should be cast. + coeff=[0.5, 3j, 2, 0.25 - 0.75j], + ) + def test_multiply(self, obs, coeff): + obs = obs.copy() + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.asarray(expected.coeffs) * complex(coeff) + self.assertEqual(obs * coeff, expected) + self.assertEqual(coeff * obs, expected) + # Check that nothing applied in-place. + self.assertEqual(obs, initial) + obs *= coeff + self.assertEqual(obs, expected) + self.assertIs(obs * AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_multiply_zero(self, obs): + initial = obs.copy() + self.assertEqual(obs * 0.0, SparseObservable.zero(initial.num_qubits)) + self.assertEqual(0.0 * obs, SparseObservable.zero(initial.num_qubits)) + self.assertEqual(obs, initial) + + obs *= 0.0 + self.assertEqual(obs, SparseObservable.zero(initial.num_qubits)) + + @combine( + obs=single_cases(), + # This includes some elements that aren't native `complex`, but still should be cast. Be + # careful that the floating-point operation should not involve rounding. + coeff=[0.5, 4j, 2, -0.25], + ) + def test_divide(self, obs, coeff): + obs = obs.copy() + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.asarray(expected.coeffs) / complex(coeff) + self.assertEqual(obs / coeff, expected) + # Check that nothing applied in-place. + self.assertEqual(obs, initial) + obs /= coeff + self.assertEqual(obs, expected) + self.assertIs(obs / AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_divide_zero_raises(self, obs): + with self.assertRaises(ZeroDivisionError): + _ = obs / 0.0j + with self.assertRaises(ZeroDivisionError): + obs /= 0.0j + + def test_add_simple(self): + num_qubits = 12 + terms = [ + ("ZXY", (5, 2, 1), 1.5j), + ("+r", (8, 0), -0.25), + ("-0l1", (10, 9, 4, 3), 0.5 + 1j), + ("XZ", (7, 5), 0.75j), + ("rl01", (5, 3, 1, 0), 0.25j), + ] + expected = SparseObservable.from_sparse_list(terms, num_qubits=num_qubits) + for pivot in range(1, len(terms) - 1): + left = SparseObservable.from_sparse_list(terms[:pivot], num_qubits=num_qubits) + left_initial = left.copy() + right = SparseObservable.from_sparse_list(terms[pivot:], num_qubits=num_qubits) + right_initial = right.copy() + # Addition is documented to be term-stacking, so structural equality without `simplify` + # should hold. + self.assertEqual(left + right, expected) + # This is a different order, so check the simplification and canonicalisation works. + self.assertEqual((right + left).simplify(), expected.simplify()) + # Neither was modified in place. + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + + left += right + self.assertEqual(left, expected) + self.assertEqual(right, right_initial) + + @ddt.idata(single_cases()) + def test_add_self(self, obs): + """Test that addition to `self` works fine, including in-place mutation. This is a case + where we might fall afoul of Rust's borrowing rules.""" + initial = obs.copy() + expected = (2.0 * obs).simplify() + self.assertEqual((obs + obs).simplify(), expected) + self.assertEqual(obs, initial) + + obs += obs + self.assertEqual(obs.simplify(), expected) + + @ddt.idata(single_cases()) + def test_add_zero(self, obs): + expected = obs.copy() + zero = SparseObservable.zero(obs.num_qubits) + self.assertEqual(obs + zero, expected) + self.assertEqual(zero + obs, expected) + + obs += zero + self.assertEqual(obs, expected) + zero += obs + self.assertEqual(zero, expected) + + def test_add_coercion(self): + """Other quantum-info operators coerce with the ``+`` operator, so we do too.""" + base = SparseObservable.zero(9) + + pauli_label = "IIIXYZIII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base + pauli_label, expected) + self.assertEqual(pauli_label + base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base + pauli, expected) + self.assertEqual(pauli + base, expected) + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base + spo, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__add__`, not our `__radd__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo + base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base + obs_label, expected) + self.assertEqual(obs_label + base, expected) + + expected = 3j * SparseObservable.from_label("IXYrlII0I") + self.assertEqual(base + expected[0], expected) + self.assertEqual(expected[0] + base, expected) + + with self.assertRaises(TypeError): + _ = base + {} + with self.assertRaises(TypeError): + _ = {} + base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base + "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" + base + + self.assertIs(base + AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + with self.assertRaisesRegex(TypeError, "invalid object for in-place addition"): + # This actually _shouldn't_ be a `TypeError` - `__iadd_` should defer to + # `AllowRightArithmetic.__radd__` in the same way that `__add__` does, but a limitation + # in PyO3 (see PyO3/pyo3#4605) prevents this. + base += AllowRightArithmetic() + + def test_add_failures(self): + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(4) + SparseObservable.zero(6) + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(6) + SparseObservable.zero(4) + + def test_sub_simple(self): + num_qubits = 12 + terms = [ + ("ZXY", (5, 2, 1), 1.5j), + ("+r", (8, 0), -0.25), + ("-0l1", (10, 9, 4, 3), 0.5 + 1j), + ("XZ", (7, 5), 0.75j), + ("rl01", (5, 3, 1, 0), 0.25j), + ] + for pivot in range(1, len(terms) - 1): + expected = SparseObservable.from_sparse_list( + [ + (label, indices, coeff if i < pivot else -coeff) + for i, (label, indices, coeff) in enumerate(terms) + ], + num_qubits=num_qubits, + ) + left = SparseObservable.from_sparse_list(terms[:pivot], num_qubits=num_qubits) + left_initial = left.copy() + right = SparseObservable.from_sparse_list(terms[pivot:], num_qubits=num_qubits) + right_initial = right.copy() + # Addition is documented to be term-stacking, so structural equality without `simplify` + # should hold. + self.assertEqual(left - right, expected) + # This is a different order, so check the simplification and canonicalisation works. + self.assertEqual((right - left).simplify(), -expected.simplify()) + # Neither was modified in place. + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + + left -= right + self.assertEqual(left, expected) + self.assertEqual(right, right_initial) + + @ddt.idata(single_cases()) + def test_sub_self(self, obs): + """Test that subtraction of `self` works fine, including in-place mutation. This is a case + where we might fall afoul of Rust's borrowing rules.""" + initial = obs.copy() + expected = SparseObservable.zero(obs.num_qubits) + self.assertEqual((obs - obs).simplify(), expected) + self.assertEqual(obs, initial) + + obs -= obs + self.assertEqual(obs.simplify(), expected) + + @ddt.idata(single_cases()) + def test_sub_zero(self, obs): + expected = obs.copy() + zero = SparseObservable.zero(obs.num_qubits) + self.assertEqual(obs - zero, expected) + self.assertEqual(zero - obs, -expected) + + obs -= zero + self.assertEqual(obs, expected) + zero -= obs + self.assertEqual(zero, -expected) + + def test_sub_coercion(self): + """Other quantum-info operators coerce with the ``-`` operator, so we do too.""" + base = SparseObservable.zero(9) + + pauli_label = "IIIXYZIII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base - pauli_label, -expected) + self.assertEqual(pauli_label - base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base - pauli, -expected) + self.assertEqual(pauli - base, expected) + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base - spo, -expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__add__`, not our `__radd__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo + base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base - obs_label, -expected) + self.assertEqual(obs_label - base, expected) + + expected = 3j * SparseObservable.from_label("IXYrlII0I") + self.assertEqual(base - expected[0], -expected) + self.assertEqual(expected[0] - base, expected) + + with self.assertRaises(TypeError): + _ = base - {} + with self.assertRaises(TypeError): + _ = {} - base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base - "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" - base + + self.assertIs(base + AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + with self.assertRaisesRegex(TypeError, "invalid object for in-place subtraction"): + # This actually _shouldn't_ be a `TypeError` - `__isub_` should defer to + # `AllowRightArithmetic.__rsub__` in the same way that `__sub__` does, but a limitation + # in PyO3 (see PyO3/pyo3#4605) prevents this. + base -= AllowRightArithmetic() + + def test_sub_failures(self): + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(4) - SparseObservable.zero(6) + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(6) - SparseObservable.zero(4) + + @ddt.idata(single_cases()) + def test_neg(self, obs): + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = -np.asarray(expected.coeffs) + self.assertEqual(-obs, expected) + # Test that there's no in-place modification. + self.assertEqual(obs, initial) + + @ddt.idata(single_cases()) + def test_pos(self, obs): + initial = obs.copy() + self.assertEqual(+obs, initial) + self.assertIsNot(+obs, obs) + + @combine(left=single_cases(), right=single_cases()) + def test_tensor(self, left, right): + + def expected(left, right): + coeffs = [] + bit_terms = [] + indices = [] + boundaries = [0] + for left_ptr in range(left.num_terms): + left_start, left_end = left.boundaries[left_ptr], left.boundaries[left_ptr + 1] + for right_ptr in range(right.num_terms): + right_start = right.boundaries[right_ptr] + right_end = right.boundaries[right_ptr + 1] + coeffs.append(left.coeffs[left_ptr] * right.coeffs[right_ptr]) + bit_terms.extend(right.bit_terms[right_start:right_end]) + bit_terms.extend(left.bit_terms[left_start:left_end]) + indices.extend(right.indices[right_start:right_end]) + indices.extend(i + right.num_qubits for i in left.indices[left_start:left_end]) + boundaries.append(len(indices)) + return SparseObservable.from_raw_parts( + left.num_qubits + right.num_qubits, coeffs, bit_terms, indices, boundaries + ) + + # We deliberately have the arguments flipped when appropriate, here. + # pylint: disable=arguments-out-of-order + + left_initial = left.copy() + right_initial = right.copy() + self.assertEqual(left.tensor(right), expected(left, right)) + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + self.assertEqual(right.tensor(left), expected(right, left)) + + self.assertEqual(left.expand(right), expected(right, left)) + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + self.assertEqual(right.expand(left), expected(left, right)) + + self.assertEqual(left.tensor(right), right.expand(left)) + self.assertEqual(left.expand(right), right.tensor(left)) + + @combine( + obs=single_cases(), identity=[SparseObservable.identity(0), SparseObservable.identity(5)] + ) + def test_tensor_identity(self, obs, identity): + initial = obs.copy() + expected_left = SparseObservable.from_raw_parts( + obs.num_qubits + identity.num_qubits, + obs.coeffs, + obs.bit_terms, + [x + identity.num_qubits for x in obs.indices], + obs.boundaries, + ) + expected_right = SparseObservable.from_raw_parts( + obs.num_qubits + identity.num_qubits, + obs.coeffs, + obs.bit_terms, + obs.indices, + obs.boundaries, + ) + self.assertEqual(obs.tensor(identity), expected_left) + self.assertEqual(identity.tensor(obs), expected_right) + self.assertEqual(obs.expand(identity), expected_right) + self.assertEqual(identity.expand(obs), expected_left) + self.assertEqual(obs ^ identity, expected_left) + self.assertEqual(identity ^ obs, expected_right) + self.assertEqual(obs, initial) + obs ^= identity + self.assertEqual(obs, expected_left) + + @combine(obs=single_cases(), zero=[SparseObservable.zero(0), SparseObservable.zero(5)]) + def test_tensor_zero(self, obs, zero): + initial = obs.copy() + expected = SparseObservable.zero(obs.num_qubits + zero.num_qubits) + self.assertEqual(obs.tensor(zero), expected) + self.assertEqual(zero.tensor(obs), expected) + self.assertEqual(obs.expand(zero), expected) + self.assertEqual(zero.expand(obs), expected) + self.assertEqual(obs ^ zero, expected) + self.assertEqual(zero ^ obs, expected) + self.assertEqual(obs, initial) + obs ^= zero + self.assertEqual(obs, expected) + + def test_tensor_coercion(self): + """Other quantum-info operators coerce with the ``tensor`` method and operator, so we do + too.""" + base = SparseObservable.identity(0) + + pauli_label = "IIXYZII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base.tensor(pauli_label), expected) + self.assertEqual(base.expand(pauli_label), expected) + self.assertEqual(base ^ pauli_label, expected) + self.assertEqual(pauli_label ^ base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base.tensor(pauli), expected) + self.assertEqual(base.expand(pauli), expected) + self.assertEqual(base ^ pauli, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `Pauli` is badly behaved in its coercion (it gets first dibs + # at `__xor__`, not our `__rxor__`), and will not return `NotImplemented` for bad types. + # This _shouldn't_ raise, and this test here is to remind us to flip it to a proper + # assertion of correctness if `Pauli` starts playing nicely. + _ = pauli ^ base + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base.tensor(spo), expected) + self.assertEqual(base.expand(spo), expected) + self.assertEqual(base ^ spo, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__xor__`, not our `__rxor__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo ^ base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base.tensor(obs_label), expected) + self.assertEqual(base.expand(obs_label), expected) + self.assertEqual(base ^ obs_label, expected) + self.assertEqual(obs_label ^ base, expected) + + with self.assertRaises(TypeError): + _ = base ^ {} + with self.assertRaises(TypeError): + _ = {} ^ base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base ^ "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" ^ base + + self.assertIs(base ^ AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_adjoint(self, obs): + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.conjugate(expected.coeffs) + self.assertEqual(obs.adjoint(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.adjoint().adjoint(), initial) + self.assertEqual(obs.adjoint(), obs.conjugate().transpose()) + self.assertEqual(obs.adjoint(), obs.transpose().conjugate()) + + @ddt.idata(single_cases()) + def test_conjugate(self, obs): + initial = obs.copy() + + term_map = {term: (term, 1.0) for term in SparseObservable.BitTerm} + term_map[SparseObservable.BitTerm.Y] = (SparseObservable.BitTerm.Y, -1.0) + term_map[SparseObservable.BitTerm.RIGHT] = (SparseObservable.BitTerm.LEFT, 1.0) + term_map[SparseObservable.BitTerm.LEFT] = (SparseObservable.BitTerm.RIGHT, 1.0) + + expected = obs.copy() + for i in range(expected.num_terms): + start, end = expected.boundaries[i], expected.boundaries[i + 1] + coeff = expected.coeffs[i] + for offset, bit_term in enumerate(expected.bit_terms[start:end]): + new_term, multiplier = term_map[bit_term] + coeff *= multiplier + expected.bit_terms[start + offset] = new_term + expected.coeffs[i] = coeff.conjugate() + + self.assertEqual(obs.conjugate(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.conjugate().conjugate(), initial) + self.assertEqual(obs.conjugate(), obs.transpose().adjoint()) + self.assertEqual(obs.conjugate(), obs.adjoint().transpose()) + + def test_conjugate_explicit(self): + # The description of conjugation on the operator is not 100% trivial to see is correct, so + # here's an explicit case to verify. + obs = SparseObservable.from_sparse_list( + [ + ("Y", (1,), 2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), 0.75), + ("rlrl", (4, 3, 2, 1), 1.0), + ("lrlr", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + expected = SparseObservable.from_sparse_list( + [ + ("Y", (1,), -2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), -1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), -0.25j), + ("YYY", (3, 2, 1), -0.75), + ("lrlr", (4, 3, 2, 1), 1.0), + ("rlrl", (4, 3, 2, 1), -1.0j), + ("", (), -1.5j), + ], + num_qubits=6, + ) + self.assertEqual(obs.conjugate(), expected) + self.assertEqual(obs.conjugate().conjugate(), obs) + + @ddt.idata(single_cases()) + def test_transpose(self, obs): + initial = obs.copy() + + term_map = {term: (term, 1.0) for term in SparseObservable.BitTerm} + term_map[SparseObservable.BitTerm.Y] = (SparseObservable.BitTerm.Y, -1.0) + term_map[SparseObservable.BitTerm.RIGHT] = (SparseObservable.BitTerm.LEFT, 1.0) + term_map[SparseObservable.BitTerm.LEFT] = (SparseObservable.BitTerm.RIGHT, 1.0) + + expected = obs.copy() + for i in range(expected.num_terms): + start, end = expected.boundaries[i], expected.boundaries[i + 1] + coeff = expected.coeffs[i] + for offset, bit_term in enumerate(expected.bit_terms[start:end]): + new_term, multiplier = term_map[bit_term] + coeff *= multiplier + expected.bit_terms[start + offset] = new_term + expected.coeffs[i] = coeff + + self.assertEqual(obs.transpose(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.transpose().transpose(), initial) + self.assertEqual(obs.transpose(), obs.conjugate().adjoint()) + self.assertEqual(obs.transpose(), obs.adjoint().conjugate()) + + def test_transpose_explicit(self): + # The description of transposition on the operator is not 100% trivial to see is correct, so + # here's a few explicit cases to verify. + obs = SparseObservable.from_sparse_list( + [ + ("Y", (1,), 2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), 0.75), + ("rlrl", (4, 3, 2, 1), 1.0), + ("lrlr", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + expected = SparseObservable.from_sparse_list( + [ + ("Y", (1,), -2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), -0.75), + ("lrlr", (4, 3, 2, 1), 1.0), + ("rlrl", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + self.assertEqual(obs.transpose(), expected) + self.assertEqual(obs.transpose().transpose(), obs) + + def test_simplify(self): + self.assertEqual((1e-10 * SparseObservable("XX")).simplify(1e-8), SparseObservable.zero(2)) + self.assertEqual((1e-10j * SparseObservable("XX")).simplify(1e-8), SparseObservable.zero(2)) + self.assertEqual( + (1e-7 * SparseObservable("XX")).simplify(1e-8), 1e-7 * SparseObservable("XX") + ) + + exact_coeff = 2.0**-10 + self.assertEqual( + (exact_coeff * SparseObservable("XX")).simplify(exact_coeff), SparseObservable.zero(2) + ) + self.assertEqual( + (exact_coeff * 1j * SparseObservable("XX")).simplify(exact_coeff), + SparseObservable.zero(2), + ) + coeff = 3e-5 + 4e-5j + self.assertEqual( + (coeff * SparseObservable("ZZ")).simplify(abs(coeff)), SparseObservable.zero(2) + ) + + sum_alike = SparseObservable.from_list( + [ + ("XX", 1.0), + ("YY", 1j), + ("XX", -1.0), + ] + ) + self.assertEqual(sum_alike.simplify(), 1j * SparseObservable("YY")) + + terms = [ + ("XYIZI", 1.5), + ("+-IYI", 2.0), + ("XYIZI", 2j), + ("+-IYI", -2.0), + ("rlIZI", -2.0), + ] + canonical_forwards = SparseObservable.from_list(terms) + canonical_backwards = SparseObservable.from_list(list(reversed(terms))) + self.assertNotEqual(canonical_forwards.simplify(), canonical_forwards) + self.assertNotEqual(canonical_forwards, canonical_backwards) + self.assertEqual(canonical_forwards.simplify(), canonical_backwards.simplify()) + self.assertEqual(canonical_forwards.simplify(), canonical_forwards.simplify().simplify()) + + @ddt.idata(single_cases()) + def test_clear(self, obs): + num_qubits = obs.num_qubits + obs.clear() + self.assertEqual(obs, SparseObservable.zero(num_qubits)) + + def test_apply_layout_list(self): + self.assertEqual( + SparseObservable.zero(5).apply_layout([4, 3, 2, 1, 0]), SparseObservable.zero(5) + ) + self.assertEqual( + SparseObservable.zero(3).apply_layout([0, 2, 1], 8), SparseObservable.zero(8) + ) + self.assertEqual( + SparseObservable.identity(2).apply_layout([1, 0]), SparseObservable.identity(2) + ) + self.assertEqual( + SparseObservable.identity(3).apply_layout([100, 10_000, 3], 100_000_000), + SparseObservable.identity(100_000_000), + ) + + terms = [ + ("ZYX", (4, 2, 1), 1j), + ("", (), -0.5), + ("+-rl01", (10, 8, 6, 4, 2, 0), 2.0), + ] + + def map_indices(terms, layout): + return [ + (terms, tuple(layout[bit] for bit in bits), coeff) for terms, bits, coeff in terms + ] + + identity = list(range(12)) + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout(identity), + SparseObservable.from_sparse_list(terms, num_qubits=12), + ) + # We've already tested elsewhere that `SparseObservable.from_sparse_list` produces termwise + # sorted indices, so these tests also ensure `apply_layout` is maintaining that invariant. + backwards = list(range(12))[::-1] + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout(backwards), + SparseObservable.from_sparse_list(map_indices(terms, backwards), num_qubits=12), + ) + shuffled = [4, 7, 1, 10, 0, 11, 3, 2, 8, 5, 6, 9] + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout(shuffled), + SparseObservable.from_sparse_list(map_indices(terms, shuffled), num_qubits=12), + ) + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout(shuffled, 100), + SparseObservable.from_sparse_list(map_indices(terms, shuffled), num_qubits=100), + ) + expanded = [78, 69, 82, 68, 32, 97, 108, 101, 114, 116, 33] + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=11).apply_layout(expanded, 120), + SparseObservable.from_sparse_list(map_indices(terms, expanded), num_qubits=120), + ) + + def test_apply_layout_transpiled(self): + base = SparseObservable.from_sparse_list( + [ + ("ZYX", (4, 2, 1), 1j), + ("", (), -0.5), + ("+-r", (3, 2, 0), 2.0), + ], + num_qubits=5, + ) + + qc = QuantumCircuit(5) + initial_list = [3, 4, 0, 2, 1] + no_routing = transpile( + qc, target=lnn_target(5), initial_layout=initial_list, seed_transpiler=2024_10_25_0 + ).layout + # It's easiest here to test against the `list` form, which we verify separately and + # explicitly. + self.assertEqual(base.apply_layout(no_routing), base.apply_layout(initial_list)) + + expanded = transpile( + qc, target=lnn_target(100), initial_layout=initial_list, seed_transpiler=2024_10_25_1 + ).layout + self.assertEqual( + base.apply_layout(expanded), base.apply_layout(initial_list, num_qubits=100) + ) + + qc = QuantumCircuit(5) + qargs = list(itertools.permutations(range(5), 2)) + random.Random(2024_10_25_2).shuffle(qargs) + for pair in qargs: + qc.cx(*pair) + + routed = transpile(qc, target=lnn_target(5), seed_transpiler=2024_10_25_3).layout + self.assertEqual( + base.apply_layout(routed), + base.apply_layout(routed.final_index_layout(filter_ancillas=True)), + ) + + routed_expanded = transpile(qc, target=lnn_target(20), seed_transpiler=2024_10_25_3).layout + self.assertEqual( + base.apply_layout(routed_expanded), + base.apply_layout( + routed_expanded.final_index_layout(filter_ancillas=True), num_qubits=20 + ), + ) + + def test_apply_layout_none(self): + self.assertEqual(SparseObservable.zero(0).apply_layout(None), SparseObservable.zero(0)) + self.assertEqual(SparseObservable.zero(0).apply_layout(None, 3), SparseObservable.zero(3)) + self.assertEqual(SparseObservable.zero(5).apply_layout(None), SparseObservable.zero(5)) + self.assertEqual(SparseObservable.zero(3).apply_layout(None, 8), SparseObservable.zero(8)) + self.assertEqual( + SparseObservable.identity(0).apply_layout(None), SparseObservable.identity(0) + ) + self.assertEqual( + SparseObservable.identity(0).apply_layout(None, 8), SparseObservable.identity(8) + ) + self.assertEqual( + SparseObservable.identity(2).apply_layout(None), SparseObservable.identity(2) + ) + self.assertEqual( + SparseObservable.identity(3).apply_layout(None, 100_000_000), + SparseObservable.identity(100_000_000), + ) + + terms = [ + ("ZYX", (2, 1, 0), 1j), + ("", (), -0.5), + ("+-rl01", (10, 8, 6, 4, 2, 0), 2.0), + ] + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout(None), + SparseObservable.from_sparse_list(terms, num_qubits=12), + ) + self.assertEqual( + SparseObservable.from_sparse_list(terms, num_qubits=12).apply_layout( + None, num_qubits=200 + ), + SparseObservable.from_sparse_list(terms, num_qubits=200), + ) + + def test_apply_layout_failures(self): + obs = SparseObservable.from_list([("IIYI", 2.0), ("IIIX", -1j)]) + with self.assertRaisesRegex(ValueError, "duplicate"): + obs.apply_layout([0, 0, 1, 2]) + with self.assertRaisesRegex(ValueError, "does not account for all contained qubits"): + obs.apply_layout([0, 1]) + with self.assertRaisesRegex(ValueError, "less than the number of qubits"): + obs.apply_layout([0, 2, 4, 6]) + with self.assertRaisesRegex(ValueError, "cannot shrink"): + obs.apply_layout([0, 1], num_qubits=2) + with self.assertRaisesRegex(ValueError, "cannot shrink"): + obs.apply_layout(None, num_qubits=2) + + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 0) + layout = transpile(qc, target=lnn_target(3), seed_transpiler=2024_10_25).layout + with self.assertRaisesRegex(ValueError, "cannot shrink"): + obs.apply_layout(layout, num_qubits=2) + + def test_pauli_bases(self): + obs = SparseObservable.from_list( + [ + ("IIIII", 1.0), + ("IXYZI", 2.0), + ("+-II+", 1j), + ("rlrlr", -0.5), + ("01010", -0.25), + ("rlYII", 1.0), + ] + ) + expected = PauliList( + [ + Pauli("IIIII"), + Pauli("IXYZI"), + Pauli("XXIIX"), + Pauli("YYYYY"), + Pauli("ZZZZZ"), + Pauli("YYYII"), + ] + ) + self.assertEqual(obs.pauli_bases(), expected) + + def test_iteration(self): + self.assertEqual(list(SparseObservable.zero(5)), []) + self.assertEqual(tuple(SparseObservable.zero(0)), ()) + + obs = SparseObservable.from_sparse_list( + [ + ("Xrl", (4, 2, 1), 2j), + ("", (), 0.5), + ("01", (3, 0), -0.25), + ("+-", (2, 1), 1.0), + ("YZ", (4, 1), 1j), + ], + num_qubits=5, + ) + bit_term = SparseObservable.BitTerm + expected = [ + SparseObservable.Term(5, 2j, [bit_term.LEFT, bit_term.RIGHT, bit_term.X], [1, 2, 4]), + SparseObservable.Term(5, 0.5, [], []), + SparseObservable.Term(5, -0.25, [bit_term.ONE, bit_term.ZERO], [0, 3]), + SparseObservable.Term(5, 1.0, [bit_term.MINUS, bit_term.PLUS], [1, 2]), + SparseObservable.Term(5, 1j, [bit_term.Z, bit_term.Y], [1, 4]), + ] + self.assertEqual(list(obs), expected) + + def test_indexing(self): + obs = SparseObservable.from_sparse_list( + [ + ("Xrl", (4, 2, 1), 2j), + ("", (), 0.5), + ("01", (3, 0), -0.25), + ("+-", (2, 1), 1.0), + ("YZ", (4, 1), 1j), + ], + num_qubits=5, + ) + bit_term = SparseObservable.BitTerm + expected = [ + SparseObservable.Term(5, 2j, [bit_term.LEFT, bit_term.RIGHT, bit_term.X], [1, 2, 4]), + SparseObservable.Term(5, 0.5, [], []), + SparseObservable.Term(5, -0.25, [bit_term.ZERO, bit_term.ONE], [3, 0]), + SparseObservable.Term(5, 1.0, [bit_term.MINUS, bit_term.PLUS], [1, 2]), + SparseObservable.Term(5, 1j, [bit_term.Y, bit_term.Z], [4, 1]), + ] + self.assertEqual(obs[0], expected[0]) + self.assertEqual(obs[-2], expected[-2]) + self.assertEqual(obs[2:4], SparseObservable(expected[2:4])) + self.assertEqual(obs[1::2], SparseObservable(expected[1::2])) + self.assertEqual(obs[:], SparseObservable(expected)) + self.assertEqual(obs[-1:-4:-1], SparseObservable(expected[-1:-4:-1])) + + @ddt.data( + SparseObservable.identity(0), + SparseObservable.identity(1_000), + SparseObservable.from_label("IIXIZI"), + SparseObservable.from_label("X"), + SparseObservable.from_list([("YIXZII", -0.25)]), + SparseObservable.from_list([("01rl+-", 0.25 + 0.5j)]), + ) + def test_term_repr(self, obs): + # The purpose of this is just to test that the `repr` doesn't crash, rather than asserting + # that it has any particular form. + term = obs[0] + self.assertIsInstance(repr(term), str) + self.assertIn("SparseObservable.Term", repr(term)) + + @ddt.data( + SparseObservable.identity(0), + 2j * SparseObservable.identity(1), + SparseObservable.identity(100), + SparseObservable.from_label("IIX+-rlYZ01IIIII"), + ) + def test_term_to_observable(self, obs): + self.assertEqual(obs[0].to_observable(), obs) + self.assertIsNot(obs[0].to_observable(), obs) + + def test_term_equality(self): + self.assertEqual( + SparseObservable.Term(5, 1.0, [], []), SparseObservable.Term(5, 1.0, [], []) + ) + self.assertNotEqual( + SparseObservable.Term(5, 1.0, [], []), SparseObservable.Term(8, 1.0, [], []) + ) + self.assertNotEqual( + SparseObservable.Term(5, 1.0, [], []), SparseObservable.Term(5, 1j, [], []) + ) + self.assertNotEqual( + SparseObservable.Term(5, 1.0, [], []), SparseObservable.Term(8, -1, [], []) + ) + + obs = SparseObservable.from_list( + [ + ("IIXIZ", 2j), + ("IIZIX", 2j), + ("++III", -1.5), + ("--III", -1.5), + ("IrIlI", 0.5), + ("IIrIl", 0.5), + ] + ) + self.assertEqual(obs[0], obs[0]) + self.assertEqual(obs[1], obs[1]) + self.assertNotEqual(obs[0], obs[1]) + self.assertEqual(obs[2], obs[2]) + self.assertEqual(obs[3], obs[3]) + self.assertNotEqual(obs[2], obs[3]) + self.assertEqual(obs[4], obs[4]) + self.assertEqual(obs[5], obs[5]) + self.assertNotEqual(obs[4], obs[5]) + + @ddt.data( + SparseObservable.identity(0), + 2j * SparseObservable.identity(1), + SparseObservable.identity(100), + SparseObservable.from_label("IIX+-rlYZ01IIIII"), + ) + def test_term_pickle(self, obs): + term = obs[0] + self.assertEqual(pickle.loads(pickle.dumps(term)), term) + self.assertEqual(copy.copy(term), term) + self.assertEqual(copy.deepcopy(term), term) + + def test_term_attributes(self): + term = SparseObservable.from_label("II+IIX0")[0] + self.assertEqual(term.num_qubits, 7) + self.assertEqual(term.coeff, 1.0) + np.testing.assert_equal( + term.bit_terms, + np.array( + [ + SparseObservable.BitTerm.ZERO, + SparseObservable.BitTerm.X, + SparseObservable.BitTerm.PLUS, + ], + dtype=np.uint8, + ), + ) + np.testing.assert_equal(term.indices, np.array([0, 1, 4], dtype=np.uintp)) + + term = SparseObservable.identity(10)[0] + self.assertEqual(term.num_qubits, 10) + self.assertEqual(term.coeff, 1.0) + self.assertEqual(list(term.bit_terms), []) + self.assertEqual(list(term.indices), []) + + term = SparseObservable.from_list([("IIrlZ", 0.5j)])[0] + self.assertEqual(term.num_qubits, 5) + self.assertEqual(term.coeff, 0.5j) + self.assertEqual( + list(term.bit_terms), + [ + SparseObservable.BitTerm.Z, + SparseObservable.BitTerm.LEFT, + SparseObservable.BitTerm.RIGHT, + ], + ) + self.assertEqual(list(term.indices), [0, 1, 2]) + + def test_term_new(self): + expected = SparseObservable.from_label("IIIX+1III")[0] + + self.assertEqual( + SparseObservable.Term( + 9, + 1.0, + [ + SparseObservable.BitTerm.ONE, + SparseObservable.BitTerm.PLUS, + SparseObservable.BitTerm.X, + ], + [3, 4, 5], + ), + expected, + ) + + # Constructor should allow being given unsorted inputs, and but them in the right order. + self.assertEqual( + SparseObservable.Term( + 9, + 1.0, + [ + SparseObservable.BitTerm.PLUS, + SparseObservable.BitTerm.X, + SparseObservable.BitTerm.ONE, + ], + [4, 5, 3], + ), + expected, + ) + self.assertEqual(list(expected.indices), [3, 4, 5]) + + with self.assertRaisesRegex(ValueError, "not term-wise increasing"): + SparseObservable.Term(2, 2j, [SparseObservable.BitTerm.RIGHT] * 2, [0, 0]) + + def test_term_pauli_base(self): + obs = SparseObservable.from_list( + [ + ("IIIII", 1.0), + ("IXYZI", 2.0), + ("+-II+", 1j), + ("rlrlr", -0.5), + ("01010", -0.25), + ("rlYII", 1.0), + ] + ) + expected = [ + Pauli("IIIII"), + Pauli("IXYZI"), + Pauli("XXIIX"), + Pauli("YYYYY"), + Pauli("ZZZZZ"), + Pauli("YYYII"), + ] + self.assertEqual([term.pauli_base() for term in obs], expected) diff --git a/test/python/result/test_mitigators.py b/test/python/result/test_mitigators.py index 8dcdedc433fe..3d04249d6f78 100644 --- a/test/python/result/test_mitigators.py +++ b/test/python/result/test_mitigators.py @@ -124,16 +124,18 @@ def test_mitigation_improvement(self): # which only supports BackendV1 at the moment: # https://github.com/Qiskit/qiskit/issues/12832 assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices) + mitigators = self.mitigators(assignment_matrices) circuit, circuit_name, num_qubits = self.ghz_3_circuit() counts_ideal, counts_noise, probs_noise = self.counts_data( circuit, assignment_matrices, shots ) unmitigated_error = self.compare_results(counts_ideal, counts_noise) - unmitigated_stddev = stddev(probs_noise, shots) + with self.assertWarns(DeprecationWarning): + unmitigated_stddev = stddev(probs_noise, shots) for mitigator in mitigators: - mitigated_quasi_probs = mitigator.quasi_probabilities(counts_noise) + with self.assertWarns(DeprecationWarning): + mitigated_quasi_probs = mitigator.quasi_probabilities(counts_noise) mitigated_probs = ( mitigated_quasi_probs.nearest_probability_distribution().binary_probabilities( num_bits=num_qubits @@ -161,7 +163,7 @@ def test_expectation_improvement(self): shots = 1024 with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices) + mitigators = self.mitigators(assignment_matrices) num_qubits = len(assignment_matrices) diagonals = [] diagonals.append("IZ0") @@ -170,20 +172,24 @@ def test_expectation_improvement(self): qubit_index = {i: i for i in range(num_qubits)} circuit, circuit_name, num_qubits = self.ghz_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) - probs_ideal, _ = counts_probability_vector(counts_ideal, qubit_index=qubit_index) - probs_noise, _ = counts_probability_vector(counts_noise, qubit_index=qubit_index) + with self.assertWarns(DeprecationWarning): + probs_ideal, _ = counts_probability_vector(counts_ideal, qubit_index=qubit_index) + probs_noise, _ = counts_probability_vector(counts_noise, qubit_index=qubit_index) for diagonal in diagonals: if isinstance(diagonal, str): - diagonal = str2diag(diagonal) - unmitigated_expectation, unmitigated_stddev = expval_with_stddev( - diagonal, probs_noise, shots=counts_noise.shots() - ) + with self.assertWarns(DeprecationWarning): + diagonal = str2diag(diagonal) + with self.assertWarns(DeprecationWarning): + unmitigated_expectation, unmitigated_stddev = expval_with_stddev( + diagonal, probs_noise, shots=counts_noise.shots() + ) ideal_expectation = np.dot(probs_ideal, diagonal) unmitigated_error = np.abs(ideal_expectation - unmitigated_expectation) for mitigator in mitigators: - mitigated_expectation, mitigated_stddev = mitigator.expectation_value( - counts_noise, diagonal - ) + with self.assertWarns(DeprecationWarning): + mitigated_expectation, mitigated_stddev = mitigator.expectation_value( + counts_noise, diagonal + ) mitigated_error = np.abs(ideal_expectation - mitigated_expectation) self.assertLess( mitigated_error, @@ -204,30 +210,31 @@ def test_clbits_parameter(self): shots = 10000 with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices) + mitigators = self.mitigators(assignment_matrices) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) counts_ideal_12 = marginal_counts(counts_ideal, [1, 2]) counts_ideal_02 = marginal_counts(counts_ideal, [0, 2]) for mitigator in mitigators: - mitigated_probs_12 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[1, 2], clbits=[1, 2]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=2) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_12 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[1, 2], clbits=[1, 2]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=2) + ) mitigated_error = self.compare_results(counts_ideal_12, mitigated_probs_12) self.assertLess( mitigated_error, 0.001, f"Mitigator {mitigator} did not correctly marginalize for qubits 1,2", ) - - mitigated_probs_02 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[0, 2], clbits=[0, 2]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=2) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_02 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[0, 2], clbits=[0, 2]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=2) + ) mitigated_error = self.compare_results(counts_ideal_02, mitigated_probs_02) self.assertLess( mitigated_error, @@ -240,7 +247,7 @@ def test_qubits_parameter(self): shots = 10000 with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices) + mitigators = self.mitigators(assignment_matrices) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) counts_ideal_012 = counts_ideal @@ -248,35 +255,36 @@ def test_qubits_parameter(self): counts_ideal_102 = Counts({"000": counts_ideal["000"], "010": counts_ideal["001"]}) for mitigator in mitigators: - mitigated_probs_012 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[0, 1, 2]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=3) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_012 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[0, 1, 2]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=3) + ) mitigated_error = self.compare_results(counts_ideal_012, mitigated_probs_012) self.assertLess( mitigated_error, 0.001, f"Mitigator {mitigator} did not correctly handle qubit order 0, 1, 2", ) - - mitigated_probs_210 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[2, 1, 0]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=3) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_210 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[2, 1, 0]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=3) + ) mitigated_error = self.compare_results(counts_ideal_210, mitigated_probs_210) self.assertLess( mitigated_error, 0.001, f"Mitigator {mitigator} did not correctly handle qubit order 2, 1, 0", ) - - mitigated_probs_102 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[1, 0, 2]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=3) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_102 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[1, 0, 2]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=3) + ) mitigated_error = self.compare_results(counts_ideal_102, mitigated_probs_102) self.assertLess( mitigated_error, @@ -289,31 +297,32 @@ def test_repeated_qubits_parameter(self): shots = 10000 with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices, qubits=[0, 1, 2]) + mitigators = self.mitigators(assignment_matrices, qubits=[0, 1, 2]) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) counts_ideal_012 = counts_ideal counts_ideal_210 = Counts({"000": counts_ideal["000"], "100": counts_ideal["001"]}) for mitigator in mitigators: - mitigated_probs_210 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[2, 1, 0]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=3) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_210 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[2, 1, 0]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=3) + ) mitigated_error = self.compare_results(counts_ideal_210, mitigated_probs_210) self.assertLess( mitigated_error, 0.001, 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 - mitigated_probs_012 = ( - mitigator.quasi_probabilities(counts_noise) - .nearest_probability_distribution() - .binary_probabilities(num_bits=3) - ) + with self.assertWarns(DeprecationWarning): + # checking qubit order 2,1,0 should not "overwrite" the default 0,1,2 + mitigated_probs_012 = ( + mitigator.quasi_probabilities(counts_noise) + .nearest_probability_distribution() + .binary_probabilities(num_bits=3) + ) mitigated_error = self.compare_results(counts_ideal_012, mitigated_probs_012) self.assertLess( mitigated_error, @@ -328,41 +337,44 @@ def test_qubits_subset_parameter(self): shots = 10000 with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices, qubits=[2, 4, 6]) + mitigators = self.mitigators(assignment_matrices, qubits=[2, 4, 6]) circuit, _, _ = self.first_qubit_h_3_circuit() counts_ideal, counts_noise, _ = self.counts_data(circuit, assignment_matrices, shots) counts_ideal_2 = marginal_counts(counts_ideal, [0]) counts_ideal_6 = marginal_counts(counts_ideal, [2]) for mitigator in mitigators: - mitigated_probs_2 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[2]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=1) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_2 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[2]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=1) + ) mitigated_error = self.compare_results(counts_ideal_2, mitigated_probs_2) self.assertLess( mitigated_error, 0.001, "Mitigator {mitigator} did not correctly handle qubit subset", ) - - mitigated_probs_6 = ( - mitigator.quasi_probabilities(counts_noise, qubits=[6]) - .nearest_probability_distribution() - .binary_probabilities(num_bits=1) - ) + with self.assertWarns(DeprecationWarning): + mitigated_probs_6 = ( + mitigator.quasi_probabilities(counts_noise, qubits=[6]) + .nearest_probability_distribution() + .binary_probabilities(num_bits=1) + ) mitigated_error = self.compare_results(counts_ideal_6, mitigated_probs_6) self.assertLess( mitigated_error, 0.001, f"Mitigator {mitigator} did not correctly handle qubit subset", ) - diagonal = str2diag("ZZ") + with self.assertWarns(DeprecationWarning): + diagonal = str2diag("ZZ") ideal_expectation = 0 - mitigated_expectation, _ = mitigator.expectation_value( - counts_noise, diagonal, qubits=[2, 6] - ) + with self.assertWarns(DeprecationWarning): + mitigated_expectation, _ = mitigator.expectation_value( + counts_noise, diagonal, qubits=[2, 6] + ) mitigated_error = np.abs(ideal_expectation - mitigated_expectation) self.assertLess( mitigated_error, @@ -382,7 +394,8 @@ def test_from_backend(self): prop.value = probs[qubit_idx][0] if prop.name == "prob_meas0_prep1": prop.value = probs[qubit_idx][1] - LRM_from_backend = LocalReadoutMitigator(backend=backend) + with self.assertWarns(DeprecationWarning): + LRM_from_backend = LocalReadoutMitigator(backend=backend) mats = [] for qubit_idx in range(num_qubits): @@ -393,7 +406,8 @@ def test_from_backend(self): ] ) mats.append(mat) - LRM_from_matrices = LocalReadoutMitigator(assignment_matrices=mats) + with self.assertWarns(DeprecationWarning): + LRM_from_matrices = LocalReadoutMitigator(assignment_matrices=mats) self.assertTrue( matrix_equal( LRM_from_backend.assignment_matrix(), LRM_from_matrices.assignment_matrix() @@ -407,7 +421,8 @@ def test_error_handling(self): good_matrix_A = np.array([[0.2, 1], [0.8, 0]]) for bad_matrix in [bad_matrix_A, bad_matrix_B]: with self.assertRaises(QiskitError) as cm: - CorrelatedReadoutMitigator(bad_matrix) + with self.assertWarns(DeprecationWarning): + CorrelatedReadoutMitigator(bad_matrix) self.assertEqual( cm.exception.message, "Assignment matrix columns must be valid probability distributions", @@ -415,7 +430,8 @@ def test_error_handling(self): with self.assertRaises(QiskitError) as cm: amats = [good_matrix_A, bad_matrix_A] - LocalReadoutMitigator(amats) + with self.assertWarns(DeprecationWarning): + LocalReadoutMitigator(amats) self.assertEqual( cm.exception.message, "Assignment matrix columns must be valid probability distributions", @@ -423,7 +439,8 @@ def test_error_handling(self): with self.assertRaises(QiskitError) as cm: amats = [bad_matrix_B, good_matrix_A] - LocalReadoutMitigator(amats) + with self.assertWarns(DeprecationWarning): + LocalReadoutMitigator(amats) self.assertEqual( cm.exception.message, "Assignment matrix columns must be valid probability distributions", @@ -433,10 +450,11 @@ def test_expectation_value_endian(self): """Test that endian for expval is little.""" with self.assertWarns(DeprecationWarning): assignment_matrices = self.assignment_matrices() - mitigators = self.mitigators(assignment_matrices) + mitigators = self.mitigators(assignment_matrices) counts = Counts({"10": 3, "11": 24, "00": 74, "01": 923}) for mitigator in mitigators: - expval, _ = mitigator.expectation_value(counts, diagonal="IZ", qubits=[0, 1]) + with self.assertWarns(DeprecationWarning): + expval, _ = mitigator.expectation_value(counts, diagonal="IZ", qubits=[0, 1]) self.assertAlmostEqual(expval, -1.0, places=0) def test_quasi_probabilities_shots_passing(self): @@ -444,13 +462,16 @@ def test_quasi_probabilities_shots_passing(self): We require the number of shots to be set in the output. """ - mitigator = LocalReadoutMitigator([np.array([[0.9, 0.1], [0.1, 0.9]])], qubits=[0]) + with self.assertWarns(DeprecationWarning): + mitigator = LocalReadoutMitigator([np.array([[0.9, 0.1], [0.1, 0.9]])], qubits=[0]) counts = Counts({"10": 3, "11": 24, "00": 74, "01": 923}) - quasi_dist = mitigator.quasi_probabilities(counts) + with self.assertWarns(DeprecationWarning): + quasi_dist = mitigator.quasi_probabilities(counts) self.assertEqual(quasi_dist.shots, sum(counts.values())) # custom number of shots - quasi_dist = mitigator.quasi_probabilities(counts, shots=1025) + with self.assertWarns(DeprecationWarning): + quasi_dist = mitigator.quasi_probabilities(counts, shots=1025) self.assertEqual(quasi_dist.shots, 1025) @@ -462,12 +483,13 @@ def test_assignment_matrix(self): qubits = [7, 2, 3] with self.assertWarns(DeprecationWarning): backend = Fake5QV1() - assignment_matrices = LocalReadoutMitigator(backend=backend)._assignment_mats[0:3] + assignment_matrices = LocalReadoutMitigator(backend=backend)._assignment_mats[0:3] expected_assignment_matrix = np.kron( np.kron(assignment_matrices[2], assignment_matrices[1]), assignment_matrices[0] ) expected_mitigation_matrix = np.linalg.inv(expected_assignment_matrix) - LRM = LocalReadoutMitigator(assignment_matrices, qubits) + with self.assertWarns(DeprecationWarning): + LRM = LocalReadoutMitigator(assignment_matrices, qubits) self.assertTrue(matrix_equal(expected_mitigation_matrix, LRM.mitigation_matrix())) self.assertTrue(matrix_equal(expected_assignment_matrix, LRM.assignment_matrix())) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b08240197211..b26a049b5567 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -16,6 +16,7 @@ import unittest import contextlib import logging +import math import numpy as np import scipy import scipy.stats @@ -23,7 +24,8 @@ from qiskit import QiskitError, transpile from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, Gate +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.converters import dag_to_circuit, circuit_to_dag from qiskit.circuit.library import ( HGate, @@ -46,6 +48,8 @@ RZXGate, CPhaseGate, CRZGate, + CRXGate, + CRYGate, RXGate, RYGate, RZGate, @@ -1426,7 +1430,7 @@ class TestTwoQubitControlledUDecompose(CheckDecompositions): def test_correct_unitary(self, seed): """Verify unitary for different gates in the decomposition""" unitary = random_unitary(4, seed=seed) - for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate]: + for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]: decomposer = TwoQubitControlledUDecomposer(gate) circ = decomposer(unitary) self.assertEqual(Operator(unitary), Operator(circ)) @@ -1440,6 +1444,88 @@ def test_not_rxx_equivalent(self): "Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message ) + @combine(seed=range(10), name="seed_{seed}") + def test_correct_unitary_custom_gate(self, seed): + """Test synthesis with a custom controlled u equivalent gate.""" + unitary = random_unitary(4, seed=seed) + + class CustomXXGate(RXXGate): + """Custom RXXGate subclass that's not a standard gate""" + + _standard_gate = None + + def __init__(self, theta, label=None): + super().__init__(theta, label) + self.name = "MyCustomXXGate" + + decomposer = TwoQubitControlledUDecomposer(CustomXXGate) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + + def test_unitary_custom_gate_raises(self): + """Test that a custom gate raises an exception, as it's not equivalent to an RXX gate""" + + class CustomXYGate(Gate): + """Custom Gate subclass that's not a standard gate and not RXX equivalent""" + + _standard_gate = None + + def __init__(self, theta: ParameterValueType, label=None): + """Create new custom rotstion XY gate.""" + super().__init__("MyCustomXYGate", 2, [theta]) + + def __array__(self, dtype=None): + """Return a Numpy.array for the custom gate.""" + theta = self.params[0] + cos = math.cos(theta) + isin = 1j * math.sin(theta) + return np.array( + [[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]], + dtype=dtype, + ) + + def inverse(self, annotated: bool = False): + return CustomXYGate(-self.params[0]) + + with self.assertRaisesRegex(QiskitError, "ControlledEquiv calculated fidelity"): + TwoQubitControlledUDecomposer(CustomXYGate) + + @combine(seed=range(10), name="seed_{seed}") + def test_correct_unitary_custom_rxx_equiv_gate(self, seed): + """Test synthesis with a custom controlled u equivalent gate.""" + + class CustomRZZeqGate(Gate): + """Custom Gate subclass that's not a standard gate""" + + _standard_gate = None + + def __init__(self, theta: ParameterValueType, invert=False, label=None): + """Create new custom rotstion XY gate.""" + super().__init__("MyCustomRZZeqGate", 2, [theta, invert], label) + + def __array__(self, dtype=None): + """Return a Numpy.array for the custom gate: h(0) rzz(0,1) h(1)""" + theta = self.params[0] + a = np.exp(-1j * theta / 2.0) / 2.0 + b = np.exp(1j * theta / 2.0) / 2.0 + c = -np.exp(-1j * theta / 2.0) / 2.0 + d = -np.exp(1j * theta / 2.0) / 2.0 + + if self.params[1]: + mat = [[b, a, b, a], [b, c, b, c], [a, b, c, d], [a, d, c, b]] + else: + mat = [[a, a, b, b], [b, d, a, c], [a, a, d, d], [b, d, c, a]] + + return np.array(mat, dtype=dtype) + + def inverse(self, annotated: bool = False): + return CustomRZZeqGate(self.params[0], not self.params[1]) + + unitary = random_unitary(4, seed=seed) + decomposer = TwoQubitControlledUDecomposer(CustomRZZeqGate) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index 3f093d18c517..38f84492ee86 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -304,7 +304,8 @@ def test_circuit_using_clbit(self): circuit.x(0) circuit.delay(100, 0, unit="dt") circuit.measure(0, 0) - circuit.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(1).c_if(0, 1) circuit.measure(2, 0) timed_circuit = self.time_conversion_pass(circuit) @@ -320,7 +321,8 @@ def test_circuit_using_clbit(self): ref_circuit.delay(1872, 1, unit="dt") # 2032 - 160 ref_circuit.delay(432, 2, unit="dt") # 2032 - 1600 ref_circuit.measure(0, 0) - ref_circuit.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.x(1).c_if(0, 1) ref_circuit.delay(160, 0, unit="dt") ref_circuit.measure(2, 0) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py index 417ff9b42212..a13ca2e12de1 100644 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py @@ -82,7 +82,8 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) - qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) with self.assertWarns(DeprecationWarning): @@ -92,7 +93,8 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.measure(0, 0) expected.delay(1000, 1) # x.c_if starts after measure - expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) expected.delay(200, 0) self.assertEqual(expected, scheduled) @@ -170,8 +172,10 @@ def test_c_if_on_different_qubits(self, schedule_pass): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, True) - qc.x(2).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(0, True) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) with self.assertWarns(DeprecationWarning): @@ -182,8 +186,10 @@ def test_c_if_on_different_qubits(self, schedule_pass): expected.measure(0, 0) expected.delay(1000, 1) expected.delay(1000, 2) - expected.x(1).c_if(0, True) - expected.x(2).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(2).c_if(0, True) expected.delay(200, 0) self.assertEqual(expected, scheduled) @@ -255,7 +261,8 @@ def test_measure_after_c_if(self, schedule_pass): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) @@ -267,7 +274,8 @@ def test_measure_after_c_if(self, schedule_pass): expected.delay(1000, 1) expected.delay(1000, 2) expected.measure(0, 0) - expected.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, 1) expected.measure(2, 0) expected.delay(1000, 0) expected.delay(800, 1) @@ -451,7 +459,8 @@ def test_measure_after_c_if_on_edge_locking(self): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) @@ -465,7 +474,8 @@ def test_measure_after_c_if_on_edge_locking(self): expected_asap = QuantumCircuit(3, 1) expected_asap.measure(0, 0) expected_asap.delay(1000, 1) - expected_asap.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_asap.x(1).c_if(0, 1) expected_asap.measure(2, 0) expected_asap.delay(200, 0) expected_asap.delay(200, 2) @@ -474,7 +484,8 @@ def test_measure_after_c_if_on_edge_locking(self): expected_alap = QuantumCircuit(3, 1) expected_alap.measure(0, 0) expected_alap.delay(1000, 1) - expected_alap.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_alap.x(1).c_if(0, 1) expected_alap.delay(200, 2) expected_alap.measure(2, 0) expected_alap.delay(200, 0) @@ -500,11 +511,14 @@ def test_active_reset_circuit(self, write_lat, cond_lat): """ qc = QuantumCircuit(1, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) with self.assertWarns(DeprecationWarning): @@ -519,15 +533,18 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) self.assertEqual(expected, actual_asap) self.assertEqual(expected, actual_alap) @@ -616,15 +633,19 @@ def test_random_complicated_circuit(self): """ qc = QuantumCircuit(3, 1) qc.delay(100, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.barrier() qc.measure(2, 0) - qc.x(1).c_if(0, 0) - qc.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 0) qc.delay(300, 0) qc.cx(1, 2) qc.x(0) - qc.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 0) qc.measure(2, 0) durations = InstructionDurations( @@ -644,19 +665,23 @@ def test_random_complicated_circuit(self): expected_asap.delay(100, 0) # due to conditional latency of 200dt expected_asap.delay(300, 1) expected_asap.delay(300, 2) - expected_asap.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_asap.x(0).c_if(0, 1) expected_asap.barrier() expected_asap.delay(1400, 0) expected_asap.delay(1200, 1) expected_asap.measure(2, 0) - expected_asap.x(1).c_if(0, 0) - expected_asap.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.x(0).c_if(0, 0) expected_asap.delay(300, 0) expected_asap.x(0) expected_asap.delay(300, 2) expected_asap.cx(1, 2) expected_asap.delay(400, 1) - expected_asap.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.cx(0, 1).c_if(0, 0) expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) expected_asap.delay( 700, 1 @@ -671,20 +696,24 @@ def test_random_complicated_circuit(self): expected_alap.delay(100, 0) # due to conditional latency of 200dt expected_alap.delay(300, 1) expected_alap.delay(300, 2) - expected_alap.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_alap.x(0).c_if(0, 1) expected_alap.barrier() expected_alap.delay(1400, 0) expected_alap.delay(1200, 1) expected_alap.measure(2, 0) - expected_alap.x(1).c_if(0, 0) - expected_alap.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.x(0).c_if(0, 0) expected_alap.delay(300, 0) expected_alap.x(0) expected_alap.delay(300, 1) expected_alap.delay(600, 2) expected_alap.cx(1, 2) expected_alap.delay(100, 1) - expected_alap.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.cx(0, 1).c_if(0, 0) expected_alap.measure(2, 0) expected_alap.delay(700, 0) expected_alap.delay(700, 1) @@ -722,8 +751,10 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): """ qc = QuantumCircuit(2, 1) qc.delay(100, 0) - qc.x(0).c_if(0, True) - qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) durations = InstructionDurations([("x", None, 160)]) with self.assertWarns(DeprecationWarning): @@ -733,8 +764,10 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): expected = QuantumCircuit(2, 1) expected.delay(100, 0) expected.delay(100, 1) # due to extra dependency on clbits - expected.x(0).c_if(0, True) - expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) self.assertEqual(expected, scheduled) diff --git a/test/python/transpiler/test_barrier_before_final_measurements.py b/test/python/transpiler/test_barrier_before_final_measurements.py index eb4f509e5301..3aaa5c7895cd 100644 --- a/test/python/transpiler/test_barrier_before_final_measurements.py +++ b/test/python/transpiler/test_barrier_before_final_measurements.py @@ -175,13 +175,15 @@ def test_preserve_measure_for_conditional(self): circuit.h(qr0) circuit.measure(qr0, cr0) - circuit.z(qr1).c_if(cr0, 1) + with self.assertWarns(DeprecationWarning): + circuit.z(qr1).c_if(cr0, 1) circuit.measure(qr1, cr1) expected = QuantumCircuit(qr0, qr1, cr0, cr1) expected.h(qr0) expected.measure(qr0, cr0) - expected.z(qr1).c_if(cr0, 1) + with self.assertWarns(DeprecationWarning): + expected.z(qr1).c_if(cr0, 1) expected.barrier(qr0, qr1) expected.measure(qr1, cr1) @@ -377,17 +379,23 @@ def test_conditioned_on_single_bit(self): circuit = QuantumCircuit(QuantumRegister(3), ClassicalRegister(2), [Clbit()]) circuit.h(range(3)) circuit.measure(range(3), range(3)) - circuit.h(0).c_if(circuit.cregs[0], 3) - circuit.h(1).c_if(circuit.clbits[-1], True) - circuit.h(2).c_if(circuit.clbits[-1], False) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(circuit.cregs[0], 3) + with self.assertWarns(DeprecationWarning): + circuit.h(1).c_if(circuit.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + circuit.h(2).c_if(circuit.clbits[-1], False) circuit.measure(range(3), range(3)) expected = circuit.copy_empty_like() expected.h(range(3)) expected.measure(range(3), range(3)) - expected.h(0).c_if(expected.cregs[0], 3) - expected.h(1).c_if(expected.clbits[-1], True) - expected.h(2).c_if(expected.clbits[-1], False) + with self.assertWarns(DeprecationWarning): + expected.h(0).c_if(expected.cregs[0], 3) + with self.assertWarns(DeprecationWarning): + expected.h(1).c_if(expected.clbits[-1], True) + with self.assertWarns(DeprecationWarning): + expected.h(2).c_if(expected.clbits[-1], False) expected.barrier(range(3)) expected.measure(range(3), range(3)) diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 9dae6c3f283a..820c4f241a1e 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -576,9 +576,12 @@ def test_unroll_1q_chain_conditional(self): circuit.rz(0.3, qr) circuit.rx(0.1, qr) circuit.measure(qr, cr) - circuit.x(qr).c_if(cr, 1) - circuit.y(qr).c_if(cr, 1) - circuit.z(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.y(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.z(qr).c_if(cr, 1) dag = circuit_to_dag(circuit) pass_ = UnrollCustomDefinitions(std_eqlib, ["u1", "u2", "u3"]) dag = pass_.run(dag) @@ -609,9 +612,12 @@ def test_unroll_1q_chain_conditional(self): ref_circuit.append(U1Gate(0.3), [qr[0]]) ref_circuit.append(U3Gate(0.1, -pi / 2, pi / 2), [qr[0]]) ref_circuit.measure(qr[0], cr[0]) - ref_circuit.append(U3Gate(pi, 0, pi), [qr[0]]).c_if(cr, 1) - ref_circuit.append(U3Gate(pi, pi / 2, pi / 2), [qr[0]]).c_if(cr, 1) - ref_circuit.append(U1Gate(pi), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U3Gate(pi, 0, pi), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U3Gate(pi, pi / 2, pi / 2), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U1Gate(pi), [qr[0]]).c_if(cr, 1) ref_dag = circuit_to_dag(ref_circuit) self.assertEqual(unrolled_dag, ref_dag) @@ -1073,7 +1079,8 @@ def test_condition_set_substitute_node(self): circ.h(0) circ.cx(0, 1) circ.measure(1, 1) - circ.h(0).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circ.h(0).c_if(cr, 1) circ_transpiled = transpile(circ, optimization_level=3, basis_gates=["cx", "id", "u"]) # ┌────────────┐ ┌────────────┐ @@ -1089,7 +1096,8 @@ def test_condition_set_substitute_node(self): expected.u(pi / 2, 0, pi, 0) expected.cx(0, 1) expected.measure(1, 1) - expected.u(pi / 2, 0, pi, 0).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + expected.u(pi / 2, 0, pi, 0).c_if(cr, 1) self.assertEqual(circ_transpiled, expected) diff --git a/test/python/transpiler/test_check_map.py b/test/python/transpiler/test_check_map.py index 2a88b272c74d..866eb9f4dfae 100644 --- a/test/python/transpiler/test_check_map.py +++ b/test/python/transpiler/test_check_map.py @@ -268,7 +268,8 @@ def test_nested_conditional_unusual_bit_order(self): # should all be fine. This kind of thing is a staple of the control-flow builders. inner_order = [cr2[0], cr1[0], cr2[1], cr1[1]] inner = QuantumCircuit(qr, inner_order, cr1, cr2) - inner.cx(0, 1).c_if(cr2, 3) + with self.assertWarns(DeprecationWarning): + inner.cx(0, 1).c_if(cr2, 3) outer = QuantumCircuit(qr, cr1, cr2) outer.if_test((cr1, 3), inner, outer.qubits, inner_order) diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 66a7fd15ad10..2a39c45bc48a 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -653,7 +653,8 @@ def test_do_not_merge_conditional_gates(self): qc.cx(1, 0) qc.x(0) qc.x(1) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.x(0) qc.x(1) qc.cx(0, 1) @@ -664,7 +665,8 @@ def test_do_not_merge_conditional_gates(self): self.assertEqual(qct.count_ops()["clifford"], 2) # Make sure that the condition on the middle gate is not lost - self.assertIsNotNone(qct.data[1].operation.condition) + with self.assertWarns(DeprecationWarning): + self.assertIsNotNone(qct.data[1].operation.condition) def test_collect_with_cliffords(self): """Make sure that collecting Clifford gates and replacing them by Clifford diff --git a/test/python/transpiler/test_collect_2q_blocks.py b/test/python/transpiler/test_collect_2q_blocks.py index 69efc210a9f6..362765101f08 100644 --- a/test/python/transpiler/test_collect_2q_blocks.py +++ b/test/python/transpiler/test_collect_2q_blocks.py @@ -129,11 +129,15 @@ def test_do_not_merge_conditioned_gates(self): qc = QuantumCircuit(qr, cr) qc.p(0.1, 0) - qc.p(0.2, 0).c_if(cr, 0) - qc.p(0.3, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.p(0.2, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.p(0.3, 0).c_if(cr, 0) qc.cx(0, 1) - qc.cx(1, 0).c_if(cr, 0) - qc.cx(0, 1).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(1, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(cr, 1) pass_manager = PassManager() pass_manager.append(Collect2qBlocks()) diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index e2446d03da2a..2d4bc8783764 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -127,7 +127,8 @@ def test_block_with_classical_register(self): if(c0==0) u1(0.25*pi) q[1]; if(c0==0) u2(0.25*pi, 0.25*pi) q[0]; """ - qc = QuantumCircuit.from_qasm_str(qasmstr) + with self.assertWarns(DeprecationWarning): + qc = QuantumCircuit.from_qasm_str(qasmstr) pass_manager = PassManager() pass_manager.append(CollectMultiQBlocks()) @@ -166,11 +167,15 @@ def test_do_not_merge_conditioned_gates(self): qc = QuantumCircuit(qr, cr) qc.p(0.1, 0) - qc.p(0.2, 0).c_if(cr, 0) - qc.p(0.3, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.p(0.2, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.p(0.3, 0).c_if(cr, 0) qc.cx(0, 1) - qc.cx(1, 0).c_if(cr, 0) - qc.cx(0, 1).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(1, 0).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(cr, 1) pass_manager = PassManager() pass_manager.append(CollectMultiQBlocks()) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 88f1d99bef31..15b7f6705787 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -564,7 +564,8 @@ def test_conditional_gates_dont_commute(self): circuit.h(0) circuit.measure(0, 0) circuit.cx(1, 2) - circuit.cx(1, 2).c_if(circuit.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + circuit.cx(1, 2).c_if(circuit.cregs[0], 0) circuit.measure([1, 2], [0, 1]) new_pm = PassManager(CommutativeCancellation()) @@ -677,8 +678,10 @@ def test_basic_classical_wires(self): """Test that transpile runs without internal errors when dealing with commutable operations with classical controls. Regression test for gh-8553.""" original = QuantumCircuit(2, 1) - original.x(0).c_if(original.cregs[0], 0) - original.x(1).c_if(original.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + original.x(0).c_if(original.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + original.x(1).c_if(original.cregs[0], 0) # This transpilation shouldn't change anything, but it should succeed. At one point it was # triggering an internal logic error and crashing. transpiled = PassManager([CommutativeCancellation()]).run(original) diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py index cd800a3bb46f..e9e85c5505f3 100644 --- a/test/python/transpiler/test_commutative_inverse_cancellation.py +++ b/test/python/transpiler/test_commutative_inverse_cancellation.py @@ -13,15 +13,16 @@ """Test transpiler pass that cancels inverse gates while exploiting the commutation relations.""" import unittest +from test import QiskitTestCase # pylint: disable=wrong-import-order + import numpy as np -from ddt import ddt, data +from ddt import data, ddt from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import RZGate, UnitaryGate +from qiskit.quantum_info import Operator from qiskit.transpiler import PassManager from qiskit.transpiler.passes import CommutativeInverseCancellation -from qiskit.quantum_info import Operator -from test import QiskitTestCase # pylint: disable=wrong-import-order @ddt @@ -414,7 +415,8 @@ def test_conditional_gates_dont_commute(self, matrix_based): circuit.h(0) circuit.measure(0, 0) circuit.cx(1, 2) - circuit.cx(1, 2).c_if(circuit.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + circuit.cx(1, 2).c_if(circuit.cregs[0], 0) circuit.measure([1, 2], [0, 1]) passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) @@ -758,25 +760,26 @@ def test_no_cancellation_across_reset(self, matrix_based): self.assertEqual(circuit, new_circuit) @data(False, True) - def test_no_cancellation_across_parameterized_gates(self, matrix_based): - """Test that parameterized gates prevent cancellation. - This test should be modified when inverse and commutativity checking - get improved to handle parameterized gates. - """ + def test_cancellation_across_parameterized_gates(self, matrix_based): + """Test that parameterized gates do not prevent cancellation.""" + theta = Parameter("Theta") circuit = QuantumCircuit(1) circuit.rz(np.pi / 2, 0) - circuit.rz(Parameter("Theta"), 0) + circuit.rz(theta, 0) circuit.rz(-np.pi / 2, 0) + expected_circuit = QuantumCircuit(1) + expected_circuit.rz(theta, 0) + passmanager = PassManager(CommutativeInverseCancellation(matrix_based=matrix_based)) new_circuit = passmanager.run(circuit) - self.assertEqual(circuit, new_circuit) + self.assertEqual(expected_circuit, new_circuit) @data(False, True) def test_parameterized_gates_do_not_cancel(self, matrix_based): """Test that parameterized gates do not cancel. - This test should be modified when inverse and commutativity checking - get improved to handle parameterized gates. + This test should be modified when inverse checking + gets improved to handle parameterized gates. """ gate = RZGate(Parameter("Theta")) diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py index 1984ad1a3dc4..83379a4eecb7 100644 --- a/test/python/transpiler/test_consolidate_blocks.py +++ b/test/python/transpiler/test_consolidate_blocks.py @@ -16,20 +16,28 @@ import unittest import numpy as np - -from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp, Gate -from qiskit.circuit.library import U2Gate, SwapGate, CXGate, CZGate, UnitaryGate +from ddt import ddt, data + +from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp, Gate, Parameter +from qiskit.circuit.library import ( + U2Gate, + SwapGate, + CXGate, + CZGate, + UnitaryGate, + SXGate, + XGate, + RZGate, +) from qiskit.converters import circuit_to_dag -from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.operators.measures import process_fidelity -from qiskit.transpiler import PassManager -from qiskit.transpiler import Target -from qiskit.transpiler.passes import Collect1qRuns -from qiskit.transpiler.passes import Collect2qBlocks +from qiskit.transpiler import PassManager, Target, generate_preset_pass_manager +from qiskit.transpiler.passes import ConsolidateBlocks, Collect1qRuns, Collect2qBlocks from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestConsolidateBlocks(QiskitTestCase): """ Tests to verify that consolidating blocks of gates into unitaries @@ -328,7 +336,8 @@ def test_classical_conditions_maintained(self): This issue was raised in #2752 """ qc = QuantumCircuit(1, 1) - qc.h(0).c_if(qc.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(qc.cregs[0], 1) qc.measure(0, 0) pass_manager = PassManager() @@ -570,6 +579,93 @@ def __init__(self): self.assertEqual(res, qc) + @data(2, 3) + def test_no_kak_gates_in_preset_pm(self, opt_level): + """Test correct initialization of ConsolidateBlocks pass when kak_gates aren't found. + Reproduces https://github.com/Qiskit/qiskit/issues/13438.""" + + qc = QuantumCircuit(2) + qc.cz(0, 1) + qc.sx([0, 1]) + qc.cz(0, 1) + + ref_pm = generate_preset_pass_manager( + optimization_level=1, basis_gates=["rz", "rzz", "sx", "x", "rx"] + ) + ref_tqc = ref_pm.run(qc) + pm = generate_preset_pass_manager( + optimization_level=opt_level, basis_gates=["rz", "rzz", "sx", "x", "rx"] + ) + tqc = pm.run(qc) + self.assertEqual(ref_tqc, tqc) + + def test_non_cx_basis_gate(self): + """Test a non-cx kak gate is consolidated correctly.""" + qc = QuantumCircuit(2) + qc.cz(0, 1) + qc.x(0) + qc.h(1) + qc.z(1) + qc.t(1) + qc.h(0) + qc.t(0) + qc.cz(1, 0) + qc.sx(0) + qc.sx(1) + qc.cz(0, 1) + qc.sx(0) + qc.sx(1) + qc.cz(1, 0) + qc.x(0) + qc.h(1) + qc.z(1) + qc.t(1) + qc.h(0) + qc.t(0) + qc.cz(0, 1) + + consolidate_pass = ConsolidateBlocks(basis_gates=["sx", "x", "rz", "cz"]) + res = consolidate_pass(qc) + self.assertEqual({"unitary": 1}, res.count_ops()) + self.assertEqual(Operator.from_circuit(qc), Operator(res.data[0].operation.params[0])) + + def test_non_cx_target(self): + """Test a non-cx kak gate is consolidated correctly.""" + qc = QuantumCircuit(2) + qc.cz(0, 1) + qc.x(0) + qc.h(1) + qc.z(1) + qc.t(1) + qc.h(0) + qc.t(0) + qc.cz(1, 0) + qc.sx(0) + qc.sx(1) + qc.cz(0, 1) + qc.sx(0) + qc.sx(1) + qc.cz(1, 0) + qc.x(0) + qc.h(1) + qc.z(1) + qc.t(1) + qc.h(0) + qc.t(0) + qc.cz(0, 1) + + phi = Parameter("phi") + target = Target(num_qubits=2) + target.add_instruction(SXGate(), {(0,): None, (1,): None}) + target.add_instruction(XGate(), {(0,): None, (1,): None}) + target.add_instruction(RZGate(phi), {(0,): None, (1,): None}) + target.add_instruction(CZGate(), {(0, 1): None, (1, 0): None}) + + consolidate_pass = ConsolidateBlocks(target=target) + res = consolidate_pass(qc) + self.assertEqual({"unitary": 1}, res.count_ops()) + self.assertEqual(Operator.from_circuit(qc), Operator(res.data[0].operation.params[0])) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_convert_conditions_to_if_ops.py b/test/python/transpiler/test_convert_conditions_to_if_ops.py index 799a71163590..d1eadda0d92d 100644 --- a/test/python/transpiler/test_convert_conditions_to_if_ops.py +++ b/test/python/transpiler/test_convert_conditions_to_if_ops.py @@ -26,13 +26,17 @@ def test_simple_loose_bits(self): base = QuantumCircuit(bits) base.h(0) - base.x(0).c_if(0, 1) - base.z(1).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + base.z(1).c_if(1, 0) base.measure(0, 0) base.measure(1, 1) base.h(0) - base.x(0).c_if(0, 1) - base.cx(0, 1).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + base.cx(0, 1).c_if(1, 0) expected = QuantumCircuit(bits) expected.h(0) @@ -48,8 +52,8 @@ def test_simple_loose_bits(self): with expected.if_test((expected.clbits[1], False)): expected.cx(0, 1) expected = canonicalize_control_flow(expected) - - output = PassManager([ConvertConditionsToIfOps()]).run(base) + with self.assertWarns(DeprecationWarning): + output = PassManager([ConvertConditionsToIfOps()]).run(base) self.assertEqual(output, expected) def test_simple_registers(self): @@ -58,13 +62,17 @@ def test_simple_registers(self): base = QuantumCircuit(*registers) base.h(0) - base.x(0).c_if(base.cregs[0], 1) - base.z(1).c_if(base.cregs[1], 0) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(base.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + base.z(1).c_if(base.cregs[1], 0) base.measure(0, 0) base.measure(1, 2) base.h(0) - base.x(0).c_if(base.cregs[0], 1) - base.cx(0, 1).c_if(base.cregs[1], 0) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(base.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + base.cx(0, 1).c_if(base.cregs[1], 0) expected = QuantumCircuit(*registers) expected.h(0) @@ -81,7 +89,8 @@ def test_simple_registers(self): expected.cx(0, 1) expected = canonicalize_control_flow(expected) - output = PassManager([ConvertConditionsToIfOps()]).run(base) + with self.assertWarns(DeprecationWarning): + output = PassManager([ConvertConditionsToIfOps()]).run(base) self.assertEqual(output, expected) def test_nested_control_flow(self): @@ -91,14 +100,18 @@ def test_nested_control_flow(self): registers = [QuantumRegister(3), ClassicalRegister(2)] base = QuantumCircuit(*registers, bits) - base.x(0).c_if(bits[0], False) + with self.assertWarns(DeprecationWarning): + base.x(0).c_if(bits[0], False) with base.if_test((base.cregs[0], 0)) as else_: - base.z(1).c_if(bits[0], False) + with self.assertWarns(DeprecationWarning): + base.z(1).c_if(bits[0], False) with else_: - base.z(1).c_if(base.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + base.z(1).c_if(base.cregs[0], 1) with base.for_loop(range(2)): with base.while_loop((base.cregs[0], 1)): - base.cx(1, 2).c_if(base.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + base.cx(1, 2).c_if(base.cregs[0], 1) base = canonicalize_control_flow(base) expected = QuantumCircuit(*registers, bits) @@ -115,8 +128,8 @@ def test_nested_control_flow(self): with expected.if_test((expected.cregs[0], 1)): expected.cx(1, 2) expected = canonicalize_control_flow(expected) - - output = PassManager([ConvertConditionsToIfOps()]).run(base) + with self.assertWarns(DeprecationWarning): + output = PassManager([ConvertConditionsToIfOps()]).run(base) self.assertEqual(output, expected) def test_no_op(self): @@ -135,5 +148,6 @@ def test_no_op(self): with base.while_loop((base.cregs[0], 1)): base.cx(1, 2) base = canonicalize_control_flow(base) - output = PassManager([ConvertConditionsToIfOps()]).run(base) + with self.assertWarns(DeprecationWarning): + output = PassManager([ConvertConditionsToIfOps()]).run(base) self.assertEqual(output, base) diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 64f08ec52682..64a9b97440ba 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -117,15 +117,19 @@ def test_decompose_conditional(self): qr = QuantumRegister(1, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr).c_if(cr, 1) - circuit.x(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr).c_if(cr, 1) dag = circuit_to_dag(circuit) pass_ = Decompose(HGate) after_dag = pass_.run(dag) ref_circuit = QuantumCircuit(qr, cr) - ref_circuit.append(U2Gate(0, pi), [qr[0]]).c_if(cr, 1) - ref_circuit.x(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U2Gate(0, pi), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.x(qr).c_if(cr, 1) ref_dag = circuit_to_dag(ref_circuit) self.assertEqual(after_dag, ref_dag) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 8f876dd261d7..0f279e4bb8c5 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -74,8 +74,11 @@ def test_rzx_number_native_weyl_decomposition(self): circuit.cx(qr[0], qr[1]) unitary_circuit = qi.Operator(circuit).data - - after = EchoRZXWeylDecomposition(self.inst_map)(circuit) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + after = EchoRZXWeylDecomposition(self.inst_map)(circuit) unitary_after = qi.Operator(after).data @@ -97,11 +100,19 @@ def test_h_number_non_native_weyl_decomposition_1(self): circuit_non_native.rzz(theta, qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -127,11 +138,19 @@ def test_h_number_non_native_weyl_decomposition_2(self): circuit_non_native.swap(qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -166,7 +185,11 @@ def test_weyl_decomposition_gate_angles(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_after = circuit_to_dag(after) @@ -221,7 +244,11 @@ def test_weyl_unitaries_random_circuit(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) unitary_after = qi.Operator(after).data diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py index 6f3051cec0d6..edef139af2ee 100644 --- a/test/python/transpiler/test_elide_permutations.py +++ b/test/python/transpiler/test_elide_permutations.py @@ -180,7 +180,8 @@ 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) + with self.assertWarns(DeprecationWarning): + qc.swap(0, 1).c_if(qc.clbits[0], 0) qc.cx(0, 1) res = self.swap_pass(qc) self.assertEqual(res, qc) diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py index c22ad4d58b41..41734d1ca8ee 100644 --- a/test/python/transpiler/test_gate_direction.py +++ b/test/python/transpiler/test_gate_direction.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 CX Direction pass""" +"""Test the Gate Direction pass""" import unittest from math import pi @@ -65,9 +65,9 @@ def test_direction_error(self): """The mapping cannot be fixed by direction mapper qr0:--------- - qr1:---(+)--- + qr1:----.----- | - qr2:----.---- + qr2:---(+)---- CouplingMap map: [2] <- [0] -> [1] """ @@ -84,9 +84,9 @@ def test_direction_error(self): def test_direction_correct(self): """The CX is in the right direction - qr0:---(+)--- + qr0:----.---- | - qr1:----.---- + qr1:---(+)---- CouplingMap map: [0] -> [1] """ @@ -103,9 +103,9 @@ def test_direction_correct(self): def test_multi_register(self): """The CX is in the right direction - qr0:---(+)--- + qr0:----.----- | - qr1:----.---- + qr1:---(+)---- CouplingMap map: [0] -> [1] """ @@ -123,15 +123,15 @@ def test_multi_register(self): def test_direction_flip(self): """Flip a CX - qr0:----.---- + qr0:---(+)--- | - qr1:---(+)--- + qr1:----.---- CouplingMap map: [0] -> [1] - qr0:-[H]-(+)-[H]-- + qr0:-[H]--.--[H]-- | - qr1:-[H]--.--[H]-- + qr1:-[H]-(+)--[H]-- """ qr = QuantumRegister(2, "qr") circuit = QuantumCircuit(qr) @@ -243,8 +243,10 @@ def test_preserves_conditions(self): cr = ClassicalRegister(1, "c") circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[0], qr[1]).c_if(cr, 0) - circuit.cx(qr[1], qr[0]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[0], qr[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[1], qr[0]).c_if(cr, 0) circuit.cx(qr[0], qr[1]) circuit.cx(qr[1], qr[0]) @@ -261,16 +263,22 @@ def test_preserves_conditions(self): # c: 1/╡ 0x0 ╞╡ 0x0 ╞╡ 0x0 ╞╡ 0x0 ╞╡ 0x0 ╞╡ 0x0 ╞════════════════════ # └─────┘└─────┘└─────┘└─────┘└─────┘└─────┘ expected = QuantumCircuit(qr, cr) - expected.cx(qr[0], qr[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.cx(qr[0], qr[1]).c_if(cr, 0) # Order of H gates is important because DAG comparison will consider # different conditional order on a creg to be a different circuit. # See https://github.com/Qiskit/qiskit-terra/issues/3164 - expected.h(qr[1]).c_if(cr, 0) - expected.h(qr[0]).c_if(cr, 0) - expected.cx(qr[0], qr[1]).c_if(cr, 0) - expected.h(qr[1]).c_if(cr, 0) - expected.h(qr[0]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.h(qr[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.h(qr[0]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.cx(qr[0], qr[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.h(qr[1]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + expected.h(qr[0]).c_if(cr, 0) expected.cx(qr[0], qr[1]) expected.h(qr[1]) @@ -484,7 +492,7 @@ def test_target_cannot_flip_message(self): circuit.append(gate, (1, 0)) pass_ = GateDirection(None, target) - with self.assertRaisesRegex(TranspilerError, "'my_2q_gate' would be supported.*"): + with self.assertRaisesRegex(TranspilerError, "my_2q_gate would be supported.*"): pass_(circuit) def test_target_cannot_flip_message_calibrated(self): @@ -500,7 +508,7 @@ def test_target_cannot_flip_message_calibrated(self): circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) pass_ = GateDirection(None, target) - with self.assertRaisesRegex(TranspilerError, "'my_2q_gate' would be supported.*"): + with self.assertRaisesRegex(TranspilerError, "my_2q_gate would be supported.*"): pass_(circuit) def test_target_unknown_gate_message(self): @@ -514,7 +522,7 @@ def test_target_unknown_gate_message(self): circuit.append(gate, (0, 1)) pass_ = GateDirection(None, target) - with self.assertRaisesRegex(TranspilerError, "'my_2q_gate'.*not supported on qubits .*"): + with self.assertRaisesRegex(TranspilerError, "my_2q_gate.*not supported on qubits .*"): pass_(circuit) def test_allows_calibrated_gates_coupling_map(self): diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index a7dd806e63e9..11aa8501afd0 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -45,9 +45,12 @@ QFTGate, IGate, MCXGate, + SGate, + QAOAAnsatz, ) -from qiskit.circuit.library.generalized_gates import LinearFunction -from qiskit.quantum_info import Clifford, Operator, Statevector +from qiskit.circuit.library import LinearFunction, PauliEvolutionGate +from qiskit.quantum_info import Clifford, Operator, Statevector, SparsePauliOp +from qiskit.synthesis.evolution import synth_pauli_network_rustiq from qiskit.synthesis.linear import random_invertible_binary_matrix from qiskit.compiler import transpile from qiskit.exceptions import QiskitError @@ -662,6 +665,18 @@ def test_synth_fails_definition_exists(self): out = hls(circuit) self.assertEqual(out.count_ops(), {"u": 1}) + def test_both_basis_gates_and_plugin_specified(self): + """Test that a gate is not synthesized when it belongs to basis_gates, + regardless of whether there is a plugin method available. + + See: https://github.com/Qiskit/qiskit/issues/13412 for more + details. + """ + qc = QAOAAnsatz(SparsePauliOp("Z"), initial_state=QuantumCircuit(1)) + pm = PassManager([HighLevelSynthesis(basis_gates=["PauliEvolution"])]) + qct = pm.run(qc) + self.assertEqual(qct.count_ops()["PauliEvolution"], 2) + class TestPMHSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the PMHSynthesisLinearFunction plugin for synthesizing linear functions.""" @@ -1489,6 +1504,137 @@ def test_transpile_power_high_level_object(self): for op in ops: self.assertIn(op, ["u", "cx", "ecr", "measure"]) + def test_simple_circuit(self): + """Test HLS on a simple circuit.""" + qc = QuantumCircuit(3) + qc.cz(1, 2) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_simple_circuit2(self): + """Test HLS on a simple circuit.""" + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 3) + qc.h(5) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u", "h"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def(self): + """Test recursive synthesis of the definition circuit.""" + inner = QuantumCircuit(2) + inner.cz(0, 1) + qc = QuantumCircuit(3) + qc.append(inner.to_gate(), [0, 2]) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def2(self): + """Test recursive synthesis of the definition circuit.""" + inner1 = QuantumCircuit(2) + inner1.cz(0, 1) + qc = QuantumCircuit(4) + qc.append(inner1.to_instruction(), [2, 3]) + pass_ = HighLevelSynthesis(basis_gates=["cz", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def3(self): + """Test recursive synthesis of the definition circuit.""" + inner2 = QuantumCircuit(2) + inner2.h(0) + inner2.cx(0, 1) + + inner1 = QuantumCircuit(4) + inner1.cz(0, 1) + inner1.append(inner2.to_instruction(), [0, 2]) + + qc = QuantumCircuit(6) + qc.h(1) + qc.h(2) + qc.cz(1, 2) + qc.append(inner1.to_instruction(), [2, 0, 4, 3]) + qc.h(2) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_mcx(self): + """Test synthesis with plugins.""" + qc = QuantumCircuit(10) + qc.mcx([3, 4, 5, 6, 7], 2) + basis_gates = ["u", "cx"] + qct = HighLevelSynthesis(basis_gates=basis_gates)(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_circuit_with_mcx_def(self): + """Test synthesis where the plugin is called within the recursive call + on the definition.""" + circuit = QuantumCircuit(6) + circuit.mcx([0, 1, 2, 3, 4], 5) + custom_gate = circuit.to_gate() + qc = QuantumCircuit(10) + qc.append(custom_gate, [3, 4, 5, 6, 7, 2]) + basis_gates = ["u", "cx"] + qct = HighLevelSynthesis(basis_gates=basis_gates)(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_circuit_with_mcx_def_rec(self): + """Test synthesis where the plugin is called within the recursive call + on the definition.""" + inner2 = QuantumCircuit(6) + inner2.mcx([0, 1, 2, 3, 4], 5) + inner1 = QuantumCircuit(7) + inner1.append(inner2.to_gate(), [1, 2, 3, 4, 5, 6]) + qc = QuantumCircuit(10) + qc.append(inner1.to_gate(), [2, 3, 4, 5, 6, 7, 8]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_annotated_gate(self): + """Test synthesis with annotated gate.""" + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(SGate().control(3, annotated=True), [0, 1, 8, 9]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_annotated_circuit(self): + """Test synthesis with annotated custom gate.""" + circ = QuantumCircuit(2) + circ.h(0) + circ.cy(0, 1) + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(circ.to_gate().control(3, annotated=True), [2, 0, 3, 7, 8]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_annotated_rec(self): + """Test synthesis with annotated custom gates and recursion.""" + inner2 = QuantumCircuit(2) + inner2.h(0) + inner2.cy(0, 1) + inner1 = QuantumCircuit(5) + inner1.h(1) + inner1.append(inner2.to_gate().control(2, annotated=True), [1, 2, 3, 4]) + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(inner1.to_gate().control(3, annotated=True), [9, 8, 7, 6, 5, 4, 3, 2]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + class TestUnrollerCompatability(QiskitTestCase): """Tests backward compatibility with the UnrollCustomDefinitions pass. @@ -1588,9 +1734,12 @@ def test_unroll_1q_chain_conditional(self): circuit.rz(0.3, qr) circuit.rx(0.1, qr) circuit.measure(qr, cr) - circuit.x(qr).c_if(cr, 1) - circuit.y(qr).c_if(cr, 1) - circuit.z(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.y(qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.z(qr).c_if(cr, 1) dag = circuit_to_dag(circuit) pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, basis_gates=["u1", "u2", "u3"]) dag = pass_.run(dag) @@ -1621,9 +1770,12 @@ def test_unroll_1q_chain_conditional(self): ref_circuit.append(U1Gate(0.3), [qr[0]]) ref_circuit.append(U3Gate(0.1, -np.pi / 2, np.pi / 2), [qr[0]]) ref_circuit.measure(qr[0], cr[0]) - ref_circuit.append(U3Gate(np.pi, 0, np.pi), [qr[0]]).c_if(cr, 1) - ref_circuit.append(U3Gate(np.pi, np.pi / 2, np.pi / 2), [qr[0]]).c_if(cr, 1) - ref_circuit.append(U1Gate(np.pi), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U3Gate(np.pi, 0, np.pi), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U3Gate(np.pi, np.pi / 2, np.pi / 2), [qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + ref_circuit.append(U1Gate(np.pi), [qr[0]]).c_if(cr, 1) ref_dag = circuit_to_dag(ref_circuit) self.assertEqual(unrolled_dag, ref_dag) @@ -2469,5 +2621,152 @@ def test_annotated_mcx(self): self.assertEqual(Operator(qc), Operator(qct)) +@ddt +class TestPauliEvolutionSynthesisPlugins(QiskitTestCase): + """Tests related to plugins for PauliEvolutionGate.""" + + def test_supported_names(self): + """Test that "default" and "rustiq" plugins do exist.""" + supported_plugin_names = high_level_synthesis_plugin_names("PauliEvolution") + self.assertIn("default", supported_plugin_names) + self.assertIn("rustiq", supported_plugin_names) + + @data("default", "rustiq") + def test_correctness(self, plugin_name): + """Test that plugins return the correct Operator.""" + op = SparsePauliOp(["XXX", "YYY", "IZZ", "XZY"], [1, 2, 3, 4]) + qc = QuantumCircuit(6) + qc.append(PauliEvolutionGate(op), [1, 2, 4]) + hls_config = HLSConfig(PauliEvolution=[plugin_name]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + self.assertEqual(count_rotation_gates(qct), 4) + self.assertEqual(Operator(qc), Operator(qct)) + + @data("default", "rustiq") + def test_trivial_rotations(self, plugin_name): + """Test that plugins return the correct Operator in the presence of + trivial (all-I) rotations. + """ + op = SparsePauliOp(["III", "XZY", "III", "III"], [1, 2, 3, 4]) + qc = QuantumCircuit(6) + qc.append(PauliEvolutionGate(op), [1, 2, 4]) + hls_config = HLSConfig(PauliEvolution=[plugin_name]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + self.assertEqual(Operator(qc), Operator(qct)) + self.assertEqual(count_rotation_gates(qct), 1) + + def test_rustiq_upto_options(self): + """Test non-default Rustiq options upto_phase and upto_clifford.""" + op = SparsePauliOp(["XXXX", "YYYY", "ZZZZ"], coeffs=[1, 2, 3]) + qc = QuantumCircuit(6) + qc.append(PauliEvolutionGate(op), [1, 2, 3, 4]) + + # These calls to Rustiq are deterministic. + # On the one hand, we may need to change these tests if we switch + # to a newer version of Rustiq that implements different heuristics. + # On the other hand, these tests serve to show that the options + # have the desired effect of reducing the number of CX-gates. + with self.subTest("default_options"): + hls_config = HLSConfig(PauliEvolution=[("rustiq", {"upto_phase": False})]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + cnt_ops = qct.count_ops() + self.assertEqual(count_rotation_gates(qct), 3) + self.assertEqual(cnt_ops["cx"], 10) + with self.subTest("upto_phase"): + hls_config = HLSConfig(PauliEvolution=[("rustiq", {"upto_phase": True})]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + cnt_ops = qct.count_ops() + self.assertEqual(count_rotation_gates(qct), 3) + self.assertEqual(cnt_ops["cx"], 9) + with self.subTest("upto_clifford"): + hls_config = HLSConfig(PauliEvolution=[("rustiq", {"upto_clifford": True})]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + cnt_ops = qct.count_ops() + self.assertEqual(count_rotation_gates(qct), 3) + self.assertEqual(cnt_ops["cx"], 5) + + def test_rustiq_preserve_order(self): + """Test non-default Rustiq option preserve_order.""" + op = SparsePauliOp(["IXX", "YYI", "IXX", "YYI", "IXX", "YYI"]) + qc = QuantumCircuit(3) + qc.append(PauliEvolutionGate(op), [0, 1, 2]) + with self.subTest("preserve_order_is_true"): + hls_config = HLSConfig(PauliEvolution=[("rustiq", {"preserve_order": True})]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + cnt_ops = qct.count_ops() + self.assertEqual(count_rotation_gates(qct), 6) + self.assertEqual(cnt_ops["cx"], 16) + with self.subTest("preserve_order_is_false"): + hls_config = HLSConfig(PauliEvolution=[("rustiq", {"preserve_order": False})]) + hls_pass = HighLevelSynthesis(hls_config=hls_config) + qct = hls_pass(qc) + cnt_ops = qct.count_ops() + self.assertEqual(count_rotation_gates(qct), 6) + self.assertEqual(cnt_ops["cx"], 4) + + def test_rustiq_upto_phase(self): + """Check that Rustiq synthesis with ``upto_phase=True`` produces a correct + circuit up to the global phase. + """ + # On this example Rustiq with the option "upto_phase=True" does produce a circuit + # with a different global phase. + op = SparsePauliOp( + [ + "IIII", + "XXII", + "XIXI", + "XIIX", + "YYII", + "YIYI", + "YIIY", + "ZZII", + "ZIZI", + "ZIIZ", + "IXIX", + "IYIY", + "IZIZ", + ] + ) + qc = QuantumCircuit(4) + qc.append(PauliEvolutionGate(op), [0, 1, 2, 3]) + default_config = HLSConfig(PauliEvolution=["default"]) + qct_default = HighLevelSynthesis(hls_config=default_config)(qc) + rustiq_config = HLSConfig(PauliEvolution=[("rustiq", {"upto_phase": True})]) + qct_rustiq = HighLevelSynthesis(hls_config=rustiq_config)(qc) + self.assertEqual(count_rotation_gates(qct_default), 12) + self.assertEqual(count_rotation_gates(qct_rustiq), 12) + self.assertTrue(Operator(qct_default).equiv(Operator(qct_rustiq))) + + def test_rustiq_with_parameterized_angles(self): + """Test Rustiq's synthesis with parameterized angles.""" + alpha = Parameter("alpha") + beta = Parameter("beta") + pauli_network = [("XXX", [0, 1, 2], alpha), ("Y", [1], beta)] + qct = synth_pauli_network_rustiq( + num_qubits=4, pauli_network=pauli_network, upto_clifford=True + ) + self.assertEqual(count_rotation_gates(qct), 2) + self.assertEqual(set(qct.parameters), {alpha, beta}) + + +def count_rotation_gates(qc: QuantumCircuit): + """Return the number of rotation gates in a quantum circuit.""" + ops = qc.count_ops() + return ( + ops.get("rx", 0) + + ops.get("ry", 0) + + ops.get("rz", 0) + + ops.get("rxx", 0) + + ops.get("ryy", 0) + + ops.get("rzz", 0) + ) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_linear_functions_passes.py b/test/python/transpiler/test_linear_functions_passes.py index 1d157c437155..39997ad816ef 100644 --- a/test/python/transpiler/test_linear_functions_passes.py +++ b/test/python/transpiler/test_linear_functions_passes.py @@ -615,7 +615,8 @@ def test_do_not_merge_conditional_gates(self): qc = QuantumCircuit(2, 1) qc.cx(1, 0) qc.swap(1, 0) - qc.cx(0, 1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 1) qc.cx(0, 1) qc.cx(1, 0) @@ -625,7 +626,8 @@ def test_do_not_merge_conditional_gates(self): self.assertEqual(qct.count_ops()["linear_function"], 2) # Make sure that the condition on the middle gate is not lost - self.assertIsNotNone(qct.data[1].operation.condition) + with self.assertWarns(DeprecationWarning): + self.assertIsNotNone(qct.data[1].operation.condition) @combine(do_commutative_analysis=[False, True]) def test_split_layers(self, do_commutative_analysis): diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 06aab474d611..fb043ff9d950 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -212,16 +212,18 @@ def test_ignores_conditional_rotations(self, basis): qr = QuantumRegister(1, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.p(0.1, qr).c_if(cr, 1) - circuit.p(0.2, qr).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + circuit.p(0.1, qr).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.p(0.2, qr).c_if(cr, 3) circuit.p(0.3, qr) circuit.p(0.4, qr) passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) - - self.assertTrue(Operator(circuit).equiv(Operator(result))) + with self.assertWarns(DeprecationWarning): + self.assertTrue(Operator(circuit).equiv(Operator(result))) @ddt.data( ["cx", "u3"], diff --git a/test/python/transpiler/test_optimize_1q_gates.py b/test/python/transpiler/test_optimize_1q_gates.py index e5483dd47499..bfa578826510 100644 --- a/test/python/transpiler/test_optimize_1q_gates.py +++ b/test/python/transpiler/test_optimize_1q_gates.py @@ -162,15 +162,19 @@ def test_ignores_conditional_rotations(self): qr = QuantumRegister(1, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(U1Gate(0.1), [qr]).c_if(cr, 1) - circuit.append(U1Gate(0.2), [qr]).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0.1), [qr]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0.2), [qr]).c_if(cr, 3) circuit.append(U1Gate(0.3), [qr]) circuit.append(U1Gate(0.4), [qr]) dag = circuit_to_dag(circuit) expected = QuantumCircuit(qr, cr) - expected.append(U1Gate(0.1), [qr]).c_if(cr, 1) - expected.append(U1Gate(0.2), [qr]).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + expected.append(U1Gate(0.1), [qr]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + expected.append(U1Gate(0.2), [qr]).c_if(cr, 3) expected.append(U1Gate(0.7), [qr]) pass_ = Optimize1qGates() @@ -190,15 +194,19 @@ def test_ignores_conditional_rotations_phase_gates(self): qr = QuantumRegister(1, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(PhaseGate(0.1), [qr]).c_if(cr, 1) - circuit.append(PhaseGate(0.2), [qr]).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + circuit.append(PhaseGate(0.1), [qr]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(PhaseGate(0.2), [qr]).c_if(cr, 3) circuit.append(PhaseGate(0.3), [qr]) circuit.append(PhaseGate(0.4), [qr]) dag = circuit_to_dag(circuit) expected = QuantumCircuit(qr, cr) - expected.append(PhaseGate(0.1), [qr]).c_if(cr, 1) - expected.append(PhaseGate(0.2), [qr]).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + expected.append(PhaseGate(0.1), [qr]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + expected.append(PhaseGate(0.2), [qr]).c_if(cr, 3) expected.append(PhaseGate(0.7), [qr]) pass_ = Optimize1qGates(["p", "u2", "u", "cx", "id"]) diff --git a/test/python/transpiler/test_optimize_swap_before_measure.py b/test/python/transpiler/test_optimize_swap_before_measure.py index 6d7c06f410a7..10455318bd09 100644 --- a/test/python/transpiler/test_optimize_swap_before_measure.py +++ b/test/python/transpiler/test_optimize_swap_before_measure.py @@ -363,12 +363,14 @@ def test_no_optimize_swap_with_condition(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.swap(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.swap(qr[0], qr[1]).c_if(cr, 1) circuit.measure(qr[0], cr[0]) dag = circuit_to_dag(circuit) expected = QuantumCircuit(qr, cr) - expected.swap(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + expected.swap(qr[0], qr[1]).c_if(cr, 1) expected.measure(qr[0], cr[0]) pass_ = OptimizeSwapBeforeMeasure() diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 00fd8944061c..1ecee97a5ed9 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -22,6 +22,7 @@ from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp +from qiskit.circuit.library import quantum_volume from qiskit.compiler import transpile from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError, Target from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate @@ -36,7 +37,7 @@ from qiskit.quantum_info import random_unitary from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.preset_passmanagers import level0, level1, level2, level3 -from qiskit.transpiler.passes import Collect2qBlocks, GatesInBasis +from qiskit.transpiler.passes import ConsolidateBlocks, GatesInBasis from qiskit.transpiler.preset_passmanagers.builtin_plugins import OptimizationPassManager from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -161,7 +162,11 @@ def test_unitary_is_preserved_if_in_basis(self, level): qc = QuantumCircuit(2) qc.unitary(random_unitary(4, seed=42), [0, 1]) qc.measure_all() - result = transpile(qc, basis_gates=["cx", "u", "unitary"], optimization_level=level) + with self.assertWarnsRegex( + DeprecationWarning, + "Providing non-standard gates \\(unitary\\) through the ``basis_gates`` argument", + ): + result = transpile(qc, basis_gates=["cx", "u", "unitary"], optimization_level=level) self.assertEqual(result, qc) @combine(level=[0, 1, 2, 3], name="level{level}") @@ -179,12 +184,16 @@ def test_unitary_is_preserved_if_in_basis_synthesis_translation(self, level): qc = QuantumCircuit(2) qc.unitary(random_unitary(4, seed=424242), [0, 1]) qc.measure_all() - result = transpile( - qc, - basis_gates=["cx", "u", "unitary"], - optimization_level=level, - translation_method="synthesis", - ) + with self.assertWarnsRegex( + DeprecationWarning, + "Providing non-standard gates \\(unitary\\) through the ``basis_gates`` argument", + ): + result = transpile( + qc, + basis_gates=["cx", "u", "unitary"], + optimization_level=level, + translation_method="synthesis", + ) self.assertEqual(result, qc) @combine(level=[0, 1, 2, 3], name="level{level}") @@ -262,16 +271,16 @@ def test_unroll_only_if_not_gates_in_basis(self): ) qv_circuit = QuantumVolume(3) gates_in_basis_true_count = 0 - collect_2q_blocks_count = 0 + consolidate_blocks_count = 0 # pylint: disable=unused-argument def counting_callback_func(pass_, dag, time, property_set, count): nonlocal gates_in_basis_true_count - nonlocal collect_2q_blocks_count + nonlocal consolidate_blocks_count if isinstance(pass_, GatesInBasis) and property_set["all_gates_in_basis"]: gates_in_basis_true_count += 1 - if isinstance(pass_, Collect2qBlocks): - collect_2q_blocks_count += 1 + if isinstance(pass_, ConsolidateBlocks): + consolidate_blocks_count += 1 transpile( qv_circuit, @@ -280,7 +289,7 @@ def counting_callback_func(pass_, dag, time, property_set, count): callback=counting_callback_func, translation_method="synthesis", ) - self.assertEqual(gates_in_basis_true_count + 2, collect_2q_blocks_count) + self.assertEqual(gates_in_basis_true_count + 2, consolidate_blocks_count) @ddt @@ -335,6 +344,24 @@ def test_v1(self, circuit, level, backend): ) self.assertIsInstance(result, QuantumCircuit) + @data(0, 1, 2, 3) + def test_quantum_volume_function_transpile(self, opt_level): + """Test quantum_volume transpilation.""" + qc = quantum_volume(10, 10, 12345) + backend = GenericBackendV2( + num_qubits=100, + basis_gates=["cz", "rz", "sx", "x", "id"], + coupling_map=CouplingMap.from_grid(10, 10), + ) + pm = generate_preset_pass_manager(opt_level, backend) + res = pm.run(qc) + for inst in res.data: + self.assertTrue( + backend.target.instruction_supported( + inst.operation.name, qargs=tuple(res.find_bit(x).index for x in inst.qubits) + ) + ) + @ddt class TestPassesInspection(QiskitTestCase): @@ -1214,7 +1241,8 @@ def test_optimization_condition(self, level): qr = QuantumRegister(2) cr = ClassicalRegister(1) qc = QuantumCircuit(qr, cr) - qc.cx(0, 1).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(cr, 1) backend = GenericBackendV2( num_qubits=20, coupling_map=TOKYO_CMAP, @@ -1227,7 +1255,8 @@ def test_optimization_condition(self, level): def test_input_dag_copy(self): """Test substitute_node_with_dag input_dag copy on condition""" qc = QuantumCircuit(2, 1) - qc.cx(0, 1).c_if(qc.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(qc.cregs[0], 1) qc.cx(1, 0) circ = transpile(qc, basis_gates=["u3", "cz"]) self.assertIsInstance(circ, QuantumCircuit) @@ -1284,7 +1313,10 @@ class TestGeneratePresetPassManagers(QiskitTestCase): @data(0, 1, 2, 3) def test_with_backend(self, optimization_level): """Test a passmanager is constructed when only a backend and optimization level.""" - with self.assertWarns(DeprecationWarning): + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex=r"qiskit\.providers\.models\.backendconfiguration\.GateConfig`", + ): backend = Fake20QV1() with self.assertWarnsRegex( DeprecationWarning, @@ -1316,7 +1348,10 @@ def test_default_optimization_level_target_first_pos_arg(self): def test_with_no_backend(self, optimization_level): """Test a passmanager is constructed with no backend and optimization level.""" target = GenericBackendV2(num_qubits=7, coupling_map=LAGOS_CMAP, seed=42) - with self.assertWarns(DeprecationWarning): + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): pm = generate_preset_pass_manager( optimization_level, coupling_map=target.coupling_map, @@ -1727,3 +1762,40 @@ def test_unsupported_targets_raise(self, optimization_level): pass with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): transpile(qc, target=target, optimization_level=optimization_level) + + @data(0, 1, 2, 3) + def test_custom_basis_gates_raise(self, optimization_level): + """Test that trying to provide a list of custom basis gates to generate_preset_pass_manager + raises a deprecation warning.""" + + with self.subTest(msg="no warning"): + # check that the warning isn't raised if the basis gates aren't custom + basis_gates = ["x", "cx"] + _ = generate_preset_pass_manager( + optimization_level=optimization_level, basis_gates=basis_gates + ) + + with self.subTest(msg="warning only basis gates"): + # check that the warning is raised if they are custom + basis_gates = ["my_gate"] + with self.assertWarnsRegex( + DeprecationWarning, + "Providing non-standard gates \\(my_gate\\) through the ``basis_gates`` argument", + ): + _ = generate_preset_pass_manager( + optimization_level=optimization_level, basis_gates=basis_gates + ) + + with self.subTest(msg="no warning custom basis gates in backend"): + # check that the warning is not raised if a loose custom gate is found in the backend + backend = GenericBackendV2(num_qubits=2) + gate = Gate(name="my_gate", num_qubits=1, params=[]) + backend.target.add_instruction(gate) + self.assertEqual( + backend.operation_names, + ["cx", "id", "rz", "sx", "x", "reset", "delay", "measure", "my_gate"], + ) + basis_gates = ["my_gate"] + _ = generate_preset_pass_manager( + optimization_level=optimization_level, basis_gates=basis_gates, backend=backend + ) diff --git a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py index 474a586448b8..f560aa2c4855 100644 --- a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py +++ b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py @@ -550,7 +550,8 @@ def test_do_not_optimize_with_conditional(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.rz(0.1, qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.rz(0.1, qr[1]).c_if(cr, 1) circuit.barrier() circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) diff --git a/test/python/transpiler/test_remove_final_measurements.py b/test/python/transpiler/test_remove_final_measurements.py index 4bc0e107109b..a91687bd8f3e 100644 --- a/test/python/transpiler/test_remove_final_measurements.py +++ b/test/python/transpiler/test_remove_final_measurements.py @@ -57,7 +57,8 @@ def expected_dag(): q0 = QuantumRegister(1, "q0") c0 = ClassicalRegister(1, "c0") qc = QuantumCircuit(q0, c0) - qc.x(0).c_if(c0[0], 0) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(c0[0], 0) return circuit_to_dag(qc) q0 = QuantumRegister(1, "q0") @@ -65,7 +66,8 @@ def expected_dag(): qc = QuantumCircuit(q0, c0) # make c0 busy - qc.x(0).c_if(c0[0], 0) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(c0[0], 0) # measure into c0 qc.measure(0, c0[0]) @@ -87,7 +89,8 @@ def expected_dag(): q0 = QuantumRegister(1, "q0") c0 = ClassicalRegister(2, "c0") qc = QuantumCircuit(q0, c0) - qc.x(q0[0]).c_if(c0[0], 0) + with self.assertWarns(DeprecationWarning): + qc.x(q0[0]).c_if(c0[0], 0) return circuit_to_dag(qc) q0 = QuantumRegister(1, "q0") @@ -95,7 +98,8 @@ def expected_dag(): qc = QuantumCircuit(q0, c0) # make c0[0] busy - qc.x(q0[0]).c_if(c0[0], 0) + with self.assertWarns(DeprecationWarning): + qc.x(q0[0]).c_if(c0[0], 0) # measure into not busy c0[1] qc.measure(0, c0[1]) diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py new file mode 100644 index 000000000000..1db392d3654b --- /dev/null +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -0,0 +1,185 @@ +# 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. + +"""Tests for the DropNegligible transpiler pass.""" + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate +from qiskit.circuit.library import ( + CPhaseGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZZGate, + XXMinusYYGate, + XXPlusYYGate, + GlobalPhaseGate, +) +from qiskit.quantum_info import Operator +from qiskit.transpiler.passes import RemoveIdentityEquivalent +from qiskit.transpiler.target import Target, InstructionProperties + +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestDropNegligible(QiskitTestCase): + """Test the DropNegligible pass.""" + + def test_drops_negligible_gates(self): + """Test that negligible gates are dropped.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + circuit.append(RXGate(1e-5), [a]) + circuit.append(RXGate(1e-8), [a]) + circuit.append(RYGate(1e-5), [a]) + circuit.append(RYGate(1e-8), [a]) + circuit.append(RZGate(1e-5), [a]) + circuit.append(RZGate(1e-8), [a]) + circuit.append(RXXGate(1e-5), [a, b]) + circuit.append(RXXGate(1e-8), [a, b]) + circuit.append(RYYGate(1e-5), [a, b]) + circuit.append(RYYGate(1e-8), [a, b]) + circuit.append(RZZGate(1e-5), [a, b]) + circuit.append(RZZGate(1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-8, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-8, 1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 2) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(circuit.count_ops()["rx"], 2) + self.assertEqual(transpiled.count_ops()["rx"], 1) + self.assertEqual(circuit.count_ops()["ry"], 2) + self.assertEqual(transpiled.count_ops()["ry"], 1) + self.assertEqual(circuit.count_ops()["rz"], 2) + self.assertEqual(transpiled.count_ops()["rz"], 1) + self.assertEqual(circuit.count_ops()["rxx"], 2) + self.assertEqual(transpiled.count_ops()["rxx"], 1) + self.assertEqual(circuit.count_ops()["ryy"], 2) + self.assertEqual(transpiled.count_ops()["ryy"], 1) + self.assertEqual(circuit.count_ops()["rzz"], 2) + self.assertEqual(transpiled.count_ops()["rzz"], 1) + self.assertEqual(circuit.count_ops()["xx_plus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_plus_yy"], 1) + self.assertEqual(circuit.count_ops()["xx_minus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_minus_yy"], 1) + np.testing.assert_allclose( + np.array(Operator(circuit)), np.array(Operator(transpiled)), atol=1e-7 + ) + + def test_handles_parameters(self): + """Test that gates with parameters are ignored gracefully.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + theta = Parameter("theta") + circuit.append(CPhaseGate(theta), [a, b]) + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_approximation_degree(self): + """Test that approximation degree handled correctly.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-4), [a, b]) + # fidelity 0.9999850001249996 which is above the threshold and not excluded + # so 1e-2 is the only gate remaining + circuit.append(CPhaseGate(1e-2), [a, b]) + circuit.append(CPhaseGate(1e-20), [a, b]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(transpiled.data[0].operation.params[0], 1e-2) + + def test_target_approx_none(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=None, target=target)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_target_approx_approx_degree(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999, target=target)( + circuit + ) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_custom_gate_no_matrix(self): + """Test that opaque gates are ignored.""" + + class CustomOpaqueGate(Gate): + """Custom opaque gate.""" + + def __init__(self): + super().__init__("opaque", 2, []) + + qc = QuantumCircuit(3) + qc.append(CustomOpaqueGate(), [0, 1]) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(qc, transpiled) + + def test_custom_gate_identity_matrix(self): + """Test that custom gates with matrix are evaluated.""" + + class CustomGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__("custom", 3, []) + + def to_matrix(self): + return np.eye(8, dtype=complex) + + qc = QuantumCircuit(3) + qc.append(CustomGate(), [0, 1, 2]) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(3) + self.assertEqual(expected, transpiled) + + def test_global_phase_ignored(self): + """Test that global phase gate isn't considered.""" + + qc = QuantumCircuit(1) + qc.id(0) + qc.append(GlobalPhaseGate(0)) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(1) + expected.append(GlobalPhaseGate(0)) + self.assertEqual(transpiled, expected) diff --git a/test/python/transpiler/test_reset_after_measure_simplification.py b/test/python/transpiler/test_reset_after_measure_simplification.py index 38f602443c06..976801e71353 100644 --- a/test/python/transpiler/test_reset_after_measure_simplification.py +++ b/test/python/transpiler/test_reset_after_measure_simplification.py @@ -26,12 +26,13 @@ def test_simple(self): qc = QuantumCircuit(1, 1) qc.measure(0, 0) qc.reset(0) - - new_qc = ResetAfterMeasureSimplification()(qc) + with self.assertWarns(DeprecationWarning): + new_qc = ResetAfterMeasureSimplification()(qc) ans_qc = QuantumCircuit(1, 1) ans_qc.measure(0, 0) - ans_qc.x(0).c_if(ans_qc.clbits[0], 1) + with self.assertWarns(DeprecationWarning): + ans_qc.x(0).c_if(ans_qc.clbits[0], 1) self.assertEqual(new_qc, ans_qc) def test_simple_null(self): @@ -52,12 +53,13 @@ def test_simple_multi_reg(self): qc = QuantumCircuit(qr, cr1, cr2) qc.measure(0, 1) qc.reset(0) - - new_qc = ResetAfterMeasureSimplification()(qc) + with self.assertWarns(DeprecationWarning): + new_qc = ResetAfterMeasureSimplification()(qc) ans_qc = QuantumCircuit(qr, cr1, cr2) ans_qc.measure(0, 1) - ans_qc.x(0).c_if(cr2[0], 1) + with self.assertWarns(DeprecationWarning): + ans_qc.x(0).c_if(cr2[0], 1) self.assertEqual(new_qc, ans_qc) @@ -69,7 +71,6 @@ def test_simple_multi_reg_null(self): qc = QuantumCircuit(qr, cr1, cr2) qc.measure(0, 1) qc.reset(1) # reset not on same qubit as meas - new_qc = ResetAfterMeasureSimplification()(qc) self.assertEqual(new_qc, qc) @@ -79,12 +80,13 @@ def test_simple_multi_resets(self): qc.measure(0, 0) qc.reset(0) qc.reset(0) - - new_qc = ResetAfterMeasureSimplification()(qc) + with self.assertWarns(DeprecationWarning): + new_qc = ResetAfterMeasureSimplification()(qc) ans_qc = QuantumCircuit(1, 2) ans_qc.measure(0, 0) - ans_qc.x(0).c_if(ans_qc.clbits[0], 1) + with self.assertWarns(DeprecationWarning): + ans_qc.x(0).c_if(ans_qc.clbits[0], 1) ans_qc.reset(0) self.assertEqual(new_qc, ans_qc) @@ -96,11 +98,13 @@ def test_simple_multi_resets_with_resets_before_measure(self): qc.reset(1) qc.measure(1, 1) - new_qc = ResetAfterMeasureSimplification()(qc) + with self.assertWarns(DeprecationWarning): + new_qc = ResetAfterMeasureSimplification()(qc) ans_qc = QuantumCircuit(2, 2) ans_qc.measure(0, 0) - ans_qc.x(0).c_if(Clbit(ClassicalRegister(2, "c"), 0), 1) + with self.assertWarns(DeprecationWarning): + ans_qc.x(0).c_if(Clbit(ClassicalRegister(2, "c"), 0), 1) ans_qc.reset(1) ans_qc.measure(1, 1) @@ -134,7 +138,8 @@ def test_bv_circuit(self): qc.reset(1) qc.x(1) qc.h(1) - new_qc = ResetAfterMeasureSimplification()(qc) + with self.assertWarns(DeprecationWarning): + new_qc = ResetAfterMeasureSimplification()(qc) for op in new_qc.data: if op.operation.name == "reset": self.assertEqual(op.qubits[0], new_qc.qubits[1]) @@ -149,7 +154,8 @@ def test_simple_if_else(self): base_expected = QuantumCircuit(1, 1) base_expected.measure(0, 0) - base_expected.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + base_expected.x(0).c_if(0, True) test = QuantumCircuit(1, 1) test.if_else( @@ -165,7 +171,8 @@ def test_simple_if_else(self): expected.clbits, ) - self.assertEqual(pass_(test), expected) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pass_(test), expected) def test_nested_control_flow(self): """Test that the pass recurses into nested control flow.""" @@ -177,7 +184,8 @@ def test_nested_control_flow(self): base_expected = QuantumCircuit(1, 1) base_expected.measure(0, 0) - base_expected.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + base_expected.x(0).c_if(0, True) body_test = QuantumCircuit(1, 1) body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, body_test.clbits) @@ -194,5 +202,5 @@ def test_nested_control_flow(self): expected.while_loop( (expected.clbits[0], True), body_expected, expected.qubits, expected.clbits ) - - self.assertEqual(pass_(test), expected) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pass_(test), expected) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7565ee17655f..2942cab30cdd 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -207,7 +207,7 @@ def test_layout_with_classical_bits(self): self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( - [layout[q] for q in qc.qubits], [11, 19, 18, 16, 26, 8, 21, 1, 5, 15, 3, 12, 14, 13] + [layout[q] for q in qc.qubits], [2, 0, 5, 1, 7, 3, 14, 6, 9, 8, 10, 11, 4, 12] ) # pylint: disable=line-too-long @@ -271,7 +271,7 @@ def test_layout_many_search_trials(self): self.assertIsInstance(res, QuantumCircuit) layout = res._layout.initial_layout self.assertEqual( - [layout[q] for q in qc.qubits], [22, 7, 2, 12, 1, 5, 14, 4, 11, 0, 16, 15, 3, 10] + [layout[q] for q in qc.qubits], [0, 12, 7, 3, 6, 11, 1, 10, 4, 9, 2, 5, 13, 8] ) def test_support_var_with_rust_fastpath(self): diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index d196e4982ea9..7cf86356ab1f 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -293,7 +293,8 @@ def test_classical_condition(self): with self.subTest("1 bit in register"): qc = QuantumCircuit(2, 1) qc.z(0) - qc.z(0).c_if(qc.cregs[0], 0) + with self.assertWarns(DeprecationWarning): + qc.z(0).c_if(qc.cregs[0], 0) cm = CouplingMap([(0, 1), (1, 0)]) expected = PassManager([TrivialLayout(cm)]).run(qc) actual = PassManager([TrivialLayout(cm), SabreSwap(cm)]).run(qc) @@ -302,8 +303,10 @@ def test_classical_condition(self): cregs = [ClassicalRegister(3), ClassicalRegister(4)] qc = QuantumCircuit(QuantumRegister(2, name="q"), *cregs) qc.z(0) - qc.z(0).c_if(cregs[0], 0) - qc.z(0).c_if(cregs[1], 0) + with self.assertWarns(DeprecationWarning): + qc.z(0).c_if(cregs[0], 0) + with self.assertWarns(DeprecationWarning): + qc.z(0).c_if(cregs[1], 0) cm = CouplingMap([(0, 1), (1, 0)]) expected = PassManager([TrivialLayout(cm)]).run(qc) actual = PassManager([TrivialLayout(cm), SabreSwap(cm)]).run(qc) @@ -316,40 +319,54 @@ def test_classical_condition_cargs(self): """ with self.subTest("missing measurement"): qc = QuantumCircuit(3, 1) - qc.cx(0, 2).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 2).c_if(0, 0) qc.measure(1, 0) - qc.h(2).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.h(2).c_if(0, 0) expected = QuantumCircuit(3, 1) expected.swap(1, 2) - expected.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.cx(0, 1).c_if(0, 0) expected.measure(2, 0) - expected.h(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.h(1).c_if(0, 0) result = SabreSwap(CouplingMap.from_line(3), seed=12345)(qc) self.assertEqual(result, expected) with self.subTest("reordered measurement"): qc = QuantumCircuit(3, 1) - qc.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 0) qc.measure(1, 0) - qc.h(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.h(0).c_if(0, 0) expected = QuantumCircuit(3, 1) - expected.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.cx(0, 1).c_if(0, 0) expected.measure(1, 0) - expected.h(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.h(0).c_if(0, 0) result = SabreSwap(CouplingMap.from_line(3), seed=12345)(qc) self.assertEqual(result, expected) def test_conditional_measurement(self): """Test that instructions with cargs and conditions are handled correctly.""" qc = QuantumCircuit(3, 2) - qc.cx(0, 2).c_if(0, 0) - qc.measure(2, 0).c_if(1, 0) - qc.h(2).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 2).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.measure(2, 0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + qc.h(2).c_if(0, 0) qc.measure(1, 1) expected = QuantumCircuit(3, 2) expected.swap(1, 2) - expected.cx(0, 1).c_if(0, 0) - expected.measure(1, 0).c_if(1, 0) - expected.h(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected.measure(1, 0).c_if(1, 0) + with self.assertWarns(DeprecationWarning): + expected.h(1).c_if(0, 0) expected.measure(2, 1) result = SabreSwap(CouplingMap.from_line(3), seed=12345)(qc) self.assertEqual(result, expected) diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index a1ae04d5e68e..f5ebc349d814 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -114,7 +114,8 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): """ qc = QuantumCircuit(2, 1) qc.measure(0, 0) - qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) pm = PassManager([schedule_pass(durations), PadDelay()]) @@ -123,7 +124,8 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.measure(0, 0) expected.delay(1000, 1) # x.c_if starts after measure - expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) expected.delay(200, 0) self.assertEqual(expected, scheduled) @@ -200,8 +202,10 @@ def test_c_if_on_different_qubits(self, schedule_pass): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, True) - qc.x(2).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(2).c_if(0, True) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) pm = PassManager([schedule_pass(durations), PadDelay()]) @@ -211,8 +215,10 @@ def test_c_if_on_different_qubits(self, schedule_pass): expected.measure(0, 0) expected.delay(1000, 1) expected.delay(1000, 2) - expected.x(1).c_if(0, True) - expected.x(2).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(2).c_if(0, True) expected.delay(200, 0) self.assertEqual(expected, scheduled) @@ -283,7 +289,8 @@ def test_measure_after_c_if(self, schedule_pass): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) @@ -294,7 +301,8 @@ def test_measure_after_c_if(self, schedule_pass): expected.delay(1000, 1) expected.delay(1000, 2) expected.measure(0, 0) - expected.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, 1) expected.measure(2, 0) expected.delay(1000, 0) expected.delay(800, 1) @@ -474,7 +482,8 @@ def test_measure_after_c_if_on_edge_locking(self): """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) - qc.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 1) qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) @@ -499,7 +508,8 @@ def test_measure_after_c_if_on_edge_locking(self): expected_asap = QuantumCircuit(3, 1) expected_asap.measure(0, 0) expected_asap.delay(1000, 1) - expected_asap.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_asap.x(1).c_if(0, 1) expected_asap.measure(2, 0) expected_asap.delay(200, 0) expected_asap.delay(200, 2) @@ -508,7 +518,8 @@ def test_measure_after_c_if_on_edge_locking(self): expected_alap = QuantumCircuit(3, 1) expected_alap.measure(0, 0) expected_alap.delay(1000, 1) - expected_alap.x(1).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_alap.x(1).c_if(0, 1) expected_alap.delay(200, 2) expected_alap.measure(2, 0) expected_alap.delay(200, 0) @@ -534,11 +545,14 @@ def test_active_reset_circuit(self, write_lat, cond_lat): """ qc = QuantumCircuit(1, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.measure(0, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) @@ -562,15 +576,18 @@ def test_active_reset_circuit(self, write_lat, cond_lat): expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) expected.measure(0, 0) if cond_lat > 0: expected.delay(cond_lat, 0) - expected.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, 1) self.assertEqual(expected, actual_asap) self.assertEqual(expected, actual_alap) @@ -659,15 +676,19 @@ def test_random_complicated_circuit(self): """ qc = QuantumCircuit(3, 1) qc.delay(100, 0) - qc.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 1) qc.barrier() qc.measure(2, 0) - qc.x(1).c_if(0, 0) - qc.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, 0) qc.delay(300, 0) qc.cx(1, 2) qc.x(0) - qc.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + qc.cx(0, 1).c_if(0, 0) qc.measure(2, 0) durations = InstructionDurations( @@ -694,19 +715,23 @@ def test_random_complicated_circuit(self): expected_asap.delay(200, 0) # due to conditional latency of 200dt expected_asap.delay(300, 1) expected_asap.delay(300, 2) - expected_asap.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_asap.x(0).c_if(0, 1) expected_asap.barrier() expected_asap.delay(1400, 0) expected_asap.delay(1200, 1) expected_asap.measure(2, 0) - expected_asap.x(1).c_if(0, 0) - expected_asap.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.x(0).c_if(0, 0) expected_asap.delay(300, 0) expected_asap.x(0) expected_asap.delay(300, 2) expected_asap.cx(1, 2) expected_asap.delay(400, 1) - expected_asap.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_asap.cx(0, 1).c_if(0, 0) expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) expected_asap.delay( 700, 1 @@ -720,20 +745,24 @@ def test_random_complicated_circuit(self): expected_alap.delay(200, 0) # due to conditional latency of 200dt expected_alap.delay(300, 1) expected_alap.delay(300, 2) - expected_alap.x(0).c_if(0, 1) + with self.assertWarns(DeprecationWarning): + expected_alap.x(0).c_if(0, 1) expected_alap.barrier() expected_alap.delay(1400, 0) expected_alap.delay(1200, 1) expected_alap.measure(2, 0) - expected_alap.x(1).c_if(0, 0) - expected_alap.x(0).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.x(1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.x(0).c_if(0, 0) expected_alap.delay(300, 0) expected_alap.x(0) expected_alap.delay(300, 1) expected_alap.delay(600, 2) expected_alap.cx(1, 2) expected_alap.delay(100, 1) - expected_alap.cx(0, 1).c_if(0, 0) + with self.assertWarns(DeprecationWarning): + expected_alap.cx(0, 1).c_if(0, 0) expected_alap.measure(2, 0) expected_alap.delay(700, 0) expected_alap.delay(700, 1) @@ -771,8 +800,10 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): """ qc = QuantumCircuit(2, 1) qc.delay(100, 0) - qc.x(0).c_if(0, True) - qc.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + qc.x(1).c_if(0, True) durations = InstructionDurations([("x", None, 160)]) pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay()]) @@ -781,8 +812,10 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): expected = QuantumCircuit(2, 1) expected.delay(100, 0) expected.delay(100, 1) # due to extra dependency on clbits - expected.x(0).c_if(0, True) - expected.x(1).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(0).c_if(0, True) + with self.assertWarns(DeprecationWarning): + expected.x(1).c_if(0, True) self.assertEqual(expected, scheduled) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 980924224d15..86f1953d43a4 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -51,6 +51,7 @@ Fake7QPulseV1, ) from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.providers.backend import QubitProperties from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) @@ -1185,6 +1186,20 @@ def test_target_serialization_preserve_variadic(self): # Perform check again, should not throw exception self.assertTrue(deserialized_target.instruction_supported("u_var", (0, 1))) + def test_target_no_num_qubits_qubit_properties(self): + """Checks that a Target can be initialized with no qubits but a list of Qubit Properities""" + + # Initialize target qubit properties + qubit_properties = [QubitProperties()] + + # Initialize the Target with only a list of qubit properties + target = Target( + qubit_properties=qubit_properties, + ) + + # Check that the Target num_qubit attribute matches the length of qubit properties + self.assertEqual(target.num_qubits, len(qubit_properties)) + class TestPulseTarget(QiskitTestCase): def setUp(self): diff --git a/test/python/transpiler/test_unroll_3q_or_more.py b/test/python/transpiler/test_unroll_3q_or_more.py index 27927bbbd4ad..15a90cdf7759 100644 --- a/test/python/transpiler/test_unroll_3q_or_more.py +++ b/test/python/transpiler/test_unroll_3q_or_more.py @@ -62,7 +62,8 @@ def test_decompose_conditional(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 0) + with self.assertWarns(DeprecationWarning): + circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 0) dag = circuit_to_dag(circuit) pass_ = Unroll3qOrMore() after_dag = pass_.run(dag) @@ -70,7 +71,8 @@ def test_decompose_conditional(self): self.assertEqual(len(op_nodes), 15) for node in op_nodes: self.assertIn(node.name, ["h", "t", "tdg", "cx"]) - self.assertEqual(node.op.condition, (cr, 0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(node.op.condition, (cr, 0)) def test_decompose_unitary(self): """Test unrolling of unitary gate over 4qubits.""" diff --git a/test/python/transpiler/test_unroll_forloops.py b/test/python/transpiler/test_unroll_forloops.py index cbf8f70ef767..a44655903370 100644 --- a/test/python/transpiler/test_unroll_forloops.py +++ b/test/python/transpiler/test_unroll_forloops.py @@ -135,7 +135,8 @@ def test_skip_continue_c_if(self): circuit.h(0) circuit.cx(0, 1) circuit.measure(0, 0) - circuit.break_loop().c_if(0, True) + with self.assertWarns(DeprecationWarning): + circuit.break_loop().c_if(0, True) passmanager = PassManager() passmanager.append(UnrollForLoops()) diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index dd69faac02cb..9eb4e3ad2c88 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -151,7 +151,8 @@ def test_wire_order(self): circuit.h(0) circuit.h(3) circuit.x(1) - circuit.x(3).c_if(cr, 10) + with self.assertWarns(DeprecationWarning): + circuit.x(3).c_if(cr, 10) expected = "\n".join( [ @@ -183,7 +184,8 @@ def test_wire_order_cregbundle(self): circuit.h(0) circuit.h(3) circuit.x(1) - circuit.x(3).c_if(cr, 10) + with self.assertWarns(DeprecationWarning): + circuit.x(3).c_if(cr, 10) expected = "\n".join( [ diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index bcf5b77d51bd..8f33f1aa3ce8 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -99,7 +99,8 @@ def test_4597(self): qr = QuantumRegister(3, "q") cr = ClassicalRegister(3, "c") circuit = QuantumCircuit(qr, cr) - circuit.x(qr[2]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[2]).c_if(cr, 2) circuit.draw(output="latex_source", cregbundle=True) circuit_drawer(circuit, filename=filename, output="latex_source") @@ -148,8 +149,10 @@ def test_teleport(self): circuit.measure(qr[0], cr[0]) circuit.measure(qr[1], cr[1]) # Apply a correction - circuit.z(qr[2]).c_if(cr, 1) - circuit.x(qr[2]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.z(qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[2]).c_if(cr, 2) circuit.measure(qr[2], cr[2]) circuit_drawer(circuit, filename=filename, output="latex_source") @@ -206,7 +209,8 @@ def test_conditional(self): # check gates are shifted over accordingly circuit.h(qr) circuit.measure(qr, cr) - circuit.h(qr[0]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 2) circuit_drawer(circuit, filename=filename, output="latex_source") @@ -570,7 +574,8 @@ def test_meas_condition(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) - circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) circuit_drawer(circuit, filename=filename, output="latex_source") self.assertEqualToReference(filename) @@ -598,8 +603,10 @@ def test_cif_single_bit(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr[1], 0) - circuit.x(qr[1]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[1]).c_if(cr[0], 1) circuit_drawer(circuit, cregbundle=False, filename=filename, output="latex_source") self.assertEqualToReference(filename) @@ -611,8 +618,10 @@ def test_cif_single_bit_cregbundle(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr[1], 0) - circuit.x(qr[1]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[1]).c_if(cr[0], 1) circuit_drawer(circuit, cregbundle=True, filename=filename, output="latex_source") self.assertEqualToReference(filename) @@ -639,8 +648,10 @@ def test_measures_with_conditions(self): circuit.h(0) circuit.h(1) circuit.measure(0, cr1[1]) - circuit.measure(1, cr2[0]).c_if(cr1, 1) - circuit.h(0).c_if(cr2, 3) + with self.assertWarns(DeprecationWarning): + circuit.measure(1, cr2[0]).c_if(cr1, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(cr2, 3) circuit_drawer(circuit, cregbundle=False, filename=filename1, output="latex_source") circuit_drawer(circuit, cregbundle=True, filename=filename2, output="latex_source") self.assertEqualToReference(filename1) @@ -654,7 +665,8 @@ def test_measures_with_conditions_with_bits(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(crx[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(crx[1], 0) circuit.measure(0, bits[3]) circuit_drawer(circuit, cregbundle=False, filename=filename1, output="latex_source") circuit_drawer(circuit, cregbundle=True, filename=filename2, output="latex_source") @@ -668,7 +680,8 @@ def test_conditions_with_bits_reverse(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(bits[3], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(bits[3], 0) circuit_drawer( circuit, cregbundle=False, reverse_bits=True, filename=filename, output="latex_source" ) @@ -680,7 +693,8 @@ def test_sidetext_with_condition(self): qr = QuantumRegister(2, "q") cr = ClassicalRegister(2, "c") circuit = QuantumCircuit(qr, cr) - circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) circuit_drawer(circuit, cregbundle=False, filename=filename, output="latex_source") self.assertEqualToReference(filename) @@ -703,7 +717,8 @@ def test_wire_order(self): circuit.h(0) circuit.h(3) circuit.x(1) - circuit.x(3).c_if(cr, 12) + with self.assertWarns(DeprecationWarning): + circuit.x(3).c_if(cr, 12) circuit_drawer( circuit, cregbundle=False, diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index e7d28aac8a9e..666255c44f23 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -388,7 +388,8 @@ def test_wire_order(self): circuit.h(0) circuit.h(3) circuit.x(1) - circuit.x(3).c_if(cr, 10) + with self.assertWarns(DeprecationWarning): + circuit.x(3).c_if(cr, 10) self.assertEqual( str( circuit_drawer( @@ -845,7 +846,8 @@ def test_text_cu1_condition(self): qr = QuantumRegister(3, "q") cr = ClassicalRegister(3, "c") circuit = QuantumCircuit(qr, cr) - circuit.append(CU1Gate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CU1Gate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=False)), expected) def test_text_rzz_condition(self): @@ -866,7 +868,8 @@ def test_text_rzz_condition(self): qr = QuantumRegister(3, "q") cr = ClassicalRegister(3, "c") circuit = QuantumCircuit(qr, cr) - circuit.append(RZZGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + circuit.append(RZZGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=False)), expected) def test_text_cp_condition(self): @@ -887,7 +890,8 @@ def test_text_cp_condition(self): qr = QuantumRegister(3, "q") cr = ClassicalRegister(3, "c") circuit = QuantumCircuit(qr, cr) - circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=False)), expected) def test_text_cu1_reverse_bits(self): @@ -1758,7 +1762,8 @@ def test_control_gate_label_with_cond_1_low(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1789,7 +1794,8 @@ def test_control_gate_label_with_cond_1_low_cregbundle(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1824,7 +1830,8 @@ def test_control_gate_label_with_cond_1_med(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1860,7 +1867,8 @@ def test_control_gate_label_with_cond_1_med_cregbundle(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1895,7 +1903,8 @@ def test_control_gate_label_with_cond_1_high(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1930,7 +1939,8 @@ def test_control_gate_label_with_cond_1_high_cregbundle(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [0, 1]) self.assertEqual( @@ -1966,7 +1976,8 @@ def test_control_gate_label_with_cond_2_med_space(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [1, 0]) self.assertEqual( @@ -1998,7 +2009,8 @@ def test_control_gate_label_with_cond_2_med(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ctrl-h").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ctrl-h").c_if(cr, 1) circ.append(controlh, [1, 0]) self.assertEqual( @@ -2034,7 +2046,8 @@ def test_control_gate_label_with_cond_2_med_cregbundle(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [1, 0]) self.assertEqual( @@ -2071,7 +2084,8 @@ def test_control_gate_label_with_cond_2_low(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [1, 0]) self.assertEqual( @@ -2108,7 +2122,8 @@ def test_control_gate_label_with_cond_2_low_cregbundle(self): cr = ClassicalRegister(1, "c") circ = QuantumCircuit(qr, cr) hgate = HGate(label="my h") - controlh = hgate.control(label="my ch").c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + controlh = hgate.control(label="my ch").c_if(cr, 1) circ.append(controlh, [1, 0]) self.assertEqual( @@ -2269,8 +2284,8 @@ def test_text_conditional_1(self): " 0x1 ", ] ) - - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2309,7 +2324,8 @@ def test_text_conditional_1_bundle(self): ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2335,7 +2351,8 @@ def test_text_conditional_reverse_bits_true(self): circuit.x(0) circuit.x(0) circuit.measure(2, 1) - circuit.x(2).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2).c_if(cr, 2) expected = "\n".join( [ @@ -2386,7 +2403,8 @@ def test_text_conditional_reverse_bits_false(self): circuit.x(0) circuit.x(0) circuit.measure(2, 1) - circuit.x(2).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2).c_if(cr, 2) expected = "\n".join( [ @@ -2486,7 +2504,8 @@ def test_text_conditional_1(self): " 0x1 ", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2523,8 +2542,8 @@ def test_text_conditional_1_bundle(self): " └─────┘", ] ) - - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2565,7 +2584,8 @@ def test_text_measure_with_spaces(self): " 0x1 ", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2603,7 +2623,8 @@ def test_text_measure_with_spaces_bundle(self): " 1 └─────┘", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2694,8 +2715,10 @@ def test_text_barrier_med_compress_3(self): qc1 = ClassicalRegister(3, "cr") qc2 = ClassicalRegister(1, "cr2") circuit = QuantumCircuit(qr, qc1, qc2) - circuit.x(0).c_if(qc1, 3) - circuit.x(0).c_if(qc2[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(qc1, 3) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(qc2[0], 1) expected = "\n".join( [ @@ -2753,8 +2776,8 @@ def test_text_conditional_1_cregbundle(self): " └─────┘", ] ) - - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2790,8 +2813,8 @@ def test_text_conditional_1(self): " 0x1 ", ] ) - - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str(circuit_drawer(circuit, output="text", initial_state=True, cregbundle=False)), expected, @@ -2819,7 +2842,8 @@ def test_text_conditional_2_cregbundle(self): " └─────┘", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2859,7 +2883,8 @@ def test_text_conditional_2(self): " 0x2 ", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str(circuit_drawer(circuit, output="text", initial_state=True, cregbundle=False)), expected, @@ -2887,7 +2912,8 @@ def test_text_conditional_3_cregbundle(self): " └─────┘", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -2931,7 +2957,8 @@ def test_text_conditional_3(self): " 0x3 ", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str(circuit_drawer(circuit, output="text", initial_state=True, cregbundle=False)), expected, @@ -2959,7 +2986,8 @@ def test_text_conditional_4(self): " └─────┘", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str( circuit_drawer( @@ -3007,7 +3035,8 @@ def test_text_conditional_5(self): " 0x5 ", ] ) - circuit = QuantumCircuit.from_qasm_str(qasm_string) + with self.assertWarns(DeprecationWarning): + circuit = QuantumCircuit.from_qasm_str(qasm_string) self.assertEqual( str(circuit_drawer(circuit, output="text", initial_state=True, cregbundle=False)), expected, @@ -3018,7 +3047,8 @@ def test_text_conditional_cz_no_space_cregbundle(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cz(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cz(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3042,7 +3072,8 @@ def test_text_conditional_cz_no_space(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cz(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cz(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3066,7 +3097,8 @@ def test_text_conditional_cz_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cz(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cz(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3092,7 +3124,8 @@ def test_text_conditional_cz(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cz(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cz(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3118,7 +3151,8 @@ def test_text_conditional_cx_ct_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3144,7 +3178,8 @@ def test_text_conditional_cx_ct(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3170,7 +3205,8 @@ def test_text_conditional_cx_tc_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[1], qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[1], qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3196,7 +3232,8 @@ def test_text_conditional_cx_tc(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cx(qr[1], qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cx(qr[1], qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3222,7 +3259,8 @@ def test_text_conditional_cu3_ct_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[0], qr[1]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[0], qr[1]]).c_if(cr, 1) expected = "\n".join( [ @@ -3248,7 +3286,8 @@ def test_text_conditional_cu3_ct(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[0], qr[1]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[0], qr[1]]).c_if(cr, 1) expected = "\n".join( [ @@ -3274,7 +3313,8 @@ def test_text_conditional_cu3_tc_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[1], qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[1], qr[0]]).c_if(cr, 1) expected = "\n".join( [ @@ -3300,7 +3340,8 @@ def test_text_conditional_cu3_tc(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[1], qr[0]]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CU3Gate(pi / 2, pi / 2, pi / 2), [qr[1], qr[0]]).c_if(cr, 1) expected = "\n".join( [ @@ -3326,7 +3367,8 @@ def test_text_conditional_ccx_cregbundle(self): qr = QuantumRegister(4, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3354,7 +3396,8 @@ def test_text_conditional_ccx(self): qr = QuantumRegister(4, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3382,7 +3425,8 @@ def test_text_conditional_ccx_no_space_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3416,7 +3460,8 @@ def test_text_conditional_ccx_no_space(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.ccx(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3442,7 +3487,8 @@ def test_text_conditional_h_cregbundle(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3466,7 +3512,8 @@ def test_text_conditional_h(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3490,7 +3537,8 @@ def test_text_conditional_swap_cregbundle(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.swap(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.swap(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3516,7 +3564,8 @@ def test_text_conditional_swap(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.swap(qr[0], qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.swap(qr[0], qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3542,7 +3591,8 @@ def test_text_conditional_cswap_cregbundle(self): qr = QuantumRegister(4, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cswap(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cswap(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3570,7 +3620,8 @@ def test_text_conditional_cswap(self): qr = QuantumRegister(4, "qr") cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.cswap(qr[0], qr[1], qr[2]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.cswap(qr[0], qr[1], qr[2]).c_if(cr, 1) expected = "\n".join( [ @@ -3599,7 +3650,8 @@ def test_conditional_reset_cregbundle(self): cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.reset(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.reset(qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3624,7 +3676,8 @@ def test_conditional_reset(self): cr = ClassicalRegister(1, "cr") circuit = QuantumCircuit(qr, cr) - circuit.reset(qr[0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.reset(qr[0]).c_if(cr, 1) expected = "\n".join( [ @@ -3649,7 +3702,8 @@ def test_conditional_multiplexer_cregbundle(self): qr = QuantumRegister(3, name="qr") cr = ClassicalRegister(1, "cr") qc = QuantumCircuit(qr, cr) - qc.append(cx_multiplexer.c_if(cr, 1), [qr[0], qr[1]]) + with self.assertWarns(DeprecationWarning): + qc.append(cx_multiplexer.c_if(cr, 1), [qr[0], qr[1]]) expected = "\n".join( [ @@ -3675,7 +3729,8 @@ def test_conditional_multiplexer(self): qr = QuantumRegister(3, name="qr") cr = ClassicalRegister(1, "cr") qc = QuantumCircuit(qr, cr) - qc.append(cx_multiplexer.c_if(cr, 1), [qr[0], qr[1]]) + with self.assertWarns(DeprecationWarning): + qc.append(cx_multiplexer.c_if(cr, 1), [qr[0], qr[1]]) expected = "\n".join( [ @@ -3702,7 +3757,8 @@ def test_text_conditional_measure_cregbundle(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) - circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3736,7 +3792,8 @@ def test_text_conditional_measure(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) - circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3762,8 +3819,10 @@ def test_text_bit_conditional(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr[0], 1) - circuit.h(qr[1]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr[1], 0) expected = "\n".join( [ @@ -3790,8 +3849,10 @@ def test_text_bit_conditional_cregbundle(self): qr = QuantumRegister(2, "qr") cr = ClassicalRegister(2, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr[0], 1) - circuit.h(qr[1]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr[1], 0) expected = "\n".join( [ @@ -3826,7 +3887,8 @@ def test_text_condition_measure_bits_true(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(crx[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(crx[1], 0) circuit.measure(0, bits[3]) expected = "\n".join( @@ -3860,7 +3922,8 @@ def test_text_condition_measure_bits_false(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(crx[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(crx[1], 0) circuit.measure(0, bits[3]) expected = "\n".join( @@ -3900,7 +3963,8 @@ def test_text_conditional_reverse_bits_1(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) - circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) expected = "\n".join( [ @@ -3930,10 +3994,14 @@ def test_text_conditional_reverse_bits_2(self): qr = QuantumRegister(3, "qr") cr = ClassicalRegister(3, "cr") circuit = QuantumCircuit(qr, cr) - circuit.h(qr[0]).c_if(cr, 6) - circuit.h(qr[1]).c_if(cr, 1) - circuit.h(qr[2]).c_if(cr, 2) - circuit.cx(0, 1).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 6) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[2]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.cx(0, 1).c_if(cr, 3) expected = "\n".join( [ @@ -3969,7 +4037,8 @@ def test_text_condition_bits_reverse(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(bits[3], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(bits[3], 0) expected = "\n".join( [ @@ -4847,9 +4916,10 @@ def test_cccz_conditional(self): qr = QuantumRegister(4, "q") cr = ClassicalRegister(1, "c") circuit = QuantumCircuit(qr, cr) - circuit.append( - ZGate().control(3, ctrl_state="101").c_if(cr, 1), [qr[0], qr[1], qr[2], qr[3]] - ) + with self.assertWarns(DeprecationWarning): + circuit.append( + ZGate().control(3, ctrl_state="101").c_if(cr, 1), [qr[0], qr[1], qr[2], qr[3]] + ) self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected) def test_cch_bot(self): @@ -5908,14 +5978,16 @@ def test_if_else_with_body_specified(self): circuit.measure(0, 1) circuit.measure(1, 2) circuit.x(2) - circuit.x(2, label="XLabel").c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2, label="XLabel").c_if(cr, 2) qr2 = QuantumRegister(3, "qr2") circuit2 = QuantumCircuit(qr2, cr) circuit2.x(1) circuit2.y(1) circuit2.z(0) - circuit2.x(0, label="X1i").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + circuit2.x(0, label="X1i").c_if(cr, 4) circuit.if_else((cr[1], 1), circuit2, None, [0, 1, 2], [0, 1, 2]) circuit.x(0, label="X1i") @@ -5981,7 +6053,8 @@ def test_if_op_nested_wire_order(self): circuit.h(0) with circuit.if_test((cr[1], 1)) as _else: - circuit.x(0, label="X c_if").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + circuit.x(0, label="X c_if").c_if(cr, 4) with circuit.if_test((cr[2], 1)): circuit.z(0) circuit.y(1) @@ -6227,12 +6300,15 @@ def test_if_else_op_from_circuit_with_conditions(self): cr = ClassicalRegister(3, "cr") circuit = QuantumCircuit(qr, cr) circuit.h(0) - circuit.x(2).c_if(cr[1], 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2).c_if(cr[1], 2) qr2 = QuantumRegister(3, "qr2") qc2 = QuantumCircuit(qr2, cr) - qc2.x(0, label="X1").c_if(cr, 4) - qc2.x(1, label="X2").c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + qc2.x(0, label="X1").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + qc2.x(1, label="X2").c_if(cr[1], 1) circuit.if_else((cr[1], 1), qc2, None, [0, 1, 2], [0, 1, 2]) self.assertEqual( diff --git a/test/python/visualization/test_dag_drawer.py b/test/python/visualization/test_dag_drawer.py index 56138c12cf00..9c9b11e42a68 100644 --- a/test/python/visualization/test_dag_drawer.py +++ b/test/python/visualization/test_dag_drawer.py @@ -98,7 +98,8 @@ def test_dag_drawer_with_dag_dep(self): qc.cx(0, 1) qc.cx(0, 2) qc.cx(0, 3) - qc.x(3).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + qc.x(3).c_if(cr[1], 1) qc.h(3) qc.x(4) qc.barrier(0, 1) diff --git a/test/python/visualization/test_utils.py b/test/python/visualization/test_utils.py index 1ef87994d001..7e0f91f3f993 100644 --- a/test/python/visualization/test_utils.py +++ b/test/python/visualization/test_utils.py @@ -328,9 +328,13 @@ def test_get_layered_instructions_op_with_cargs(self): qc.h(0) qc.measure(0, 0) qc_2 = QuantumCircuit(1, 1, name="add_circ") - qc_2.h(0).c_if(qc_2.cregs[0], 1) + with self.assertWarns(DeprecationWarning): + qc_2.h(0).c_if(qc_2.cregs[0], 1) qc_2.measure(0, 0) - qc.append(qc_2, [1], [0]) + # This append calls ends up calling .to_instruction() which calls + # .c_if() internally + with self.assertWarns(DeprecationWarning): + qc.append(qc_2, [1], [0]) (_, _, layered_ops) = _utils._get_layered_instructions(qc) diff --git a/test/python/visualization/timeline/test_core.py b/test/python/visualization/timeline/test_core.py index 16a5457e8d42..895aa48d954e 100644 --- a/test/python/visualization/timeline/test_core.py +++ b/test/python/visualization/timeline/test_core.py @@ -30,13 +30,17 @@ def setUp(self): circ.cx(0, 2) circ.cx(1, 3) - self.circ = transpile( - circ, - scheduling_method="alap", - basis_gates=["h", "cx"], - instruction_durations=[("h", 0, 200), ("cx", [0, 2], 1000), ("cx", [1, 3], 1000)], - optimization_level=0, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + self.circ = transpile( + circ, + scheduling_method="alap", + basis_gates=["h", "cx"], + instruction_durations=[("h", 0, 200), ("cx", [0, 2], 1000), ("cx", [1, 3], 1000)], + optimization_level=0, + ) def test_time_range(self): """Test calculating time range.""" @@ -153,13 +157,17 @@ def test_multi_measurement_with_clbit_not_shown(self): circ.measure(0, 0) circ.measure(1, 1) - circ = transpile( - circ, - scheduling_method="alap", - basis_gates=[], - instruction_durations=[("measure", 0, 2000), ("measure", 1, 2000)], - optimization_level=0, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + circ = transpile( + circ, + scheduling_method="alap", + basis_gates=[], + instruction_durations=[("measure", 0, 2000), ("measure", 1, 2000)], + optimization_level=0, + ) canvas = core.DrawerCanvas(stylesheet=self.style) canvas.formatter.update({"control.show_clbits": False}) @@ -184,13 +192,17 @@ def test_multi_measurement_with_clbit_shown(self): circ.measure(0, 0) circ.measure(1, 1) - circ = transpile( - circ, - scheduling_method="alap", - basis_gates=[], - instruction_durations=[("measure", 0, 2000), ("measure", 1, 2000)], - optimization_level=0, - ) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The `target` parameter should be used instead", + ): + circ = transpile( + circ, + scheduling_method="alap", + basis_gates=[], + instruction_durations=[("measure", 0, 2000), ("measure", 1, 2000)], + optimization_level=0, + ) canvas = core.DrawerCanvas(stylesheet=self.style) canvas.formatter.update({"control.show_clbits": True}) diff --git a/test/qpy_compat/process_version.sh b/test/qpy_compat/process_version.sh index c56c44f554bb..01998e01667f 100755 --- a/test/qpy_compat/process_version.sh +++ b/test/qpy_compat/process_version.sh @@ -42,14 +42,14 @@ package="$1" version="$2" our_dir="$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")")" -cache_dir="$(pwd -P)/qpy_$version" -venv_dir="$(pwd -P)/${version}" +cache_dir="$(pwd -P)/qpy_cache/$version" +venv_dir="$(pwd -P)/venvs/$package-$version" if [[ ! -d $cache_dir ]] ; then echo "Building venv for $package==$version" "$python" -m venv "$venv_dir" "$venv_dir/bin/pip" install -c "${our_dir}/qpy_test_constraints.txt" "${package}==${version}" - mkdir "$cache_dir" + mkdir -p "$cache_dir" pushd "$cache_dir" echo "Generating QPY files with $package==$version" "$venv_dir/bin/python" "${our_dir}/test_qpy.py" generate --version="$version" diff --git a/test/qpy_compat/run_tests.sh b/test/qpy_compat/run_tests.sh index 979306a83784..a6ff67b14a94 100755 --- a/test/qpy_compat/run_tests.sh +++ b/test/qpy_compat/run_tests.sh @@ -14,6 +14,7 @@ set -e set -o pipefail set -x +shopt -s nullglob # Set fixed hash seed to ensure set orders are identical between saving and # loading. @@ -21,35 +22,72 @@ export PYTHONHASHSEED=$(python -S -c "import random; print(random.randint(1, 429 echo "PYTHONHASHSEED=$PYTHONHASHSEED" our_dir="$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")")" +repo_root="$(realpath -- "$our_dir/../..")" -qiskit_venv="$(pwd -P)/qiskit_venv" +# First, prepare a wheel file for the dev version. We install several venvs with this, and while +# cargo will cache some rust artefacts, it still has to re-link each time, so the wheel build takes +# a little while. +wheel_dir="$(pwd -P)/wheels" +python -m pip wheel --no-deps --wheel-dir "$wheel_dir" "$repo_root" +all_wheels=("$wheel_dir"/*.whl) +qiskit_dev_wheel="${all_wheels[0]}" + +# Now set up a "base" development-version environment, which we'll use for most of the backwards +# compatibility checks. +qiskit_venv="$(pwd -P)/venvs/dev" qiskit_python="$qiskit_venv/bin/python" python -m venv "$qiskit_venv" + # `packaging` is needed for the `get_versions.py` script. -# symengine is pinned to 0.13 to explicitly test the migration path reusing the venv -"$qiskit_venv/bin/pip" install -c "$our_dir/../../constraints.txt" "$our_dir/../.." packaging "symengine~=0.13" +"$qiskit_venv/bin/pip" install -c "$repo_root/constraints.txt" "$qiskit_dev_wheel" packaging +# Run all of the tests of cross-Qiskit-version compatibility. "$qiskit_python" "$our_dir/get_versions.py" | parallel --colsep=" " bash "$our_dir/process_version.sh" -p "$qiskit_python" -# Test dev compatibility +# Test dev compatibility with itself. dev_version="$("$qiskit_python" -c 'import qiskit; print(qiskit.__version__)')" +mkdir -p "dev-files/base" +pushd "dev-files/base" "$qiskit_python" "$our_dir/test_qpy.py" generate --version="$dev_version" "$qiskit_python" "$our_dir/test_qpy.py" load --version="$dev_version" - -# Test dev compatibility with different symengine versions across 0.11 and 0.13 -# -# NOTE: When symengine >= 0.14.0 is released we will need to modify this to build an explicit -# symengine 0.13.0 venv instead of reusing $qiskit_venv. -# -symengine_11_venv="$(pwd -P)/qiskit_symengine_11_venv" -symengine_11_python="$symengine_11_venv/bin/python" -python -m venv "$symengine_11_venv" -"$symengine_11_venv/bin/pip" install -c "$our_dir/../../constraints.txt" "$our_dir/../.." "symengine==0.11.0" -# Load symengine 0.13.0 generated payload with symengine 0.11 -"$symengine_11_python" "$our_dir/test_qpy.py" load --version="$dev_version" -# Load symengine 0.11.0 generated payload with symengine 0.13.0 -mkdir symengine_11_qpy_files -pushd symengine_11_qpy_files -"$symengine_11_python" "$our_dir/test_qpy.py" generate --version="$dev_version" -"$qiskit_python" "$our_dir/test_qpy.py" load --version="$dev_version" popd + + +# Test dev compatibility with all supported combinations of symengine between generator and loader. +# This will likely duplicate the base dev-compatibility test, but the tests are fairly fast, and +# it's better safe than sorry with the serialisation tests. + +symengine_versions=( + '>=0.11,<0.12' + '>=0.13,<0.14' +) +symengine_venv_prefix="$(pwd -P)/venvs/dev-symengine-" +symengine_files_prefix="$(pwd -P)/dev-files/symengine-" + +# Create the venvs and QPY files for each symengine version. +for i in "${!symengine_versions[@]}"; do + specifier="${symengine_versions[$i]}" + symengine_venv="$symengine_venv_prefix$i" + files_dir="$symengine_files_prefix$i" + python -m venv "$symengine_venv" + "$symengine_venv/bin/pip" install -c "$repo_root/constraints.txt" "$qiskit_dev_wheel" "symengine$specifier" + mkdir -p "$files_dir" + pushd "$files_dir" + "$symengine_venv/bin/python" -c 'import symengine; print(symengine.__version__)' > "SYMENGINE_VERSION" + "$symengine_venv/bin/python" "$our_dir/test_qpy.py" generate --version="$dev_version" + popd +done + +# For each symengine version, try loading the QPY files from every other symengine version. +for loader_num in "${!symengine_versions[@]}"; do + loader_venv="$symengine_venv_prefix$loader_num" + loader_version="$(< "$symengine_files_prefix$loader_num/SYMENGINE_VERSION")" + for generator_num in "${!symengine_versions[@]}"; do + generator_files="$symengine_files_prefix$generator_num" + generator_version="$(< "$generator_files/SYMENGINE_VERSION")" + echo "Using symengine==$loader_version to load files generated with symengine==$generator_version" + pushd "$generator_files" + "$loader_venv/bin/python" "$our_dir/test_qpy.py" load --version="$dev_version" + popd + done +done diff --git a/test/utils/base.py b/test/utils/base.py index dded6bbda2b2..133666cfc7ad 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -176,6 +176,27 @@ def setUpClass(cls): module="qiskit.providers.fake_provider.fake_backend", ) + warnings.filterwarnings( + "default", + category=DeprecationWarning, + message=r".*The property.*condition.*is deprecated.*", + module="qiskit_aer", + ) + + # Remove with the condition attribute in 2.0: + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*The property.*condition.*is deprecated.*", + module="qiskit.visualization", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*The property.*condition_bits.*is deprecated.*", + module="qiskit.transpiler.passes.scheduling", + ) + allow_DeprecationWarning_message = [ r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", ] diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index 4da725c4b5d6..b51b61cb7a90 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -343,7 +343,8 @@ def test_conditional(self): # check gates are shifted over accordingly circuit.h(qr) circuit.measure(qr, cr) - circuit.h(qr[0]).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr, 2) fname = "reg_conditional.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -366,8 +367,10 @@ def test_bit_conditional_with_cregbundle(self): circuit.x(qr[0]) circuit.measure(qr, cr) - circuit.h(qr[0]).c_if(cr[0], 1) - circuit.x(qr[1]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[1]).c_if(cr[1], 0) fname = "bit_conditional_bundle.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -390,8 +393,10 @@ def test_bit_conditional_no_cregbundle(self): circuit.x(qr[0]) circuit.measure(qr, cr) - circuit.h(qr[0]).c_if(cr[0], 1) - circuit.x(qr[1]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[0]).c_if(cr[0], 1) + with self.assertWarns(DeprecationWarning): + circuit.x(qr[1]).c_if(cr[1], 0) fname = "bit_conditional_no_bundle.png" self.circuit_drawer(circuit, output="mpl", filename=fname, cregbundle=False) @@ -1077,7 +1082,8 @@ def test_meas_condition(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[0]) - circuit.h(qr[1]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr, 1) fname = "meas_condition.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -1103,7 +1109,8 @@ def test_reverse_bits_condition(self): circuit.x(0) circuit.x(0) circuit.measure(2, 1) - circuit.x(2).c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2).c_if(cr, 2) fname = "reverse_bits_cond_true.png" self.circuit_drawer( @@ -1398,8 +1405,10 @@ def test_measures_with_conditions(self): circuit.h(0) circuit.h(1) circuit.measure(0, cr1[1]) - circuit.measure(1, cr2[0]).c_if(cr1, 1) - circuit.h(0).c_if(cr2, 3) + with self.assertWarns(DeprecationWarning): + circuit.measure(1, cr2[0]).c_if(cr1, 1) + with self.assertWarns(DeprecationWarning): + circuit.h(0).c_if(cr2, 3) fname = "measure_cond_false.png" self.circuit_drawer(circuit, output="mpl", cregbundle=False, filename=fname) @@ -1431,7 +1440,8 @@ def test_conditions_measures_with_bits(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(3, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(crx[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(crx[1], 0) circuit.measure(0, bits[3]) fname = "measure_cond_bits_false.png" @@ -1465,8 +1475,10 @@ def test_conditional_gates_right_of_measures_with_bits(self): circuit = QuantumCircuit(qr, cr) circuit.h(qr[0]) circuit.measure(qr[0], cr[1]) - circuit.h(qr[1]).c_if(cr[1], 0) - circuit.h(qr[2]).c_if(cr[0], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[1]).c_if(cr[1], 0) + with self.assertWarns(DeprecationWarning): + circuit.h(qr[2]).c_if(cr[0], 0) fname = "measure_cond_bits_right.png" self.circuit_drawer(circuit, output="mpl", cregbundle=False, filename=fname) @@ -1486,7 +1498,8 @@ def test_conditions_with_bits_reverse(self): cr = ClassicalRegister(2, "cr") crx = ClassicalRegister(2, "cs") circuit = QuantumCircuit(bits, cr, [Clbit()], crx) - circuit.x(0).c_if(bits[3], 0) + with self.assertWarns(DeprecationWarning): + circuit.x(0).c_if(bits[3], 0) fname = "cond_bits_reverse.png" self.circuit_drawer( @@ -1507,7 +1520,8 @@ def test_sidetext_with_condition(self): qr = QuantumRegister(2, "q") cr = ClassicalRegister(2, "c") circuit = QuantumCircuit(qr, cr) - circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) + with self.assertWarns(DeprecationWarning): + circuit.append(CPhaseGate(pi / 2), [qr[0], qr[1]]).c_if(cr[1], 1) fname = "sidetext_condition.png" self.circuit_drawer(circuit, output="mpl", cregbundle=False, filename=fname) @@ -1527,22 +1541,38 @@ def test_fold_with_conditions(self): cr = ClassicalRegister(5, "cr") circuit = QuantumCircuit(qr, cr) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 1) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 3) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 5) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 7) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 9) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 11) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 13) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 15) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 17) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 19) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 21) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 23) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 25) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 27) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 29) - circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 31) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 1) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 3) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 5) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 7) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 9) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 11) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 13) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 15) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 17) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 19) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 21) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 23) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 25) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 27) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 29) + with self.assertWarns(DeprecationWarning): + circuit.append(U1Gate(0).control(1), [1, 0]).c_if(cr, 31) fname = "fold_with_conditions.png" self.circuit_drawer(circuit, output="mpl", cregbundle=False, filename=fname) @@ -1583,7 +1613,8 @@ def test_wire_order(self): circuit.h(0) circuit.h(3) circuit.x(1) - circuit.x(3).c_if(cr, 10) + with self.assertWarns(DeprecationWarning): + circuit.x(3).c_if(cr, 10) fname = "wire_order.png" self.circuit_drawer( @@ -1732,14 +1763,16 @@ def test_if_else_with_body(self): circuit.measure(0, 1) circuit.measure(1, 2) circuit.x(2) - circuit.x(2, label="XLabel").c_if(cr, 2) + with self.assertWarns(DeprecationWarning): + circuit.x(2, label="XLabel").c_if(cr, 2) qr2 = QuantumRegister(3, "qr2") qc2 = QuantumCircuit(qr2, cr) qc2.x(1) qc2.y(1) qc2.z(0) - qc2.x(0, label="X1i").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + qc2.x(0, label="X1i").c_if(cr, 4) circuit.if_else((cr[1], 1), qc2, None, [0, 1, 2], [0, 1, 2]) circuit.x(0, label="X1i") @@ -1764,7 +1797,8 @@ def test_if_else_op_nested(self): circuit.h(0) with circuit.if_test((cr[1], 1)) as _else: - circuit.x(0, label="X c_if").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + circuit.x(0, label="X c_if").c_if(cr, 4) with circuit.if_test((cr[2], 1)): circuit.z(0) circuit.y(1) @@ -1805,7 +1839,8 @@ def test_if_else_op_wire_order(self): circuit.h(0) with circuit.if_test((cr[1], 1)) as _else: - circuit.x(0, label="X c_if").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + circuit.x(0, label="X c_if").c_if(cr, 4) with circuit.if_test((cr[2], 1)): circuit.z(0) circuit.y(1) @@ -1852,7 +1887,8 @@ def test_if_else_op_fold(self): circuit.h(0) with circuit.if_test((cr[1], 1)) as _else: - circuit.x(0, label="X c_if").c_if(cr, 4) + with self.assertWarns(DeprecationWarning): + circuit.x(0, label="X c_if").c_if(cr, 4) with circuit.if_test((cr[2], 1)): circuit.z(0) circuit.y(1) diff --git a/tools/build_standard_commutations.py b/tools/build_standard_commutations.py index 31f1fe03822b..2e13d741c93b 100644 --- a/tools/build_standard_commutations.py +++ b/tools/build_standard_commutations.py @@ -19,10 +19,19 @@ import itertools from functools import lru_cache from typing import List -from qiskit.circuit import Gate, CommutationChecker + import qiskit.circuit.library.standard_gates as stdg +from qiskit.circuit import CommutationChecker, Gate +from qiskit.circuit.library import PauliGate from qiskit.dagcircuit import DAGOpNode +SUPPORTED_ROTATIONS = { + "rxx": PauliGate("XX"), + "ryy": PauliGate("YY"), + "rzz": PauliGate("ZZ"), + "rzx": PauliGate("XZ"), +} + @lru_cache(maxsize=10**3) def _persistent_id(op_name: str) -> int: @@ -83,7 +92,6 @@ def _get_relative_placement(first_qargs, second_qargs) -> tuple: return tuple(qubits_g2.get(q_g0, None) for q_g0 in first_qargs) -@lru_cache(None) def _get_unparameterizable_gates() -> List[Gate]: """Retrieve a list of non-parmaterized gates with up to 3 qubits, using the python inspection module Return: @@ -95,6 +103,17 @@ def _get_unparameterizable_gates() -> List[Gate]: return [g for g in gates if len(g.params) == 0] +def _get_rotation_gates() -> List[Gate]: + """Retrieve a list of parmaterized gates we know the commutation relations of with up + to 3 qubits, using the python inspection module + Return: + A list of parameterized gates(that we know how to commute) to also be considered + in the commutation library + """ + gates = list(stdg.get_standard_gate_name_mapping().values()) + return [g for g in gates if g.name in SUPPORTED_ROTATIONS] + + def _generate_commutation_dict(considered_gates: List[Gate] = None) -> dict: """Compute the commutation relation of considered gates @@ -110,7 +129,11 @@ def _generate_commutation_dict(considered_gates: List[Gate] = None) -> dict: cc = CommutationChecker() for gate0 in considered_gates: - node0 = DAGOpNode(op=gate0, qargs=list(range(gate0.num_qubits)), cargs=[]) + node0 = DAGOpNode( + op=SUPPORTED_ROTATIONS.get(gate0.name, gate0), + qargs=list(range(gate0.num_qubits)), + cargs=[], + ) for gate1 in considered_gates: # only consider canonical entries @@ -143,12 +166,16 @@ def _generate_commutation_dict(considered_gates: List[Gate] = None) -> dict: gate1_qargs.append(next_non_overlapping_qubit_idx) next_non_overlapping_qubit_idx += 1 - node1 = DAGOpNode(op=gate1, qargs=gate1_qargs, cargs=[]) + node1 = DAGOpNode( + op=SUPPORTED_ROTATIONS.get(gate1.name, gate1), + qargs=gate1_qargs, + cargs=[], + ) # replace non-overlapping qubits with None to act as a key in the commutation library relative_placement = _get_relative_placement(node0.qargs, node1.qargs) - if not gate0.is_parameterized() and not gate1.is_parameterized(): + if not node0.op.is_parameterized() and not node1.op.is_parameterized(): # if no gate includes parameters, compute commutation relation using # matrix multiplication op1 = node0.op @@ -219,6 +246,7 @@ def _dump_commuting_dict_as_python( cgates = [ g for g in _get_unparameterizable_gates() if g.name not in ["reset", "measure", "delay"] ] + cgates += _get_rotation_gates() commutation_dict = _generate_commutation_dict(considered_gates=cgates) commutation_dict = _simplify_commuting_dict(commutation_dict) _dump_commuting_dict_as_python(commutation_dict)