diff --git a/.azure/docs-linux.yml b/.azure/docs-linux.yml index 0f8e3015c0eb..83026963219d 100644 --- a/.azure/docs-linux.yml +++ b/.azure/docs-linux.yml @@ -30,8 +30,8 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel -c constraints.txt - pip install -U tox -c constraints.txt + python -m pip install --upgrade pip setuptools wheel + pip install -U tox sudo apt-get update sudo apt-get install -y graphviz displayName: 'Install dependencies' diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index 5fddd6afef3f..fef32e8acdb7 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -28,13 +28,13 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -U -c constraints.txt -e . pip install -U "qiskit-aer" -c constraints.txt - python setup.py build_ext --inplace + pip install -e . displayName: 'Install dependencies' env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index 14d1c090f822..51a9afc90880 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -62,7 +62,7 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job displayName: "Prepare venv" @@ -71,6 +71,8 @@ jobs: set -e source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt + # Install setuptools-rust for building sdist + pip install -U -c constraints.txt setuptools-rust python setup.py sdist pip install -U -c constraints.txt dist/qiskit-terra*.tar.gz displayName: "Install Terra from sdist" @@ -85,7 +87,6 @@ jobs: env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - - bash: | set -e source test-job/bin/activate @@ -111,6 +112,7 @@ jobs: popd env: QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: 'Run tests' - bash: | diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 4417dd8add68..2219d853702a 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -6,7 +6,7 @@ parameters: jobs: - job: "MacOS_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test macOS Python ${{ parameters.pythonVersion }}" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} variables: QISKIT_SUPPRESS_PACKAGING_WARNINGS: Y @@ -41,12 +41,11 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -U -c constraints.txt -e . - python setup.py build_ext --inplace pip check displayName: 'Install dependencies' env: @@ -61,6 +60,7 @@ jobs: python ./tools/verify_parallel_map.py env: QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: "Run tests" - bash: | diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 8140a6ce4fbb..14753a78ed5d 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -30,13 +30,12 @@ jobs: - bash: | set -e - python -m pip install --upgrade pip 'setuptools<64.0.0' wheel virtualenv -c constraints.txt + python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/Scripts/activate pip install -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -c constraints.txt -e . pip install "z3-solver" -c constraints.txt - python setup.py build_ext --inplace pip check displayName: 'Install dependencies' env: @@ -54,6 +53,7 @@ jobs: LANG: 'C.UTF-8' PYTHONIOENCODING: 'utf-8:backslashreplace' QISKIT_PARALLEL: FALSE + RUST_BACKTRACE: 1 displayName: 'Run tests' - bash: | diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml index 2ce7cb9026c8..fe49db79bf1d 100644 --- a/.azure/tutorials-linux.yml +++ b/.azure/tutorials-linux.yml @@ -34,7 +34,6 @@ jobs: pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt pip install -c constraints.txt -e . pip install "qiskit-ibmq-provider" "qiskit-aer" "z3-solver" "qiskit-ignis" "matplotlib>=3.3.0" sphinx nbsphinx sphinx_rtd_theme cvxpy -c constraints.txt - python setup.py build_ext --inplace sudo apt-get update sudo apt-get install -y graphviz pandoc pip check diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index f96f84e0ed78..4c9f8e0f86e7 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -24,6 +24,8 @@ jobs: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run randomized tests run: make test_randomized + env: + RUST_BACKTRACE: 1 - name: Create comment on failed test run if: ${{ failure() }} uses: peter-evans/create-or-update-comment@v1 diff --git a/Cargo.lock b/Cargo.lock index 9283953439c7..eb2736a8a7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -90,9 +90,15 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "getrandom" @@ -105,6 +111,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", + "rayon", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -131,27 +147,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "rayon", ] [[package]] name = "indoc" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e" +checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "lock_api" @@ -260,9 +276,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "parking_lot" @@ -287,6 +303,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -295,21 +321,21 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" +checksum = "0220c44442c9b239dd4357aa856ac468a4f5e1f0df19ddb89b2522952eb4c6ca" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "indoc", "libc", "num-bigint", @@ -323,9 +349,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +checksum = "9c819d397859445928609d0ec5afc2da5204e0d0f73d6bf9e153b04e83c9cdc2" dependencies = [ "once_cell", "target-lexicon", @@ -333,9 +359,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +checksum = "ca882703ab55f54702d7bfe1189b41b0af10272389f04cae38fe4cd56c65f75f" dependencies = [ "libc", "pyo3-build-config", @@ -343,9 +369,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" +checksum = "568749402955ad7be7bad9a09b8593851cd36e549ac90bfd44079cea500f3f21" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -355,9 +381,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.5" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" +checksum = "611f64e82d98f447787e82b8e7b0ebc681e1eb78fc1252668b2c605ffb4e1eb8" dependencies = [ "proc-macro2", "quote", @@ -369,7 +395,7 @@ name = "qiskit-terra" version = "0.22.0" dependencies = [ "ahash 0.8.0", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "ndarray", "num-bigint", @@ -380,13 +406,14 @@ dependencies = [ "rand_distr", "rand_pcg", "rayon", + "retworkx-core", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -472,13 +499,26 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] +[[package]] +name = "retworkx-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353bcdcdab6c754ea32bce39ee7a763c8a3c16c91a8dd648befd14fbcb0d5b68" +dependencies = [ + "ahash 0.7.6", + "hashbrown 0.11.2", + "indexmap", + "petgraph", + "rayon", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -493,9 +533,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", @@ -510,15 +550,15 @@ checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unindent" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" +checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 881f7bd09a42..ab8cbb9fcc6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ rand_distr = "0.4.3" ahash = "0.8.0" num-complex = "0.4" num-bigint = "0.4" +retworkx-core = "0.11" [dependencies.pyo3] -version = "0.16.5" +version = "0.16.6" features = ["extension-module", "hashbrown", "num-complex", "num-bigint"] [dependencies.ndarray] diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a0947861a159..de6655e02348 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -212,12 +212,12 @@ stages: - template: ".azure/wheels.yml" parameters: jobName: "macos" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} - template: ".azure/wheels.yml" parameters: jobName: "macos_arm" - pool: {vmImage: 'macOS-10.15'} + pool: {vmImage: 'macOS-11'} env: CIBW_BEFORE_ALL: rustup target add aarch64-apple-darwin CIBW_ARCHS_MACOS: arm64 universal2 diff --git a/constraints.txt b/constraints.txt index bf4e304596c5..3cb8598cb345 100644 --- a/constraints.txt +++ b/constraints.txt @@ -13,7 +13,3 @@ pyparsing<3.0.0 # to work with the new jinja version (the jinja maintainers aren't going to # fix things) pin to the previous working version. jinja2==3.0.3 - -# setuptools 64.0.0 breaks editable installs. Pin to an old version until -# see https://github.com/pypa/setuptools/issues/3498 -setuptools==63.3.0 diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index 5b765c7c95e7..9154a9420fc6 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -36,6 +36,7 @@ Qiskit Terra API Reference transpiler_passes transpiler_preset transpiler_plugins + transpiler_synthesis_plugins transpiler_builtin_plugins utils utils_mitigation diff --git a/docs/apidocs/transpiler_plugins.rst b/docs/apidocs/transpiler_plugins.rst index 33646fe2d58f..b5de3efc8ff6 100644 --- a/docs/apidocs/transpiler_plugins.rst +++ b/docs/apidocs/transpiler_plugins.rst @@ -1,6 +1,6 @@ .. _qiskit-transpiler-plugins: -.. automodule:: qiskit.transpiler.passes.synthesis.plugin +.. automodule:: qiskit.transpiler.preset_passmanagers.plugin :no-members: :no-inherited-members: :no-special-members: diff --git a/docs/apidocs/transpiler_synthesis_plugins.rst b/docs/apidocs/transpiler_synthesis_plugins.rst new file mode 100644 index 000000000000..70bef1190f40 --- /dev/null +++ b/docs/apidocs/transpiler_synthesis_plugins.rst @@ -0,0 +1,6 @@ +.. _qiskit-transpiler-synthesis-plugins: + +.. automodule:: qiskit.transpiler.passes.synthesis.plugin + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/pyproject.toml b/pyproject.toml index 728e2a2be55a..9282c3b8c8d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools-rust<1.5.0"] +requires = ["setuptools", "wheel", "setuptools-rust"] build-backend = "setuptools.build_meta" [tool.black] diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 63e44798a710..966986f3b7ec 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -94,6 +94,17 @@ VQD +Variational Quantum Time Evolution +---------------------------------- + +Classes used by variational quantum time evolution algorithms - VarQITE and VarQRTE. + +.. autosummary:: + :toctree: ../stubs/ + + evolvers.variational + + Evolvers -------- @@ -108,11 +119,14 @@ RealEvolver ImaginaryEvolver TrotterQRTE + VarQITE + VarQRTE PVQD PVQDResult EvolutionResult EvolutionProblem + Factorizers ----------- @@ -187,8 +201,18 @@ IterativePhaseEstimation +State Fidelities +---------------- + +Algorithms that compute the fidelity of pairs of quantum states. + +.. autosummary:: + :toctree: ../stubs/ + + state_fidelities + Exceptions -========== +---------- .. autosummary:: :toctree: ../stubs/ @@ -205,8 +229,19 @@ :toctree: ../stubs/ eval_observables -""" +Utility classes +--------------- + +Utility classes used by algorithms (mainly for type-hinting purposes). + +.. autosummary:: + :toctree: ../stubs/ + + AlgorithmJob + +""" +from .algorithm_job import AlgorithmJob from .algorithm_result import AlgorithmResult from .evolvers import EvolutionResult, EvolutionProblem from .evolvers.real_evolver import RealEvolver @@ -248,9 +283,13 @@ from .exceptions import AlgorithmError from .aux_ops_evaluator import eval_observables from .evolvers.trotterization import TrotterQRTE +from .evolvers.variational.var_qite import VarQITE +from .evolvers.variational.var_qrte import VarQRTE + from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ + "AlgorithmJob", "AlgorithmResult", "VariationalAlgorithm", "VariationalResult", @@ -273,6 +312,8 @@ "RealEvolver", "ImaginaryEvolver", "TrotterQRTE", + "VarQITE", + "VarQRTE", "EvolutionResult", "EvolutionProblem", "LinearSolverResult", diff --git a/qiskit/algorithms/algorithm_job.py b/qiskit/algorithms/algorithm_job.py new file mode 100644 index 000000000000..16db4df93dfc --- /dev/null +++ b/qiskit/algorithms/algorithm_job.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +AlgorithmJob class +""" +from qiskit.primitives.primitive_job import PrimitiveJob + + +class AlgorithmJob(PrimitiveJob): + """ + This empty class is introduced for typing purposes. + """ + + pass diff --git a/qiskit/algorithms/eigen_solvers/vqd.py b/qiskit/algorithms/eigen_solvers/vqd.py index d5070fad4367..29d237820273 100644 --- a/qiskit/algorithms/eigen_solvers/vqd.py +++ b/qiskit/algorithms/eigen_solvers/vqd.py @@ -108,10 +108,11 @@ def __init__( Args: ansatz: A parameterized circuit used as ansatz for the wave function. k: the number of eigenvalues to return. Returns the lowest k eigenvalues. - betas: beta parameter in the VQD paper. Should have size k -1, the number of excited states. - It is a hyperparameter that balances the contribution of the overlap - term to the cost function and has a default value computed as - mean square sum of coefficients of observable. + betas: beta parameters in the VQD paper. + Should have length k - 1, with k the number of excited states. + These hyperparameters balance the contribution of each overlap term to the cost + function and have a default value computed as the mean square sum of the + coefficients of the observable. optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable that takes an array as input and returns a Qiskit or SciPy optimization result. initial_point: An optional initial point (i.e. initial parameter values) @@ -141,8 +142,8 @@ def __init__( callback: a callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. - These are: the evaluation count, the optimizer parameters for the - ansatz, the evaluated mean and the evaluated standard deviation.` + These are: the evaluation count, the optimizer parameters for the ansatz, the + evaluated mean, the evaluated standard deviation, and the current step. quantum_instance: Quantum Instance or Backend """ @@ -264,12 +265,12 @@ def include_custom(self, include_custom: bool): self.expectation = None @property - def callback(self) -> Optional[Callable[[int, np.ndarray, float, float], None]]: + def callback(self) -> Optional[Callable[[int, np.ndarray, float, float, int], None]]: """Returns callback""" return self._callback @callback.setter - def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float], None]]): + def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float, int], None]]): """Sets callback""" self._callback = callback @@ -475,7 +476,8 @@ def _eval_aux_ops( aux_op_results = zip(aux_op_means, std_devs) # Return None eigenvalues for None operators if aux_operators is a list. - # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. + # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a + # dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) @@ -608,7 +610,8 @@ def compute_eigenvalues( if step == 1: logger.info( - "Ground state optimization complete in %s seconds.\nFound opt_params %s in %s evals", + "Ground state optimization complete in %s seconds.\n" + "Found opt_params %s in %s evals", eval_time, result.optimal_point, self._eval_count, @@ -616,7 +619,8 @@ def compute_eigenvalues( else: logger.info( ( - "%s excited state optimization complete in %s s.\nFound opt_parms %s in %s evals" + "%s excited state optimization complete in %s s.\n" + "Found opt_params %s in %s evals" ), str(step - 1), eval_time, @@ -624,7 +628,7 @@ def compute_eigenvalues( self._eval_count, ) - # To match the siignature of NumpyEigenSolver Result + # To match the signature of NumpyEigenSolver Result result.eigenstates = ListOp([StateFn(vec) for vec in result.eigenstates]) result.eigenvalues = np.array(result.eigenvalues) result.optimal_point = np.array(result.optimal_point) @@ -649,7 +653,7 @@ def get_energy_evaluation( This return value is the objective function to be passed to the optimizer for evaluation. Args: - step: level of enegy being calculated. 0 for ground, 1 for first excited state and so on. + step: level of energy being calculated. 0 for ground, 1 for first excited state... operator: The operator whose energy to evaluate. return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to evaluate other @@ -695,22 +699,29 @@ def get_energy_evaluation( def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) - # Create dict associating each parameter with the lists of parameterization values for it + # Dict associating each parameter with the lists of parameterization values for it param_bindings = dict(zip(ansatz_params, parameter_sets.transpose().tolist())) sampled_expect_op = self._circuit_sampler.convert(expect_op, params=param_bindings) - mean = np.real(sampled_expect_op.eval()) + means = np.real(sampled_expect_op.eval()) for state in range(step - 1): sampled_final_op = self._circuit_sampler.convert( overlap_op[state], params=param_bindings ) cost = sampled_final_op.eval() - mean += np.real(self.betas[state] * np.conj(cost) * cost) - - self._eval_count += len(mean) + means += np.real(self.betas[state] * np.conj(cost) * cost) + + if self._callback is not None: + variance = np.real(expectation.compute_variance(sampled_expect_op)) + estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) + for i, param_set in enumerate(parameter_sets): + self._eval_count += 1 + self._callback(self._eval_count, param_set, means[i], estimator_error[i], step) + else: + self._eval_count += len(means) - return mean if len(mean) > 1 else mean[0] + return means if len(means) > 1 else means[0] if return_expectation: return energy_evaluation, expectation diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index e0f9fe3063c6..175beecd6bf6 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -35,7 +35,7 @@ def __init__( aux_operators: Optional[ListOrDict[OperatorBase]] = None, truncation_threshold: float = 1e-12, t_param: Optional[Parameter] = None, - hamiltonian_value_dict: Optional[Dict[Parameter, complex]] = None, + param_value_dict: Optional[Dict[Parameter, complex]] = None, ): """ Args: @@ -50,15 +50,15 @@ def __init__( Used when ``aux_operators`` is provided. t_param: Time parameter in case of a time-dependent Hamiltonian. This free parameter must be within the ``hamiltonian``. - hamiltonian_value_dict: If the Hamiltonian contains free parameters, this - dictionary maps all these parameters to values. + param_value_dict: Maps free parameters in the problem to values. Depending on the + algorithm, it might refer to e.g. a Hamiltonian or an initial state. Raises: ValueError: If non-positive time of evolution is provided. """ self.t_param = t_param - self.hamiltonian_value_dict = hamiltonian_value_dict + self.param_value_dict = param_value_dict self.hamiltonian = hamiltonian self.time = time self.initial_state = initial_state @@ -95,9 +95,9 @@ def validate_params(self) -> None: if self.t_param is not None: t_param_set.add(self.t_param) hamiltonian_dict_param_set = set() - if self.hamiltonian_value_dict is not None: + if self.param_value_dict is not None: hamiltonian_dict_param_set = hamiltonian_dict_param_set.union( - set(self.hamiltonian_value_dict.keys()) + set(self.param_value_dict.keys()) ) params_set = t_param_set.union(hamiltonian_dict_param_set) hamiltonian_param_set = set(self.hamiltonian.parameters) diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py index abe02e95c156..05b8266605b7 100644 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py @@ -187,7 +187,7 @@ def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: f"PauliSumOp | SummedOp, {type(hamiltonian)} provided." ) if isinstance(hamiltonian, OperatorBase): - hamiltonian = hamiltonian.bind_parameters(evolution_problem.hamiltonian_value_dict) + hamiltonian = hamiltonian.bind_parameters(evolution_problem.param_value_dict) if isinstance(hamiltonian, SummedOp): hamiltonian = self._summed_op_to_pauli_sum_op(hamiltonian) # the evolution gate diff --git a/qiskit/algorithms/evolvers/variational/__init__.py b/qiskit/algorithms/evolvers/variational/__init__.py new file mode 100644 index 000000000000..8936d9030853 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/__init__.py @@ -0,0 +1,139 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Variational Quantum Time Evolutions (:mod:`qiskit.algorithms.evolvers.variational`) +=================================================================================== + +Algorithms for performing Variational Quantum Time Evolution of quantum states, +which can be tailored to near-term devices. +:class:`~qiskit.algorithms.evolvers.variational.VarQTE` base class exposes an interface, compliant +with the Quantum Time Evolution Framework in Qiskit Terra, that is implemented by +:class:`~qiskit.algorithms.VarQRTE` and :class:`~qiskit.algorithms.VarQITE` classes for real and +imaginary time evolution respectively. The variational approach is taken according to a variational +principle chosen by a user. + +Examples: + +.. code-block:: + + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + + # define a Hamiltonian + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + # define a parametrized initial state to be evolved + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + + # define values of initial parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + + # define a variational principle + var_principle = ImaginaryMcLachlanPrinciple() + + # optionally define a backend + backend = BasicAer.get_backend("statevector_simulator") + + # define evolution time + time = 1 + + # define evolution problem + evolution_problem = EvolutionProblem(observable, time) + + # instantiate the algorithm + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + + # run the algorithm/evolve the state + evolution_result = var_qite.evolve(evolution_problem) + +.. currentmodule:: qiskit.algorithms.evolvers.variational + +Variational Principles +---------------------- + +Variational principles can be used to simulate quantum time evolution by propagating the parameters +of a parameterized quantum circuit. + +They can be divided into two categories: + + 1) Variational Quantum Imaginary Time Evolution + Given a Hamiltonian, a time and a variational ansatz, the variational principle describes a + variational principle according to the normalized Wick-rotated Schroedinger equation. + + 2) Variational Quantum Real Time Evolution + Given a Hamiltonian, a time and a variational ansatz, the variational principle describes a + variational principle according to the Schroedinger equation. + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VariationalPrinciple + RealVariationalPrinciple + ImaginaryVariationalPrinciple + RealMcLachlanPrinciple + ImaginaryMcLachlanPrinciple + +ODE solvers +----------- +ODE solvers that implement the SciPy ODE Solver interface. The Forward Euler Solver is +a preferred choice in the presence of noise. One might also use solvers provided by SciPy directly, +e.g. RK45. + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + ForwardEulerSolver + +""" +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .var_qte import VarQTE +from .variational_principles.variational_principle import VariationalPrinciple +from .variational_principles import RealVariationalPrinciple, ImaginaryVariationalPrinciple +from .variational_principles.imaginary_mc_lachlan_principle import ( + ImaginaryMcLachlanPrinciple, +) +from .variational_principles.real_mc_lachlan_principle import ( + RealMcLachlanPrinciple, +) + + +__all__ = [ + "ForwardEulerSolver", + "VarQTE", + "VariationalPrinciple", + "RealVariationalPrinciple", + "ImaginaryVariationalPrinciple", + "RealMcLachlanPrinciple", + "ImaginaryMcLachlanPrinciple", +] diff --git a/qiskit/algorithms/evolvers/variational/solvers/__init__.py b/qiskit/algorithms/evolvers/variational/solvers/__init__.py new file mode 100644 index 000000000000..6d273c0618c9 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/__init__.py @@ -0,0 +1,44 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Solvers (:mod:`qiskit.algorithms.evolvers.variational.solvers`) +=============================================================== + +This package contains the necessary classes to solve systems of equations arising in the +Variational Quantum Time Evolution. They include ordinary differential equations (ODE) which +describe ansatz parameter propagation and systems of linear equations. + + +Systems of Linear Equations Solver +---------------------------------- + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VarQTELinearSolver + + +ODE Solver +---------- +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/class_no_inherited_members.rst + + VarQTEOdeSolver +""" + +from qiskit.algorithms.evolvers.variational.solvers.ode.var_qte_ode_solver import VarQTEOdeSolver +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import VarQTELinearSolver + +__all__ = ["VarQTELinearSolver", "VarQTEOdeSolver"] diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py b/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py new file mode 100644 index 000000000000..8fbaef6f85d1 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""ODE Solvers""" diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py b/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py new file mode 100644 index 000000000000..a443a26d1888 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/abstract_ode_function.py @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Abstract class for generating ODE functions.""" + +from abc import ABC, abstractmethod +from typing import Iterable, Dict, Optional +from qiskit.circuit import Parameter +from ..var_qte_linear_solver import ( + VarQTELinearSolver, +) + + +class AbstractOdeFunction(ABC): + """Abstract class for generating ODE functions.""" + + def __init__( + self, + varqte_linear_solver: VarQTELinearSolver, + error_calculator, + param_dict: Dict[Parameter, complex], + t_param: Optional[Parameter] = None, + ) -> None: + + self._varqte_linear_solver = varqte_linear_solver + self._error_calculator = error_calculator + self._param_dict = param_dict + self._t_param = t_param + + @abstractmethod + def var_qte_ode_function(self, time: float, parameters_values: Iterable) -> Iterable: + """ + Evaluates an ODE function for a given time and parameter values. It is used by an ODE + solver. + + Args: + time: Current time of evolution. + parameters_values: Current values of parameters. + + Returns: + ODE gradient arising from solving a system of linear equations. + """ + pass diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py b/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py new file mode 100644 index 000000000000..284b3106fa8a --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/forward_euler_solver.py @@ -0,0 +1,73 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Forward Euler ODE solver.""" + +from typing import Sequence + +import numpy as np +from scipy.integrate import OdeSolver +from scipy.integrate._ivp.base import ConstantDenseOutput + + +class ForwardEulerSolver(OdeSolver): + """Forward Euler ODE solver.""" + + def __init__( + self, + function: callable, + t0: float, + y0: Sequence, + t_bound: float, + vectorized: bool = False, + support_complex: bool = False, + num_t_steps: int = 15, + ): + """ + Forward Euler ODE solver that implements an interface from SciPy. + + Args: + function: Right-hand side of the system. The calling signature is ``fun(t, y)``. Here + ``t`` is a scalar, and there are two options for the ndarray ``y``: + It can either have shape (n,); then ``fun`` must return array_like with + shape (n,). Alternatively it can have shape (n, k); then ``fun`` + must return an array_like with shape (n, k), i.e., each column + corresponds to a single column in ``y``. The choice between the two + options is determined by `vectorized` argument (see below). The + vectorized implementation allows a faster approximation of the Jacobian + by finite differences (required for this solver). + t0: Initial time. + y0: Initial state. + t_bound: Boundary time - the integration won't continue beyond it. It also determines + the direction of the integration. + vectorized: Whether ``fun`` is implemented in a vectorized fashion. Default is False. + support_complex: Whether integration in a complex domain should be supported. + Generally determined by a derived solver class capabilities. Default is False. + num_t_steps: Number of time steps for the forward Euler method. + """ + self.y_old = None + self.step_length = (t_bound - t0) / num_t_steps + super().__init__(function, t0, y0, t_bound, vectorized, support_complex) + + def _step_impl(self): + """ + Takes an Euler step. + """ + try: + self.y_old = self.y + self.y = list(np.add(self.y, self.step_length * self.fun(self.t, self.y))) + self.t += self.step_length + return True, None + except Exception as ex: # pylint: disable=broad-except + return False, f"Unknown ODE solver error: {str(ex)}." + + def _dense_output_impl(self): + return ConstantDenseOutput(self.t_old, self.t, self.y_old) diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py new file mode 100644 index 000000000000..0d142262868c --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function.py @@ -0,0 +1,43 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for generating ODE functions based on ODE gradients.""" +from typing import Iterable + +from ..ode.abstract_ode_function import ( + AbstractOdeFunction, +) + + +class OdeFunction(AbstractOdeFunction): + """Class for generating ODE functions based on ODE gradients.""" + + def var_qte_ode_function(self, time: float, parameters_values: Iterable) -> Iterable: + """ + Evaluates an ODE function for a given time and parameter values. It is used by an ODE + solver. + + Args: + time: Current time of evolution. + parameters_values: Current values of parameters. + + Returns: + ODE gradient arising from solving a system of linear equations. + """ + current_param_dict = dict(zip(self._param_dict.keys(), parameters_values)) + + ode_grad_res, _, _ = self._varqte_linear_solver.solve_lse( + current_param_dict, + time, + ) + + return ode_grad_res diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py new file mode 100644 index 000000000000..2dce88a817b1 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/ode_function_factory.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Abstract class for generating ODE functions.""" + +from abc import ABC +from enum import Enum +from typing import Dict, Any, Optional, Callable + +import numpy as np + +from qiskit.circuit import Parameter +from .abstract_ode_function import AbstractOdeFunction +from .ode_function import OdeFunction +from ..var_qte_linear_solver import ( + VarQTELinearSolver, +) + + +class OdeFunctionType(Enum): + """Types of ODE functions for VatQTE algorithms.""" + + # more will be supported in the near future + STANDARD_ODE = "STANDARD_ODE" + + +class OdeFunctionFactory(ABC): + """Factory for building ODE functions.""" + + def __init__( + self, + ode_function_type: OdeFunctionType = OdeFunctionType.STANDARD_ODE, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + ) -> None: + """ + Args: + ode_function_type: An Enum that defines a type of an ODE function to be built. If + not provided, a default ``STANDARD_ODE`` is used. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + """ + self.ode_function_type = ode_function_type + self.lse_solver = lse_solver + + def _build( + self, + varqte_linear_solver: VarQTELinearSolver, + error_calculator: Any, + param_dict: Dict[Parameter, complex], + t_param: Optional[Parameter] = None, + ) -> AbstractOdeFunction: + """ + Initializes an ODE function specified in the class. + + Args: + varqte_linear_solver: Solver of LSE for the VarQTE algorithm. + error_calculator: Calculator of errors for error-based ODE functions. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + t_param: Time parameter in case of a time-dependent Hamiltonian. + + Returns: + An ODE function. + + Raises: + ValueError: If unsupported ODE function provided. + + """ + if self.ode_function_type == OdeFunctionType.STANDARD_ODE: + return OdeFunction(varqte_linear_solver, error_calculator, param_dict, t_param) + raise ValueError( + f"Unsupported ODE function provided: {self.ode_function_type}." + f" Only {[tp.value for tp in OdeFunctionType]} are supported." + ) diff --git a/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py b/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py new file mode 100644 index 000000000000..525769ddc96c --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/ode/var_qte_ode_solver.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for solving ODEs for Quantum Time Evolution.""" +from functools import partial +from typing import List, Union, Type, Optional + +import numpy as np +from scipy.integrate import OdeSolver, solve_ivp + +from .abstract_ode_function import ( + AbstractOdeFunction, +) +from .forward_euler_solver import ForwardEulerSolver + + +class VarQTEOdeSolver: + """Class for solving ODEs for Quantum Time Evolution.""" + + def __init__( + self, + init_params: List[complex], + ode_function: AbstractOdeFunction, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + num_timesteps: Optional[int] = None, + ) -> None: + """ + Initialize ODE Solver. + + Args: + init_params: Set of initial parameters for time 0. + ode_function: Generates the ODE function. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + """ + self._init_params = init_params + self._ode_function = ode_function.var_qte_ode_function + self._ode_solver = ode_solver + self._num_timesteps = num_timesteps + + def run(self, evolution_time: float) -> List[complex]: + """ + Finds numerical solution with ODE Solver. + + Args: + evolution_time: Evolution time. + + Returns: + List of parameters found by an ODE solver for a given ODE function callable. + """ + # determine the number of timesteps and set the timestep + num_timesteps = ( + int(np.ceil(evolution_time / 0.01)) + if self._num_timesteps is None + else self._num_timesteps + ) + + if self._ode_solver == ForwardEulerSolver: + solve = partial(solve_ivp, num_t_steps=num_timesteps) + else: + solve = solve_ivp + + sol = solve( + self._ode_function, + (0, evolution_time), + self._init_params, + method=self._ode_solver, + ) + final_params_vals = [lst[-1] for lst in sol.y] + + return final_params_vals diff --git a/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py b/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py new file mode 100644 index 000000000000..1c4a61963374 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/solvers/var_qte_linear_solver.py @@ -0,0 +1,160 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for solving linear equations for Quantum Time Evolution.""" + +from typing import Union, List, Dict, Optional, Callable + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.algorithms.evolvers.variational.variational_principles.variational_principle import ( + VariationalPrinciple, +) +from qiskit.circuit import Parameter +from qiskit.opflow import ( + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.providers import Backend +from qiskit.utils import QuantumInstance +from qiskit.utils.backend_utils import is_aer_provider + + +class VarQTELinearSolver: + """Class for solving linear equations for Quantum Time Evolution.""" + + def __init__( + self, + var_principle: VariationalPrinciple, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + gradient_params: List[Parameter], + t_param: Optional[Parameter] = None, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + imag_part_tol: float = 1e-7, + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + """ + Args: + var_principle: Variational Principle to be used. + hamiltonian: + Operator used for Variational Quantum Time Evolution. + The operator may be given either as a composed op consisting of a Hermitian + observable and a ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a + ``ComboFn``. + The latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + gradient_params: List of parameters with respect to which gradients should be computed. + t_param: Time parameter in case of a time-dependent Hamiltonian. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + expectation: An instance of ``ExpectationBase`` used for calculating a metric tensor + and an evolution gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + self._var_principle = var_principle + self._hamiltonian = hamiltonian + self._ansatz = ansatz + self._gradient_params = gradient_params + self._bind_params = gradient_params + [t_param] if t_param else gradient_params + self._time_param = t_param + self.lse_solver = lse_solver + self._quantum_instance = None + self._circuit_sampler = None + self._imag_part_tol = imag_part_tol + self._expectation = expectation + if quantum_instance is not None: + self.quantum_instance = quantum_instance + + @property + def lse_solver(self) -> Callable[[np.ndarray, np.ndarray], np.ndarray]: + """Returns an LSE solver callable.""" + return self._lse_solver + + @lse_solver.setter + def lse_solver( + self, lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] + ) -> None: + """Sets an LSE solver. Uses a ``np.linalg.lstsq`` callable if ``None`` provided.""" + if lse_solver is None: + lse_solver = lambda a, b: np.linalg.lstsq(a, b, rcond=1e-2)[0] + + self._lse_solver = lse_solver + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Returns quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: + """Sets quantum_instance""" + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + + self._quantum_instance = quantum_instance + self._circuit_sampler = CircuitSampler( + quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) + ) + + def solve_lse( + self, + param_dict: Dict[Parameter, complex], + time_value: Optional[float] = None, + ) -> (Union[List, np.ndarray], Union[List, np.ndarray], np.ndarray): + """ + Solve the system of linear equations underlying McLachlan's variational principle for the + calculation without error bounds. + + Args: + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + time_value: Time value that will be bound to ``t_param``. It is required if ``t_param`` + is not ``None``. + + Returns: + Solution to the LSE, A from Ax=b, b from Ax=b. + """ + param_values = list(param_dict.values()) + if self._time_param is not None: + param_values.append(time_value) + + metric_tensor_lse_lhs = self._var_principle.metric_tensor( + self._ansatz, + self._bind_params, + self._gradient_params, + param_values, + self._expectation, + self._quantum_instance, + ) + evolution_grad_lse_rhs = self._var_principle.evolution_grad( + self._hamiltonian, + self._ansatz, + self._circuit_sampler, + param_dict, + self._bind_params, + self._gradient_params, + param_values, + self._expectation, + self._quantum_instance, + ) + + x = self._lse_solver(metric_tensor_lse_lhs, evolution_grad_lse_rhs) + + return np.real(x), metric_tensor_lse_lhs, evolution_grad_lse_rhs diff --git a/qiskit/algorithms/evolvers/variational/var_qite.py b/qiskit/algorithms/evolvers/variational/var_qite.py new file mode 100644 index 000000000000..5d53cc1eef63 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qite.py @@ -0,0 +1,125 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Variational Quantum Imaginary Time Evolution algorithm.""" +from typing import Optional, Union, Type, Callable, List, Dict + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ExpectationBase, OperatorBase +from qiskit.algorithms.evolvers.imaginary_evolver import ImaginaryEvolver +from qiskit.utils import QuantumInstance +from . import ImaginaryMcLachlanPrinciple +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .variational_principles import ImaginaryVariationalPrinciple +from .var_qte import VarQTE + + +class VarQITE(VarQTE, ImaginaryEvolver): + """Variational Quantum Imaginary Time Evolution algorithm. + + .. code-block::python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.ordered_parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = ImaginaryMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: Optional[ImaginaryVariationalPrinciple] = None, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. Defaults to + ``ImaginaryMcLachlanPrinciple``. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on NumPy matrix multiplication + (which might be slow for larger numbers of qubits). + """ + if variational_principle is None: + variational_principle = ImaginaryMcLachlanPrinciple() + super().__init__( + ansatz, + variational_principle, + initial_parameters, + ode_solver, + lse_solver=lse_solver, + num_timesteps=num_timesteps, + expectation=expectation, + imag_part_tol=imag_part_tol, + num_instability_tol=num_instability_tol, + quantum_instance=quantum_instance, + ) diff --git a/qiskit/algorithms/evolvers/variational/var_qrte.py b/qiskit/algorithms/evolvers/variational/var_qrte.py new file mode 100644 index 000000000000..c0846a7159b7 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qrte.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Variational Quantum Real Time Evolution algorithm.""" +from typing import Optional, Union, Type, Callable, List, Dict + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.algorithms.evolvers.real_evolver import RealEvolver +from qiskit.circuit import Parameter +from qiskit.opflow import ExpectationBase, OperatorBase +from qiskit.utils import QuantumInstance +from . import RealMcLachlanPrinciple +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .variational_principles import RealVariationalPrinciple +from .var_qte import VarQTE + + +class VarQRTE(VarQTE, RealEvolver): + """Variational Quantum Real Time Evolution algorithm. + + .. code-block::python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = RealMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qrte = VarQRTE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: Optional[RealVariationalPrinciple] = None, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. Defaults to + ``RealMcLachlanPrinciple``. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be + non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + if variational_principle is None: + variational_principle = RealMcLachlanPrinciple() + super().__init__( + ansatz, + variational_principle, + initial_parameters, + ode_solver, + lse_solver=lse_solver, + num_timesteps=num_timesteps, + expectation=expectation, + imag_part_tol=imag_part_tol, + num_instability_tol=num_instability_tol, + quantum_instance=quantum_instance, + ) diff --git a/qiskit/algorithms/evolvers/variational/var_qte.py b/qiskit/algorithms/evolvers/variational/var_qte.py new file mode 100644 index 000000000000..7edc898037e0 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/var_qte.py @@ -0,0 +1,303 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Variational Quantum Time Evolution Interface""" +from abc import ABC +from typing import Optional, Union, Dict, List, Any, Type, Callable + +import numpy as np +from scipy.integrate import OdeSolver + +from qiskit import QuantumCircuit +from qiskit.algorithms.aux_ops_evaluator import eval_observables +from qiskit.algorithms.evolvers.evolution_problem import EvolutionProblem +from qiskit.algorithms.evolvers.evolution_result import EvolutionResult +from qiskit.circuit import Parameter +from qiskit.providers import Backend +from qiskit.utils import QuantumInstance +from qiskit.opflow import ( + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.utils.backend_utils import is_aer_provider +from .solvers.ode.forward_euler_solver import ForwardEulerSolver +from .solvers.ode.ode_function_factory import OdeFunctionFactory +from .solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from .variational_principles.variational_principle import ( + VariationalPrinciple, +) +from .solvers.ode.var_qte_ode_solver import ( + VarQTEOdeSolver, +) + + +class VarQTE(ABC): + """Variational Quantum Time Evolution. + + Algorithms that use variational principles to compute a time evolution for a given + Hermitian operator (Hamiltonian) and a quantum state prepared by a parameterized quantum circuit. + + References: + + [1] Benjamin, Simon C. et al. (2019). + Theory of variational quantum simulation. ``_ + """ + + def __init__( + self, + ansatz: Union[OperatorBase, QuantumCircuit], + variational_principle: VariationalPrinciple, + initial_parameters: Optional[ + Union[Dict[Parameter, complex], List[complex], np.ndarray] + ] = None, + ode_solver: Union[Type[OdeSolver], str] = ForwardEulerSolver, + lse_solver: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, + num_timesteps: Optional[int] = None, + expectation: Optional[ExpectationBase] = None, + imag_part_tol: float = 1e-7, + num_instability_tol: float = 1e-7, + quantum_instance: Optional[QuantumInstance] = None, + ) -> None: + r""" + Args: + ansatz: Ansatz to be used for variational time evolution. + variational_principle: Variational Principle to be used. + initial_parameters: Initial parameter values for an ansatz. If ``None`` provided, + they are initialized uniformly at random. + ode_solver: ODE solver callable that implements a SciPy ``OdeSolver`` interface or a + string indicating a valid method offered by SciPy. + lse_solver: Linear system of equations solver callable. It accepts ``A`` and ``b`` to + solve ``Ax=b`` and returns ``x``. If ``None``, the default ``np.linalg.lstsq`` + solver is used. + num_timesteps: The number of timesteps to take. If None, it is + automatically selected to achieve a timestep of approximately 0.01. Only + relevant in case of the ``ForwardEulerSolver``. + expectation: An instance of ``ExpectationBase`` which defines a method for calculating + a metric tensor and an evolution gradient and, if provided, expectation values of + ``EvolutionProblem.aux_operators``. + imag_part_tol: Allowed value of an imaginary part that can be neglected if no + imaginary part is expected. + num_instability_tol: The amount of negative value that is allowed to be + rounded up to 0 for quantities that are expected to be + non-negative. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + """ + super().__init__() + self.ansatz = ansatz + self.variational_principle = variational_principle + self.initial_parameters = initial_parameters + self._quantum_instance = None + if quantum_instance is not None: + self.quantum_instance = quantum_instance + self.expectation = expectation + self.num_timesteps = num_timesteps + self.lse_solver = lse_solver + # OdeFunction abstraction kept for potential extensions - unclear at the moment; + # currently hidden from the user + self._ode_function_factory = OdeFunctionFactory(lse_solver=lse_solver) + self.ode_solver = ode_solver + self.imag_part_tol = imag_part_tol + self.num_instability_tol = num_instability_tol + + @property + def quantum_instance(self) -> Optional[QuantumInstance]: + """Returns quantum instance.""" + return self._quantum_instance + + @quantum_instance.setter + def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: + """Sets quantum_instance""" + if not isinstance(quantum_instance, QuantumInstance): + quantum_instance = QuantumInstance(quantum_instance) + + self._quantum_instance = quantum_instance + self._circuit_sampler = CircuitSampler( + quantum_instance, param_qobj=is_aer_provider(quantum_instance.backend) + ) + + def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: + """ + Apply Variational Quantum Imaginary Time Evolution (VarQITE) w.r.t. the given + operator. + + Args: + evolution_problem: Instance defining an evolution problem. + Returns: + Result of the evolution which includes a quantum circuit with bound parameters as an + evolved state and, if provided, observables evaluated on the evolved state using + a ``quantum_instance`` and ``expectation`` provided. + + Raises: + ValueError: If no ``initial_state`` is included in the ``evolution_problem``. + """ + self._validate_aux_ops(evolution_problem) + + if evolution_problem.initial_state is not None: + raise ValueError("initial_state provided but not applicable to VarQTE.") + + init_state_param_dict = self._create_init_state_param_dict( + self.initial_parameters, self.ansatz.parameters + ) + + error_calculator = None # TODO will be supported in another PR + + evolved_state = self._evolve( + init_state_param_dict, + evolution_problem.hamiltonian, + evolution_problem.time, + evolution_problem.t_param, + error_calculator, + ) + + evaluated_aux_ops = None + if evolution_problem.aux_operators is not None: + evaluated_aux_ops = eval_observables( + self.quantum_instance, + evolved_state, + evolution_problem.aux_operators, + self.expectation, + ) + + return EvolutionResult(evolved_state, evaluated_aux_ops) + + def _evolve( + self, + init_state_param_dict: Dict[Parameter, complex], + hamiltonian: OperatorBase, + time: float, + t_param: Optional[Parameter] = None, + error_calculator: Any = None, + ) -> OperatorBase: + r""" + Helper method for performing time evolution. Works both for imaginary and real case. + + Args: + init_state_param_dict: Parameter dictionary with initial values for a given + parametrized state/ansatz. If no initial parameter values are provided, they are + initialized uniformly at random. + hamiltonian: + Operator used for Variational Quantum Imaginary Time Evolution (VarQTE). + The operator may be given either as a composed op consisting of a Hermitian + observable and a ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a + ``ComboFn``. + The latter case enables the evaluation of a Quantum Natural Gradient. + time: Total time of evolution. + t_param: Time parameter in case of a time-dependent Hamiltonian. + error_calculator: Not yet supported. Calculator of errors for error-based ODE functions. + + Returns: + Result of the evolution which is a quantum circuit with bound parameters as an + evolved state. + """ + + init_state_parameters = list(init_state_param_dict.keys()) + init_state_parameters_values = list(init_state_param_dict.values()) + + linear_solver = VarQTELinearSolver( + self.variational_principle, + hamiltonian, + self.ansatz, + init_state_parameters, + t_param, + self._ode_function_factory.lse_solver, + self.imag_part_tol, + self.expectation, + self._quantum_instance, + ) + + # Convert the operator that holds the Hamiltonian and ansatz into a NaturalGradient operator + ode_function = self._ode_function_factory._build( + linear_solver, error_calculator, init_state_param_dict, t_param + ) + + ode_solver = VarQTEOdeSolver( + init_state_parameters_values, ode_function, self.ode_solver, self.num_timesteps + ) + parameter_values = ode_solver.run(time) + param_dict_from_ode = dict(zip(init_state_parameters, parameter_values)) + + return self.ansatz.assign_parameters(param_dict_from_ode) + + @staticmethod + def _create_init_state_param_dict( + param_values: Union[Dict[Parameter, complex], List[complex], np.ndarray], + init_state_parameters: List[Parameter], + ) -> Dict[Parameter, complex]: + r""" + If ``param_values`` is a dictionary, it looks for parameters present in an initial state + (an ansatz) in a ``param_values``. Based on that, it creates a new dictionary containing + only parameters present in an initial state and their respective values. + If ``param_values`` is a list of values, it creates a new dictionary containing + parameters present in an initial state and their respective values. + If no ``param_values`` is provided, parameter values are chosen uniformly at random. + + Args: + param_values: Dictionary which relates parameter values to the parameters or a list of + values. + init_state_parameters: Parameters present in a quantum state. + + Returns: + Dictionary that maps parameters of an initial state to some values. + + Raises: + ValueError: If the dictionary with parameter values provided does not include all + parameters present in the initial state or if the list of values provided is not the + same length as the list of parameters. + TypeError: If an unsupported type of ``param_values`` provided. + """ + if param_values is None: + init_state_parameter_values = np.random.random(len(init_state_parameters)) + elif isinstance(param_values, dict): + init_state_parameter_values = [] + for param in init_state_parameters: + if param in param_values.keys(): + init_state_parameter_values.append(param_values[param]) + else: + raise ValueError( + f"The dictionary with parameter values provided does not " + f"include all parameters present in the initial state." + f"Parameters present in the state: {init_state_parameters}, " + f"parameters in the dictionary: " + f"{list(param_values.keys())}." + ) + elif isinstance(param_values, (list, np.ndarray)): + if len(init_state_parameters) != len(param_values): + raise ValueError( + f"Initial state has {len(init_state_parameters)} parameters and the" + f" list of values has {len(param_values)} elements. They should be" + f"equal in length." + ) + init_state_parameter_values = param_values + else: + raise TypeError(f"Unsupported type of param_values provided: {type(param_values)}.") + + init_state_param_dict = dict(zip(init_state_parameters, init_state_parameter_values)) + return init_state_param_dict + + def _validate_aux_ops(self, evolution_problem: EvolutionProblem) -> None: + if evolution_problem.aux_operators is not None: + if self.quantum_instance is None: + raise ValueError( + "aux_operators where provided for evaluations but no ``quantum_instance`` " + "was provided." + ) + + if self.expectation is None: + raise ValueError( + "aux_operators where provided for evaluations but no ``expectation`` " + "was provided." + ) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py b/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py new file mode 100644 index 000000000000..7c508f3921de --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/__init__.py @@ -0,0 +1,25 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Variational Principles""" + +from .imaginary_mc_lachlan_principle import ImaginaryMcLachlanPrinciple +from .imaginary_variational_principle import ImaginaryVariationalPrinciple +from .real_mc_lachlan_principle import RealMcLachlanPrinciple +from .real_variational_principle import RealVariationalPrinciple + +__all__ = [ + "ImaginaryMcLachlanPrinciple", + "ImaginaryVariationalPrinciple", + "RealMcLachlanPrinciple", + "RealVariationalPrinciple", +] diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py new file mode 100644 index 000000000000..7a0c46b794f2 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_mc_lachlan_principle.py @@ -0,0 +1,76 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for an Imaginary McLachlan's Variational Principle.""" +from typing import Dict, List, Optional + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import StateFn, OperatorBase, CircuitSampler, ExpectationBase +from qiskit.utils import QuantumInstance +from .imaginary_variational_principle import ( + ImaginaryVariationalPrinciple, +) + + +class ImaginaryMcLachlanPrinciple(ImaginaryVariationalPrinciple): + """Class for an Imaginary McLachlan's Variational Principle. It aims to minimize the distance + between both sides of the Wick-rotated Schrödinger equation with a quantum state given as a + parametrized trial state. The principle leads to a system of linear equations handled by a + linear solver. The imaginary variant means that we consider imaginary time dynamics. + """ + + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + if self._evolution_gradient_callable is None: + operator = StateFn(hamiltonian, is_measurement=True) @ StateFn(ansatz) + self._evolution_gradient_callable = self._evolution_gradient.gradient_wrapper( + operator, bind_params, gradient_params, quantum_instance, expectation + ) + evolution_grad_lse_rhs = -0.5 * self._evolution_gradient_callable(param_values) + + return evolution_grad_lse_rhs diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py new file mode 100644 index 000000000000..bcd60241942a --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/imaginary_variational_principle.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Abstract class for an Imaginary Variational Principle.""" + +from abc import ABC + +from ..variational_principles.variational_principle import ( + VariationalPrinciple, +) + + +class ImaginaryVariationalPrinciple(VariationalPrinciple, ABC): + """Abstract class for an Imaginary Variational Principle. The imaginary variant means that we + consider imaginary time dynamics.""" diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py new file mode 100644 index 000000000000..ddc6a17ed879 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/real_mc_lachlan_principle.py @@ -0,0 +1,150 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for a Real McLachlan's Variational Principle.""" +from typing import Union, Dict, List, Optional + +import numpy as np +from numpy import real + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ( + StateFn, + SummedOp, + Y, + I, + PauliExpectation, + CircuitQFI, + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.opflow.gradients.circuit_gradients import LinComb +from qiskit.utils import QuantumInstance +from .real_variational_principle import ( + RealVariationalPrinciple, +) + + +class RealMcLachlanPrinciple(RealVariationalPrinciple): + """Class for a Real McLachlan's Variational Principle. It aims to minimize the distance + between both sides of the Schrödinger equation with a quantum state given as a parametrized + trial state. The principle leads to a system of linear equations handled by a linear solver. + The real variant means that we consider real time dynamics. + """ + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + ) -> None: + """ + Args: + qfi_method: The method used to compute the QFI. Can be either + ``'lin_comb_full'`` or ``'overlap_block_diag'`` or ``'overlap_diag'`` or + ``CircuitQFI``. + """ + self._grad_method = LinComb(aux_meas_op=-Y) + self._energy_param = None + self._energy = None + + super().__init__(qfi_method) + + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + if self._evolution_gradient_callable is None: + self._energy_param = Parameter("alpha") + modified_hamiltonian = self._construct_expectation( + hamiltonian, ansatz, self._energy_param + ) + + self._evolution_gradient_callable = self._evolution_gradient.gradient_wrapper( + modified_hamiltonian, + bind_params + [self._energy_param], + gradient_params, + quantum_instance, + expectation, + ) + + energy = StateFn(hamiltonian, is_measurement=True) @ StateFn(ansatz) + if expectation is None: + expectation = PauliExpectation() + self._energy = expectation.convert(energy) + + if circuit_sampler is not None: + energy = circuit_sampler.convert(self._energy, param_dict).eval() + else: + energy = self._energy.assign_parameters(param_dict).eval() + + param_values.append(real(energy)) + evolution_grad = 0.5 * self._evolution_gradient_callable(param_values) + + # quick fix due to an error on opflow; to be addressed in a separate PR + evolution_grad = (-1) * evolution_grad + return evolution_grad + + @staticmethod + def _construct_expectation( + hamiltonian: OperatorBase, ansatz: QuantumCircuit, energy_param: Parameter + ) -> OperatorBase: + """ + Modifies a Hamiltonian according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + energy_param: Parameter for energy correction. + + Returns: + An modified Hamiltonian composed with an ansatz. + """ + energy_term = I ^ hamiltonian.num_qubits + energy_term *= -1 + energy_term *= energy_param + modified_hamiltonian = SummedOp([hamiltonian, energy_term]).reduce() + return StateFn(modified_hamiltonian, is_measurement=True) @ StateFn(ansatz) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py new file mode 100644 index 000000000000..881e1f3827c7 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/real_variational_principle.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for a Real Variational Principle.""" + +from abc import ABC +from typing import Union + +from qiskit.opflow import ( + CircuitQFI, +) +from .variational_principle import ( + VariationalPrinciple, +) + + +class RealVariationalPrinciple(VariationalPrinciple, ABC): + """Class for a Real Variational Principle. The real variant means that we consider real time + dynamics.""" + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + ) -> None: + """ + Args: + qfi_method: The method used to compute the QFI. Can be either ``'lin_comb_full'`` or + ``'overlap_block_diag'`` or ``'overlap_diag'`` or ``CircuitQFI``. + """ + super().__init__( + qfi_method, + self._grad_method, + ) diff --git a/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py b/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py new file mode 100644 index 000000000000..d3d6cbc20b67 --- /dev/null +++ b/qiskit/algorithms/evolvers/variational/variational_principles/variational_principle.py @@ -0,0 +1,129 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class for a Variational Principle.""" + +from abc import ABC, abstractmethod +from typing import Union, List, Optional, Dict + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.opflow import ( + CircuitQFI, + CircuitGradient, + QFI, + Gradient, + CircuitStateFn, + CircuitSampler, + OperatorBase, + ExpectationBase, +) +from qiskit.utils import QuantumInstance + + +class VariationalPrinciple(ABC): + """A Variational Principle class. It determines the time propagation of parameters in a + quantum state provided as a parametrized quantum circuit (ansatz).""" + + def __init__( + self, + qfi_method: Union[str, CircuitQFI] = "lin_comb_full", + grad_method: Union[str, CircuitGradient] = "lin_comb", + ) -> None: + """ + Args: + grad_method: The method used to compute the state gradient. Can be either + ``'param_shift'`` or ``'lin_comb'`` or ``'fin_diff'`` or ``CircuitGradient``. + qfi_method: The method used to compute the QFI. Can be either + ``'lin_comb_full'`` or ``'overlap_block_diag'`` or ``'overlap_diag'`` or + ``CircuitQFI``. + """ + self._qfi_method = qfi_method + self.qfi = QFI(qfi_method) + self._grad_method = grad_method + self._evolution_gradient = Gradient(self._grad_method) + self._qfi_gradient_callable = None + self._evolution_gradient_callable = None + + def metric_tensor( + self, + ansatz: QuantumCircuit, + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates a metric tensor according to the rules of this variational principle. + + Args: + ansatz: Quantum state in the form of a parametrized quantum circuit. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating a metric tensor. + If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + Metric tensor. + """ + if self._qfi_gradient_callable is None: + self._qfi_gradient_callable = self.qfi.gradient_wrapper( + CircuitStateFn(ansatz), bind_params, gradient_params, quantum_instance, expectation + ) + metric_tensor = 0.25 * self._qfi_gradient_callable(param_values) + + return metric_tensor + + @abstractmethod + def evolution_grad( + self, + hamiltonian: OperatorBase, + ansatz: QuantumCircuit, + circuit_sampler: CircuitSampler, + param_dict: Dict[Parameter, complex], + bind_params: List[Parameter], + gradient_params: List[Parameter], + param_values: List[complex], + expectation: Optional[ExpectationBase] = None, + quantum_instance: Optional[QuantumInstance] = None, + ) -> np.ndarray: + """ + Calculates an evolution gradient according to the rules of this variational principle. + + Args: + hamiltonian: Operator used for Variational Quantum Time Evolution. The operator may be + given either as a composed op consisting of a Hermitian observable and a + ``CircuitStateFn`` or a ``ListOp`` of a ``CircuitStateFn`` with a ``ComboFn``. The + latter case enables the evaluation of a Quantum Natural Gradient. + ansatz: Quantum state in the form of a parametrized quantum circuit. + circuit_sampler: A circuit sampler. + param_dict: Dictionary which relates parameter values to the parameters in the ansatz. + bind_params: List of parameters that are supposed to be bound. + gradient_params: List of parameters with respect to which gradients should be computed. + param_values: Values of parameters to be bound. + expectation: An instance of ``ExpectationBase`` used for calculating an evolution + gradient. If ``None`` provided, a ``PauliExpectation`` is used. + quantum_instance: Backend used to evaluate the quantum circuit outputs. If ``None`` + provided, everything will be evaluated based on matrix multiplication (which is + slow). + + Returns: + An evolution gradient. + """ + pass diff --git a/qiskit/algorithms/factorizers/shor.py b/qiskit/algorithms/factorizers/shor.py index 1030747ae89f..06f9f1610e09 100644 --- a/qiskit/algorithms/factorizers/shor.py +++ b/qiskit/algorithms/factorizers/shor.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020. +# (C) Copyright IBM 2019, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -30,6 +30,7 @@ from qiskit.utils.arithmetic import is_power from qiskit.utils.quantum_instance import QuantumInstance from qiskit.utils.validation import validate_min +from qiskit.utils.deprecation import deprecate_function from ..algorithm_result import AlgorithmResult from ..exceptions import AlgorithmError @@ -40,7 +41,12 @@ class Shor: - """Shor's factoring algorithm. + """The deprecated Shor's factoring algorithm. + + The Shor class is deprecated as of Qiskit Terra 0.22.0 + and will be removed no sooner than 3 months after the release date. + It is replaced by the tutorial at + `Shor `_ Shor's Factoring algorithm is one of the most well-known quantum algorithms and finds the prime factors for input integer :math:`N` in polynomial time. @@ -50,6 +56,10 @@ class Shor: See also https://arxiv.org/abs/quant-ph/0205095 """ + @deprecate_function( + "The Shor class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None) -> None: """ Args: @@ -480,7 +490,7 @@ def factor( class ShorResult(AlgorithmResult): - """Shor Result.""" + """The deprecated Shor Result.""" def __init__(self) -> None: super().__init__() diff --git a/qiskit/algorithms/linear_solvers/__init__.py b/qiskit/algorithms/linear_solvers/__init__.py index 2b8214fb7cf2..746fb8e40496 100644 --- a/qiskit/algorithms/linear_solvers/__init__.py +++ b/qiskit/algorithms/linear_solvers/__init__.py @@ -11,8 +11,8 @@ # that they have been altered from the originals. """ -Linear solvers (:mod:`qiskit.algorithms.linear_solvers`) -========================================================= +The deprecated Linear solvers (:mod:`qiskit.algorithms.linear_solvers`) +======================================================================= It contains classical and quantum algorithms to solve systems of linear equations such as :class:`~qiskit.algorithms.HHL`. Although the quantum algorithm accepts a general Hermitian matrix as input, Qiskit's default diff --git a/qiskit/algorithms/linear_solvers/hhl.py b/qiskit/algorithms/linear_solvers/hhl.py index 6e59de4babcb..c5bf52a0380d 100644 --- a/qiskit/algorithms/linear_solvers/hhl.py +++ b/qiskit/algorithms/linear_solvers/hhl.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -32,6 +32,7 @@ from qiskit.providers import Backend from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function from .linear_solver import LinearSolver, LinearSolverResult from .matrices.numpy_matrix import NumPyMatrix @@ -39,7 +40,8 @@ class HHL(LinearSolver): - r"""Systems of linear equations arise naturally in many real-life applications in a wide range + r"""The deprecated systems of linear equations arise naturally in many real-life applications + in a wide range of areas, such as in the solution of Partial Differential Equations, the calibration of financial models, fluid simulation or numerical field calculation. The problem can be defined as, given a matrix :math:`A\in\mathbb{C}^{N\times N}` and a vector @@ -64,24 +66,29 @@ class HHL(LinearSolver): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.hhl import HHL from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz from qiskit.algorithms.linear_solvers.observables import MatrixFunctional - matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) - right_hand_side = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, 1 / 2) - rhs = right_hand_side / np.linalg.norm(right_hand_side) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) + right_hand_side = [1.0, -2.1, 3.2, -4.3] + observable = MatrixFunctional(1, 1 / 2) + rhs = right_hand_side / np.linalg.norm(right_hand_side) # Initial state circuit num_qubits = matrix.num_state_qubits qc = QuantumCircuit(num_qubits) qc.isometry(rhs, list(range(num_qubits)), None) - hhl = HHL() - solution = hhl.solve(matrix, qc, observable) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + hhl = HHL() + solution = hhl.solve(matrix, qc, observable) approx_result = solution.observable References: @@ -96,6 +103,10 @@ class HHL(LinearSolver): """ + @deprecate_function( + "The HHL class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, epsilon: float = 1e-2, diff --git a/qiskit/algorithms/linear_solvers/linear_solver.py b/qiskit/algorithms/linear_solvers/linear_solver.py index 093d8cd30e93..97a68226ebb9 100644 --- a/qiskit/algorithms/linear_solvers/linear_solver.py +++ b/qiskit/algorithms/linear_solvers/linear_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -19,18 +19,23 @@ from qiskit import QuantumCircuit from qiskit.result import Result from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_function from .observables.linear_system_observable import LinearSystemObservable from ..algorithm_result import AlgorithmResult class LinearSolverResult(AlgorithmResult): - """A base class for linear systems results. + """The deprecated base class for linear systems results. The linear systems algorithms return an object of the type ``LinearSystemsResult`` with the information about the solution obtained. """ + @deprecate_function( + "The LinearSolverResult class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self) -> None: super().__init__() @@ -93,7 +98,14 @@ def circuit_results(self, results: Union[List[float], List[Result]]): class LinearSolver(ABC): - """An abstract class for linear system solvers in Qiskit.""" + """The deprecated abstract class for linear system solvers in Qiskit.""" + + @deprecate_function( + "The LinearSolver class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + pass @abstractmethod def solve( diff --git a/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py b/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py index 745fdca2b4f2..26e53860dc1f 100644 --- a/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py +++ b/qiskit/algorithms/linear_solvers/matrices/linear_system_matrix.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -17,11 +17,16 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import BlueprintCircuit +from qiskit.utils.deprecation import deprecate_function class LinearSystemMatrix(BlueprintCircuit, ABC): - """Base class for linear system matrices.""" + """The deprecated base class for linear system matrices.""" + @deprecate_function( + "The LinearSystemMatrix class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, num_state_qubits: int, diff --git a/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py b/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py index 1fc94893c6ab..fdf30017c925 100644 --- a/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py +++ b/qiskit/algorithms/linear_solvers/matrices/numpy_matrix.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -17,22 +17,26 @@ import scipy as sp from qiskit import QuantumCircuit, QuantumRegister +from qiskit.utils.deprecation import deprecate_function from .linear_system_matrix import LinearSystemMatrix class NumPyMatrix(LinearSystemMatrix): - """Class of matrices given as a numpy array. + """The deprecated class of matrices given as a numpy array. Examples: .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.matrices.numpy_matrix import NumPyMatrix - matrix = NumPyMatrix(np.array([[1 / 2, 1 / 6, 0, 0], [1 / 6, 1 / 2, 1 / 6, 0], + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = NumPyMatrix(np.array([[1 / 2, 1 / 6, 0, 0], [1 / 6, 1 / 2, 1 / 6, 0], [0, 1 / 6, 1 / 2, 1 / 6], [0, 0, 1 / 6, 1 / 2]])) power = 2 @@ -44,6 +48,10 @@ class NumPyMatrix(LinearSystemMatrix): qc.append(matrix.power(power).control(), list(range(circ_qubits))) """ + @deprecate_function( + "The NumPyMatrix class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, matrix: np.ndarray, diff --git a/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py b/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py index 6dfbd0ad3c8a..6dda2716b680 100644 --- a/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py +++ b/qiskit/algorithms/linear_solvers/matrices/tridiagonal_toeplitz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -18,12 +18,13 @@ from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister from qiskit.circuit.library import UGate, MCMTVChain +from qiskit.utils.deprecation import deprecate_function from .linear_system_matrix import LinearSystemMatrix class TridiagonalToeplitz(LinearSystemMatrix): - r"""Class of tridiagonal Toeplitz symmetric matrices. + r"""The deprecated class of tridiagonal Toeplitz symmetric matrices. Given the main entry, :math:`a`, and the off diagonal entry, :math:`b`, the :math:`4\times 4` dimensional tridiagonal Toeplitz symmetric matrix is @@ -41,11 +42,14 @@ class TridiagonalToeplitz(LinearSystemMatrix): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz - matrix = TridiagonalToeplitz(2, 1, -1 / 3) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, -1 / 3) power = 3 # Controlled power (as within QPE) @@ -56,6 +60,10 @@ class TridiagonalToeplitz(LinearSystemMatrix): qc.append(matrix.power(power).control(), list(range(circ_qubits))) """ + @deprecate_function( + "The TridiagonalToeplitz class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__( self, num_state_qubits: int, diff --git a/qiskit/algorithms/linear_solvers/numpy_linear_solver.py b/qiskit/algorithms/linear_solvers/numpy_linear_solver.py index bbbb3a35f50f..0a3cbbd57191 100644 --- a/qiskit/algorithms/linear_solvers/numpy_linear_solver.py +++ b/qiskit/algorithms/linear_solvers/numpy_linear_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -17,13 +17,14 @@ from qiskit import QuantumCircuit from qiskit.quantum_info import Operator, Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_function from .linear_solver import LinearSolverResult, LinearSolver from .observables.linear_system_observable import LinearSystemObservable class NumPyLinearSolver(LinearSolver): - """The Numpy Linear Solver algorithm (classical). + """The deprecated Numpy Linear Solver algorithm (classical). This linear system solver computes the exact value of the given observable(s) or the full solution vector if no observable is specified. @@ -32,21 +33,33 @@ class NumPyLinearSolver(LinearSolver): .. jupyter-execute:: + import warnings import numpy as np from qiskit.algorithms import NumPyLinearSolver from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz from qiskit.algorithms.linear_solvers.observables import MatrixFunctional - matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) - right_hand_side = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, 1 / 2) - rhs = right_hand_side / np.linalg.norm(right_hand_side) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) + right_hand_side = [1.0, -2.1, 3.2, -4.3] + observable = MatrixFunctional(1, 1 / 2) + rhs = right_hand_side / np.linalg.norm(right_hand_side) - np_solver = NumPyLinearSolver() - solution = np_solver.solve(matrix, rhs, observable) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + np_solver = NumPyLinearSolver() + solution = np_solver.solve(matrix, rhs, observable) result = solution.observable """ + @deprecate_function( + "The NumPyLinearSolver class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + super().__init__() + def solve( self, matrix: Union[np.ndarray, QuantumCircuit], diff --git a/qiskit/algorithms/linear_solvers/observables/absolute_average.py b/qiskit/algorithms/linear_solvers/observables/absolute_average.py index 9c9f644dd571..300c31edaed9 100644 --- a/qiskit/algorithms/linear_solvers/observables/absolute_average.py +++ b/qiskit/algorithms/linear_solvers/observables/absolute_average.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -18,12 +18,13 @@ from qiskit import QuantumCircuit from qiskit.opflow import I, Z, TensoredOp from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_function from .linear_system_observable import LinearSystemObservable class AbsoluteAverage(LinearSystemObservable): - r"""An observable for the absolute average of a linear system of equations solution. + r"""The deprecated observable for the absolute average of a linear system of equations solution. For a vector :math:`x=(x_1,...,x_N)`, the absolute average is defined as :math:`\abs{\frac{1}{N}\sum_{i=1}^{N}x_i}`. @@ -32,13 +33,16 @@ class AbsoluteAverage(LinearSystemObservable): .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.observables.absolute_average import \ AbsoluteAverage from qiskit.opflow import StateFn - observable = AbsoluteAverage() + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + observable = AbsoluteAverage() vector = [1.0, -2.1, 3.2, -4.3] init_state = vector / np.linalg.norm(vector) @@ -59,6 +63,13 @@ class AbsoluteAverage(LinearSystemObservable): exact = observable.evaluate_classically(init_state) """ + @deprecate_function( + "The AbsoluteAverage class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + super().__init__() + def observable(self, num_qubits: int) -> Union[TensoredOp, List[TensoredOp]]: """The observable operator. diff --git a/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py b/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py index 048291d3d96d..fd6ea2339738 100644 --- a/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py +++ b/qiskit/algorithms/linear_solvers/observables/linear_system_observable.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -18,10 +18,18 @@ from qiskit import QuantumCircuit from qiskit.opflow import TensoredOp +from qiskit.utils.deprecation import deprecate_function class LinearSystemObservable(ABC): - """An abstract class for linear system observables in Qiskit.""" + """The deprecated abstract class for linear system observables in Qiskit.""" + + @deprecate_function( + "The LinearSystemObservable class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) + def __init__(self) -> None: + pass @abstractmethod def observable(self, num_qubits: int) -> Union[TensoredOp, List[TensoredOp]]: diff --git a/qiskit/algorithms/linear_solvers/observables/matrix_functional.py b/qiskit/algorithms/linear_solvers/observables/matrix_functional.py index 8611620377e2..ca0cfecf16bc 100644 --- a/qiskit/algorithms/linear_solvers/observables/matrix_functional.py +++ b/qiskit/algorithms/linear_solvers/observables/matrix_functional.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,23 +13,26 @@ """The matrix functional of the vector solution to the linear systems.""" from typing import Union, List +import warnings import numpy as np from scipy.sparse import diags from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit.opflow import I, Z, TensoredOp +from qiskit.utils.deprecation import deprecate_function from .linear_system_observable import LinearSystemObservable class MatrixFunctional(LinearSystemObservable): - """A class for the matrix functional of the vector solution to the linear systems. + """The deprecated class for the matrix functional of the vector solution to the linear systems. Examples: .. jupyter-execute:: + import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.observables.matrix_functional import \ @@ -40,7 +43,9 @@ class MatrixFunctional(LinearSystemObservable): tpass = RemoveResetInZeroState() vector = [1.0, -2.1, 3.2, -4.3] - observable = MatrixFunctional(1, -1 / 3) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + observable = MatrixFunctional(1, -1 / 3) init_state = vector / np.linalg.norm(vector) num_qubits = int(np.log2(len(vector))) @@ -70,6 +75,10 @@ class MatrixFunctional(LinearSystemObservable): exact = observable.evaluate_classically(init_state) """ + @deprecate_function( + "The MatrixFunctional class is deprecated as of Qiskit Terra 0.22.0 " + "and will be removed no sooner than 3 months after the release date. " + ) def __init__(self, main_diag: float, off_diag: int) -> None: """ Args: @@ -78,6 +87,9 @@ def __init__(self, main_diag: float, off_diag: int) -> None: off_diag: The off diagonal of the tridiagonal Toeplitz symmetric matrix to compute the functional. """ + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + super().__init__() self._main_diag = main_diag self._off_diag = off_diag diff --git a/qiskit/algorithms/optimizers/__init__.py b/qiskit/algorithms/optimizers/__init__.py index a6efc11c49d8..c1e6ef58bbd5 100644 --- a/qiskit/algorithms/optimizers/__init__.py +++ b/qiskit/algorithms/optimizers/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -37,6 +37,26 @@ OptimizerResult OptimizerSupportLevel Optimizer + Minimizer + +Steppable Optimizer Base Class +============================== + +.. autosummary:: + :toctree: ../stubs/ + + optimizer_utils + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + SteppableOptimizer + AskData + TellData + OptimizerState + + Local Optimizers ================ @@ -52,6 +72,7 @@ L_BFGS_B GSLS GradientDescent + GradientDescentState NELDER_MEAD NFT P_BFGS @@ -109,7 +130,7 @@ from .cg import CG from .cobyla import COBYLA from .gsls import GSLS -from .gradient_descent import GradientDescent +from .gradient_descent import GradientDescent, GradientDescentState from .imfil import IMFIL from .l_bfgs_b import L_BFGS_B from .nelder_mead import NELDER_MEAD @@ -119,6 +140,7 @@ from .nlopts.direct_l_rand import DIRECT_L_RAND from .nlopts.esch import ESCH from .nlopts.isres import ISRES +from .steppable_optimizer import SteppableOptimizer, AskData, TellData, OptimizerState from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel from .p_bfgs import P_BFGS from .powell import POWELL @@ -133,12 +155,19 @@ __all__ = [ "Optimizer", "OptimizerSupportLevel", + "SteppableOptimizer", + "AskData", + "TellData", + "OptimizerState", + "OptimizerResult", + "Minimizer", "ADAM", "AQGD", "CG", "COBYLA", "GSLS", "GradientDescent", + "GradientDescentState", "L_BFGS_B", "NELDER_MEAD", "NFT", diff --git a/qiskit/algorithms/optimizers/gradient_descent.py b/qiskit/algorithms/optimizers/gradient_descent.py index 38ed55048623..a354aa383a2b 100644 --- a/qiskit/algorithms/optimizers/gradient_descent.py +++ b/qiskit/algorithms/optimizers/gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -12,17 +12,36 @@ """A standard gradient descent optimizer.""" -from typing import Iterator, Optional, Union, Callable, Dict, Any, List, Tuple -from functools import partial - +from dataclasses import dataclass, field +from typing import Dict, Any, Union, Callable, Optional, Tuple, List, Iterator import numpy as np - from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT +from .steppable_optimizer import AskData, TellData, OptimizerState, SteppableOptimizer +from .optimizer_utils import LearningRate CALLBACK = Callable[[int, np.ndarray, float, float], None] -class GradientDescent(Optimizer): +@dataclass +class GradientDescentState(OptimizerState): + """State of :class:`~.GradientDescent`. + + Dataclass with all the information of an optimizer plus the learning_rate and the stepsize. + """ + + stepsize: Optional[float] + """Norm of the gradient on the last step.""" + + learning_rate: LearningRate = field(compare=False) + """Learning rate at the current step of the optimization process. + + It behaves like a generator, (use ``next(learning_rate)`` to get the learning rate for the + next step) but it can also return the current learning rate with ``learning_rate.current``. + + """ + + +class GradientDescent(SteppableOptimizer): r"""The gradient descent minimization routine. For a function :math:`f` and an initial point :math:`\vec\theta_0`, the standard (or "vanilla") @@ -31,14 +50,14 @@ class GradientDescent(Optimizer): .. math:: - \vec\theta_{n+1} = \vec\theta_{n} - \vec\eta\nabla f(\vec\theta_{n}), + \vec\theta_{n+1} = \vec\theta_{n} - \eta_n \vec\nabla f(\vec\theta_{n}), - for a small learning rate :math:`\eta > 0`. + for a small learning rate :math:`\eta_n > 0`. - You can either provide the analytic gradient :math:`\vec\nabla f` as ``gradient_function`` - in the ``optimize`` method, or, if you do not provide it, use a finite difference approximation - of the gradient. To adapt the size of the perturbation in the finite difference gradients, - set the ``perturbation`` property in the initializer. + You can either provide the analytic gradient :math:`\vec\nabla f` as ``jac`` + in the :meth:`~.minimize` method, or, if you do not provide it, use a finite difference + approximation of the gradient. To adapt the size of the perturbation in the finite difference + gradients, set the ``perturbation`` property in the initializer. This optimizer supports a callback function. If provided in the initializer, the optimizer will call the callback in each iteration with the following information in this order: @@ -60,14 +79,14 @@ def f(x): initial_point = np.array([1, 0.5, -0.2]) optimizer = GradientDescent(maxiter=100) - x_opt, fx_opt, nfevs = optimizer.optimize(initial_point.size, - f, - initial_point=initial_point) - print(f"Found minimum {x_opt} at a value of {fx_opt} using {nfevs} evaluations.") + result = optimizer.minimize(fun=fun, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") An example where the learning rate is an iterator and we supply the analytic gradient. - Note how much faster this convergences (i.e. less ``nfevs``) compared to the previous + Note how much faster this convergences (i.e. less ``nfev``) compared to the previous example. .. code-block:: python @@ -77,7 +96,6 @@ def f(x): def learning_rate(): power = 0.6 constant_coeff = 0.1 - def powerlaw(): n = 0 while True: @@ -95,42 +113,151 @@ def grad_f(x): initial_point = np.array([1, 0.5, -0.2]) optimizer = GradientDescent(maxiter=100, learning_rate=learning_rate) - x_opt, fx_opt, nfevs = optimizer.optimize(initial_point.size, - f, - gradient_function=grad_f, - initial_point=initial_point) + result = optimizer.minimize(fun=fun, jac=grad_f, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") + + + An other example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing the + result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + - print(f"Found minimum {x_opt} at a value of {fx_opt} using {nfevs} evaluations.") + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + tell_data = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html """ def __init__( self, maxiter: int = 100, - learning_rate: Union[float, Callable[[], Iterator]] = 0.01, + learning_rate: Union[float, List[float], np.ndarray, Callable[[], Iterator]] = 0.01, tol: float = 1e-7, callback: Optional[CALLBACK] = None, perturbation: Optional[float] = None, ) -> None: - r""" + """ Args: maxiter: The maximum number of iterations. - learning_rate: A constant or generator yielding learning rates for the parameter - updates. See the docstring for an example. + learning_rate: A constant, list, array or factory of generators yielding learning rates + for the parameter updates. See the docstring for an example. tol: If the norm of the parameter update is smaller than this threshold, the - optimizer is converged. - perturbation: If no gradient is passed to ``GradientDescent.optimize`` the gradient is - approximated with a symmetric finite difference scheme with ``perturbation`` + optimizer has converged. + perturbation: If no gradient is passed to :meth:`~.minimize` the gradient is + approximated with a forward finite difference scheme with ``perturbation`` perturbation in both directions (defaults to 1e-2 if required). - Ignored if a gradient callable is passed to ``GradientDescent.optimize``. + Ignored when we have an explicit function for the gradient. + Raises: + ValueError: If ``learning_rate`` is an array and its lenght is less than ``maxiter``. """ - super().__init__() - - self.maxiter = maxiter - self.learning_rate = learning_rate - self.perturbation = perturbation - self.tol = tol + super().__init__(maxiter=maxiter) self.callback = callback + self._state: Optional[GradientDescentState] = None + self._perturbation = perturbation + self._tol = tol + # if learning rate is an array, check it is sufficiently long. + if isinstance(learning_rate, (list, np.ndarray)): + if len(learning_rate) < maxiter: + raise ValueError( + f"Length of learning_rate ({len(learning_rate)}) " + f"is smaller than maxiter ({maxiter})." + ) + self.learning_rate = learning_rate + + @property + def state(self) -> GradientDescentState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: GradientDescentState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + @property + def tol(self) -> float: + """Returns the tolerance of the optimizer. + + Any step with smaller stepsize than this value will stop the optimization.""" + return self._tol + + @tol.setter + def tol(self, tol: float) -> None: + """Set the tolerance.""" + self._tol = tol + + @property + def perturbation(self) -> Optional[float]: + """Returns the perturbation. + + This is the perturbation used in the finite difference gradient approximation. + """ + return self._perturbation + + @perturbation.setter + def perturbation(self, perturbation: Optional[float]) -> None: + """Set the perturbation.""" + self._perturbation = perturbation + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accomodate GradientDescent. + + Will call :attr:`~.callback` and pass the following arguments: + current number of function values, current parameters, current function value, + norm of current gradient. + """ + if self.callback is not None: + self.callback( + self.state.nfev, + self.state.x, + self.state.fun(self.state.x), + self.state.stepsize, + ) @property def settings(self) -> Dict[str, Any]: @@ -149,60 +276,114 @@ def settings(self) -> Dict[str, Any]: "callback": self.callback, } - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Optional[Callable[[POINT], POINT]] = None, - bounds: Optional[List[Tuple[float, float]]] = None, - ) -> OptimizerResult: - # set learning rate - if isinstance(self.learning_rate, float): - eta = constant(self.learning_rate) - else: - eta = self.learning_rate() + def ask(self) -> AskData: + """Returns an object with the data needed to evaluate the gradient. + + If this object contains a gradient function the gradient can be evaluated directly. Otherwise + approximate it with a finite difference scheme. + """ + return AskData( + x_jac=self.state.x, + ) + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """ + Updates :attr:`.~GradientDescentState.x` by an ammount proportional to the learning + rate and value of the gradient at that point. + + Args: + ask_data: The data used to evaluate the function. + tell_data: The data from the function evaluation. + + Raises: + ValueError: If the gradient passed doesn't have the right dimension. + """ + if np.shape(self.state.x) != np.shape(tell_data.eval_jac): + raise ValueError("The gradient does not have the correct dimension") + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) + self.state.nit += 1 + + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the gradient. - if jac is None: - eps = 0.01 if self.perturbation is None else self.perturbation - jac = partial( - Optimizer.gradient_num_diff, - f=fun, + It does so either by evaluating an analytic gradient or by approximating it with a + finite difference scheme. It will either add ``1`` to the number of gradient evaluations or add + ``N+1`` to the number of function evaluations (Where N is the dimension of the gradient). + + Args: + ask_data: It contains the point where the gradient is to be evaluated and the gradient + function or, in its absence, the objective function to perform a finite difference + approximation. + + Returns: + The data containing the gradient evaluation. + """ + if self.state.jac is None: + eps = 0.01 if (self.perturbation is None) else self.perturbation + grad = Optimizer.gradient_num_diff( + x_center=ask_data.x_jac, + f=self.state.fun, epsilon=eps, max_evals_grouped=self._max_evals_grouped, ) + self.state.nfev += 1 + len(ask_data.x_jac) + else: + grad = self.state.jac(ask_data.x_jac) + self.state.njev += 1 - # prepare some initials - x = np.asarray(x0) - nfevs = 0 + return TellData(eval_jac=grad) - for _ in range(1, self.maxiter + 1): - # compute update -- gradient evaluation counts as one function evaluation - update = jac(x) - nfevs += 1 + def create_result(self) -> OptimizerResult: + """Creates a result of the optimization process. - # compute next parameter value - x_next = x - next(eta) * update + This result contains the best point, the best function value, the number of function/gradient + evaluations and the number of iterations. - # send information to callback - stepsize = np.linalg.norm(update) - if self.callback is not None: - self.callback(nfevs, x_next, fun(x_next), stepsize) + Returns: + The result of the optimization process. + """ + result = OptimizerResult() + result.x = self.state.x + result.fun = self.state.fun(self.state.x) + result.nfev = self.state.nfev + result.njev = self.state.njev + result.nit = self.state.nit + return result - # update parameters - x = x_next + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> None: - # check termination - if stepsize < self.tol: - break + self.state = GradientDescentState( + fun=fun, + jac=jac, + x=np.asarray(x0), + nit=0, + nfev=0, + njev=0, + learning_rate=LearningRate(learning_rate=self.learning_rate), + stepsize=None, + ) + + def continue_condition(self) -> bool: + """ + Condition that indicates the optimization process should come to an end. - # TODO the optimizer result should contain the number of gradient evaluations, - # if the gradient is passed - result = OptimizerResult() - result.x = x - result.fun = fun(x) - result.nfev = nfevs + When the stepsize is smaller than the tolerance, the optimization process is considered + finished. - return result + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + if self.state.stepsize is None: + return True + else: + return (self.state.stepsize > self.tol) and super().continue_condition() def get_support_level(self): """Get the support level dictionary.""" @@ -211,10 +392,3 @@ def get_support_level(self): "bounds": OptimizerSupportLevel.ignored, "initial_point": OptimizerSupportLevel.required, } - - -def constant(eta=0.01): - """Yield a constant.""" - - while True: - yield eta diff --git a/qiskit/algorithms/optimizers/optimizer.py b/qiskit/algorithms/optimizers/optimizer.py index b8acefbf9d2c..6f2e4e1077f9 100644 --- a/qiskit/algorithms/optimizers/optimizer.py +++ b/qiskit/algorithms/optimizers/optimizer.py @@ -110,7 +110,25 @@ def nit(self, nit: Optional[int]) -> None: class Minimizer(Protocol): - """Callback Protocol for minimizer.""" + """Callable Protocol for minimizer. + + This interface is based on `SciPy's optimize module + `__. + + This protocol defines a callable taking the following parameters: + + fun + The objective function to minimize (for example the energy in the case of the VQE). + x0 + The initial point for the optimization. + jac + The gradient of the objective function. + bounds + Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + and which returns a minimization result object (either SciPy's or Qiskit's). + """ # pylint: disable=invalid-name def __call__( diff --git a/qiskit/algorithms/optimizers/optimizer_utils/__init__.py b/qiskit/algorithms/optimizers/optimizer_utils/__init__.py new file mode 100644 index 000000000000..33c5bc90b087 --- /dev/null +++ b/qiskit/algorithms/optimizers/optimizer_utils/__init__.py @@ -0,0 +1,27 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Utils for optimizers + +Optimizer Utils (:mod:`qiskit.algorithms.optimizers.optimizer_utils`) +===================================================================== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + LearningRate + +""" + +from .learning_rate import LearningRate + +__all__ = ["LearningRate"] diff --git a/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py b/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py new file mode 100644 index 000000000000..8ba8a0c69ca5 --- /dev/null +++ b/qiskit/algorithms/optimizers/optimizer_utils/learning_rate.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A class to represent the Learning Rate.""" + +from typing import Union, Callable, Optional, List, Iterator, Generator +from itertools import tee +import numpy as np + + +class LearningRate(Generator): + """Represents a Learning Rate. + Will be an attribute of :class:`~.GradientDescentState`. Note that :class:`~.GradientDescent` also + has a learning rate. That learning rate can be a float, a list, an array, a function returning + a generator and will be used to create a generator to be used during the + optimization process. + This class wraps ``Generator`` so that we can also access the last yielded value. + """ + + def __init__( + self, learning_rate: Union[float, List[float], np.ndarray, Callable[[], Iterator]] + ): + """ + Args: + learning_rate: Used to create a generator to iterate on. + """ + if isinstance(learning_rate, (float, int)): + self._gen = constant(learning_rate) + elif isinstance(learning_rate, Generator): + learning_rate, self._gen = tee(learning_rate) + elif isinstance(learning_rate, (list, np.ndarray)): + self._gen = (eta for eta in learning_rate) + else: + self._gen = learning_rate() + + self._current: Optional[float] = None + + def send(self, value): + """Send a value into the generator. + Return next yielded value or raise StopIteration. + """ + self._current = next(self._gen) + return self.current + + def throw(self, typ, val=None, tb=None): + """Raise an exception in the generator. + Return next yielded value or raise StopIteration. + """ + if val is None: + if tb is None: + raise typ + val = typ() + if tb is not None: + val = val.with_traceback(tb) + raise val + + @property + def current(self): + """Returns the current value of the learning rate.""" + return self._current + + +def constant(learning_rate: float = 0.01) -> Generator[float, None, None]: + """Returns a python generator that always yields the same value. + + Args: + learning_rate: The value to yield. + + Yields: + The learning rate for the next iteration. + """ + + while True: + yield learning_rate diff --git a/qiskit/algorithms/optimizers/spsa.py b/qiskit/algorithms/optimizers/spsa.py index 707409efe4cf..10d81dfdd41c 100644 --- a/qiskit/algorithms/optimizers/spsa.py +++ b/qiskit/algorithms/optimizers/spsa.py @@ -727,7 +727,7 @@ def _batch_evaluate(function, points, max_evals_grouped): num_batches += 1 # split the points - batched_points = np.split(np.asarray(points), num_batches) + batched_points = np.array_split(np.asarray(points), num_batches) results = [] for batch in batched_points: diff --git a/qiskit/algorithms/optimizers/steppable_optimizer.py b/qiskit/algorithms/optimizers/steppable_optimizer.py new file mode 100644 index 000000000000..928777133026 --- /dev/null +++ b/qiskit/algorithms/optimizers/steppable_optimizer.py @@ -0,0 +1,302 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""SteppableOptimizer interface""" + +from abc import abstractmethod, ABC +from dataclasses import dataclass +from typing import Union, Callable, Optional, Tuple, List +from .optimizer import Optimizer, POINT, OptimizerResult + + +@dataclass +class AskData(ABC): + """Base class for return type of :meth:`~.SteppableOptimizer.ask`. + + Args: + x_fun: Point or list of points where the function needs to be evaluated to compute the next + state of the optimizer. + x_jac: Point or list of points where the gradient/jacobian needs to be evaluated to compute + the next state of the optimizer. + + """ + + x_fun: Optional[Union[POINT, List[POINT]]] = None + x_jac: Optional[Union[POINT, List[POINT]]] = None + + +@dataclass +class TellData(ABC): + """Base class for argument type of :meth:`~.SteppableOptimizer.tell`. + + Args: + eval_fun: Image of the function at :attr:`~.ask_data.x_fun`. + eval_jac: Image of the gradient-jacobian at :attr:`~.ask_data.x_jac`. + + """ + + eval_fun: Union[float, List[float], None] = None + eval_jac: Union[POINT, List[POINT], None] = None + + +@dataclass +class OptimizerState: + """Base class representing the state of the optimizer. + + This class stores the current state of the optimizer, given by the current point and + (optionally) information like the function value, the gradient or the number of + function evaluations. This dataclass can also store any other individual variables that + change during the optimization. + + """ + + x: POINT # pylint: disable=invalid-name + """Current optimization parameters.""" + fun: Optional[Callable[[POINT], float]] + """Function being optimized.""" + jac: Optional[Callable[[POINT], POINT]] + """Jacobian of the function being optimized.""" + nfev: Optional[int] + """Number of function evaluations so far in the optimization.""" + njev: Optional[int] + """Number of jacobian evaluations so far in the opimization.""" + nit: Optional[int] + """Number of optmization steps performed so far in the optimization.""" + + +class SteppableOptimizer(Optimizer): + """ + Base class for a steppable optimizer. + + This family of optimizers uses the `ask and tell interface + `_. + When using this interface the user has to call :meth:`~.ask` to get information about + how to evaluate the fucntion (we are asking the optimizer about how to do the evaluation). + This information is typically the next points at which the function is evaluated, but depending + on the optimizer it can also determine whether to evaluate the function or its gradient. + Once the function has been evaluated, the user calls the method :meth:`~..tell` + to tell the optimizer what the result of the function evaluation(s) is. The optimizer then + updates its state accordingly and the user can decide whether to stop the optimization process + or to repeat a step. + + This interface is more customizable, and allows the user to have full control over the evaluation + of the function. + + Examples: + + An example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing + the result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + cf = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html + + + """ + + def __init__( + self, + maxiter: int = 100, + ): + """ + Args: + maxiter: Number of steps in the optimization process before ending the loop. + """ + super().__init__() + self._state: Optional[OptimizerState] = None + self.maxiter = maxiter + + @property + def state(self) -> OptimizerState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: OptimizerState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + def ask(self) -> AskData: + """Ask the optimizer for a set of points to evaluate. + + This method asks the optimizer which are the next points to evaluate. + These points can, e.g., correspond to function values and/or its derivative. + It may also correspond to variables that let the user infer which points to evaluate. + It is the first method inside of a :meth:`~.step` in the optimization process. + + Returns: + An object containing the data needed to make the funciton evaluation to advance the + optimization process. + + """ + raise NotImplementedError + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """Updates the optimization state using the results of the function evaluation. + + A canonical optimization example using :meth:`~.ask` and :meth:`~.tell` can be seen + in :meth:`~.step`. + + Args: + ask_data: Contains the information on how the evaluation was done. + tell_data: Contains all relevant information about the evaluation of the objective + function. + """ + raise NotImplementedError + + @abstractmethod + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the function according to the instructions contained in :attr:`~.ask_data`. + + If the user decides to use :meth:`~.step` instead of :meth:`~.ask` and :meth:`~.tell` + this function will contain the logic on how to evaluate the function. + + Args: + ask_data: Contains the information on how to do the evaluation. + + Returns: + Data of all relevant information about the function evaluation. + + """ + raise NotImplementedError + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accomodate each optimizer. + """ + pass + + def step(self) -> None: + """Performs one step in the optimization process. + + This method composes :meth:`~.ask`, :meth:`~.evaluate`, and :meth:`~.tell` to make a "step" + in the optimization process. + """ + ask_data = self.ask() + tell_data = self.evaluate(ask_data=ask_data) + self.tell(ask_data=ask_data, tell_data=tell_data) + + # pylint: disable=invalid-name + @abstractmethod + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> None: + """Populates the state of the optimizer with the data provided and sets all the counters to 0. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + """ + raise NotImplementedError + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Optional[Callable[[POINT], POINT]] = None, + bounds: Optional[List[Tuple[float, float]]] = None, + ) -> OptimizerResult: + """Minimizes the function. + + For well behaved functions the user can call this method to minimize a function. + If the user wants more control on how to evaluate the function a custom loop can be + created using :meth:`~.ask` and :meth:`~.tell` and evaluating the function manually. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + Returns: + Object containing the result of the optimization. + + """ + self.start(x0=x0, fun=fun, jac=jac, bounds=bounds) + while self.continue_condition(): + self.step() + self._callback_wrapper() + return self.create_result() + + @abstractmethod + def create_result(self) -> OptimizerResult: + """Returns the result of the optimization. + + All the information needed to create such a result should be stored in the optimizer state + and will typically contain the best point found, the function value and gradient at that point, + the number of function and gradient evaluation and the number of iterations in the optimization. + + Returns: + The result of the optimization process. + + """ + raise NotImplementedError + + def continue_condition(self) -> bool: + """Condition that indicates the optimization process should continue. + + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + return self.state.nit < self.maxiter diff --git a/qiskit/algorithms/state_fidelities/__init__.py b/qiskit/algorithms/state_fidelities/__init__.py new file mode 100644 index 000000000000..ea8e4e03bf89 --- /dev/null +++ b/qiskit/algorithms/state_fidelities/__init__.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +===================================================================== +State Fidelity Interfaces (:mod:`qiskit.algorithms.state_fidelities`) +===================================================================== + +.. currentmodule:: qiskit.algorithms.state_fidelities + +State Fidelities +================ + +.. autosummary:: + :toctree: ../stubs/ + + BaseStateFidelity + ComputeUncompute + +Results +======= + + .. autosummary:: + :toctree: ../stubs/ + + StateFidelityResult + +""" + +from .base_state_fidelity import BaseStateFidelity +from .compute_uncompute import ComputeUncompute +from .state_fidelity_result import StateFidelityResult + +__all__ = ["BaseStateFidelity", "ComputeUncompute", "StateFidelityResult"] diff --git a/qiskit/algorithms/state_fidelities/base_state_fidelity.py b/qiskit/algorithms/state_fidelities/base_state_fidelity.py new file mode 100644 index 000000000000..75f4d632396d --- /dev/null +++ b/qiskit/algorithms/state_fidelities/base_state_fidelity.py @@ -0,0 +1,306 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Base state fidelity interface +""" + +from __future__ import annotations +from abc import ABC, abstractmethod +from collections.abc import Sequence, Mapping +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.algorithms import AlgorithmJob +from qiskit.circuit import ParameterVector +from .state_fidelity_result import StateFidelityResult + + +class BaseStateFidelity(ABC): + r""" + An interface to calculate state fidelities (state overlaps) for pairs of + (parametrized) quantum circuits. The calculation depends on the particular + fidelity method implementation, but can be always defined as the state overlap: + + .. math:: + + |\langle\psi(x)|\phi(y)\rangle|^2 + + where :math:`x` and :math:`y` are optional parametrizations of the + states :math:`\psi` and :math:`\phi` prepared by the circuits + ``circuit_1`` and ``circuit_2``, respectively. + + """ + + def __init__(self) -> None: + + # use cache for preventing unnecessary circuit compositions + self._circuit_cache: Mapping[(int, int), QuantumCircuit] = {} + + @staticmethod + def _preprocess_values( + circuits: QuantumCircuit | Sequence[QuantumCircuit], + values: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> Sequence[Sequence[float]]: + """ + Checks whether the passed values match the shape of the parameters + of the corresponding circuits and formats values to 2D list. + + Args: + circuits: List of circuits to be checked. + values: Parameter values corresponding to the circuits to be checked. + + Returns: + A 2D value list if the values match the circuits, or an empty 2D list + if values is None. + + Raises: + ValueError: if the number of parameter values doesn't match the number of + circuit parameters + TypeError: if the input values are not a sequence. + """ + + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + + if values is None: + for circuit in circuits: + if circuit.num_parameters != 0: + raise ValueError( + f"`values` cannot be `None` because circuit <{circuit.name}> has " + f"{circuit.num_parameters} free parameters." + ) + return [[]] + else: + + # Support ndarray + if isinstance(values, np.ndarray): + values = values.tolist() + if len(values) > 0 and isinstance(values[0], np.ndarray): + values = [v.tolist() for v in values] + + if not isinstance(values, Sequence): + raise TypeError( + f"Expected a sequence of numerical parameter values, " + f"but got input type {type(values)} instead." + ) + + # ensure 2d + if len(values) > 0 and not isinstance(values[0], Sequence): + values = [values] + return values + + def _check_qubits_match(self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit) -> None: + """ + Checks that the number of qubits of 2 circuits matches. + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Raises: + ValueError: when ``circuit_1`` and ``circuit_2`` don't have the + same number of qubits. + """ + + if circuit_1.num_qubits != circuit_2.num_qubits: + raise ValueError( + f"The number of qubits for the first circuit ({circuit_1.num_qubits}) " + f"and second circuit ({circuit_2.num_qubits}) are not the same." + ) + + @abstractmethod + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + """ + Implementation-dependent method to create a fidelity circuit + from 2 circuit inputs. + + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Returns: + The fidelity quantum circuit corresponding to ``circuit_1`` and ``circuit_2``. + """ + raise NotImplementedError + + def _construct_circuits( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + ) -> Sequence[QuantumCircuit]: + """ + Constructs the list of fidelity circuits to be evaluated. + These circuits represent the state overlap between pairs of input circuits, + and their construction depends on the fidelity method implementations. + + Args: + circuits_1: (Parametrized) quantum circuits. + circuits_2: (Parametrized) quantum circuits. + + Returns: + List of constructed fidelity circuits. + + Raises: + ValueError: if the length of the input circuit lists doesn't match. + """ + + if isinstance(circuits_1, QuantumCircuit): + circuits_1 = [circuits_1] + if isinstance(circuits_2, QuantumCircuit): + circuits_2 = [circuits_2] + + if len(circuits_1) != len(circuits_2): + raise ValueError( + f"The length of the first circuit list({len(circuits_1)}) " + f"and second circuit list ({len(circuits_2)}) is not the same." + ) + + circuits = [] + for (circuit_1, circuit_2) in zip(circuits_1, circuits_2): + + # TODO: improve caching, what if the circuit is modified without changing the id? + circuit = self._circuit_cache.get((id(circuit_1), id(circuit_2))) + + if circuit is not None: + circuits.append(circuit) + else: + self._check_qubits_match(circuit_1, circuit_2) + + # re-parametrize input circuits + # TODO: make smarter checks to avoid unnecesary reparametrizations + parameters_1 = ParameterVector("x", circuit_1.num_parameters) + parametrized_circuit_1 = circuit_1.assign_parameters(parameters_1) + parameters_2 = ParameterVector("y", circuit_2.num_parameters) + parametrized_circuit_2 = circuit_2.assign_parameters(parameters_2) + + circuit = self.create_fidelity_circuit( + parametrized_circuit_1, parametrized_circuit_2 + ) + circuits.append(circuit) + # update cache + self._circuit_cache[id(circuit_1), id(circuit_2)] = circuit + + return circuits + + def _construct_value_list( + self, + circuits_1: Sequence[QuantumCircuit], + circuits_2: Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> list[float]: + """ + Preprocesses input parameter values to match the fidelity + circuit parametrization, and return in list format. + + Args: + circuits_1: (Parametrized) quantum circuits preparing the + first list of quantum states. + circuits_2: (Parametrized) quantum circuits preparing the + second list of quantum states. + values_1: Numerical parameters to be bound to the first circuits. + values_2: Numerical parameters to be bound to the second circuits. + + Returns: + List of parameter values for fidelity circuit. + + """ + values_1 = self._preprocess_values(circuits_1, values_1) + values_2 = self._preprocess_values(circuits_2, values_2) + + values = [] + if len(values_2[0]) == 0: + values = list(values_1) + elif len(values_1[0]) == 0: + values = list(values_2) + else: + for (val_1, val_2) in zip(values_1, values_2): + values.append(val_1 + val_2) + + return values + + @abstractmethod + def _run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **run_options, + ) -> StateFidelityResult: + r""" + Computes the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first set of circuits + values_2: Numerical parameters to be bound to the second set of circuits. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The result of the fidelity calculation. + """ + raise NotImplementedError + + def run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **run_options, + ) -> AlgorithmJob: + r""" + Runs asynchronously the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). This calculation depends on the particular + fidelity method implementation. + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first set of circuits. + values_2: Numerical parameters to be bound to the second set of circuits. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + Primitive job for the fidelity calculation. + The job's result is an instance of ``StateFidelityResult``. + """ + + job = AlgorithmJob(self._run, circuits_1, circuits_2, values_1, values_2, **run_options) + + job.submit() + return job + + def _truncate_fidelities(self, fidelities: Sequence[float]) -> Sequence[float]: + """ + Ensures fidelity result in [0,1]. + + Args: + fidelities: Sequence of raw fidelity results. + + Returns: + List of truncated fidelities. + + """ + return np.clip(fidelities, 0, 1).tolist() diff --git a/qiskit/algorithms/state_fidelities/compute_uncompute.py b/qiskit/algorithms/state_fidelities/compute_uncompute.py new file mode 100644 index 000000000000..ff9080e5d3ba --- /dev/null +++ b/qiskit/algorithms/state_fidelities/compute_uncompute.py @@ -0,0 +1,145 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Compute-uncompute fidelity interface using primitives +""" + +from __future__ import annotations +from collections.abc import Sequence +from copy import copy + +from qiskit import QuantumCircuit +from qiskit.algorithms import AlgorithmError +from qiskit.primitives import BaseSampler + +from .base_state_fidelity import BaseStateFidelity +from .state_fidelity_result import StateFidelityResult + + +class ComputeUncompute(BaseStateFidelity): + r""" + This class leverages the sampler primitive to calculate the state + fidelity of two quantum circuits following the compute-uncompute + method (see [1] for further reference). + The fidelity can be defined as the state overlap. + + .. math:: + + |\langle\psi(x)|\phi(y)\rangle|^2 + + where :math:`x` and :math:`y` are optional parametrizations of the + states :math:`\psi` and :math:`\phi` prepared by the circuits + ``circuit_1`` and ``circuit_2``, respectively. + + **Reference:** + [1] Havlíček, V., Córcoles, A. D., Temme, K., Harrow, A. W., Kandala, + A., Chow, J. M., & Gambetta, J. M. (2019). Supervised learning + with quantum-enhanced feature spaces. Nature, 567(7747), 209-212. + `arXiv:1804.11326v2 [quant-ph] `_ + + """ + + def __init__(self, sampler: BaseSampler, **run_options) -> None: + """ + Args: + sampler: Sampler primitive instance. + run_options: Backend runtime options used for circuit execution. + + Raises: + ValueError: If the sampler is not an instance of ``BaseSampler``. + """ + if not isinstance(sampler, BaseSampler): + raise ValueError( + f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" + ) + self._sampler: BaseSampler = sampler + self._default_run_options = run_options + super().__init__() + + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + """ + Combines ``circuit_1`` and ``circuit_2`` to create the + fidelity circuit following the compute-uncompute method. + + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Returns: + The fidelity quantum circuit corresponding to circuit_1 and circuit_2. + """ + circuit = circuit_1.compose(circuit_2.inverse()) + circuit.measure_all() + return circuit + + def _run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **run_options, + ) -> StateFidelityResult: + r""" + Computes the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second) following the compute-uncompute method. + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first circuits. + values_2: Numerical parameters to be bound to the second circuits. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The result of the fidelity calculation. + + Raises: + ValueError: At least one pair of circuits must be defined. + AlgorithmError: If the sampler job is not completed successfully. + """ + + circuits = self._construct_circuits(circuits_1, circuits_2) + if len(circuits) == 0: + raise ValueError( + "At least one pair of circuits must be defined to calculate the state overlap." + ) + values = self._construct_value_list(circuits_1, circuits_2, values_1, values_2) + + # The priority of run options is as follows: + # run_options in `evaluate` method > fidelity's default run_options > + # primitive's default run_options. + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + + job = self._sampler.run(circuits=circuits, parameter_values=values, **run_opts) + + try: + result = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed!") from exc + + raw_fidelities = [prob_dist.get(0, 0) for prob_dist in result.quasi_dists] + fidelities = self._truncate_fidelities(raw_fidelities) + + return StateFidelityResult( + fidelities=fidelities, + raw_fidelities=raw_fidelities, + metadata=result.metadata, + run_options=run_opts, + ) diff --git a/qiskit/algorithms/state_fidelities/state_fidelity_result.py b/qiskit/algorithms/state_fidelities/state_fidelity_result.py new file mode 100644 index 000000000000..04d4aa0ca411 --- /dev/null +++ b/qiskit/algorithms/state_fidelities/state_fidelity_result.py @@ -0,0 +1,35 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Fidelity result class +""" + +from __future__ import annotations + +from collections.abc import Sequence, Mapping +from typing import Any +from dataclasses import dataclass + + +@dataclass(frozen=True) +class StateFidelityResult: + """This class stores the result of StateFidelity computations.""" + + fidelities: Sequence[float] + """List of truncated fidelity values for each pair of input circuits, ensured to be in [0,1].""" + raw_fidelities: Sequence[float] + """List of raw fidelity values for each pair of input circuits, which might not be in [0,1] + depending on the error mitigation method used.""" + metadata: Sequence[Mapping[str, Any]] + """Additional information about the fidelity calculation.""" + run_options: Mapping[str, Any] + """Runtime options for the execution of the fidelity job.""" diff --git a/qiskit/circuit/barrier.py b/qiskit/circuit/barrier.py index 62ecc04512e4..7081fbcd6604 100644 --- a/qiskit/circuit/barrier.py +++ b/qiskit/circuit/barrier.py @@ -25,9 +25,18 @@ class Barrier(Instruction): _directive = True - def __init__(self, num_qubits): - """Create new barrier instruction.""" - super().__init__("barrier", num_qubits, 0, []) + def __init__(self, num_qubits, label=None): + """Create new barrier instruction. + + Args: + num_qubits (int): the number of qubits for the barrier type [Default: 0]. + label (str): the barrier label + + Raises: + TypeError: if barrier label is invalid. + """ + self._label = label + super().__init__("barrier", num_qubits, 0, [], label=label) def inverse(self): """Special case. Return self.""" diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 4bc61ee33561..a975e20aec9b 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -80,6 +80,11 @@ def commute( Returns: bool: whether two operations commute. """ + # We don't support commutation of conditional gates for now due to bugs in + # CommutativeCancellation. See gh-8553. + if getattr(op1, "condition") is not None or getattr(op2, "condition") is not None: + return False + # These lines are adapted from dag_dependency and say that two gates over # different quantum and classical bits necessarily commute. This is more # permissive that the check from commutation_analysis, as for example it @@ -91,15 +96,13 @@ def commute( if not (intersection_q or intersection_c): return True - # These lines are adapted from commutation_analysis, which is more restrictive - # than the check from dag_dependency when considering nodes with "_directive" - # or "condition". It would be nice to think which optimizations - # from dag_dependency can indeed be used. + # These lines are adapted from commutation_analysis, which is more restrictive than the + # check from dag_dependency when considering nodes with "_directive". It would be nice to + # think which optimizations from dag_dependency can indeed be used. for op in [op1, op2]: if ( getattr(op, "_directive", False) or op.name in {"measure", "reset", "delay"} - or getattr(op, "condition", None) or op.is_parameterized() ): return False diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index e80d009cb0ba..5440efdb4d1c 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -12,16 +12,15 @@ """Gate equivalence library.""" -import io from collections import namedtuple +from retworkx.visualization import graphviz_draw import retworkx as rx -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.exceptions import InvalidFileError from .exceptions import CircuitError from .parameterexpression import ParameterExpression - Key = namedtuple("Key", ["name", "num_qubits"]) Entry = namedtuple("Entry", ["search_base", "equivalences"]) @@ -148,56 +147,20 @@ def draw(self, filename=None): IPython SVG if in a jupyter notebook, or as a PIL.Image otherwise. Raises: - MissingOptionalLibraryError: when pydot or pillow are not installed. + InvalidFileError: if filename is not valid. """ - try: - import pydot - - has_pydot = True - except ImportError: - has_pydot = False - try: - from PIL import Image - - has_pil = True - except ImportError: - has_pil = False - - if not has_pydot: - raise MissingOptionalLibraryError( - libname="pydot", - name="EquivalenceLibrary.draw", - pip_install="pip install pydot", - ) - if not has_pil and not filename: - raise MissingOptionalLibraryError( - libname="pillow", - name="EquivalenceLibrary.draw", - pip_install="pip install pillow", - ) - - try: - from IPython.display import SVG - - has_ipython = True - except ImportError: - has_ipython = False - - dot_str = self._build_basis_graph().to_dot( - lambda node: {"label": node["label"]}, lambda edge: edge - ) - dot = pydot.graph_from_dot_data(dot_str)[0] + image_type = None if filename: - extension = filename.split(".")[-1] - dot.write(filename, format=extension) - return None - - if has_ipython: - svg = dot.create_svg(prog="dot") - return SVG(svg) - - png = dot.create_png(prog="dot") - return Image.open(io.BytesIO(png)) + if "." not in filename: + raise InvalidFileError("Parameter 'filename' must be in format 'name.extension'") + image_type = filename.split(".")[-1] + return graphviz_draw( + self._build_basis_graph(), + lambda node: {"label": node["label"]}, + lambda edge: edge, + filename=filename, + image_type=image_type, + ) def _build_basis_graph(self): graph = rx.PyDiGraph() diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 43592514da1f..943e74f445b5 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.exceptions import QiskitError -from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates.x import CXGate, XGate from qiskit.circuit.library.standard_gates.h import HGate @@ -427,8 +427,9 @@ def prepare_state(self, state, qubits=None, label=None): to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - qubits (QuantumRegister or int): + qubits (QuantumRegister or Qubit or int): * QuantumRegister: A list of qubits to be initialized [Default: None]. + * Qubit: Single qubit to be initialized [Default: None]. * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. label (str): An optional label for the gate @@ -505,7 +506,7 @@ def prepare_state(self, state, qubits=None, label=None): if qubits is None: qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice)): + elif isinstance(qubits, (int, np.integer, slice, Qubit)): qubits = [qubits] num_qubits = len(qubits) if isinstance(state, int) else None diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index c3d464d66879..d1345e13d8f2 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -36,6 +36,7 @@ CXGate CYGate CZGate + CCZGate HGate IGate MCPhaseGate @@ -54,6 +55,8 @@ ECRGate SGate SdgGate + CSGate + CSdgGate SwapGate iSwapGate SXGate @@ -84,7 +87,7 @@ from .xx_minus_yy import XXMinusYYGate from .xx_plus_yy import XXPlusYYGate from .ecr import ECRGate -from .s import SGate, SdgGate +from .s import SGate, SdgGate, CSGate, CSdgGate from .swap import SwapGate, CSwapGate from .iswap import iSwapGate from .sx import SXGate, SXdgGate, CSXGate @@ -97,6 +100,6 @@ from .x import XGate, CXGate, CCXGate, C3XGate, C3SXGate, C4XGate, RCCXGate, RC3XGate from .x import MCXGate, MCXGrayCode, MCXRecursive, MCXVChain from .y import YGate, CYGate -from .z import ZGate, CZGate +from .z import ZGate, CZGate, CCZGate from .multi_control_rotation_gates import mcrx, mcry, mcrz diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 65184b6dfbd2..f847d9a62eed 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -36,6 +36,8 @@ RZXGate, SGate, SdgGate, + CSGate, + CSdgGate, SwapGate, CSwapGate, iSwapGate, @@ -61,6 +63,7 @@ ECRGate, ZGate, CZGate, + CCZGate, ) @@ -252,6 +255,20 @@ def_rxx.append(inst, qargs, cargs) _sel.add_equivalence(RXXGate(theta), def_rxx) +# RXX to RZZ +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rxx_to_rzz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), + (RZZGate(theta), [q[0], q[1]], []), + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), +]: + rxx_to_rzz.append(inst, qargs, cargs) +_sel.add_equivalence(RXXGate(theta), rxx_to_rzz) + # RZXGate # # ┌─────────┐ @@ -324,6 +341,20 @@ def_ryy.append(inst, qargs, cargs) _sel.add_equivalence(RYYGate(theta), def_ryy) +# RYY to RZZ +q = QuantumRegister(2, "q") +theta = Parameter("theta") +ryy_to_rzz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (RXGate(pi / 2), [q[0]], []), + (RXGate(pi / 2), [q[1]], []), + (RZZGate(theta), [q[0], q[1]], []), + (RXGate(-pi / 2), [q[0]], []), + (RXGate(-pi / 2), [q[1]], []), +]: + ryy_to_rzz.append(inst, qargs, cargs) +_sel.add_equivalence(RYYGate(theta), ryy_to_rzz) + # RZGate # global phase: -ϴ/2 # ┌───────┐ ┌───────┐ @@ -382,6 +413,35 @@ def_rzz.append(inst, qargs, cargs) _sel.add_equivalence(RZZGate(theta), def_rzz) +# RZZ to RXX +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rzz_to_rxx = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), + (RXXGate(theta), [q[0], q[1]], []), + (HGate(), [q[0]], []), + (HGate(), [q[1]], []), +]: + rzz_to_rxx.append(inst, qargs, cargs) +_sel.add_equivalence(RZZGate(theta), rzz_to_rxx) + +# RZZ to RYY +q = QuantumRegister(2, "q") +theta = Parameter("theta") +rzz_to_ryy = QuantumCircuit(q) +for inst, qargs, cargs in [ + (RXGate(-pi / 2), [q[0]], []), + (RXGate(-pi / 2), [q[1]], []), + (RYYGate(theta), [q[0], q[1]], []), + (RXGate(pi / 2), [q[0]], []), + (RXGate(pi / 2), [q[1]], []), +]: + rzz_to_ryy.append(inst, qargs, cargs) +_sel.add_equivalence(RZZGate(theta), rzz_to_ryy) + + # RZXGate # # ┌─────────┐ @@ -439,6 +499,33 @@ def_sdg.append(U1Gate(-pi / 2), [q[0]], []) _sel.add_equivalence(SdgGate(), def_sdg) +# CSGate +# +# q_0: ──■── q_0: ───────■──────── +# ┌─┴─┐ ┌───┐┌─┴──┐┌───┐ +# q_1: ┤ S ├ = q_1: ┤ H ├┤ Sx ├┤ H ├ +# └───┘ └───┘└────┘└───┘ +q = QuantumRegister(2, "q") +def_cs = QuantumCircuit(q) +def_cs.append(HGate(), [q[1]], []) +def_cs.append(CSXGate(), [q[0], q[1]], []) +def_cs.append(HGate(), [q[1]], []) +_sel.add_equivalence(CSGate(), def_cs) + +# CSdgGate +# +# q_0: ───■─── q_0: ───────■────■──────── +# ┌──┴──┐ ┌───┐┌─┴─┐┌─┴──┐┌───┐ +# q_1: ┤ Sdg ├ = q_1: ┤ H ├┤ X ├┤ Sx ├┤ H ├ +# └─────┘ └───┘└───┘└────┘└───┘ +q = QuantumRegister(2, "q") +def_csdg = QuantumCircuit(q) +def_csdg.append(HGate(), [q[1]], []) +def_csdg.append(CXGate(), [q[0], q[1]], []) +def_csdg.append(CSXGate(), [q[0], q[1]], []) +def_csdg.append(HGate(), [q[1]], []) +_sel.add_equivalence(CSdgGate(), def_csdg) + # SdgGate # # ┌─────┐ ┌───┐┌───┐ @@ -1165,6 +1252,24 @@ def_cz.append(inst, qargs, cargs) _sel.add_equivalence(CZGate(), def_cz) +# CCZGate +# +# q_0: ─■─ q_0: ───────■─────── +# │ │ +# q_1: ─■─ = q_1: ───────■─────── +# │ ┌───┐┌─┴─┐┌───┐ +# q_2: ─■─ q_2: ┤ H ├┤ X ├┤ H ├ +# └───┘└───┘└───┘ +q = QuantumRegister(3, "q") +def_ccz = QuantumCircuit(q) +for inst, qargs, cargs in [ + (HGate(), [q[2]], []), + (CCXGate(), [q[0], q[1], q[2]], []), + (HGate(), [q[2]], []), +]: + def_ccz.append(inst, qargs, cargs) +_sel.add_equivalence(CCZGate(), def_ccz) + # XGate # global phase: π/2 # ┌───┐ ┌───────┐ diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index a9c2d96555a4..5bda08fa2a69 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -10,11 +10,12 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The S and Sdg gate.""" +"""The S, Sdg, CS and CSdg gates.""" -from typing import Optional +from typing import Optional, Union import numpy from qiskit.qasm import pi +from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister @@ -135,3 +136,155 @@ def inverse(self): def __array__(self, dtype=None): """Return a numpy.array for the Sdg gate.""" return numpy.array([[1, 0], [0, -1j]], dtype=dtype) + + +class CSGate(ControlledGate): + r"""Controlled-S gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.cs` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ──■── + ┌─┴─┐ + q_1: ┤ S ├ + └───┘ + + **Matrix representation:** + + .. math:: + + CS \ q_0, q_1 = + I \otimes |0 \rangle\langle 0| + S \otimes |1 \rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & i + \end{pmatrix} + """ + # Define class constants. This saves future allocation time. + _matrix1 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1j], + ] + ) + _matrix0 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1j, 0], + [0, 0, 0, 1], + ] + ) + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CS gate.""" + super().__init__( + "cs", 2, [], label=label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=SGate() + ) + + def _define(self): + """ + gate cs a,b { h b; cp(pi/2) a,b; h b; } + """ + # pylint: disable=cyclic-import + from .p import CPhaseGate + + self.definition = CPhaseGate(theta=pi / 2).definition + + def inverse(self): + """Return inverse of CSGate (CSdgGate).""" + return CSdgGate(ctrl_state=self.ctrl_state) + + def __array__(self, dtype=None): + """Return a numpy.array for the CS gate.""" + mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0 + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat + + +class CSdgGate(ControlledGate): + r"""Controlled-S^\dagger gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.csdg` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ───■─── + ┌──┴──┐ + q_1: ┤ Sdg ├ + └─────┘ + + **Matrix representation:** + + .. math:: + + CS^\dagger \ q_0, q_1 = + I \otimes |0 \rangle\langle 0| + S^\dagger \otimes |1 \rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & -i + \end{pmatrix} + """ + # Define class constants. This saves future allocation time. + _matrix1 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, -1j], + ] + ) + _matrix0 = numpy.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, -1j, 0], + [0, 0, 0, 1], + ] + ) + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CSdg gate.""" + super().__init__( + "csdg", + 2, + [], + label=label, + num_ctrl_qubits=1, + ctrl_state=ctrl_state, + base_gate=SdgGate(), + ) + + def _define(self): + """ + gate csdg a,b { h b; cp(-pi/2) a,b; h b; } + """ + # pylint: disable=cyclic-import + from .p import CPhaseGate + + self.definition = CPhaseGate(theta=-pi / 2).definition + + def inverse(self): + """Return inverse of CSdgGate (CSGate).""" + return CSGate(ctrl_state=self.ctrl_state) + + def __array__(self, dtype=None): + """Return a numpy.array for the CSdg gate.""" + mat = self._matrix1 if self.ctrl_state == 1 else self._matrix0 + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index df57352e869a..9fad215878f9 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Z and CZ gates.""" +"""Z, CZ and CCZ gates.""" from typing import Optional, Union import numpy @@ -18,6 +18,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit._utils import _compute_control_matrix class ZGate(Gate): @@ -187,3 +188,79 @@ def __array__(self, dtype=None): return numpy.array( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]], dtype=dtype ) + + +class CCZGate(ControlledGate): + r"""CCZ gate. + + This is a symmetric gate. + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + with the :meth:`~qiskit.circuit.QuantumCircuit.ccz` method. + + **Circuit symbol:** + + .. parsed-literal:: + + q_0: ─■─ + │ + q_1: ─■─ + │ + q_2: ─■─ + + **Matrix representation:** + + .. math:: + + CCZ\ q_0, q_1, q_2 = + I \otimes I \otimes |0\rangle\langle 0| + CZ \otimes |1\rangle\langle 1| = + \begin{pmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & -1 + \end{pmatrix} + + In the computational basis, this gate flips the phase of + the target qubit if the control qubits are in the :math:`|11\rangle` state. + """ + + def __init__(self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None): + """Create new CCZ gate.""" + super().__init__( + "ccz", 3, [], label=label, num_ctrl_qubits=2, ctrl_state=ctrl_state, base_gate=ZGate() + ) + + def _define(self): + """ + gate ccz a,b,c { h c; ccx a,b,c; h c; } + """ + # pylint: disable=cyclic-import + from qiskit.circuit.quantumcircuit import QuantumCircuit + from .h import HGate + from .x import CCXGate + + q = QuantumRegister(3, "q") + qc = QuantumCircuit(q, name=self.name) + rules = [(HGate(), [q[2]], []), (CCXGate(), [q[0], q[1], q[2]], []), (HGate(), [q[2]], [])] + for instr, qargs, cargs in rules: + qc._append(instr, qargs, cargs) + + self.definition = qc + + def inverse(self): + """Return inverted CCZ gate (itself).""" + return CCZGate(ctrl_state=self.ctrl_state) # self-inverse + + def __array__(self, dtype=None): + """Return a numpy.array for the CCZ gate.""" + mat = _compute_control_matrix( + self.base_gate.to_matrix(), self.num_ctrl_qubits, ctrl_state=self.ctrl_state + ) + if dtype is not None: + return numpy.asarray(mat, dtype=dtype) + return mat diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2254a9b0ce7a..a337102ea644 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -835,7 +835,8 @@ def compose( this can be anything that :obj:`.append` will accept. qubits (list[Qubit|int]): qubits of self to compose onto. clbits (list[Clbit|int]): clbits of self to compose onto. - front (bool): If True, front composition will be performed (not implemented yet). + front (bool): If True, front composition will be performed. This is not possible within + control-flow builder context managers. inplace (bool): If True, modify the object. Otherwise return composed circuit. wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on whether it contains only unitary instructions) before composing it onto self. @@ -844,12 +845,18 @@ def compose( QuantumCircuit: the composed circuit (returns None if inplace==True). Raises: - CircuitError: if composing on the front. - QiskitError: if ``other`` is wider or there are duplicate edge mappings. + CircuitError: if no correct wire mapping can be made between the two circuits, such as + if ``other`` is wider than ``self``. + CircuitError: if trying to emit a new circuit while ``self`` has a partially built + control-flow context active, such as the context-manager forms of :meth:`if_test`, + :meth:`for_loop` and :meth:`while_loop`. + CircuitError: if trying to compose to the front of a circuit when a control-flow builder + block is active; there is no clear meaning to this action. - Examples:: + Examples: + .. code-block:: python - lhs.compose(rhs, qubits=[3, 2], inplace=True) + >>> lhs.compose(rhs, qubits=[3, 2], inplace=True) .. parsed-literal:: @@ -869,6 +876,19 @@ def compose( lcr_1: 0 ═══════════ lcr_1: 0 ═══════════════════════ """ + if inplace and front and self._control_flow_scopes: + # If we're composing onto ourselves while in a stateful control-flow builder context, + # there's no clear meaning to composition to the "front" of the circuit. + raise CircuitError( + "Cannot compose to the front of a circuit while a control-flow context is active." + ) + if not inplace and self._control_flow_scopes: + # If we're inside a stateful control-flow builder scope, even if we successfully cloned + # the partial builder scope (not simple), the scope wouldn't be controlled by an active + # `with` statement, so the output circuit would be permanently broken. + raise CircuitError( + "Cannot emit a new composed circuit while a control-flow context is active." + ) if inplace: dest = self @@ -962,8 +982,9 @@ def compose( mapped_instrs += dest.data dest.data.clear() dest._parameter_table.clear() + append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append for instr in mapped_instrs: - dest._append(instr) + append(instr) for gate, cals in other.calibrations.items(): dest._calibrations[gate].update(cals) @@ -1677,6 +1698,7 @@ def qasm( "sx", "sxdg", "cz", + "ccz", "cy", "swap", "ch", @@ -1689,6 +1711,8 @@ def qasm( "cp", "cu3", "csx", + "cs", + "csdg", "cu", "rxx", "rzz", @@ -2499,7 +2523,63 @@ def global_phase(self, angle: ParameterValueType): @property def parameters(self) -> ParameterView: - """Convenience function to get the parameters defined in the parameter table.""" + """The parameters defined in the circuit. + + This attribute returns the :class:`.Parameter` objects in the circuit sorted + alphabetically. Note that parameters instantiated with a :class:`.ParameterVector` + are still sorted numerically. + + Examples: + + The snippet below shows that insertion order of parameters does not matter. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter + >>> a, b, elephant = Parameter("a"), Parameter("b"), Parameter("elephant") + >>> circuit = QuantumCircuit(1) + >>> circuit.rx(b, 0) + >>> circuit.rz(elephant, 0) + >>> circuit.ry(a, 0) + >>> circuit.parameters # sorted alphabetically! + ParameterView([Parameter(a), Parameter(b), Parameter(elephant)]) + + Bear in mind that alphabetical sorting might be unituitive when it comes to numbers. + The literal "10" comes before "2" in strict alphabetical sorting. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter + >>> angles = [Parameter("angle_1"), Parameter("angle_2"), Parameter("angle_10")] + >>> circuit = QuantumCircuit(1) + >>> circuit.u(*angles, 0) + >>> circuit.draw() + ┌─────────────────────────────┐ + q: ┤ U(angle_1,angle_2,angle_10) ├ + └─────────────────────────────┘ + >>> circuit.parameters + ParameterView([Parameter(angle_1), Parameter(angle_10), Parameter(angle_2)]) + + To respect numerical sorting, a :class:`.ParameterVector` can be used. + + .. code-block:: python + + >>> from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector + >>> x = ParameterVector("x", 12) + >>> circuit = QuantumCircuit(1) + >>> for x_i in x: + ... circuit.rx(x_i, 0) + >>> circuit.parameters + ParameterView([ + ParameterVectorElement(x[0]), ParameterVectorElement(x[1]), + ParameterVectorElement(x[2]), ParameterVectorElement(x[3]), + ..., ParameterVectorElement(x[11]) + ]) + + + Returns: + The sorted :class:`.Parameter` objects in the circuit. + """ # parameters from gates if self._parameters is None: unsorted = self._unsorted_parameters() @@ -2510,7 +2590,7 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: - """Convenience function to get the number of parameter objects in the circuit.""" + """The number of parameter objects in the circuit.""" return len(self._unsorted_parameters()) def _unsorted_parameters(self) -> Set[Parameter]: @@ -2528,18 +2608,20 @@ def assign_parameters( ) -> Optional["QuantumCircuit"]: """Assign parameters to new parameters or values. - The keys of the parameter dictionary must be Parameter instances in the current circuit. The - values of the dictionary can either be numeric values or new parameter objects. + If ``parameters`` is passed as a dictionary, the keys must be :class:`.Parameter` + instances in the current circuit. The values of the dictionary can either be numeric values + or new parameter objects. + + If ``parameters`` is passed as a list or array, the elements are assigned to the + current parameters in the order of :attr:`parameters` which is sorted + alphabetically (while respecting the ordering in :class:`.ParameterVector` objects). + The values can be assigned to the current circuit object or to a copy of it. Args: - parameters (dict or iterable): Either a dictionary or iterable specifying the new - parameter values. If a dict, it specifies the mapping from ``current_parameter`` to - ``new_parameter``, where ``new_parameter`` can be a new parameter object or a - numeric value. If an iterable, the elements are assigned to the existing parameters - in the order of ``QuantumCircuit.parameters``. - inplace (bool): If False, a copy of the circuit with the bound parameters is - returned. If True the circuit instance itself is modified. + parameters: Either a dictionary or iterable specifying the new parameter values. + inplace: If False, a copy of the circuit with the bound parameters is returned. + If True the circuit instance itself is modified. Raises: CircuitError: If parameters is a dict and contains parameters not present in the @@ -2548,8 +2630,7 @@ def assign_parameters( parameters in the circuit. Returns: - Optional(QuantumCircuit): A copy of the circuit with bound parameters, if - ``inplace`` is False, otherwise None. + A copy of the circuit with bound parameters, if ``inplace`` is False, otherwise None. Examples: @@ -2572,7 +2653,7 @@ def assign_parameters( print('Assigned in-place:') print(circuit.draw()) - Bind the values out-of-place and get a copy of the original circuit. + Bind the values out-of-place by list and get a copy of the original circuit. .. jupyter-execute:: @@ -2583,7 +2664,7 @@ def assign_parameters( circuit.ry(params[0], 0) circuit.crx(params[1], 0, 1) - bound_circuit = circuit.assign_parameters({params[0]: 1, params[1]: 2}) + bound_circuit = circuit.assign_parameters([1, 2]) print('Bound circuit:') print(bound_circuit.draw()) @@ -2638,18 +2719,21 @@ def bind_parameters( ) -> "QuantumCircuit": """Assign numeric parameters to values yielding a new circuit. + If the values are given as list or array they are bound to the circuit in the order + of :attr:`parameters` (see the docstring for more details). + To assign new Parameter objects or bind the values in-place, without yielding a new circuit, use the :meth:`assign_parameters` method. Args: - values (dict or iterable): {parameter: value, ...} or [value1, value2, ...] + values: ``{parameter: value, ...}`` or ``[value1, value2, ...]`` Raises: CircuitError: If values is a dict and contains parameters not present in the circuit. TypeError: If values contains a ParameterExpression. Returns: - QuantumCircuit: copy of self with assignment substitution. + Copy of self with assignment substitution. """ if isinstance(values, dict): if any(isinstance(value, ParameterExpression) for value in values.values()): @@ -2782,10 +2866,14 @@ def _rebind_definition( inner.operation.params[idx] = param.bind({parameter: value}) self._rebind_definition(inner.operation, parameter, value) - def barrier(self, *qargs: QubitSpecifier) -> InstructionSet: + def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: """Apply :class:`~qiskit.circuit.Barrier`. If qargs is empty, applies to all qubits in the circuit. + Args: + qargs (QubitSpecifier): Specification for one or more qubit arguments. + label (str): The string label of the barrier. + Returns: qiskit.circuit.InstructionSet: handle to the added instructions. """ @@ -2808,7 +2896,7 @@ def barrier(self, *qargs: QubitSpecifier) -> InstructionSet: else: qubits.append(qarg) - return self.append(Barrier(len(qubits)), qubits, []) + return self.append(Barrier(len(qubits), label=label), qubits, []) def delay( self, @@ -3375,6 +3463,66 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: return self.append(SdgGate(), [qubit], []) + def cs( + self, + control_qubit: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + """Apply :class:`~qiskit.circuit.library.CSGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit: The qubit(s) used as the control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '1'). Defaults to controlling + on the '1' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.s import CSGate + + return self.append( + CSGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + ) + + def csdg( + self, + control_qubit: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + """Apply :class:`~qiskit.circuit.library.CSdgGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit: The qubit(s) used as the control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '1'). Defaults to controlling + on the '1' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.s import CSdgGate + + return self.append( + CSdgGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + ) + def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SwapGate`. @@ -4145,6 +4293,38 @@ def cz( CZGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], [] ) + def ccz( + self, + control_qubit1: QubitSpecifier, + control_qubit2: QubitSpecifier, + target_qubit: QubitSpecifier, + label: Optional[str] = None, + ctrl_state: Optional[Union[str, int]] = None, + ) -> InstructionSet: + r"""Apply :class:`~qiskit.circuit.library.CCZGate`. + + For the full matrix form of this gate, see the underlying gate documentation. + + Args: + control_qubit1: The qubit(s) used as the first control. + control_qubit2: The qubit(s) used as the second control. + target_qubit: The qubit(s) targeted by the gate. + label: The string label of the gate in the circuit. + ctrl_state: + The control state in decimal, or as a bitstring (e.g. '10'). Defaults to controlling + on the '11' state. + + Returns: + A handle to the instructions created. + """ + from .library.standard_gates.z import CCZGate + + return self.append( + CCZGate(label=label, ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], + [], + ) + def pauli( self, pauli_string: str, diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index ac9ccc7c0db4..d5545e9fdda6 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -80,6 +80,8 @@ def transpile( unitary_synthesis_method: str = "default", unitary_synthesis_plugin_config: dict = None, target: Target = None, + init_method: str = None, + optimization_method: str = None, ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -150,17 +152,29 @@ def transpile( [qr[0], None, None, qr[1], None, qr[2]] - layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre') + layout_method: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre'). + This can also be the external plugin name to use for the ``layout`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"layout"`` for the ``stage_name`` argument. routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre', 'toqm', 'none'). Note that to use method 'toqm', package 'qiskit-toqm' must be installed. + This can also be the external plugin name to use for the ``routing`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"routing"`` for the ``stage_name`` argument. translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') + This can also be the external plugin name to use for the ``translation`` stage. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"translation"`` for the ``stage_name`` argument. scheduling_method: Name of scheduling pass. * ``'as_soon_as_possible'``: Schedule instructions greedily, as early as possible on a qubit resource. (alias: ``'asap'``) * ``'as_late_as_possible'``: Schedule instructions late, i.e. keeping qubits in the ground state when possible. (alias: ``'alap'``) - If ``None``, no scheduling will be done. + If ``None``, no scheduling will be done. This can also be the external plugin name + to use for the ``scheduling`` stage. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"scheduling"`` for the ``stage_name`` + argument. instruction_durations: Durations of instructions. Applicable only if scheduling_method is specified. The gate lengths defined in ``backend.properties`` are used as default. @@ -246,6 +260,16 @@ def callback_func(**kwargs): the ``backend`` argument, but if you have manually constructed a :class:`~qiskit.transpiler.Target` object you can specify it manually here. This will override the target from ``backend``. + init_method: The plugin name to use for the ``init`` stage. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"init"`` for the stage + name argument. + optimization_method: The plugin name to use for the + ``optimization`` stage. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"optimization"`` for the + ``stage_name`` argument. + Returns: The transpiled circuit(s). @@ -310,6 +334,8 @@ def callback_func(**kwargs): unitary_synthesis_method, unitary_synthesis_plugin_config, target, + init_method, + optimization_method, ) # Get transpile_args to configure the circuit transpilation job(s) if coupling_map in unique_transpile_args: @@ -392,7 +418,7 @@ def _log_transpile_time(start_time, end_time): def _combine_args(shared_transpiler_args, unique_config): # Pop optimization_level to exclude it from the kwargs when building a # PassManagerConfig - level = shared_transpiler_args.pop("optimization_level") + level = shared_transpiler_args.get("optimization_level") pass_manager_config = shared_transpiler_args pass_manager_config.update(unique_config.pop("pass_manager_config")) pass_manager_config = PassManagerConfig(**pass_manager_config) @@ -560,6 +586,8 @@ def _parse_transpile_args( unitary_synthesis_method, unitary_synthesis_plugin_config, target, + init_method, + optimization_method, ) -> Tuple[List[Dict], Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -627,6 +655,8 @@ def _parse_transpile_args( shared_dict = { "optimization_level": optimization_level, "basis_gates": basis_gates, + "init_method": init_method, + "optimization_method": optimization_method, } list_transpile_args = [] diff --git a/qiskit/extensions/quantum_initializer/initializer.py b/qiskit/extensions/quantum_initializer/initializer.py index 67b1dfa4baf6..7d6c658174ad 100644 --- a/qiskit/extensions/quantum_initializer/initializer.py +++ b/qiskit/extensions/quantum_initializer/initializer.py @@ -18,6 +18,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.circuit import Instruction +from qiskit.circuit import Qubit from qiskit.circuit.library.data_preparation import StatePreparation _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -106,8 +107,9 @@ class to prepare the qubits in a specified state. to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - qubits (QuantumRegister or int): + qubits (QuantumRegister or Qubit or int): * QuantumRegister: A list of qubits to be initialized [Default: None]. + * Qubit: Single qubit to be initialized [Default: None]. * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. @@ -182,7 +184,7 @@ class to prepare the qubits in a specified state. """ if qubits is None: qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice)): + elif isinstance(qubits, (int, np.integer, slice, Qubit)): qubits = [qubits] num_qubits = len(qubits) if isinstance(params, int) else None diff --git a/qiskit/extensions/quantum_initializer/uc.py b/qiskit/extensions/quantum_initializer/uc.py index 61618efb3563..d859ed4a96de 100644 --- a/qiskit/extensions/quantum_initializer/uc.py +++ b/qiskit/extensions/quantum_initializer/uc.py @@ -110,6 +110,9 @@ def inverse(self): definition = QuantumCircuit(*self.definition.qregs) for inst in reversed(self._definition): definition._append(inst.replace(operation=inst.operation.inverse())) + + definition.global_phase = -self.definition.global_phase + inverse_gate.definition = definition return inverse_gate diff --git a/qiskit/extensions/simulator/snapshot.py b/qiskit/extensions/simulator/snapshot.py index a5806da879e8..434b6da7c344 100644 --- a/qiskit/extensions/simulator/snapshot.py +++ b/qiskit/extensions/simulator/snapshot.py @@ -60,26 +60,6 @@ def snapshot_type(self): """Return snapshot type""" return self._snapshot_type - @property - def label(self): - """Return snapshot label""" - return self._label - - @label.setter - def label(self, name): - """Set snapshot label to name - - Args: - name (str or None): label to assign unitary - - Raises: - TypeError: name is not string or None. - """ - if isinstance(name, str): - self._label = name - else: - raise TypeError("label expects a string") - def c_if(self, classical, val): raise QiskitError("Snapshots are simulator directives and cannot be conditional.") diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index ff166044e82a..b47bd80c7017 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -156,7 +156,7 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): mat = self.to_matrix() cmat = _compute_control_matrix(mat, num_ctrl_qubits, ctrl_state=None) iso = isometry.Isometry(cmat, 0, 0) - cunitary = ControlledGate( + return ControlledGate( "c-unitary", num_qubits=self.num_qubits + num_ctrl_qubits, params=[mat], @@ -166,18 +166,6 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): ctrl_state=ctrl_state, base_gate=self.copy(), ) - from qiskit.quantum_info import Operator - - # hack to correct global phase; should fix to prevent need for correction here - pmat = Operator(iso.inverse()).data @ cmat - diag = numpy.diag(pmat) - if not numpy.allclose(diag, diag[0]): - raise ExtensionError("controlled unitary generation failed") - phase = numpy.angle(diag[0]) - if phase: - # need to apply to _definition since open controls creates temporary definition - cunitary._definition.global_phase = phase - return cunitary def qasm(self): """The qasm for a custom unitary gate diff --git a/qiskit/opflow/gradients/derivative_base.py b/qiskit/opflow/gradients/derivative_base.py index 7eff1e4e57c8..1e1b3fadb5b7 100644 --- a/qiskit/opflow/gradients/derivative_base.py +++ b/qiskit/opflow/gradients/derivative_base.py @@ -105,7 +105,7 @@ def gradient_wrapper( """ from ..converters import CircuitSampler - if not grad_params: + if grad_params is None: grad_params = bind_params grad = self.convert(operator, grad_params) @@ -113,15 +113,18 @@ def gradient_wrapper( expectation = PauliExpectation() grad = expectation.convert(grad) + sampler = CircuitSampler(backend=backend) if backend is not None else None + def gradient_fn(p_values): p_values_dict = dict(zip(bind_params, p_values)) if not backend: converter = grad.assign_parameters(p_values_dict) return np.real(converter.eval()) else: - p_values_dict = {k: [v] for k, v in p_values_dict.items()} - converter = CircuitSampler(backend=backend).convert(grad, p_values_dict) - return np.real(converter.eval()[0]) + p_values_list = {k: [v] for k, v in p_values_dict.items()} + sampled = sampler.convert(grad, p_values_list) + fully_bound = sampled.bind_parameters(p_values_dict) + return np.real(fully_bound.eval()[0]) return gradient_fn diff --git a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py index 0b4abf1713f1..97cc441b5b49 100644 --- a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py @@ -356,32 +356,59 @@ def find_Z2_symmetries(cls, operator: PauliSumOp) -> "Z2Symmetries": return cls(pauli_symmetries, sq_paulis, sq_list, None) - def taper(self, operator: PauliSumOp) -> OperatorBase: - """ - Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`. - The `tapering_values` will be stored into the resulted operator for a record. + def convert_clifford(self, operator: PauliSumOp) -> OperatorBase: + """This method operates the first part of the tapering. + It converts the operator by composing it with the clifford unitaries defined in the current + symmetry. Args: - operator: the to-be-tapered operator. + operator: to-be-tapered operator Returns: - If tapering_values is None: [:class`PauliSumOp`]; otherwise, :class:`PauliSumOp` + :class:`PauliSumOp` corresponding to the converted operator. + Raises: OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + """ + if not self._symmetries or not self._sq_paulis or not self._sq_list: raise OpflowError( "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." ) - # If the operator is zero then we can skip the following. We still need to taper the - # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could - # taper to 0*"II" when symmetries remove two qubits. if not operator.is_zero(): for clifford in self.cliffords: operator = cast(PauliSumOp, clifford @ operator @ clifford) operator = operator.reduce(atol=0) + return operator + + def taper_clifford(self, operator: PauliSumOp) -> OperatorBase: + """This method operates the second part of the tapering. + This function assumes that the input operators have already been transformed using + :meth:`convert_clifford`. The redundant qubits due to the symmetries are dropped and + replaced by their two possible eigenvalues. + The `tapering_values` will be stored into the resulted operator for a record. + + Args: + operator: Partially tapered operator resulting from a call to :meth:`convert_clifford` + + Returns: + If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp` + + Raises: + OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + + """ + + if not self._symmetries or not self._sq_paulis or not self._sq_list: + raise OpflowError( + "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." + ) + # If the operator is zero then we can skip the following. We still need to taper the + # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could + # taper to 0*"II" when symmetries remove two qubits. if self._tapering_values is None: tapered_ops_list = [ self._taper(operator, list(coeff)) @@ -393,6 +420,42 @@ def taper(self, operator: PauliSumOp) -> OperatorBase: return tapered_ops + def taper(self, operator: PauliSumOp) -> OperatorBase: + """ + Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`. + The `tapering_values` will be stored into the resulted operator for a record. + + The tapering is a two-step algorithm which first converts the operator into a + :class:`PauliSumOp` with same eigenvalues but where some qubits are only acted upon + with the Pauli operators I or X. + The number M of these redundant qubits is equal to the number M of identified symmetries. + + The second step of the reduction consists in replacing these qubits with the possible + eigenvalues of the corresponding Pauli X, giving 2^M new operators with M less qubits. + If an eigenvalue sector was previously identified for the solution, then this reduces to + 1 new operator with M less qubits. + + Args: + operator: the to-be-tapered operator + + Returns: + If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp` + + Raises: + OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty + + """ + + if not self._symmetries or not self._sq_paulis or not self._sq_list: + raise OpflowError( + "Z2 symmetries, single qubit pauli and single qubit list cannot be empty." + ) + + converted_ops = self.convert_clifford(operator) + tapered_ops = self.taper_clifford(converted_ops) + + return tapered_ops + def _taper(self, op: PauliSumOp, curr_tapering_values: List[int]) -> OperatorBase: pauli_list = [] for pauli_term in op: diff --git a/qiskit/primitives/base_estimator.py b/qiskit/primitives/base_estimator.py index 830cb54c12fc..998e162e9497 100644 --- a/qiskit/primitives/base_estimator.py +++ b/qiskit/primitives/base_estimator.py @@ -212,7 +212,7 @@ def __new__( return self @deprecate_function( - "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseEstimator should be initialized directly.", ) @@ -220,7 +220,7 @@ def __enter__(self): return self @deprecate_function( - "The BaseEstimator.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseEstimator.__exit__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseEstimator should be initialized directly.", ) @@ -259,7 +259,7 @@ def parameters(self) -> tuple[ParameterView, ...]: return tuple(self._parameters) @deprecate_function( - "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "Use run method instead.", ) diff --git a/qiskit/primitives/base_sampler.py b/qiskit/primitives/base_sampler.py index 42591e582045..2b2f14b0d32f 100644 --- a/qiskit/primitives/base_sampler.py +++ b/qiskit/primitives/base_sampler.py @@ -172,7 +172,7 @@ def __new__( return self @deprecate_function( - "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseSampler should be initialized directly.", ) @@ -180,7 +180,7 @@ def __enter__(self): return self @deprecate_function( - "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "BaseSampler should be initialized directly.", ) @@ -210,7 +210,7 @@ def parameters(self) -> tuple[ParameterView, ...]: return tuple(self._parameters) @deprecate_function( - "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.21.0 " + "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.22.0 " "and will be removed no sooner than 3 months after the releasedate. " "Use run method instead.", ) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index e0ae5c9924e3..0715abfcd859 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -30,7 +30,7 @@ from .base_estimator import BaseEstimator from .estimator_result import EstimatorResult from .primitive_job import PrimitiveJob -from .utils import init_circuit, init_observable +from .utils import bound_circuit_to_instruction, init_circuit, init_observable class Estimator(BaseEstimator): @@ -102,7 +102,9 @@ def _call( f"the number of parameters ({len(self._parameters[i])})." ) bound_circuits.append( - self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) + self._circuits[i] + if len(value) == 0 + else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) ) sorted_observables = [self._observables[i] for i in observables] expectation_values = [] @@ -112,7 +114,7 @@ def _call( f"The number of qubits of a circuit ({circ.num_qubits}) does not match " f"the number of qubits of a observable ({obs.num_qubits})." ) - final_state = Statevector(circ) + final_state = Statevector(bound_circuit_to_instruction(circ)) expectation_value = final_state.expectation_value(obs) if shots is None: expectation_values.append(expectation_value) diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index e5cee820ffce..f9c1c2910ecb 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -29,7 +29,7 @@ from .base_sampler import BaseSampler from .primitive_job import PrimitiveJob from .sampler_result import SamplerResult -from .utils import final_measurement_mapping, init_circuit +from .utils import bound_circuit_to_instruction, final_measurement_mapping, init_circuit class Sampler(BaseSampler): @@ -106,15 +106,14 @@ def _call( f"The number of values ({len(value)}) does not match " f"the number of parameters ({len(self._parameters[i])})." ) - bound_circuit = ( + bound_circuits.append( self._circuits[i] if len(value) == 0 else self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) ) - bound_circuits.append(bound_circuit) qargs_list.append(self._qargs_list[i]) probabilities = [ - Statevector(circ).probabilities(qargs=qargs) + Statevector(bound_circuit_to_instruction(circ)).probabilities(qargs=qargs) for circ, qargs in zip(bound_circuits, qargs_list) ] if shots is not None: diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index 1e2c16ccad3c..36fbae8f15f9 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -15,7 +15,7 @@ from __future__ import annotations -from qiskit.circuit import ParameterExpression, QuantumCircuit +from qiskit.circuit import ParameterExpression, QuantumCircuit, Instruction from qiskit.extensions.quantum_initializer.initializer import Initialize from qiskit.opflow import PauliSumOp from qiskit.quantum_info import SparsePauliOp, Statevector @@ -111,3 +111,35 @@ def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: # Sort so that classical bits are in numeric order low->high. mapping = dict(sorted(mapping.items(), key=lambda item: item[1])) return mapping + + +def bound_circuit_to_instruction(circuit: QuantumCircuit) -> Instruction: + """Build an :class:`~qiskit.circuit.Instruction` object from + a :class:`~qiskit.circuit.QuantumCircuit` + + This is a specialized version of :func:`~qiskit.converters.circuit_to_instruction` + to avoid deep copy. This requires a quantum circuit whose parameters are all bound. + Because this does not take a copy of the input circuit, this assumes that the input + circuit won't be modified. + + If https://github.com/Qiskit/qiskit-terra/issues/7983 is resolved, + we can remove this function. + + Args: + circuit(QuantumCircuit): Input quantum circuit + + Returns: + An :class:`~qiskit.circuit.Instruction` object + """ + if len(circuit.qregs) > 1: + return circuit.to_instruction() + + # len(circuit.qregs) == 1 -> No need to flatten qregs + inst = Instruction( + name=circuit.name, + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + params=[], + ) + inst.definition = circuit + return inst diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 2690583bd575..5506043db868 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -18,15 +18,12 @@ from abc import ABC from abc import abstractmethod import datetime -import logging from typing import List, Union, Iterable, Tuple from qiskit.providers.provider import Provider from qiskit.providers.models.backendstatus import BackendStatus from qiskit.circuit.gate import Instruction -logger = logging.getLogger(__name__) - class Backend: """Base common type for all versioned Backend abstract classes. @@ -351,15 +348,6 @@ def operations(self) -> List[Instruction]: @property def operation_names(self) -> List[str]: """A list of instruction names that the backend supports.""" - non_global_ops = self.target.get_non_global_operation_names(strict_direction=True) - if non_global_ops: - invalid_str = ",".join(non_global_ops) - msg = ( - f"This backend's operations: {invalid_str} only apply to a subset of " - "qubits. Using this property to get 'basis_gates' for the " - "transpiler may potentially create invalid output" - ) - logger.warning(msg) return list(self.target.operation_names) @property @@ -553,7 +541,7 @@ def control_channel(self, qubits: Iterable[int]): ``(control_qubit, target_qubit)``. Returns: - List[ControlChannel]: The Qubit measurement acquisition line. + List[ControlChannel]: The multi qubit control line. Raises: NotImplementedError: if the backend doesn't support querying the diff --git a/qiskit/providers/basicaer/unitary_simulator.py b/qiskit/providers/basicaer/unitary_simulator.py index a4a0328a1007..63745f7967ee 100644 --- a/qiskit/providers/basicaer/unitary_simulator.py +++ b/qiskit/providers/basicaer/unitary_simulator.py @@ -392,8 +392,6 @@ def _validate(self, qobj): for operation in experiment.instructions: if operation.name in ["measure", "reset"]: raise BasicAerError( - 'Unsupported "%s" instruction "%s" ' + 'in circuit "%s" ', - self.name(), - operation.name, - name, + f'Unsupported "{self.name()}" instruction "{operation.name}"' + f' in circuit "{name}".' ) diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index 5bae916970f0..7554f819dce5 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -17,10 +17,12 @@ """ import warnings +import collections import json import os +import re -from typing import List +from typing import List, Iterable from qiskit import circuit from qiskit.providers.models import BackendProperties @@ -84,6 +86,36 @@ def __init__(self): self._target = None self.sim = None + if "channels" in self._conf_dict: + self._parse_channels(self._conf_dict["channels"]) + + def _parse_channels(self, channels): + type_map = { + "acquire": pulse.AcquireChannel, + "drive": pulse.DriveChannel, + "measure": pulse.MeasureChannel, + "control": pulse.ControlChannel, + } + identifier_pattern = re.compile(r"\D+(?P\d+)") + + channels_map = { + "acquire": collections.defaultdict(list), + "drive": collections.defaultdict(list), + "measure": collections.defaultdict(list), + "control": collections.defaultdict(list), + } + for identifier, spec in channels.items(): + channel_type = spec["type"] + out = re.match(identifier_pattern, identifier) + if out is None: + # Identifier is not a valid channel name format + continue + channel_index = int(out.groupdict()["index"]) + qubit_index = tuple(spec["operates"]["qubits"]) + chan_obj = type_map[channel_type](channel_index) + channels_map[channel_type][qubit_index].append(chan_obj) + setattr(self, "channels_map", channels_map) + def _setup_sim(self): if _optionals.HAS_AER: from qiskit.providers import aer @@ -193,6 +225,73 @@ def meas_map(self) -> List[List[int]]: """ return self._conf_dict.get("meas_map") + def drive_channel(self, qubit: int): + """Return the drive channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + DriveChannel: The Qubit drive channel + """ + drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) + qubits = (qubit,) + if qubits in drive_channels_map: + return drive_channels_map[qubits][0] + return None + + def measure_channel(self, qubit: int): + """Return the measure stimulus channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + MeasureChannel: The Qubit measurement stimulus line + """ + measure_channels_map = getattr(self, "channels_map", {}).get("measure", {}) + qubits = (qubit,) + if qubits in measure_channels_map: + return measure_channels_map[qubits][0] + return None + + def acquire_channel(self, qubit: int): + """Return the acquisition channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + AcquireChannel: The Qubit measurement acquisition line. + """ + acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {}) + qubits = (qubit,) + if qubits in acquire_channels_map: + return acquire_channels_map[qubits][0] + return None + + def control_channel(self, qubits: Iterable[int]): + """Return the secondary drive channel for the given qubit + + This is typically utilized for controlling multiqubit interactions. + This channel is derived from other channels. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Args: + qubits: Tuple or list of qubits of the form + ``(control_qubit, target_qubit)``. + + Returns: + List[ControlChannel]: The multi qubit control line. + """ + control_channels_map = getattr(self, "channels_map", {}).get("control", {}) + qubits = tuple(qubits) + if qubits in control_channels_map: + return control_channels_map[qubits] + return [] + def run(self, run_input, **options): """Run on the fake backend using a simulator. diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 74452ddde4f9..e0a956d7bfae 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -110,7 +110,7 @@ class Exporter: - """QASM3 expoter main class.""" + """QASM3 exporter main class.""" def __init__( self, diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index ff30f00e21c4..49ca334e4a33 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -320,6 +320,9 @@ def _parse_custom_operation(custom_operations, gate_name, params, version, vecto base_gate = _read_instruction( base_gate_obj, None, registers, custom_operations, version, vectors ) + if ctrl_state < 2**num_ctrl_qubits - 1: + # If open controls, we need to discard the control suffix when setting the name. + gate_name = gate_name.rsplit("_", 1)[0] inst_obj = ControlledGate( gate_name, num_qubits, @@ -623,14 +626,21 @@ def _write_custom_operation(file_obj, name, operation, custom_operations): has_definition = True data = common.data_to_binary(operation, _write_pauli_evolution_gate) size = len(data) - elif operation.definition is not None: + elif type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: + # For ControlledGate, we have to access and store the private `_definition` rather than the + # public one, because the public one is mutated to include additional logic if the control + # state is open, and the definition setter (during a subsequent read) uses the "fully + # excited" control definition only. has_definition = True - data = common.data_to_binary(operation.definition, write_circuit) + data = common.data_to_binary(operation._definition, write_circuit) size = len(data) - if type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state base_gate = operation.base_gate + elif operation.definition is not None: + has_definition = True + data = common.data_to_binary(operation.definition, write_circuit) + size = len(data) if base_gate is None: base_gate_raw = b"" else: diff --git a/qiskit/quantum_info/operators/symplectic/pauli_utils.py b/qiskit/quantum_info/operators/symplectic/pauli_utils.py index b3578d2ec98f..e4a3552b5076 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_utils.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_utils.py @@ -13,40 +13,21 @@ PauliList utility functions. """ -import warnings - -import numpy as np - from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList -from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable -def pauli_basis(num_qubits, weight=False, pauli_list=False): +def pauli_basis(num_qubits, weight=False): """Return the ordered PauliTable or PauliList for the n-qubit Pauli basis. Args: num_qubits (int): number of qubits weight (bool): if True optionally return the basis sorted by Pauli weight rather than lexicographic order (Default: False) - pauli_list (bool): if True, the return type becomes PauliList, otherwise PauliTable. Returns: PauliTable, PauliList: the Paulis for the basis """ - if pauli_list: - pauli_1q = PauliList(["I", "X", "Y", "Z"]) - else: - warnings.warn( - "The return type of 'pauli_basis' will change from PauliTable to PauliList in a " - "future release of Qiskit Terra. Returning PauliTable is deprecated as of " - "Qiskit Terra 0.19, and will be removed in a future release. To immediately switch " - "to the new behaviour, pass the keyword argument 'pauli_list=True'.", - FutureWarning, - stacklevel=2, - ) - pauli_1q = PauliTable( - np.array([[False, False], [True, False], [True, True], [False, True]], dtype=bool) - ) + pauli_1q = PauliList(["I", "X", "Y", "Z"]) if num_qubits == 1: return pauli_1q pauli = pauli_1q diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 114895e1cb7f..b3f04b4b3e33 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -28,9 +28,7 @@ from qiskit.quantum_info.operators.operator import Operator from qiskit.quantum_info.operators.symplectic.pauli import BasePauli from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList -from qiskit.quantum_info.operators.symplectic.pauli_table import PauliTable from qiskit.quantum_info.operators.symplectic.pauli_utils import pauli_basis -from qiskit.utils.deprecation import deprecate_function class SparsePauliOp(LinearOp): @@ -54,7 +52,7 @@ def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True): """Initialize an operator object. Args: - data (PauliList or SparsePauliOp or PauliTable or Pauli or list or str): Pauli list of + data (PauliList or SparsePauliOp or Pauli or list or str): Pauli list of terms. A list of Pauli strings or a Pauli string is also allowed. coeffs (np.ndarray): complex coefficients for Pauli terms. @@ -173,29 +171,6 @@ def __len__(self): """Return the size.""" return self.size - # pylint: disable=bad-docstring-quotes - - @property - @deprecate_function( - "The SparsePauliOp.table method is deprecated as of Qiskit Terra 0.19.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "Use SparsePauliOp.paulis method instead.", - ) - def table(self): - """DEPRECATED - Return the the PauliTable.""" - return PauliTable(np.column_stack((self.paulis.x, self.paulis.z))) - - @table.setter - @deprecate_function( - "The SparsePauliOp.table method is deprecated as of Qiskit Terra 0.19.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "Use SparsePauliOp.paulis method instead.", - ) - def table(self, value): - if not isinstance(value, PauliTable): - value = PauliTable(value) - self._pauli_list = PauliList(value) - @property def paulis(self): """Return the the PauliList.""" @@ -673,7 +648,7 @@ def from_operator(obj, atol=None, rtol=None): # Non-normalized basis factor denom = 2**num_qubits # Compute coefficients from basis - basis = pauli_basis(num_qubits, pauli_list=True) + basis = pauli_basis(num_qubits) for i, mat in enumerate(basis.matrix_iter()): coeff = np.trace(mat.dot(data)) / denom if not np.isclose(coeff, 0, atol=atol, rtol=rtol): @@ -848,7 +823,7 @@ def label_iter(self): use the :meth:`to_labels` method. Returns: - LabelIterator: label iterator object for the PauliTable. + LabelIterator: label iterator object for the SparsePauliOp. """ class LabelIterator(CustomIterator): diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 356c29b4a480..0f4dc26d12bb 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -19,14 +19,13 @@ onto a device with this coupling. """ -import io import warnings import numpy as np import retworkx as rx +from retworkx.visualization import graphviz_draw from qiskit.transpiler.exceptions import CouplingError -from qiskit.exceptions import MissingOptionalLibraryError class CouplingMap: @@ -410,36 +409,12 @@ def __str__(self): def draw(self): """Draws the coupling map. - This function needs `pydot `_, - which in turn needs `Graphviz `_ to be - installed. Additionally, `pillow `_ will - need to be installed. + This function calls the :func:`~retworkx.visualization.graphviz_draw` function from the + ``retworkx`` package to draw the :class:`CouplingMap` object. Returns: PIL.Image: Drawn coupling map. - Raises: - MissingOptionalLibraryError: when pydot or pillow are not installed. """ - try: - import pydot - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="pydot", - name="coupling map drawer", - pip_install="pip install pydot", - ) from ex - try: - from PIL import Image - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="pillow", - name="coupling map drawer", - pip_install="pip install pillow", - ) from ex - dot_str = self.graph.to_dot() - dot = pydot.graph_from_dot_data(dot_str)[0] - png = dot.create_png(prog="neato") - - return Image.open(io.BytesIO(png)) + return graphviz_draw(self.graph, method="neato") diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 733a72c1a006..8a08566c4a65 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -85,6 +85,7 @@ HoareOptimizer TemplateOptimization EchoRZXWeylDecomposition + ResetAfterMeasureSimplification OptimizeCliffords Calibration @@ -222,6 +223,7 @@ from .optimization import InverseCancellation from .optimization import EchoRZXWeylDecomposition from .optimization import CollectLinearFunctions +from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords # circuit analysis diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index 408a95271917..062e97558cfb 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -91,7 +91,11 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: continue # TODO: allow choosing among multiple decomposition rules rule = node.op.definition.data - if len(rule) == 1 and len(node.qargs) == len(rule[0].qubits) == 1: + if ( + len(rule) == 1 + and len(node.qargs) == len(rule[0].qubits) == 1 # to preserve gate order + and len(node.cargs) == len(rule[0].clbits) == 0 + ): if node.op.definition.global_phase: dag.global_phase += node.op.definition.global_phase dag.substitute_node(node, rule[0].operation, inplace=True) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 9ba3fd1265c3..3c3abdaf3dce 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -49,20 +49,42 @@ class SabreLayout(AnalysisPass): `arXiv:1809.02573 `_ """ - def __init__(self, coupling_map, routing_pass=None, seed=None, max_iterations=3): + def __init__( + self, coupling_map, routing_pass=None, seed=None, max_iterations=3, swap_trials=None + ): """SabreLayout initializer. Args: coupling_map (Coupling): directed graph representing a coupling map. routing_pass (BasePass): the routing pass to use while iterating. + This is mutually exclusive with the ``swap_trials`` argument and + if both are set an error will be raised. seed (int): seed for setting a random first trial layout. max_iterations (int): number of forward-backward iterations. + swap_trials (int): The number of trials to run of + :class:`~.SabreSwap` for each iteration. This is equivalent to + the ``trials`` argument on :class:`~.SabreSwap`. If this is not + specified (and ``routing_pass`` isn't set) by default the number + of physical CPUs on your local system will be used. For + reproducibility between environments it is best to set this + to an explicit number because the output will potentially depend + on the number of trials run. This option is mutually exclusive + with the ``routing_pass`` argument and an error will be raised + if both are used. + + Raises: + TranspilerError: If both ``routing_pass`` and ``swap_trials`` are + specified """ super().__init__() self.coupling_map = coupling_map + if routing_pass is not None and swap_trials is not None: + raise TranspilerError("Both routing_pass and swap_trials can't be set at the same time") self.routing_pass = routing_pass self.seed = seed self.max_iterations = max_iterations + self.trials = swap_trials + self.swap_trials = swap_trials def run(self, dag): """Run the SabreLayout pass on `dag`. @@ -86,7 +108,9 @@ def run(self, dag): initial_layout = Layout({q: dag.qubits[i] for i, q in enumerate(physical_qubits)}) if self.routing_pass is None: - self.routing_pass = SabreSwap(self.coupling_map, "decay", seed=self.seed, fake_run=True) + self.routing_pass = SabreSwap( + self.coupling_map, "decay", seed=self.seed, fake_run=True, trials=self.swap_trials + ) else: self.routing_pass.fake_run = True diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 48888639f027..5678de1031ec 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -32,4 +32,5 @@ from .collect_1q_runs import Collect1qRuns from .echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition from .collect_linear_functions import CollectLinearFunctions +from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords diff --git a/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py new file mode 100644 index 000000000000..22f1f87e2d79 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/reset_after_measure_simplification.py @@ -0,0 +1,46 @@ +# 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. + +"""Replace resets after measure with a conditional XGate.""" + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.library.standard_gates.x import XGate +from qiskit.circuit.reset import Reset +from qiskit.circuit.measure import Measure +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOpNode + + +class ResetAfterMeasureSimplification(TransformationPass): + """This pass replaces reset after measure with a conditional X gate. + + This optimization is suitable for use on IBM Quantum systems where the + reset operation is performed by a measurement followed by a conditional + x-gate. It might not be desireable on other backends if reset is implemented + differently. + """ + + def run(self, dag): + """Run the pass on a dag.""" + for node in dag.op_nodes(Measure): + succ = next(dag.quantum_successors(node)) + if isinstance(succ, DAGOpNode) and isinstance(succ.op, Reset): + new_x = XGate() + new_x.condition = (node.cargs[0], 1) + new_dag = DAGCircuit() + new_dag.add_qubits(node.qargs) + new_dag.add_clbits(node.cargs) + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + new_dag.apply_operation_back(new_x, node.qargs) + dag.remove_op_node(succ) + dag.substitute_node_with_dag(node, new_dag) + return dag diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 3f83d83b04dd..0e37bc8b4b7d 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -13,7 +13,6 @@ """Routing via SWAP insertion using the SABRE method from Li et al.""" import logging -from collections import defaultdict from copy import copy, deepcopy import numpy as np @@ -24,25 +23,19 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.dagcircuit import DAGOpNode +from qiskit.tools.parallel import CPU_COUNT # pylint: disable=import-error from qiskit._accelerate.sabre_swap import ( - sabre_score_heuristic, + build_swap_map, Heuristic, - EdgeList, - QubitsDecay, NeighborTable, - SabreRng, + SabreDAG, ) from qiskit._accelerate.stochastic_swap import NLayout # pylint: disable=import-error logger = logging.getLogger(__name__) -EXTENDED_SET_SIZE = 20 # Size of lookahead window. TODO: set dynamically to len(current_layout) - -DECAY_RATE = 0.001 # Decay coefficient for penalizing serial swaps. -DECAY_RESET_INTERVAL = 5 # How often to reset all decay rates to 1. - class SabreSwap(TransformationPass): r"""Map input circuit onto a backend topology via insertion of SWAPs. @@ -69,6 +62,11 @@ class SabreSwap(TransformationPass): scored according to some heuristic cost function. The best SWAP is implemented and ``current_layout`` updated. + This transpiler pass adds onto the SABRE algorithm in that it will run + multiple trials of the algorithm with different seeds. The best output, + deteremined by the trial with the least amount of SWAPed inserted, will + be selected from the random trials. + **References:** [1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem @@ -76,13 +74,7 @@ class SabreSwap(TransformationPass): `arXiv:1809.02573 `_ """ - def __init__( - self, - coupling_map, - heuristic="basic", - seed=None, - fake_run=False, - ): + def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, trials=None): r"""SabreSwap initializer. Args: @@ -92,6 +84,12 @@ def __init__( seed (int): random seed used to tie-break among candidate swaps. fake_run (bool): if true, it only pretend to do routing, i.e., no swap is effectively added. + trials (int): The number of seed trials to run sabre with. These will + be run in parallel (unless the PassManager is already running in + parallel). If not specified this defaults to the number of physical + CPUs on the local system. For reproducible results it is recommended + that you set this explicitly, as the output will be deterministic for + a fixed number of trials. Raises: TranspilerError: If the specified heuristic is not valid. @@ -166,10 +164,14 @@ def __init__( self.seed = np.random.default_rng(None).integers(0, ii32.max, dtype=int) else: self.seed = seed + if trials is None: + self.trials = CPU_COUNT + else: + self.trials = trials + self.fake_run = fake_run - self.required_predecessors = None - self.qubits_decay = None - self._bit_indices = None + self._qubit_indices = None + self._clbit_indices = None self.dist_matrix = None def run(self, dag): @@ -189,18 +191,8 @@ def run(self, dag): if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - max_iterations_without_progress = 10 * len(dag.qubits) # Arbitrary. - ops_since_progress = [] - extended_set = None - - # Normally this isn't necessary, but here we want to log some objects that have some - # non-trivial cost to create. - do_expensive_logging = logger.isEnabledFor(logging.DEBUG) - self.dist_matrix = self.coupling_map.distance_matrix - rng = SabreRng(self.seed) - # Preserve input DAG's name, regs, wire_map, etc. but replace the graph. mapped_dag = None if not self.fake_run: @@ -208,244 +200,69 @@ def run(self, dag): canonical_register = dag.qregs["q"] current_layout = Layout.generate_trivial_layout(canonical_register) - self._bit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + self._clbit_indices = {bit: idx for idx, bit in enumerate(dag.clbits)} layout_mapping = { - self._bit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() + self._qubit_indices[k]: v for k, v in current_layout.get_virtual_bits().items() } layout = NLayout(layout_mapping, len(dag.qubits), self.coupling_map.size()) - - # A decay factor for each qubit used to heuristically penalize recently - # used qubits (to encourage parallelism). - self.qubits_decay = QubitsDecay(len(dag.qubits)) - - # Start algorithm from the front layer and iterate until all gates done. - self.required_predecessors = self._build_required_predecessors(dag) - num_search_steps = 0 - front_layer = dag.front_layer() - - while front_layer: - execute_gate_list = [] - - # Remove as many immediately applicable gates as possible - new_front_layer = [] - for node in front_layer: - if len(node.qargs) == 2: - v0 = self._bit_indices[node.qargs[0]] - v1 = self._bit_indices[node.qargs[1]] - if self.coupling_map.graph.has_edge( - layout.logical_to_physical(v0), layout.logical_to_physical(v1) - ): - execute_gate_list.append(node) - else: - new_front_layer.append(node) - else: # Single-qubit gates as well as barriers are free - execute_gate_list.append(node) - front_layer = new_front_layer - - if not execute_gate_list and len(ops_since_progress) > max_iterations_without_progress: - # Backtrack to the last time we made progress, then greedily insert swaps to route - # the gate with the smallest distance between its arguments. This is a release - # valve for the algorithm to avoid infinite loops only, and should generally not - # come into play for most circuits. - self._undo_operations(ops_since_progress, mapped_dag, layout) - self._add_greedy_swaps(front_layer, mapped_dag, layout, canonical_register) - continue - - if execute_gate_list: - for node in execute_gate_list: - self._apply_gate(mapped_dag, node, layout, canonical_register) - for successor in self._successors(node, dag): - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - front_layer.append(successor) - - if node.qargs: - self.qubits_decay.reset() - - # Diagnostics - if do_expensive_logging: - logger.debug( - "free! %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in execute_gate_list - ], - ) - logger.debug( - "front_layer: %s", - [ - (n.name if isinstance(n, DAGOpNode) else None, n.qargs) - for n in front_layer - ], - ) - - ops_since_progress = [] - extended_set = None - continue - - # After all free gates are exhausted, heuristically find - # the best swap and insert it. When two or more swaps tie - # for best score, pick one randomly. - - if extended_set is None: - extended_set = self._obtain_extended_set(dag, front_layer) - extended_set_list = EdgeList(len(extended_set)) - for x in extended_set: - extended_set_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] - ) - - front_layer_list = EdgeList(len(front_layer)) - for x in front_layer: - front_layer_list.append( - self._bit_indices[x.qargs[0]], self._bit_indices[x.qargs[1]] + original_layout = layout.copy() + + dag_list = [] + for node in dag.topological_op_nodes(): + dag_list.append( + ( + node._node_id, + [self._qubit_indices[x] for x in node.qargs], + [self._clbit_indices[x] for x in node.cargs], ) - best_swap = sabre_score_heuristic( - front_layer_list, - layout, - self._neighbor_table, - extended_set_list, - self.dist_matrix, - self.qubits_decay, - self.heuristic, - rng, ) - best_swap_qargs = [canonical_register[best_swap[0]], canonical_register[best_swap[1]]] - swap_node = self._apply_gate( - mapped_dag, - DAGOpNode(op=SwapGate(), qargs=best_swap_qargs), - layout, - canonical_register, - ) - layout.swap_logical(*best_swap) - ops_since_progress.append(swap_node) - - num_search_steps += 1 - if num_search_steps % DECAY_RESET_INTERVAL == 0: - self.qubits_decay.reset() - else: - self.qubits_decay[best_swap[0]] += DECAY_RATE - self.qubits_decay[best_swap[1]] += DECAY_RATE - - # Diagnostics - if do_expensive_logging: - logger.debug("SWAP Selection...") - logger.debug("extended_set: %s", [(n.name, n.qargs) for n in extended_set]) - logger.debug("best swap: %s", best_swap) - logger.debug("qubits decay: %s", self.qubits_decay) + front_layer = np.asarray([x._node_id for x in dag.front_layer()], dtype=np.uintp) + sabre_dag = SabreDAG(len(dag.qubits), len(dag.clbits), dag_list, front_layer) + swap_map, gate_order = build_swap_map( + len(dag.qubits), + sabre_dag, + self._neighbor_table, + self.dist_matrix, + self.heuristic, + self.seed, + layout, + self.trials, + ) + layout_mapping = layout.layout_mapping() output_layout = Layout({dag.qubits[k]: v for (k, v) in layout_mapping}) self.property_set["final_layout"] = output_layout if not self.fake_run: + for node_id in gate_order: + node = dag._multi_graph[node_id] + self._process_swaps(swap_map, node, mapped_dag, original_layout, canonical_register) + self._apply_gate(mapped_dag, node, original_layout, canonical_register) return mapped_dag return dag + def _process_swaps(self, swap_map, node, mapped_dag, current_layout, canonical_register): + if node._node_id in swap_map: + for swap in swap_map[node._node_id]: + swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] + self._apply_gate( + mapped_dag, + DAGOpNode(op=SwapGate(), qargs=swap_qargs), + current_layout, + canonical_register, + ) + current_layout.swap_logical(*swap) + def _apply_gate(self, mapped_dag, node, current_layout, canonical_register): new_node = self._transform_gate_for_layout(node, current_layout, canonical_register) if self.fake_run: return new_node return mapped_dag.apply_operation_back(new_node.op, new_node.qargs, new_node.cargs) - def _build_required_predecessors(self, dag): - out = defaultdict(int) - # We don't need to count in- or out-wires: outs can never be predecessors, and all input - # wires are automatically satisfied at the start. - for node in dag.op_nodes(): - for successor in self._successors(node, dag): - out[successor] += 1 - return out - - def _successors(self, node, dag): - """Return an iterable of the successors along each wire from the given node. - - This yields the same successor multiple times if there are parallel wires (e.g. two adjacent - operations that have one clbit and qubit in common), which is important in the swapping - algorithm for detecting if each wire has been accounted for.""" - for _, successor, _ in dag.edges(node): - if isinstance(successor, DAGOpNode): - yield successor - - def _is_resolved(self, node): - """Return True if all of a node's predecessors in dag are applied.""" - return self.required_predecessors[node] == 0 - - def _obtain_extended_set(self, dag, front_layer): - """Populate extended_set by looking ahead a fixed number of gates. - For each existing element add a successor until reaching limit. - """ - extended_set = [] - decremented = [] - tmp_front_layer = front_layer - done = False - while tmp_front_layer and not done: - new_tmp_front_layer = [] - for node in tmp_front_layer: - for successor in self._successors(node, dag): - decremented.append(successor) - self.required_predecessors[successor] -= 1 - if self._is_resolved(successor): - new_tmp_front_layer.append(successor) - if len(successor.qargs) == 2: - extended_set.append(successor) - if len(extended_set) >= EXTENDED_SET_SIZE: - done = True - break - tmp_front_layer = new_tmp_front_layer - for node in decremented: - self.required_predecessors[node] += 1 - return extended_set - - def _add_greedy_swaps(self, front_layer, dag, layout, qubits): - """Mutate ``dag`` and ``layout`` by applying greedy swaps to ensure that at least one gate - can be routed.""" - target_node = min( - front_layer, - key=lambda node: self.dist_matrix[ - layout.logical_to_physical(self._bit_indices[node.qargs[0]]), - layout.logical_to_physical(self._bit_indices[node.qargs[1]]), - ], - ) - for pair in _shortest_swap_path( - tuple(target_node.qargs), self.coupling_map, layout, qubits - ): - self._apply_gate(dag, DAGOpNode(op=SwapGate(), qargs=pair), layout, qubits) - layout.swap_logical(*[self._bit_indices[x] for x in pair]) - - def _undo_operations(self, operations, dag, layout): - """Mutate ``dag`` and ``layout`` by undoing the swap gates listed in ``operations``.""" - if dag is None: - for operation in reversed(operations): - layout.swap_logical(*[self._bit_indices[x] for x in operation.qargs]) - else: - for operation in reversed(operations): - dag.remove_op_node(operation) - p0 = self._bit_indices[operation.qargs[0]] - p1 = self._bit_indices[operation.qargs[1]] - layout.swap_logical(p0, p1) - def _transform_gate_for_layout(self, op_node, layout, device_qreg): """Return node implementing a virtual op on given layout.""" mapped_op_node = copy(op_node) mapped_op_node.qargs = tuple( - device_qreg[layout.logical_to_physical(self._bit_indices[x])] for x in op_node.qargs + device_qreg[layout.logical_to_physical(self._qubit_indices[x])] for x in op_node.qargs ) return mapped_op_node - - -def _shortest_swap_path(target_qubits, coupling_map, layout, qreg): - """Return an iterator that yields the swaps between virtual qubits needed to bring the two - virtual qubits in ``target_qubits`` together in the coupling map.""" - v_start, v_goal = target_qubits - start, goal = layout.logical_to_physical(qreg.index(v_start)), layout.logical_to_physical( - qreg.index(v_goal) - ) - # TODO: remove the list call once using retworkx 0.12, as the return value can be sliced. - path = list(retworkx.dijkstra_shortest_paths(coupling_map.graph, start, target=goal)[goal]) - # Swap both qubits towards the "centre" (as opposed to applying the same swaps to one) to - # parallelise and reduce depth. - split = len(path) // 2 - forwards, backwards = path[1:split], reversed(path[split:-1]) - for swap in forwards: - yield v_start, qreg[layout.physical_to_logical(swap)] - for swap in backwards: - yield v_goal, qreg[layout.physical_to_logical(swap)] diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 33f168319ced..b199e0ffdbe8 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -27,6 +27,10 @@ which enable packages external to qiskit to advertise they include a synthesis plugin. +See :mod:`qiskit.transpiler.preset_passmanagers.plugin` for details on how +to write plugins for transpiler stages. + + Writing Plugins =============== diff --git a/qiskit/transpiler/passes/utils/gates_basis.py b/qiskit/transpiler/passes/utils/gates_basis.py index 34ea539890ed..06943ea2d652 100644 --- a/qiskit/transpiler/passes/utils/gates_basis.py +++ b/qiskit/transpiler/passes/utils/gates_basis.py @@ -13,7 +13,6 @@ """Check if all gates in the DAGCircuit are in the specified basis gates.""" from qiskit.transpiler.basepasses import AnalysisPass -from qiskit.transpiler.exceptions import TranspilerError class GatesInBasis(AnalysisPass): @@ -26,15 +25,9 @@ def __init__(self, basis_gates=None, target=None): basis_gates (list): The list of strings representing the set of basis gates. target (Target): The target representing the backend. If specified this will be used instead of the ``basis_gates`` parameter - - Raises: - TranspilerError: If neither basis_gates or target is set. """ super().__init__() - if basis_gates is None and target is None: - raise TranspilerError( - "A value for 'basis_gates' or 'target' must be set to use this pass" - ) + self._basis_gates = None if basis_gates is not None: self._basis_gates = set(basis_gates).union( {"measure", "reset", "barrier", "snapshot", "delay"} @@ -43,6 +36,9 @@ def __init__(self, basis_gates=None, target=None): def run(self, dag): """Run the GatesInBasis pass on `dag`.""" + if self._basis_gates is None and self._target is None: + self.property_set["all_gates_in_basis"] = True + return gates_out_of_basis = False if self._target is not None: qubit_map = {qubit: index for index, qubit in enumerate(dag.qubits)} diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 929d1aa13366..fc412dc4f286 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -39,6 +39,9 @@ def __init__( unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, target=None, + init_method=None, + optimization_method=None, + optimization_level=None, ): """Initialize a PassManagerConfig object @@ -50,12 +53,16 @@ def __init__( coupling_map (CouplingMap): Directed graph represented a coupling map. layout_method (str): the pass to use for choosing initial qubit - placement. + placement. This will be the plugin name if an external layout stage + plugin is being used. routing_method (str): the pass to use for routing qubits on the - architecture. + architecture. This will be a plugin name if an external routing stage + plugin is being used. translation_method (str): the pass to use for translating gates to - basis_gates. - scheduling_method (str): the pass to use for scheduling instructions. + basis_gates. This will be a plugin name if an external translation stage + plugin is being used. + scheduling_method (str): the pass to use for scheduling instructions. This will + be a plugin name if an external scheduling stage plugin is being used. instruction_durations (InstructionDurations): Dictionary of duration (in dt) for each instruction. backend_properties (BackendProperties): Properties returned by a @@ -70,14 +77,20 @@ def __init__( :class:`~qiskit.transpiler.passes.UnitarySynthesis` pass. Will search installed plugins for a valid method. target (Target): The backend target + init_method (str): The plugin name for the init stage plugin to use + optimization_method (str): The plugin name for the optimization stage plugin + to use. + optimization_level (int): The optimization level being used for compilation. """ self.initial_layout = initial_layout self.basis_gates = basis_gates self.inst_map = inst_map self.coupling_map = coupling_map + self.init_method = init_method self.layout_method = layout_method self.routing_method = routing_method self.translation_method = translation_method + self.optimization_method = optimization_method self.scheduling_method = scheduling_method self.instruction_durations = instruction_durations self.backend_properties = backend_properties @@ -87,6 +100,7 @@ def __init__( self.unitary_synthesis_method = unitary_synthesis_method self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target + self.optimization_level = optimization_level @classmethod def from_backend(cls, backend, **pass_manager_options): diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index eb2f0d2aed39..0af4e40dd031 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -55,6 +55,8 @@ def generate_preset_pass_manager( seed_transpiler=None, unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, + init_method=None, + optimization_method=None, ): """Generate a preset :class:`~.PassManager` @@ -103,18 +105,30 @@ def generate_preset_pass_manager( layout_method (str): The :class:`~.Pass` to use for choosing initial qubit placement. Valid choices are ``'trivial'``, ``'dense'``, ``'noise_adaptive'``, and, ``'sabre'`` repsenting :class:`~.TrivialLayout`, :class:`~DenseLayout`, - :class:`~.NoiseAdaptiveLayout`, :class:`~.SabreLayout` respectively. + :class:`~.NoiseAdaptiveLayout`, :class:`~.SabreLayout` respectively. This can also + be the external plugin name to use for the ``layout`` stage of the output + :class:`~.StagedPassManager`. You can see a list of installed plugins by using + :func:`~.list_stage_plugins` with ``"layout"`` for the ``stage_name`` argument. routing_method (str): The pass to use for routing qubits on the architecture. Valid choices are ``'basic'``, ``'lookahead'``, ``'stochastic'``, ``'sabre'``, and ``'none'`` representing :class:`~.BasicSwap`, :class:`~.LookaheadSwap`, :class:`~.StochasticSwap`, :class:`~.SabreSwap`, and - erroring if routing is required respectively. + erroring if routing is required respectively. This can also be the external plugin + name to use for the ``routing`` stage of the output :class:`~.StagedPassManager`. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"routing"`` for the ``stage_name`` argument. translation_method (str): The method to use for translating gates to basis gates. Valid choices ``'unroller'``, ``'translator'``, ``'synthesis'`` representing :class:`~.Unroller`, :class:`~.BasisTranslator`, and - :class:`~.UnitarySynthesis` respectively. + :class:`~.UnitarySynthesis` respectively. This can also be the external plugin + name to use for the ``translation`` stage of the output :class:`~.StagedPassManager`. + You can see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"translation"`` for the ``stage_name`` argument. scheduling_method (str): The pass to use for scheduling instructions. Valid choices - are ``'alap'`` and ``'asap'``. + are ``'alap'`` and ``'asap'``. This can also be the external plugin name to use + for the ``scheduling`` stage of the output :class:`~.StagedPassManager`. You can + see a list of installed plugins by using :func:`~.list_stage_plugins` with + ``"scheduling"`` for the ``stage_name`` argument. backend_properties (BackendProperties): Properties returned by a backend, including information on gate errors, readout errors, qubit coherence times, etc. @@ -134,6 +148,17 @@ def generate_preset_pass_manager( the ``unitary_synthesis`` argument. As this is custom for each unitary synthesis plugin refer to the plugin documentation for how to use this option. + init_method (str): The plugin name to use for the ``init`` stage of + the output :class:`~.StagedPassManager`. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"init"`` for the stage + name argument. + optimization_method (str): The plugin name to use for the + ``optimization`` stage of the output + :class:`~.StagedPassManager`. By default an external + plugin is not used. You can see a list of installed plugins by + using :func:`~.list_stage_plugins` with ``"optimization"`` for the + ``stage_name`` argument. Returns: StagedPassManager: The preset pass manager for the given options @@ -172,6 +197,9 @@ def generate_preset_pass_manager( unitary_synthesis_method=unitary_synthesis_method, unitary_synthesis_plugin_config=unitary_synthesis_plugin_config, initial_layout=initial_layout, + init_method=init_method, + optimization_method=optimization_method, + optimization_level=optimization_level, ) if backend is not None: diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py new file mode 100644 index 000000000000..d3bce75e11bb --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -0,0 +1,281 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Built-in transpiler stage plugins for preset pass managers.""" + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes import BasicSwap +from qiskit.transpiler.passes import LookaheadSwap +from qiskit.transpiler.passes import StochasticSwap +from qiskit.transpiler.passes import SabreSwap +from qiskit.transpiler.passes import Error +from qiskit.transpiler.preset_passmanagers import common +from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin + + +class BasicSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.BasicSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + routing_pass = BasicSwap(coupling_map) + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class StochasticSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.StochasticSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 3: + routing_pass = StochasticSwap(coupling_map, trials=200, seed=seed_transpiler) + else: + routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) + + if optimization_level == 0: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level in {2, 3}: + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class LookaheadSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.LookaheadSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + routing_pass = LookaheadSwap(coupling_map, search_depth=2, search_width=2) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + routing_pass = LookaheadSwap(coupling_map, search_depth=4, search_width=4) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class SabreSwapPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with :class:`~.SabreSwap`""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + backend_properties = pass_manager_config.backend_properties + vf2_call_limit = common.get_vf2_call_limit( + optimization_level, + pass_manager_config.layout_method, + pass_manager_config.initial_layout, + ) + if optimization_level == 0: + routing_pass = SabreSwap( + coupling_map, heuristic="basic", seed=seed_transpiler, trials=5 + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 1: + routing_pass = SabreSwap( + coupling_map, heuristic="lookahead", seed=seed_transpiler, trials=5 + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=True, + ) + if optimization_level == 2: + routing_pass = SabreSwap( + coupling_map, heuristic="decay", seed=seed_transpiler, trials=10 + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + if optimization_level == 3: + routing_pass = SabreSwap( + coupling_map, heuristic="decay", seed=seed_transpiler, trials=20 + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) + raise TranspilerError(f"Invalid optimization level specified: {optimization_level}") + + +class NoneRoutingPassManager(PassManagerStagePlugin): + """Plugin class for routing stage with error on routing.""" + + def pass_manager(self, pass_manager_config, optimization_level=None) -> PassManager: + """Build routing stage PassManager.""" + seed_transpiler = pass_manager_config.seed_transpiler + target = pass_manager_config.target + coupling_map = pass_manager_config.coupling_map + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) + return common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=True, + ) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index d66037e616b9..7e5513316042 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -14,6 +14,8 @@ """Common preset passmanager generators.""" +from typing import Optional + from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.transpiler.passmanager import PassManager @@ -47,6 +49,7 @@ from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.layout import Layout def generate_unroll_3q( @@ -391,3 +394,20 @@ def _require_alignment(property_set): scheduling.append(PadDelay()) return scheduling + + +def get_vf2_call_limit( + optimization_level: int, + layout_method: Optional[str] = None, + initial_layout: Optional[Layout] = None, +) -> Optional[int]: + """Get the vf2 call limit for vf2 based layout passes.""" + vf2_call_limit = None + if layout_method is None and initial_layout is None: + if optimization_level == 1: + vf2_call_limit = int(5e4) # Set call limit to ~100ms with retworkx 0.10.2 + elif optimization_level == 2: + vf2_call_limit = int(5e6) # Set call limit to ~10 sec with retworkx 0.10.2 + elif optimization_level == 3: + vf2_call_limit = int(3e7) # Set call limit to ~60 sec with retworkx 0.10.2 + return vf2_call_limit diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 37aadc1c4452..9779ed5dd0d4 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -25,12 +25,11 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import Error from qiskit.transpiler.preset_passmanagers import common +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM @@ -54,13 +53,16 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "trivial" routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -84,21 +86,14 @@ def _choose_layout_condition(property_set): elif layout_method == "noise_adaptive": _choose_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout = SabreLayout(coupling_map, max_iterations=1, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) + _choose_layout = SabreLayout( + coupling_map, max_iterations=1, seed=seed_transpiler, swap_trials=5 + ) toqm_pass = False # Choose routing pass - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=2, search_width=2) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler) - elif routing_method == "toqm": + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO0, latencies_from_target @@ -116,14 +111,17 @@ def _choose_layout_condition(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=0 + ) unroll_3q = None # Build pass manager @@ -135,30 +133,34 @@ def _choose_layout_condition(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout, condition=_choose_layout_condition) - layout += common.generate_embed_passmanager(coupling_map) - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=0 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout, condition=_choose_layout_condition) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=0 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -170,16 +172,33 @@ def _choose_layout_condition(property_set): pre_opt += translation else: pre_opt = None - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=0 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=0 + ) + else: + init = unroll_3q + optimization = None + if optimization_method is not None: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=0 + ) return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, translation=translation, pre_optimization=pre_opt, + optimization=optimization, scheduling=sched, ) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 5da52fb23052..48a798b62827 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -19,6 +19,7 @@ from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passmanager import StagedPassManager +from qiskit.transpiler import ConditionalController from qiskit.transpiler.passes import CXCancellation from qiskit.transpiler.passes import SetLayout @@ -27,21 +28,21 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import Layout2qDistance -from qiskit.transpiler.passes import Error +from qiskit.transpiler.passes import GatesInBasis from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: @@ -65,13 +66,16 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "dense" + init_method = pass_manager_config.init_method routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -142,20 +146,14 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _improve_layout = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _improve_layout = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) + _improve_layout = SabreLayout( + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5 + ) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=4, search_width=4) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler) - elif routing_method == "toqm": + routing_pm = None + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO1, latencies_from_target @@ -173,14 +171,26 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 1, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", + routing_method, + pass_manager_config, + optimization_level=1, + ) # Build optimization loop: merge 1q rotations and cancel CNOT gates iteratively # until no more change in depth @@ -202,38 +212,38 @@ def _opt_control(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_trivial_not_perfect) - layout.append(_improve_layout, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(5e4) # Set call limit to ~100ms with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - check_trivial=True, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=1 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_trivial_not_perfect) + layout.append(_improve_layout, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + + routing = routing_pm + else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=1 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -241,20 +251,48 @@ def _opt_control(property_set): if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) ): - pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + pre_optimization = common.generate_pre_op_passmanager( + target, coupling_map, remove_reset_in_zero=True + ) else: pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - opt_loop = _opt + unroll + _depth_check + _size_check - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + # Build nested Flow controllers + def _unroll_condition(property_set): + return not property_set["all_gates_in_basis"] + + # Check if any gate is not in the basis, and if so, run unroll passes + _unroll_if_out_of_basis = [ + GatesInBasis(basis_gates, target=target), + ConditionalController(unroll, condition=_unroll_condition), + ] + + optimization.append(_depth_check + _size_check) + opt_loop = _opt + _unroll_if_out_of_basis + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + else: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=1 + ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=1 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=1 + ) + else: + init = unroll_3q return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index c2ee13ad8500..d1599fd0bc9f 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -20,6 +20,7 @@ from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passmanager import StagedPassManager +from qiskit.transpiler import ConditionalController from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout @@ -27,21 +28,21 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import CommutativeCancellation -from qiskit.transpiler.passes import Error +from qiskit.transpiler.passes import GatesInBasis from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: @@ -67,13 +68,16 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "dense" routing_method = pass_manager_config.routing_method or "stochastic" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -125,20 +129,14 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout_1 = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) + _choose_layout_1 = SabreLayout( + coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=10 + ) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=5) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) - elif routing_method == "toqm": + routing_pm = None + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO2, latencies_from_target @@ -156,14 +154,22 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 2, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=2 + ) # Build optimization loop: 1q rotation merge and commutative cancellation iteratively until # no more change in depth @@ -188,36 +194,35 @@ def _opt_control(property_set): unitary_synthesis_method, unitary_synthesis_plugin_config, ) - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(5e6) # Set call limit to ~10 sec with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=2 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=2 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation @@ -227,16 +232,42 @@ def _opt_control(property_set): pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) else: pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - opt_loop = _opt + unroll + _depth_check + _size_check - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + # Build nested Flow controllers + def _unroll_condition(property_set): + return not property_set["all_gates_in_basis"] + + # Check if any gate is not in the basis, and if so, run unroll passes + _unroll_if_out_of_basis = [ + GatesInBasis(basis_gates, target=target), + ConditionalController(unroll, condition=_unroll_condition), + ] + optimization.append(_depth_check + _size_check) + opt_loop = _opt + _unroll_if_out_of_basis + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + else: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=2 + ) + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + else: + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=2 + ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=2 + ) + else: + init = unroll_3q + return StagedPassManager( - init=unroll_3q, + init=init, layout=layout, pre_routing=pre_routing, routing=routing, diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 7a0f22319ecb..2092e23519e2 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -28,10 +28,6 @@ from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BasicSwap -from qiskit.transpiler.passes import LookaheadSwap -from qiskit.transpiler.passes import StochasticSwap -from qiskit.transpiler.passes import SabreSwap from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size @@ -43,10 +39,14 @@ from qiskit.transpiler.passes import Collect2qBlocks from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import Error +from qiskit.transpiler.passes import GatesInBasis +from qiskit.transpiler.runningpassmanager import ConditionalController from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason - +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM @@ -74,13 +74,16 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa Raises: TranspilerError: if the passmanager config is invalid. """ + plugin_manager = PassManagerStagePluginManager() basis_gates = pass_manager_config.basis_gates inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout + init_method = pass_manager_config.init_method layout_method = pass_manager_config.layout_method or "sabre" routing_method = pass_manager_config.routing_method or "sabre" translation_method = pass_manager_config.translation_method or "translator" + optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method instruction_durations = pass_manager_config.instruction_durations seed_transpiler = pass_manager_config.seed_transpiler @@ -90,6 +93,11 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target + # Override an unset optimization_level for stage plugin use. + # it will be restored to None before this is returned + optimization_level = pass_manager_config.optimization_level + if optimization_level is None: + pass_manager_config.optimization_level = 3 # Layout on good qubits if calibration info available, otherwise on dense links _given_layout = SetLayout(initial_layout) @@ -132,20 +140,13 @@ def _vf2_match_not_found(property_set): elif layout_method == "noise_adaptive": _choose_layout_1 = NoiseAdaptiveLayout(backend_properties) elif layout_method == "sabre": - _choose_layout_1 = SabreLayout(coupling_map, max_iterations=4, seed=seed_transpiler) - else: - raise TranspilerError("Invalid layout method %s." % layout_method) + _choose_layout_1 = SabreLayout( + coupling_map, max_iterations=4, seed=seed_transpiler, swap_trials=20 + ) toqm_pass = False - if routing_method == "basic": - routing_pass = BasicSwap(coupling_map) - elif routing_method == "stochastic": - routing_pass = StochasticSwap(coupling_map, trials=200, seed=seed_transpiler) - elif routing_method == "lookahead": - routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) - elif routing_method == "sabre": - routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) - elif routing_method == "toqm": + # TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface + if routing_method == "toqm" and "toqm" not in list_stage_plugins("routing"): HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO3, latencies_from_target @@ -163,14 +164,22 @@ def _vf2_match_not_found(property_set): ) ), ) - elif routing_method == "none": - routing_pass = Error( - msg="No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}", - action="raise", + vf2_call_limit = common.get_vf2_call_limit( + 3, pass_manager_config.layout_method, pass_manager_config.initial_layout + ) + routing_pm = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) else: - raise TranspilerError("Invalid routing method %s." % routing_method) + routing_pm = plugin_manager.get_passmanager_stage( + "routing", routing_method, pass_manager_config, optimization_level=3 + ) # 8. Optimize iteratively until no more change in depth. Removes useless gates # after reset and before measure, commutes gates and optimizes contiguous blocks. @@ -197,80 +206,121 @@ def _opt_control(property_set): ] # Build pass manager - init = common.generate_unroll_3q( - target, - basis_gates, - approximation_degree, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if init_method is not None: + init = plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=3 + ) + else: + init = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) init.append(RemoveResetInZeroState()) init.append(OptimizeSwapBeforeMeasure()) init.append(RemoveDiagonalGatesBeforeMeasure()) if coupling_map or initial_layout: - layout = PassManager() - layout.append(_given_layout) - layout.append(_choose_layout_0, condition=_choose_layout_condition) - layout.append(_choose_layout_1, condition=_vf2_match_not_found) - layout += common.generate_embed_passmanager(coupling_map) - vf2_call_limit = None - if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: - vf2_call_limit = int(3e7) # Set call limit to ~60 sec with retworkx 0.10.2 - routing = common.generate_routing_passmanager( - routing_pass, - target, - coupling_map=coupling_map, - vf2_call_limit=vf2_call_limit, - backend_properties=backend_properties, - seed_transpiler=seed_transpiler, - use_barrier_before_measurement=not toqm_pass, - ) + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + layout = plugin_manager.get_passmanager_stage( + "layout", layout_method, pass_manager_config, optimization_level=3 + ) + else: + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + routing = routing_pm else: layout = None routing = None - translation = common.generate_translation_passmanager( - target, - basis_gates, - translation_method, - approximation_degree, - coupling_map, - backend_properties, - unitary_synthesis_method, - unitary_synthesis_plugin_config, - ) + if translation_method not in {"translator", "synthesis", "unroller"}: + translation = plugin_manager.get_passmanager_stage( + "translation", translation_method, pass_manager_config, optimization_level=3 + ) + else: + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) pre_routing = None if toqm_pass: pre_routing = translation - optimization = PassManager() - unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] - optimization.append(_depth_check + _size_check) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) - _direction = [ - pass_ - for x in common.generate_pre_op_passmanager(target, coupling_map).passes() - for pass_ in x["passes"] + if optimization_method is None: + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + # Build nested Flow controllers + def _unroll_condition(property_set): + return not property_set["all_gates_in_basis"] + + # Check if any gate is not in the basis, and if so, run unroll passes + _unroll_if_out_of_basis = [ + GatesInBasis(basis_gates, target=target), + ConditionalController(unroll, condition=_unroll_condition), ] - # For transpiling to a target we need to run GateDirection in the - # optimization loop to correct for incorrect directions that might be - # inserted by UnitarySynthesis which is direction aware but only via - # the coupling map which with a target doesn't give a full picture - if target is not None: + + optimization.append(_depth_check + _size_check) + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + _direction = [ + pass_ + for x in common.generate_pre_op_passmanager(target, coupling_map).passes() + for pass_ in x["passes"] + ] + # For transpiling to a target we need to run GateDirection in the + # optimization loop to correct for incorrect directions that might be + # inserted by UnitarySynthesis which is direction aware but only via + # the coupling map which with a target doesn't give a full picture + if target is not None and optimization is not None: + optimization.append( + _opt + _unroll_if_out_of_basis + _depth_check + _size_check + _direction, + do_while=_opt_control, + ) + elif optimization is not None: + optimization.append( + _opt + _unroll_if_out_of_basis + _depth_check + _size_check, + do_while=_opt_control, + ) + else: + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) optimization.append( - _opt + unroll + _depth_check + _size_check + _direction, do_while=_opt_control + _opt + _unroll_if_out_of_basis + _depth_check + _size_check, do_while=_opt_control ) + opt_loop = _depth_check + _opt + _unroll_if_out_of_basis + optimization.append(opt_loop, do_while=_opt_control) + else: + optimization = plugin_manager.get_passmanager_stage( + "optimization", optimization_method, pass_manager_config, optimization_level=3 + ) + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) else: - optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) + + if scheduling_method is None or scheduling_method in {"alap", "asap"}: + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) else: - pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) - optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) - opt_loop = _depth_check + _opt + unroll - optimization.append(opt_loop, do_while=_opt_control) - sched = common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map - ) + sched = plugin_manager.get_passmanager_stage( + "scheduling", scheduling_method, pass_manager_config, optimization_level=3 + ) + + # Restore PassManagerConfig optimization_level override + pass_manager_config.optimization_level = optimization_level + return StagedPassManager( init=init, layout=layout, diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py new file mode 100644 index 000000000000..a442bbb49383 --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -0,0 +1,299 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +======================================================================================= +Transpiler Stage Plugin Interface (:mod:`qiskit.transpiler.preset_passmanagers.plugin`) +======================================================================================= + +.. currentmodule:: qiskit.transpiler.preset_passmanagers.plugin + +This module defines the plugin interface for providing custom stage +implementations for the preset pass managers and the :func:`~.transpile` +function. This enables external Python packages to provide +:class:`~.PassManager` objects that can be used for each stage. + +The plugin interfaces are built using setuptools +`entry points `__ +which enable packages external to Qiskit to advertise they include a transpiler stage. + +See :mod:`qiskit.transpiler.passes.synthesis.plugin` for details on how to +write plugins for synthesis methods which are used by the transpiler. + +.. _stage_table: + +Plugin Stages +============= + +Currently there are 6 stages in the preset pass managers used by and corresponding entrypoints. + +.. list-table:: Stages + :header-rows: 1 + + * - Stage Name + - Entry Point + - Reserved Names + - Description and expectations + * - ``init`` + - ``qiskit.transpiler.init`` + - No reserved names + - This stage runs first and is typically used for any initial logical optimization. Because most + layout and routing algorithms are only designed to work with 1 and 2 qubit gates, this stage + is also used to translate any gates that operate on more than 2 qubits into gates that only + operate on 1 or 2 qubits. + * - ``layout`` + - ``qiskit.transpiler.layout`` + - ``trivial``, ``dense``, ``noise_adaptive``, ``sabre`` + - The output from this stage is expected to have the ``layout`` property + set field set with a :class:`~.Layout` object. Additionally, the circuit is + typically expected to be embedded so that it is expanded to include all + qubits and the :class:`~.ApplyLayout` pass is expected to be run to apply the + layout. The embedding of the :class:`~.Layout` can be generated with + :func:`~.generate_embed_passmanager`. + * - ``routing`` + - ``qiskit.transpiler.routing`` + - ``basic``, ``stochastic``, ``lookahead``, ``sabre``, ``toqm`` + - The output from this stage is expected to have the circuit match the + connectivity constraints of the target backend. This does not necessarily + need to match the directionality of the edges in the target as a later + stage typically will adjust directional gates to match that constraint + (but there is no penalty for doing that in the ``routing`` stage). + * - ``translation`` + - ``qiskit.transpiler.translation`` + - ``translator``, ``synthesis``, ``unroller`` + - The output of this stage is expected to have every operation be a native + instruction on the target backend. + * - ``optimization`` + - ``qiskit.transpiler.optimization`` + - There are no reserved plugin names + - This stage is expected to perform optimization and simplification. + The constraints from earlier stages still apply to the output of this + stage. After the ``optimization`` stage is run we expect the circuit + to still be executable on the target. + * - ``scheduling`` + - ``qiskit.transpiler.scheduling`` + - ``alap``, ``asap`` + - This is the last stage run and it is expected to output a scheduled + circuit such that all idle periods in the circuit are marked by explicit + :class:`~qiskit.circuit.Delay` instructions. + +Writing Plugins +=============== + +To write a pass manager stage plugin there are 2 main steps. The first step is +to create a subclass of the abstract plugin class +:class:`~.PassManagerStagePluginManager` which is used to define how the :class:`~.PassManager` +for the stage will be constructed. For example, to create a ``layout`` stage plugin that just +runs :class:`~.VF2Layout` and will fallback to use :class:`~.TrivialLayout` if +:class:`~VF2Layout` is unable to find a perfect layout:: + + from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin + from qiskit.transpiler.preset_passmanagers import common + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import VF2Layout, TrivialLayout + from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason + + + def _vf2_match_not_found(property_set): + return property_set["layout"] is None or ( + property_set["VF2Layout_stop_reason"] is not None + and property_set["VF2Layout_stop_reason"] is not VF2LayoutStopReason.SOLUTION_FOUND + + + class VF2LayoutPlugin(PassManagerStagePlugin): + + def pass_manager(self, pass_manager_config): + layout_pm = PassManager( + [ + VF2Layout( + coupling_map=pass_manager_config.coupling_map, + properties=pass_manager_config.backend_properties, + target=pass_manager_config.target + ) + ] + ) + layout_pm.append( + TrivialLayout(pass_manager_config.coupling_map), + condition=_vf2_match_not_found, + ) + layout_pm += common.generate_embed_passmanager(pass_manager_config.coupling_map) + return layout_pm + +The second step is to expose the :class:`~.PassManagerStagePluginManager` +subclass as a setuptools entry point in the package metadata. This can be done +by simply adding an ``entry_points`` entry to the ``setuptools.setup`` call in +the ``setup.py`` or 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, entrypoints, and expectations from the stage in :ref:`stage_table`. +For example, continuing from the example plugin above:: + + entry_points = { + 'qiskit.transpiler.layout': [ + 'vf2 = qiskit_plugin_pkg.module.plugin:VF2LayoutPlugin', + ] + }, + +(note that the entry point ``name = path`` is a single string not a Python +expression). There isn't a limit to the number of plugins a single package can +include as long as each plugin has a unique name. So a single package can +expose multiple plugins if necessary. Refer to :ref:`stage_table` for a list +of reserved names for each stage. + +Plugin API +========== + +.. autosummary:: + :toctree: ../stubs/ + + PassManagerStagePlugin + PassManagerStagePluginManager + list_stage_plugins +""" + +import abc +from typing import List, Optional + +import stevedore + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passmanager_config import PassManagerConfig + + +class PassManagerStagePlugin(abc.ABC): + """A ``PassManagerStagePlugin`` is a plugin interface object for using custom + stages in :func:`~.transpile`. + + A ``PassManagerStagePlugin`` object can be added to an external package and + integrated into the :func:`~.transpile` function with an entrypoint. This + will enable users to use the output of :meth:`.pass_manager` to implement + a stage in the compilation process. + """ + + @abc.abstractmethod + def pass_manager( + self, pass_manager_config: PassManagerConfig, optimization_level: Optional[int] = None + ) -> PassManager: + """This method is designed to return a :class:`~.PassManager` for the stage this implements + + Args: + pass_manager_config: A configuration object that defines all the target device + specifications and any user specified options to :func:`~.transpile` or + :func:`~.generate_preset_pass_manager` + optimization_level: The optimization level of the transpilation, if set this + should be used to set values for any tunable parameters to trade off runtime + for potential optimization. Valid values should be ``0``, ``1``, ``2``, or ``3`` + and the higher the number the more optimization is expected. + """ + pass + + +class PassManagerStagePluginManager: + """Manager class for preset pass manager stage plugins.""" + + def __init__(self): + super().__init__() + self.init_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.init", invoke_on_load=True, propagate_map_exceptions=True + ) + self.layout_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.layout", invoke_on_load=True, propagate_map_exceptions=True + ) + self.routing_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.routing", invoke_on_load=True, propagate_map_exceptions=True + ) + self.translation_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.translation", invoke_on_load=True, propagate_map_exceptions=True + ) + self.optimization_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.optimization", invoke_on_load=True, propagate_map_exceptions=True + ) + self.scheduling_plugins = stevedore.ExtensionManager( + "qiskit.transpiler.scheduling", invoke_on_load=True, propagate_map_exceptions=True + ) + + def get_passmanager_stage( + self, + stage_name: str, + plugin_name: str, + pm_config: PassManagerConfig, + optimization_level=None, + ) -> PassManager: + """Get a stage""" + if stage_name == "init": + return self._build_pm( + self.init_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "layout": + return self._build_pm( + self.layout_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "routing": + return self._build_pm( + self.routing_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "translation": + return self._build_pm( + self.translation_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "optimization": + return self._build_pm( + self.optimization_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + elif stage_name == "scheduling": + return self._build_pm( + self.scheduling_plugins, stage_name, plugin_name, pm_config, optimization_level + ) + else: + raise TranspilerError(f"Invalid stage name: {stage_name}") + + def _build_pm( + self, + stage_obj: stevedore.ExtensionManager, + stage_name: str, + plugin_name: str, + pm_config: PassManagerConfig, + optimization_level: Optional[int] = None, + ): + if plugin_name not in stage_obj: + raise TranspilerError(f"Invalid plugin name {plugin_name} for stage {stage_name}") + plugin_obj = stage_obj[plugin_name] + return plugin_obj.obj.pass_manager(pm_config, optimization_level) + + +def list_stage_plugins(stage_name: str) -> List[str]: + """Get a list of installed plugins for a stage. + + Args: + stage_name: The stage name to get the plugin names for + + Returns: + plugins: The list of installed plugin names for the specified stages + + Raises: + TranspilerError: If an invalid stage name is specified. + """ + plugin_mgr = PassManagerStagePluginManager() + if stage_name == "init": + return plugin_mgr.init_plugins.names() + elif stage_name == "layout": + return plugin_mgr.layout_plugins.names() + elif stage_name == "routing": + return plugin_mgr.routing_plugins.names() + elif stage_name == "translation": + return plugin_mgr.translation_plugins.names() + elif stage_name == "optimization": + return plugin_mgr.optimization_plugins.names() + elif stage_name == "scheduling": + return plugin_mgr.scheduling_plugins.names() + else: + raise TranspilerError(f"Invalid stage name: {stage_name}") diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 1a73803ab4f3..633c2472288d 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017. +# (C) Copyright IBM 2017, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -14,16 +14,17 @@ import functools import warnings +from typing import Type -def deprecate_arguments(kwarg_map): +def deprecate_arguments(kwarg_map, category: Type[Warning] = DeprecationWarning): """Decorator to automatically alias deprecated argument names and warn upon use.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if kwargs: - _rename_kwargs(func.__name__, kwargs, kwarg_map) + _rename_kwargs(func.__name__, kwargs, kwarg_map, category) return func(*args, **kwargs) return wrapper @@ -31,12 +32,13 @@ def wrapper(*args, **kwargs): return decorator -def deprecate_function(msg, stacklevel=2): +def deprecate_function(msg: str, stacklevel: int = 2, category: Type[Warning] = DeprecationWarning): """Emit a warning prior to calling decorated function. Args: - msg (str): Warning message to emit. - stacklevel (int): The warning stackevel to use, defaults to 2. + msg: Warning message to emit. + stacklevel: The warning stackevel to use, defaults to 2. + category: warning category, defaults to DeprecationWarning Returns: Callable: The decorated, deprecated callable. @@ -45,7 +47,7 @@ def deprecate_function(msg, stacklevel=2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) + warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) return wrapper @@ -53,17 +55,25 @@ def wrapper(*args, **kwargs): return decorator -def _rename_kwargs(func_name, kwargs, kwarg_map): +def _rename_kwargs(func_name, kwargs, kwarg_map, category: Type[Warning] = DeprecationWarning): for old_arg, new_arg in kwarg_map.items(): if old_arg in kwargs: if new_arg in kwargs: raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") - warnings.warn( - "{} keyword argument {} is deprecated and " - "replaced with {}.".format(func_name, old_arg, new_arg), - DeprecationWarning, - stacklevel=3, - ) - - kwargs[new_arg] = kwargs.pop(old_arg) + if new_arg is None: + warnings.warn( + f"{func_name} keyword argument {old_arg} is deprecated and " + "will in future be removed.", + category=category, + stacklevel=3, + ) + else: + warnings.warn( + f"{func_name} keyword argument {old_arg} is deprecated and " + f"replaced with {new_arg}.", + category=category, + stacklevel=3, + ) + + kwargs[new_arg] = kwargs.pop(old_arg) diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py index c25d94797417..65d9c5ffeb0b 100644 --- a/qiskit/visualization/circuit_visualization.py +++ b/qiskit/visualization/circuit_visualization.py @@ -27,6 +27,7 @@ import logging import os +import shutil import subprocess import tempfile from warnings import warn @@ -488,7 +489,7 @@ def _latex_circuit_drawer( image = utils._trim(image) if filename: if filename.endswith(".pdf"): - os.rename(base + ".pdf", filename) + shutil.move(base + ".pdf", filename) else: try: image.save(filename) diff --git a/qiskit/visualization/latex.py b/qiskit/visualization/latex.py index 968111352051..2818365a3904 100644 --- a/qiskit/visualization/latex.py +++ b/qiskit/visualization/latex.py @@ -603,7 +603,10 @@ def _build_barrier(self, node, col): first = last = index pos = self._wire_map[self._qubits[first]] self._latex[pos][col - 1] += " \\barrier[0em]{" + str(last - first) + "}" - self._latex[pos][col] = "\\qw" + if node.op.label is not None: + pos = indexes[0] + label = node.op.label.replace(" ", "\\,") + self._latex[pos][col] = "\\cds{0}{^{\\mathrm{%s}}}" % label def _add_controls(self, wire_list, ctrlqargs, ctrl_state, col): """Add one or more controls to a gate""" diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py index 463a1e5b629c..5f157b42b22d 100644 --- a/qiskit/visualization/matplotlib.py +++ b/qiskit/visualization/matplotlib.py @@ -414,7 +414,9 @@ def _get_layer_widths(self): self._data[node] = {} self._data[node]["width"] = WID num_ctrl_qubits = 0 if not hasattr(op, "num_ctrl_qubits") else op.num_ctrl_qubits - if getattr(op, "_directive", False) or isinstance(op, Measure): + if ( + getattr(op, "_directive", False) and (not op.label or not self._plot_barriers) + ) or isinstance(op, Measure): self._data[node]["raw_gate_text"] = op.name continue @@ -1013,11 +1015,16 @@ def _measure(self, node): def _barrier(self, node): """Draw a barrier""" - for xy in self._data[node]["q_xy"]: + for i, xy in enumerate(self._data[node]["q_xy"]): xpos, ypos = xy + # For the topmost barrier, reduce the rectangle if there's a label to allow for the text. + if i == 0 and node.op.label is not None: + ypos_adj = -0.35 + else: + ypos_adj = 0.0 self._ax.plot( [xpos, xpos], - [ypos + 0.5, ypos - 0.5], + [ypos + 0.5 + ypos_adj, ypos - 0.5], linewidth=self._lwidth1, linestyle="dashed", color=self._style["lc"], @@ -1026,7 +1033,7 @@ def _barrier(self, node): box = self._patches_mod.Rectangle( xy=(xpos - (0.3 * WID), ypos - 0.5), width=0.6 * WID, - height=1, + height=1.0 + ypos_adj, fc=self._style["bc"], ec=None, alpha=0.6, @@ -1035,6 +1042,21 @@ def _barrier(self, node): ) self._ax.add_patch(box) + # display the barrier label at the top if there is one + if i == 0 and node.op.label is not None: + dir_ypos = ypos + 0.65 * HIG + self._ax.text( + xpos, + dir_ypos, + node.op.label, + ha="center", + va="top", + fontsize=self._fs, + color=self._data[node]["tc"], + clip_on=True, + zorder=PORDER_TEXT, + ) + def _gate(self, node, xy=None): """Draw a 1-qubit gate""" if xy is None: diff --git a/qiskit/visualization/pulse_v2/generators/waveform.py b/qiskit/visualization/pulse_v2/generators/waveform.py index c398a26a6c20..6172a283d063 100644 --- a/qiskit/visualization/pulse_v2/generators/waveform.py +++ b/qiskit/visualization/pulse_v2/generators/waveform.py @@ -391,7 +391,7 @@ def _draw_shaped_waveform( channels=channel, xvals=re_xvals, yvals=re_yvals, - fill=True, + fill=formatter["control.fill_waveform"], meta=re_meta, styles=re_style, ) @@ -417,7 +417,7 @@ def _draw_shaped_waveform( channels=channel, xvals=im_xvals, yvals=im_yvals, - fill=True, + fill=formatter["control.fill_waveform"], meta=im_meta, styles=im_style, ) diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py index 180dc274a52c..50e39ce21a9c 100644 --- a/qiskit/visualization/pulse_v2/interface.py +++ b/qiskit/visualization/pulse_v2/interface.py @@ -228,6 +228,9 @@ def draw( formatter.axis_break.max_length: Length of new waveform or idle time duration after axis break is applied. Longer intervals are truncated to this length (default `1000`). + formatter.control.fill_waveform: Set `True` to fill waveforms with face color + (default `True`). When you disable this option, you should set finite line width + to `formatter.line_width.fill_waveform`, otherwise nothing will appear in the graph. formatter.control.apply_phase_modulation: Set `True` to apply phase modulation to the waveforms (default `True`). formatter.control.show_snapshot_channel: Set `True` to show snapshot instructions diff --git a/qiskit/visualization/pulse_v2/stylesheet.py b/qiskit/visualization/pulse_v2/stylesheet.py index cf781bd68162..838c82fdd45c 100644 --- a/qiskit/visualization/pulse_v2/stylesheet.py +++ b/qiskit/visualization/pulse_v2/stylesheet.py @@ -296,6 +296,7 @@ def default_style() -> Dict[str, Any]: "formatter.box_height.opaque_shape": 0.5, "formatter.axis_break.length": 3000, "formatter.axis_break.max_length": 1000, + "formatter.control.fill_waveform": True, "formatter.control.apply_phase_modulation": True, "formatter.control.show_snapshot_channel": True, "formatter.control.show_acquire_channel": True, diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index e4491a4eaf16..40b38df245d2 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -43,6 +43,11 @@ def plot_state_hinton( ): """Plot a hinton diagram for the density matrix of a quantum state. + The hinton diagram represents the values of a matrix using + squares, whose size indicate the magnitude of their corresponding value + and their color, its sign. A white square means the value is positive and + a black one means negative. + Args: state (Statevector or DensityMatrix or ndarray): An N-qubit quantum state. title (str): a string that represents the plot title @@ -181,7 +186,8 @@ def plot_state_hinton( def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartesian"): """Plot the Bloch sphere. - Plot a sphere, axes, the Bloch vector, and its projections onto each axis. + Plot a Bloch sphere with the specified coordinates, that can be given in both + cartesian and spherical systems. Args: bloch (list[double]): array of three elements where [, , ] (Cartesian) @@ -242,9 +248,13 @@ def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartes def plot_bloch_multivector( state, title="", figsize=None, *, rho=None, reverse_bits=False, filename=None ): - """Plot the Bloch sphere. + r"""Plot a Bloch sphere for each qubit. - Plot a sphere, axes, the Bloch vector, and its projections onto each axis. + Each component :math:`(x,y,z)` of the Bloch sphere labeled as 'qubit i' represents the expected + value of the corresponding Pauli operator acting only on that qubit, that is, the expected value + of :math:`I_{N-1} \otimes\dotsb\otimes I_{i+1}\otimes P_i \otimes I_{i-1}\otimes\dotsb\otimes + I_0`, where :math:`N` is the number of qubits, :math:`P\in \{X,Y,Z\}` and :math:`I` is the + identity operator. Args: state (Statevector or DensityMatrix or ndarray): an N-qubit quantum state. @@ -360,21 +370,39 @@ def plot_state_city( ValueError: When 'color' is not a list of len=2. VisualizationError: if input is not a valid N-qubit state. - Example: + Examples: .. jupyter-execute:: + # You can choose different colors for the real and imaginary parts of the density matrix. + from qiskit import QuantumCircuit from qiskit.quantum_info import DensityMatrix from qiskit.visualization import plot_state_city - %matplotlib inline qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) - state = DensityMatrix.from_instruction(qc) - plot_state_city(state, color=['midnightblue', 'midnightblue'], - title="New State City") + state = DensityMatrix(qc) + plot_state_city(state, color=['midnightblue', 'crimson'], title="New State City") + + .. jupyter-execute:: + + # You can make the bars more transparent to better see the ones that are behind + # if they overlap. + + import numpy as np + from qiskit.quantum_info import Statevector + + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.cz(0,1) + qc.ry(np.pi/3, 0) + qc.rx(np.pi/5, 1) + + state = Statevector(qc) + plot_state_city(state, alpha=0.6) + """ from matplotlib import pyplot as plt from mpl_toolkits.mplot3d.art3d import Poly3DCollection diff --git a/qiskit/visualization/text.py b/qiskit/visualization/text.py index 51ca5c9d568f..7f89bdbaad0d 100644 --- a/qiskit/visualization/text.py +++ b/qiskit/visualization/text.py @@ -376,18 +376,18 @@ def __init__(self, label=""): class Barrier(DirectOnQuWire): - """Draws a barrier. + """Draws a barrier with a label at the top if there is one. :: - top: ░ ░ + top: ░ label mid: ─░─ ───░─── bot: ░ ░ """ def __init__(self, label=""): super().__init__("░") - self.top_connect = "░" + self.top_connect = label if label else "░" self.bot_connect = "░" self.top_connector = {} self.bot_connector = {} @@ -867,9 +867,7 @@ def should_compress(self, top_line, bot_line): for top, bot in zip(top_line, bot_line): if top in ["┴", "╨"] and bot in ["┬", "╥"]: return False - for line in (bot_line, top_line): - no_spaces = line.replace(" ", "") - if len(no_spaces) > 0 and all(c.isalpha() or c.isnumeric() for c in no_spaces): + if (top.isalnum() and bot != " ") or (bot.isalnum() and top != " "): return False return True @@ -959,6 +957,8 @@ def merge_lines(top, bot, icod="top"): ret += "╫" elif topc in "║╫╬" and botc in " ": ret += "║" + elif topc in "│┼╪" and botc in " ": + ret += "│" elif topc == "└" and botc == "┌" and icod == "top": ret += "├" elif topc == "┘" and botc == "┐": @@ -1089,9 +1089,10 @@ def add_connected_gate(node, gates, layer, current_cons): if not self.plotbarriers: return layer, current_cons, current_cons_cond, connection_label - for qubit in node.qargs: + for i, qubit in enumerate(node.qargs): if qubit in self.qubits: - layer.set_qubit(qubit, Barrier()) + label = op.label if i == 0 else "" + layer.set_qubit(qubit, Barrier(label)) elif isinstance(op, SwapGate): # swap @@ -1517,8 +1518,10 @@ def connect_with(self, wire_char): wire_char = "║" if index == 0 and len(affected_bits) > 1: affected_bit.connect(wire_char, ["bot"]) - else: + elif index == len(affected_bits) - 1: affected_bit.connect(wire_char, ["top"]) + else: + affected_bit.connect(wire_char, ["bot", "top"]) else: if index == 0: affected_bit.connect(wire_char, ["bot"]) diff --git a/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml b/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml new file mode 100644 index 000000000000..9687f19bcc8e --- /dev/null +++ b/releasenotes/notes/add-barrier-label-8e677979cb37461e.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added a ``label`` parameter to the :class:`.Barrier` which now allows + a user to enter a label for the ``barrier`` directive and the label + will be printed at the top of the ``barrier`` in the `mpl`, `latex`, + and `text` circuit drawers. Printing of existing ``snapshot`` labels + to the 3 circuit drawers was also added. + + .. code-block:: python + + from qiskit import QuantumCircuit + + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.h(1) + circuit.barrier(label="After H") + circuit.draw('mpl') diff --git a/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml b/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml new file mode 100644 index 000000000000..c15c8f52ed5d --- /dev/null +++ b/releasenotes/notes/add-ccz-cs-and-csdg-gates-4ad05e323f1dec4d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add new gates :class:`.CZZGate`, :class:`.CSGate`, and :class:`.CSdgGate`. + Added their equivalences into the standard :class:`EquivalenceLibrary`. diff --git a/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml b/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml new file mode 100644 index 000000000000..96e1977096ba --- /dev/null +++ b/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Add new algorithms to calculate state fidelities/overlaps + for pairs of quantum circuits (that can be parametrized). Apart from + the base class (:class:`qiskit.algorithms.state_fidelities.BaseStateFidelity`), + there is now an implementation of the compute-uncompute method that leverages + the sampler primitive (:class:`qiskit.algorithms.state_fidelities.ComputeUncompute`). + + Example:: + .. code-block:: python + + import numpy as np + from qiskit.primitives import Sampler + from qiskit.algorithms.state_fidelities import ComputeUncompute + from qiskit. import RealAmplitudes + + sampler = Sampler(...) + fidelity = ComputeUncompute(sampler) + circuit = RealAmplitudes(2) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + job = fidelity.run([circuit], [circuit], [values], [values+shift]) + fidelities = job.result().fidelities + + diff --git a/releasenotes/notes/add-pulse-drawer-option-936b6d943de9a270.yaml b/releasenotes/notes/add-pulse-drawer-option-936b6d943de9a270.yaml new file mode 100644 index 000000000000..17406410d26b --- /dev/null +++ b/releasenotes/notes/add-pulse-drawer-option-936b6d943de9a270.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + New pulse drawer option "formatter.control.fill_waveform" has been added to + the style sheets. This option removes the face color of pulses in the drawer. + For example: + + .. code-block:: python + + from qiskit.visualization.pulse_v2 import IQXStandard + + my_style = IQXStandard( + **{"formatter.control.fill_waveform": False, "formatter.line_width.fill_waveform": 2} + ) + + my_sched.draw(style=my_style) + + This code allows a user to draw pulses only with lines. diff --git a/releasenotes/notes/add-reset-simplification-pass-82377d80dd0081fd.yaml b/releasenotes/notes/add-reset-simplification-pass-82377d80dd0081fd.yaml new file mode 100644 index 000000000000..6722958653f9 --- /dev/null +++ b/releasenotes/notes/add-reset-simplification-pass-82377d80dd0081fd.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added a new transpiler pass, :class:`~ResetAfterMeasureSimplification`, + which is used to replace a :class:`~.Reset` operation after a + :class:`~.Measure` with a conditional :class:`~.XGate`. This pass can + be used on backends where a :class:`~.Reset` operation is performed by + doing a measurement and then a conditional X gate so that this will + remove the duplicate implicit :class:`~.Measure` from the :class:`~.Reset` + operation. diff --git a/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml b/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml new file mode 100644 index 000000000000..fc4d0fb891d3 --- /dev/null +++ b/releasenotes/notes/add-variational-quantum-time-evolution-112ffeaf62782fea.yaml @@ -0,0 +1,50 @@ +--- +features: + - | + Add algorithms for Variational Quantum Time Evolution that implement a new interface for + Quantum Time Evolution. The feature supports real (:class:`qiskit.algorithms.VarQRTE`.) and + imaginary (:class:`qiskit.algorithms.VarQITE`.) quantum time evolution according to a + variational principle passed. Each algorithm accepts a variational principle and the following + are provided: + :class:`qiskit.algorithms.evolvers.variational.ImaginaryMcLachlanPrinciple`, + :class:`qiskit.algorithms.evolvers.variational.RealMcLachlanPrinciple`, + :class:`qiskit.algorithms.evolvers.variational.RealTimeDependentPrinciple`. + Both algorithms require solving ODE equations and linear equations which is handled by classes + implemented in `qiskit.algorithms.evolvers.variational.solvers` module. + + .. code-block:: python + + from qiskit.algorithms import EvolutionProblem + from qiskit.algorithms import VarQITE + from qiskit import BasicAer + from qiskit.circuit.library import EfficientSU2 + from qiskit.opflow import SummedOp, I, Z, Y, X + from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, + ) + from qiskit.algorithms import EvolutionProblem + import numpy as np + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ).reduce() + + ansatz = EfficientSU2(observable.num_qubits, reps=1) + parameters = ansatz.parameters + init_param_values = np.zeros(len(ansatz.parameters)) + for i in range(len(ansatz.parameters)): + init_param_values[i] = np.pi / 2 + param_dict = dict(zip(parameters, init_param_values)) + var_principle = ImaginaryMcLachlanPrinciple() + backend = BasicAer.get_backend("statevector_simulator") + time = 1 + evolution_problem = EvolutionProblem(observable, time) + var_qite = VarQITE(ansatz, var_principle, param_dict, quantum_instance=backend) + evolution_result = var_qite.evolve(evolution_problem) diff --git a/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml b/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml new file mode 100644 index 000000000000..eae30452782b --- /dev/null +++ b/releasenotes/notes/bugfix-ucgate-inverse-global_phase-c9655c13c22e5cf4.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes the :meth:`.UCGate.inverse` method which previously did not invert the + global phase. + - | + Fixes the global phase problem of the isometry decomposition. Refer to + `#4687 ` for more + details. diff --git a/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml b/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml new file mode 100644 index 000000000000..0648666c6147 --- /dev/null +++ b/releasenotes/notes/circuit-initialize-and-prepare-single-qubit-e25dacc8f873bc01.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :meth:`.QuantumCircuit.initialize` and :meth:`.QuantumCircuit.prepare_state` + that caused them to not accept a single :class:`Qubit` as argument to initialize. diff --git a/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml b/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml new file mode 100644 index 000000000000..2d7034394f5d --- /dev/null +++ b/releasenotes/notes/deprecate-linear-solvers-factorizers-bbf5302484cb6831.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + Modules :mod:`qiskit.algorithms.factorizers` and + :mod:`qiskit.algorithms.linear_solvers` are deprecated and will + be removed in a future release. + They are replaced by tutorials in the Qiskit Textbook: + `Shor `__ + `HHL `__ diff --git a/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml new file mode 100644 index 000000000000..c6674cf888df --- /dev/null +++ b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.compose` will now function correctly when used with + the ``inplace=True`` argument within control-flow builder contexts. + Previously the instructions would be added outside the control-flow scope. + Fixed `#8433 `__. diff --git a/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml b/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml new file mode 100644 index 000000000000..74d8d1faa83b --- /dev/null +++ b/releasenotes/notes/fix-decomp-1q-1c-84f369f9a897a5b7.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed a bug where decomposing an instruction with one qubit and one classical bit + containing a single quantum gate failed. Now the following decomposes as expected:: + + block = QuantumCircuit(1, 1) + block.h(0) + + circuit = QuantumCircuit(1, 1) + circuit.append(block, [0], [0]) + + decomposed = circuit.decompose() diff --git a/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml b/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml new file mode 100644 index 000000000000..07de4aa4eb0b --- /dev/null +++ b/releasenotes/notes/fix-gradient-wrapper-2f9ab45941739044.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed the following issues with the function + :func:`~qiskit.opflow.gradients.derivative_base.gradient_wrapper`: + - reusing a circuit sampler between the calls, + - binding nested parameters. diff --git a/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml b/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml new file mode 100644 index 000000000000..91fd099d3451 --- /dev/null +++ b/releasenotes/notes/fix-latex-split-filesystem-0c38a1ade2f36e85.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an ``OSError`` in the LaTeX circuit drawer on systems whose temporary + directories (*e.g* ``/tmp``) are on a different filesystem to the working + directory. See `#8542 `__ + for more detail. diff --git a/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml new file mode 100644 index 000000000000..d7a45238b2f4 --- /dev/null +++ b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed QPY serialisation and deserialisation of :class:`.ControlledGate` + with open controls (*i.e.* those whose ``ctrl_state`` is not all ones). + Fixed `#8549 `__. diff --git a/releasenotes/notes/fix-text-drawer-compression-a80a5636957e8eec.yaml b/releasenotes/notes/fix-text-drawer-compression-a80a5636957e8eec.yaml new file mode 100644 index 000000000000..a91cd87f0943 --- /dev/null +++ b/releasenotes/notes/fix-text-drawer-compression-a80a5636957e8eec.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + There were two bugs in the ``text`` circuit drawer that were fixed. + These appeared when ``vertical_compression`` was set to ``medium``, + which is the default. The first would sometimes cause text to overwrite + other text or gates, and the second would sometimes cause the connections + between a gate and its controls to break. + See `#8588 `__. diff --git a/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml b/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml new file mode 100644 index 000000000000..ca23df6dffea --- /dev/null +++ b/releasenotes/notes/implements_two_step_tapering-f481a8cac3990cd5.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Splits the internal procedure in :meth:`taper()` into two methods :meth:`convert_clifford()` and :meth:`taper_clifford()`. The logic remains the same but the methods are now exposed in the public API. Also improves the documentation of the method `taper()`. + diff --git a/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml b/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml new file mode 100644 index 000000000000..7c39e26200f5 --- /dev/null +++ b/releasenotes/notes/make-use-of-callback-in-vqd-99e3c85f03181298.yaml @@ -0,0 +1,9 @@ +features: + - | + Add calls of the callback function during the :meth:`energy_evaluation` to track the progress of + the algorithm. Also adds a ``step`` argument to the callback to track which eigenstates of the + Hamiltonian is currently being optimized. +issues: + - | + The callback function in the :class:`VQD` was defined but never used. + diff --git a/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml b/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml new file mode 100644 index 000000000000..388ecc954ff1 --- /dev/null +++ b/releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + The :class:`~.SabreSwap` transpiler pass has a new keyword argument on its + constructor, ``trials``. The ``trials`` argument is used to specify the + number of random seed trials to attempt. The output from the + `SABRE algorithm `__ can differ greatly + based on the seed used for the random number. :class:`~.SabreSwap` will + now run the algorithm with ``trials`` number of random seeds and pick the + best (with the fewest swaps inserted). If ``trials`` is not specified the + pass will default to use the number of physical CPUs on the local system. + - | + The :class:`~.SabreLayout` transpiler pass has a new keyword argument on + its constructor, ``swap_trials``. The ``swap_trials`` argument is used + to specify how many random seed trials to run on the :class:`~.SabreSwap` + pass internally. It corresponds to the ``trials`` arugment on the + :class:`~.SabreSwap` pass. When set, each iteration of + :class:`~.SabreSwap` will be run internally ``swap_trials`` times. + If ``swap_trials`` is not specified the will default to use + the number of physical CPUs on the local system. diff --git a/releasenotes/notes/qiskit-nature-797-8f1b0975309b8756.yaml b/releasenotes/notes/qiskit-nature-797-8f1b0975309b8756.yaml new file mode 100644 index 000000000000..08412ffcf95c --- /dev/null +++ b/releasenotes/notes/qiskit-nature-797-8f1b0975309b8756.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + When the class :class:`~.SPSA` was using `np.split` (from NumPy) for splitting the jobs in even batches, + resulting in an exception if a perfectly even split was not possible. Now, it uses `np.array_split`, which is safer + for these cases. diff --git a/releasenotes/notes/remove-deprecated-methods-in-pauli-c874d463ba1f7a0e.yaml b/releasenotes/notes/remove-deprecated-methods-in-pauli-c874d463ba1f7a0e.yaml new file mode 100644 index 000000000000..48c7acc6a516 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-methods-in-pauli-c874d463ba1f7a0e.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The deprecated method ``SparsePauliOp.table`` has been removed. + It was originally deprecated in Qiskit Terra 0.19. + Returning PauliTable of :func:`~pauli_basis` has been removed. + The argument ``pauli_list`` in :func:`pauli_basis` has been removed. diff --git a/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml new file mode 100644 index 000000000000..ea4c88c097df --- /dev/null +++ b/releasenotes/notes/stage-plugin-interface-47daae40f7d0ad3c.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Introduced a new plugin interface for transpiler stages which is used to + enable alternative :class:`~.PassManager` objects from an external package + in a particular stage as part of :func:`~.transpile` or the + :class:`~.StagedPassManager` output from + :func:`~.generate_preset_pass_manager`, :func:`~.level_0_pass_manager`, + :func:`~.level_1_pass_manager`, :func:`~.level_2_pass_manager`, and + :func:`~.level_3_pass_manager`. Users can select a plugin to use for a + transpiler stage with the ``init_method``, ``layout_method``, + ``routing_method``, ``translation_method``, ``optimization_method``, and + ``scheduling_method`` keyword arguments on :func:`~.transpile` and + :func:`~.generate_preset_pass_manager`. A full list of plugin names + currently installed can be found with the :func:`.list_stage_plugins` + function. For creating plugins refer to the + :mod:`qiskit.transpiler.preset_passmanagers.plugin` module documentation + which includes a guide for writing stage plugins. + - | + The :func:`~.transpile` has two new keyword arguments, ``init_method`` and + ``optimization_method`` which are used to specify alternative plugins to + use for the ``init`` stage and ``optimization`` stages respectively. + - | + The :class:`~.PassManagerConfig` class has 3 new attributes, + :attr:`~.PassManagerConfig.init_method`, + :attr:`~.PassManagerConfig.optimization_method`, and + :attr:`~.PassManagerConfig.optimization_level` along with matching keyword + arguments on the constructor methods. The first two attributes represent + the user specified ``init`` and ``optimization`` plugins to use for + compilation. The :attr:`~.PassManagerConfig.optimization_level` attribute + represents the compilations optimization level if specified which can + be used to inform stage plugin behavior. diff --git a/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml b/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml new file mode 100644 index 000000000000..6bde2fc01a40 --- /dev/null +++ b/releasenotes/notes/steppable-optimizers-9d9b48ba78bd58bb.yaml @@ -0,0 +1,96 @@ +--- +features: + - | + The :class:`~.SteppableOptimizer` class is added. It allows one to perfore classical + optimizations step-by-step using the :meth:`~.SteppableOptimizer.step` method. These + optimizers implement the "ask and tell" interface which (optionally) allows to manually compute + the required function or gradient evaluations and plug them back into the optimizer. + For more information about this interface see: `ask and tell interface + `_. + A very simple use case when the user might want to do the optimization step by step is for + readout: + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + for _ in range(maxiter): + state = optimizer.state + # Here you can manually read out anything from the optimizer state. + optimizer.step() + + result = optimizer.create_result() + + A more complex case would be error handling. Imagine that the funciton you are evaluating has + a random chance of failing. In this case you can catch the error and run the function again + until it yields the desired result before continuing the optimization process. In this case + one would use the ask and tell interface. + + .. code-block:: python + + import random + import numpy as np + from qiskit.algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optmizer.state.nit += 1 + + cf = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + Transitioned GradientDescent to be a subclass of SteppableOptimizer. + +fixes: + - | + :class:`.GradientDescent` will now correctly count the number of iterations, function evaluations and + gradient evaluations. Also the documentation now correctly states that the gradient is approximated + by a forward finite difference method. + + diff --git a/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml b/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml new file mode 100644 index 000000000000..9d01cc969f05 --- /dev/null +++ b/releasenotes/notes/support-channels-in--fake-backend-v2-82f0650006495fbe.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + All fake backends in :mod:`qiskit.providers.fake_provider.backends` have been + updated to return the corresponding pulse channel objects with the method call of + :meth:`~BackendV2.drive_channel`, :meth:`~BackendV2.measure_channel`, + :meth:`~BackendV2.acquire_channel`, :meth:`~BackendV2.control_channel`. diff --git a/requirements-dev.txt b/requirements-dev.txt index fb5e2cbeae18..72da159e4ff5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -setuptools-rust<1.5.0 coverage>=4.4.0 hypothesis>=4.24.3 python-constraint>=1.4 @@ -13,8 +12,6 @@ pydot astroid==2.5.6 pylint==2.8.3 stestr>=2.0.0 -PyGithub -wheel pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 diff --git a/setup.py b/setup.py index 5cdccc12ceea..ed7d0489f8d3 100755 --- a/setup.py +++ b/setup.py @@ -100,6 +100,13 @@ "qiskit.unitary_synthesis": [ "default = qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis", "aqc = qiskit.transpiler.synthesis.aqc.aqc_plugin:AQCSynthesisPlugin", - ] + ], + "qiskit.transpiler.routing": [ + "basic = qiskit.transpiler.preset_passmanagers.builtin_plugins:BasicSwapPassManager", + "stochastic = qiskit.transpiler.preset_passmanagers.builtin_plugins:StochasticSwapPassManager", + "lookahead = qiskit.transpiler.preset_passmanagers.builtin_plugins:LookaheadSwapPassManager", + "sabre = qiskit.transpiler.preset_passmanagers.builtin_plugins:SabreSwapPassManager", + "none = qiskit.transpiler.preset_passmanagers.builtin_plugins:NoneRoutingPassManager", + ], }, ) diff --git a/src/nlayout.rs b/src/nlayout.rs index e4ca1223b33d..2d4ff5a29880 100644 --- a/src/nlayout.rs +++ b/src/nlayout.rs @@ -112,4 +112,8 @@ impl NLayout { pub fn swap_physical(&mut self, bit_a: usize, bit_b: usize) { self.swap(bit_a, bit_b) } + + pub fn copy(&self) -> NLayout { + self.clone() + } } diff --git a/src/sabre_swap/edge_list.rs b/src/sabre_swap/edge_list.rs deleted file mode 100644 index a1dbf0fb55e7..000000000000 --- a/src/sabre_swap/edge_list.rs +++ /dev/null @@ -1,101 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; - -/// A simple container that contains a vector representing edges in the -/// coupling map that are found to be optimal by the swap mapper. -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct EdgeList { - pub edges: Vec<[usize; 2]>, -} - -impl Default for EdgeList { - fn default() -> Self { - Self::new(None) - } -} - -#[pymethods] -impl EdgeList { - #[new] - pub fn new(capacity: Option) -> Self { - match capacity { - Some(size) => EdgeList { - edges: Vec::with_capacity(size), - }, - None => EdgeList { edges: Vec::new() }, - } - } - - /// Append an edge to the list. - /// - /// Args: - /// edge_start (int): The start qubit of the edge. - /// edge_end (int): The end qubit of the edge. - #[pyo3(text_signature = "(self, edge_start, edge_end, /)")] - pub fn append(&mut self, edge_start: usize, edge_end: usize) { - self.edges.push([edge_start, edge_end]); - } - - pub fn __iter__(slf: PyRef) -> PyResult> { - let iter = EdgeListIter { - inner: slf.edges.clone().into_iter(), - }; - Py::new(slf.py(), iter) - } - - pub fn __len__(&self) -> usize { - self.edges.len() - } - - pub fn __contains__(&self, object: [usize; 2]) -> bool { - self.edges.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult<[usize; 2]> { - if object >= self.edges.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - Ok(self.edges[object]) - } - - fn __getstate__(&self) -> Vec<[usize; 2]> { - self.edges.clone() - } - - fn __setstate__(&mut self, state: Vec<[usize; 2]>) { - self.edges = state - } -} - -#[pyclass] -pub struct EdgeListIter { - inner: std::vec::IntoIter<[usize; 2]>, -} - -#[pymethods] -impl EdgeListIter { - fn __iter__(slf: PyRef) -> PyRef { - slf - } - - fn __next__(mut slf: PyRefMut) -> Option<[usize; 2]> { - slf.inner.next() - } -} diff --git a/src/sabre_swap/mod.rs b/src/sabre_swap/mod.rs index 73323cd446d4..1798a0dd0e39 100644 --- a/src/sabre_swap/mod.rs +++ b/src/sabre_swap/mod.rs @@ -12,29 +12,38 @@ #![allow(clippy::too_many_arguments)] -pub mod edge_list; pub mod neighbor_table; -pub mod qubits_decay; -pub mod sabre_rng; +pub mod sabre_dag; +pub mod swap_map; +use std::cmp::Ordering; + +use hashbrown::{HashMap, HashSet}; use ndarray::prelude::*; +use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::wrap_pyfunction; use pyo3::Python; - -use hashbrown::HashSet; use rand::prelude::SliceRandom; +use rand::prelude::*; +use rand_pcg::Pcg64Mcg; use rayon::prelude::*; +use retworkx_core::dictmap::*; +use retworkx_core::petgraph::prelude::*; +use retworkx_core::petgraph::visit::EdgeRef; +use retworkx_core::shortest_path::dijkstra; use crate::getenv_use_multiple_threads; use crate::nlayout::NLayout; -use edge_list::EdgeList; use neighbor_table::NeighborTable; -use qubits_decay::QubitsDecay; -use sabre_rng::SabreRng; +use sabre_dag::SabreDAG; +use swap_map::SwapMap; +const EXTENDED_SET_SIZE: usize = 20; // Size of lookahead window. +const DECAY_RATE: f64 = 0.001; // Decay coefficient for penalizing serial swaps. +const DECAY_RESET_INTERVAL: u8 = 5; // How often to reset all decay rates to 1. const EXTENDED_SET_WEIGHT: f64 = 0.5; // Weight of lookahead window compared to front_layer. #[pyclass] @@ -44,6 +53,12 @@ pub enum Heuristic { Decay, } +struct TrialResult { + out_map: HashMap>, + gate_order: Vec, + layout: NLayout, +} + /// Return a set of candidate swaps that affect qubits in front_layer. /// /// For each virtual qubit in front_layer, find its current location @@ -53,16 +68,15 @@ pub enum Heuristic { /// /// Candidate swaps are sorted so SWAP(i,j) and SWAP(j,i) are not duplicated. fn obtain_swaps( - front_layer: &EdgeList, + front_layer: &[[usize; 2]], neighbors: &NeighborTable, layout: &NLayout, ) -> HashSet<[usize; 2]> { // This will likely under allocate as it's a function of the number of // neighbors for the qubits in the layer too, but this is basically a // minimum allocation assuming each qubit has only 1 unique neighbor - let mut candidate_swaps: HashSet<[usize; 2]> = - HashSet::with_capacity(2 * front_layer.edges.len()); - for node in &front_layer.edges { + let mut candidate_swaps: HashSet<[usize; 2]> = HashSet::with_capacity(2 * front_layer.len()); + for node in front_layer { for v in node { let physical = layout.logic_to_phys[*v]; for neighbor in &neighbors.neighbors[physical] { @@ -79,49 +93,348 @@ fn obtain_swaps( candidate_swaps } -/// Run the sabre heuristic scoring +fn obtain_extended_set( + dag: &SabreDAG, + front_layer: &[NodeIndex], + required_predecessors: &mut [u32], +) -> Vec<[usize; 2]> { + let mut extended_set: Vec<[usize; 2]> = Vec::new(); + let mut decremented: Vec = Vec::new(); + let mut tmp_front_layer: Vec = front_layer.to_vec(); + let mut done: bool = false; + while !tmp_front_layer.is_empty() && !done { + let mut new_tmp_front_layer = Vec::new(); + for node in tmp_front_layer { + for edge in dag.dag.edges(node) { + let successor_index = edge.target(); + let successor = successor_index.index(); + decremented.push(successor); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + new_tmp_front_layer.push(successor_index); + let node_weight = dag.dag.node_weight(successor_index).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let extended_set_edges: [usize; 2] = [qargs[0], qargs[1]]; + extended_set.push(extended_set_edges); + } + } + } + if extended_set.len() >= EXTENDED_SET_SIZE { + done = true; + break; + } + } + tmp_front_layer = new_tmp_front_layer; + } + for node in decremented { + required_predecessors[node] += 1; + } + extended_set +} + +fn cmap_from_neighor_table(neighbor_table: &NeighborTable) -> DiGraph<(), ()> { + DiGraph::<(), ()>::from_edges(neighbor_table.neighbors.iter().enumerate().flat_map( + |(u, targets)| { + targets + .iter() + .map(move |v| (NodeIndex::new(u), NodeIndex::new(*v))) + }, + )) +} + +/// Run sabre swap on a circuit /// -/// Args: -/// layers (EdgeList): The input layer edge list to score and find the -/// best swaps -/// layout (NLayout): The current layout -/// neighbor_table (NeighborTable): The table of neighbors for each node -/// in the coupling graph -/// extended_set (EdgeList): The extended set -/// distance_matrix (ndarray): The 2D array distance matrix for the coupling -/// graph -/// qubits_decay (QubitsDecay): The current qubit decay factors for -/// heuristic (Heuristic): The chosen heuristic method to use /// Returns: -/// ndarray: A 2d array of the best swap candidates all with the minimum score +/// (SwapMap, gate_order): A tuple where the first element is a mapping of +/// DAGCircuit node ids to a list of virtual qubit swaps that should be +/// added before that operation. The second element is a numpy array of +/// node ids that represents the traversal order used by sabre. #[pyfunction] -pub fn sabre_score_heuristic( - layer: EdgeList, - layout: &mut NLayout, +pub fn build_swap_map( + py: Python, + num_qubits: usize, + dag: &SabreDAG, neighbor_table: &NeighborTable, - extended_set: EdgeList, distance_matrix: PyReadonlyArray2, - qubits_decay: QubitsDecay, heuristic: &Heuristic, - rng: &mut SabreRng, + seed: u64, + layout: &mut NLayout, + num_trials: usize, +) -> (SwapMap, PyObject) { + let run_in_parallel = getenv_use_multiple_threads(); + let dist = distance_matrix.as_array(); + let coupling_graph: DiGraph<(), ()> = cmap_from_neighor_table(neighbor_table); + let outer_rng = Pcg64Mcg::seed_from_u64(seed); + let seed_vec: Vec = outer_rng + .sample_iter(&rand::distributions::Standard) + .take(num_trials) + .collect(); + let result = if run_in_parallel { + seed_vec + .into_par_iter() + .enumerate() + .map(|(index, seed_trial)| { + ( + index, + swap_map_trial( + num_qubits, + dag, + neighbor_table, + &dist, + &coupling_graph, + heuristic, + seed_trial, + layout.clone(), + ), + ) + }) + .min_by_key(|(index, result)| { + [ + result.out_map.values().map(|x| x.len()).sum::(), + *index, + ] + }) + .unwrap() + .1 + } else { + seed_vec + .into_iter() + .map(|seed_trial| { + swap_map_trial( + num_qubits, + dag, + neighbor_table, + &dist, + &coupling_graph, + heuristic, + seed_trial, + layout.clone(), + ) + }) + .min_by_key(|result| result.out_map.values().map(|x| x.len()).sum::()) + .unwrap() + }; + *layout = result.layout; + ( + SwapMap { + map: result.out_map, + }, + result.gate_order.into_pyarray(py).into(), + ) +} + +fn swap_map_trial( + num_qubits: usize, + dag: &SabreDAG, + neighbor_table: &NeighborTable, + dist: &ArrayView2, + coupling_graph: &DiGraph<(), ()>, + heuristic: &Heuristic, + seed: u64, + mut layout: NLayout, +) -> TrialResult { + let max_iterations_without_progress = 10 * neighbor_table.neighbors.len(); + let mut gate_order: Vec = Vec::with_capacity(dag.dag.node_count()); + let mut ops_since_progress: Vec<[usize; 2]> = Vec::new(); + let mut out_map: HashMap> = HashMap::new(); + let mut front_layer: Vec = dag.first_layer.clone(); + let mut required_predecessors: Vec = vec![0; dag.dag.node_count()]; + let mut extended_set: Option> = None; + let mut num_search_steps: u8 = 0; + let mut qubits_decay: Vec = vec![1.; num_qubits]; + let mut rng = Pcg64Mcg::seed_from_u64(seed); + + for node in dag.dag.node_indices() { + for edge in dag.dag.edges(node) { + required_predecessors[edge.target().index()] += 1; + } + } + while !front_layer.is_empty() { + let mut execute_gate_list: Vec = Vec::new(); + // Remove as many immediately applicable gates as possible + let mut new_front_layer: Vec = Vec::new(); + for node in front_layer { + let node_weight = dag.dag.node_weight(node).unwrap(); + let qargs = &node_weight.1; + if qargs.len() == 2 { + let physical_qargs: [usize; 2] = [ + layout.logic_to_phys[qargs[0]], + layout.logic_to_phys[qargs[1]], + ]; + if coupling_graph + .find_edge( + NodeIndex::new(physical_qargs[0]), + NodeIndex::new(physical_qargs[1]), + ) + .is_none() + { + new_front_layer.push(node); + } else { + execute_gate_list.push(node); + } + } else { + execute_gate_list.push(node); + } + } + front_layer = new_front_layer.clone(); + + // Backtrack to the last time we made progress, then greedily insert swaps to route + // the gate with the smallest distance between its arguments. This is f block a release + // valve for the algorithm to avoid infinite loops only, and should generally not + // come into play for most circuits. + if execute_gate_list.is_empty() + && ops_since_progress.len() > max_iterations_without_progress + { + // If we're stuck in a loop without making progress first undo swaps: + ops_since_progress + .drain(..) + .rev() + .for_each(|swap| layout.swap_logical(swap[0], swap[1])); + // Then pick the closest pair in the current layer + let target_qubits = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .min_by(|qargs_a, qargs_b| { + let dist_a = dist[[ + layout.logic_to_phys[qargs_a[0]], + layout.logic_to_phys[qargs_a[1]], + ]]; + let dist_b = dist[[ + layout.logic_to_phys[qargs_b[0]], + layout.logic_to_phys[qargs_b[1]], + ]]; + dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal) + }) + .unwrap(); + // find Shortest path between target qubits + let mut shortest_paths: DictMap> = DictMap::new(); + let u = layout.logic_to_phys[target_qubits[0]]; + let v = layout.logic_to_phys[target_qubits[1]]; + (dijkstra( + &coupling_graph, + NodeIndex::::new(u), + Some(NodeIndex::::new(v)), + |_| Ok(1.), + Some(&mut shortest_paths), + ) as PyResult>>) + .unwrap(); + let shortest_path: Vec = shortest_paths + .get(&NodeIndex::new(v)) + .unwrap() + .iter() + .map(|n| n.index()) + .collect(); + // Insert greedy swaps along that shortest path + let split: usize = shortest_path.len() / 2; + let forwards = &shortest_path[1..split]; + let backwards = &shortest_path[split..shortest_path.len() - 1]; + let mut greedy_swaps: Vec<[usize; 2]> = Vec::with_capacity(split); + for swap in forwards { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[0], logical_swap_bit]); + layout.swap_logical(target_qubits[0], logical_swap_bit); + } + backwards.iter().rev().for_each(|swap| { + let logical_swap_bit = layout.phys_to_logic[*swap]; + greedy_swaps.push([target_qubits[1], logical_swap_bit]); + layout.swap_logical(target_qubits[1], logical_swap_bit); + }); + ops_since_progress = greedy_swaps; + continue; + } + if !execute_gate_list.is_empty() { + for node in execute_gate_list { + let node_weight = dag.dag.node_weight(node).unwrap(); + gate_order.push(node_weight.0); + let out_swaps: Vec<[usize; 2]> = ops_since_progress.drain(..).collect(); + if !out_swaps.is_empty() { + out_map.insert(dag.dag.node_weight(node).unwrap().0, out_swaps); + } + for edge in dag.dag.edges(node) { + let successor = edge.target().index(); + required_predecessors[successor] -= 1; + if required_predecessors[successor] == 0 { + front_layer.push(edge.target()); + } + } + } + qubits_decay.fill_with(|| 1.); + extended_set = None; + continue; + } + let first_layer: Vec<[usize; 2]> = front_layer + .iter() + .map(|n| { + let node_weight = dag.dag.node_weight(*n).unwrap(); + let qargs = &node_weight.1; + [qargs[0], qargs[1]] + }) + .collect(); + if extended_set.is_none() { + extended_set = Some(obtain_extended_set( + dag, + &front_layer, + &mut required_predecessors, + )); + } + + let best_swap = sabre_score_heuristic( + &first_layer, + &mut layout, + neighbor_table, + extended_set.as_ref().unwrap(), + dist, + &qubits_decay, + heuristic, + &mut rng, + ); + num_search_steps += 1; + if num_search_steps >= DECAY_RESET_INTERVAL { + qubits_decay.fill_with(|| 1.); + num_search_steps = 0; + } else { + qubits_decay[best_swap[0]] += DECAY_RATE; + qubits_decay[best_swap[1]] += DECAY_RATE; + } + ops_since_progress.push(best_swap); + } + TrialResult { + out_map, + gate_order, + layout, + } +} + +fn sabre_score_heuristic( + layer: &[[usize; 2]], + layout: &mut NLayout, + neighbor_table: &NeighborTable, + extended_set: &[[usize; 2]], + dist: &ArrayView2, + qubits_decay: &[f64], + heuristic: &Heuristic, + rng: &mut Pcg64Mcg, ) -> [usize; 2] { // Run in parallel only if we're not already in a multiprocessing context // unless force threads is set. - let run_in_parallel = getenv_use_multiple_threads(); - let dist = distance_matrix.as_array(); - let candidate_swaps = obtain_swaps(&layer, neighbor_table, layout); + let candidate_swaps = obtain_swaps(layer, neighbor_table, layout); let mut min_score = f64::MAX; let mut best_swaps: Vec<[usize; 2]> = Vec::new(); for swap_qubits in candidate_swaps { layout.swap_logical(swap_qubits[0], swap_qubits[1]); let score = score_heuristic( heuristic, - &layer.edges, - &extended_set.edges, + layer, + extended_set, layout, &swap_qubits, - &dist, - &qubits_decay.decay, + dist, + qubits_decay, ); if score < min_score { min_score = score; @@ -132,12 +445,10 @@ pub fn sabre_score_heuristic( } layout.swap_logical(swap_qubits[0], swap_qubits[1]); } - if run_in_parallel { - best_swaps.par_sort_unstable(); - } else { - best_swaps.sort_unstable(); - } - *best_swaps.choose(&mut rng.rng).unwrap() + best_swaps.sort_unstable(); + let best_swap = *best_swaps.choose(rng).unwrap(); + layout.swap_logical(best_swap[0], best_swap[1]); + best_swap } #[inline] @@ -196,11 +507,10 @@ fn score_heuristic( #[pymodule] pub fn sabre_swap(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(sabre_score_heuristic))?; + m.add_wrapped(wrap_pyfunction!(build_swap_map))?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/sabre_swap/qubits_decay.rs b/src/sabre_swap/qubits_decay.rs deleted file mode 100644 index 0a5899af1bc5..000000000000 --- a/src/sabre_swap/qubits_decay.rs +++ /dev/null @@ -1,85 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -use numpy::IntoPyArray; -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; -use pyo3::Python; - -/// A container for qubit decay values for each qubit -/// -/// This class tracks the qubit decay for the sabre heuristic. When initialized -/// all qubits are set to a value of ``1.``. This class implements the sequence -/// protocol and can be modified in place like any python sequence. -/// -/// Args: -/// qubit_count (int): The number of qubits -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] -#[derive(Clone, Debug)] -pub struct QubitsDecay { - pub decay: Vec, -} - -#[pymethods] -impl QubitsDecay { - #[new] - pub fn new(qubit_count: usize) -> Self { - QubitsDecay { - decay: vec![1.; qubit_count], - } - } - - // Mapping Protocol - pub fn __len__(&self) -> usize { - self.decay.len() - } - - pub fn __contains__(&self, object: f64) -> bool { - self.decay.contains(&object) - } - - pub fn __getitem__(&self, object: usize) -> PyResult { - match self.decay.get(object) { - Some(val) => Ok(*val), - None => Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))), - } - } - - pub fn __setitem__(mut slf: PyRefMut, object: usize, value: f64) -> PyResult<()> { - if object >= slf.decay.len() { - return Err(PyIndexError::new_err(format!( - "Index {} out of range for this EdgeList", - object - ))); - } - slf.decay[object] = value; - Ok(()) - } - - pub fn __array__(&self, py: Python) -> PyObject { - self.decay.clone().into_pyarray(py).into() - } - - pub fn __str__(&self) -> PyResult { - Ok(format!("{:?}", self.decay)) - } - - /// Reset decay for all qubits back to default ``1.`` - #[pyo3(text_signature = "(self, /)")] - pub fn reset(mut slf: PyRefMut) { - slf.decay.fill_with(|| 1.); - } -} diff --git a/src/sabre_swap/sabre_dag.rs b/src/sabre_swap/sabre_dag.rs new file mode 100644 index 000000000000..ae349d30020a --- /dev/null +++ b/src/sabre_swap/sabre_dag.rs @@ -0,0 +1,69 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use hashbrown::HashMap; +use numpy::PyReadonlyArray1; +use pyo3::prelude::*; +use retworkx_core::petgraph::prelude::*; + +/// A DAG object used to represent the data interactions from a DAGCircuit +/// to run the the sabre algorithm. This is structurally identical to the input +/// DAGCircuit, but the contents of the node are a tuple of DAGCircuit node ids, +/// a list of qargs and a list of cargs +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[pyo3(text_signature = "(num_qubits, num_clbits, nodes, front_layer, /)")] +#[derive(Clone, Debug)] +pub struct SabreDAG { + pub dag: DiGraph<(usize, Vec, Vec), ()>, + pub first_layer: Vec, +} + +#[pymethods] +impl SabreDAG { + #[new] + pub fn new( + num_qubits: usize, + num_clbits: usize, + nodes: Vec<(usize, Vec, Vec)>, + front_layer: PyReadonlyArray1, + ) -> PyResult { + let mut qubit_pos: Vec = vec![usize::MAX; num_qubits]; + let mut clbit_pos: Vec = vec![usize::MAX; num_clbits]; + let mut reverse_index_map: HashMap = HashMap::with_capacity(nodes.len()); + let mut dag: DiGraph<(usize, Vec, Vec), ()> = + Graph::with_capacity(nodes.len(), 2 * nodes.len()); + for node in &nodes { + let qargs = &node.1; + let cargs = &node.2; + let gate_index = dag.add_node(node.clone()); + reverse_index_map.insert(node.0, gate_index); + for x in qargs { + if qubit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(qubit_pos[*x]), gate_index, ()); + } + qubit_pos[*x] = gate_index.index(); + } + for x in cargs { + if clbit_pos[*x] != usize::MAX { + dag.add_edge(NodeIndex::new(clbit_pos[*x]), gate_index, ()); + } + clbit_pos[*x] = gate_index.index(); + } + } + let first_layer = front_layer + .as_slice()? + .iter() + .map(|x| reverse_index_map[x]) + .collect(); + Ok(SabreDAG { dag, first_layer }) + } +} diff --git a/src/sabre_swap/sabre_rng.rs b/src/sabre_swap/sabre_rng.rs deleted file mode 100644 index 79a4a70acb13..000000000000 --- a/src/sabre_swap/sabre_rng.rs +++ /dev/null @@ -1,35 +0,0 @@ -// This code is part of Qiskit. -// -// (C) Copyright IBM 2022 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -use pyo3::prelude::*; -use rand::prelude::*; -use rand_pcg::Pcg64Mcg; - -/// A rng container that shares an rng state between python and sabre's rust -/// code. It should be initialized once and passed to -/// ``sabre_score_heuristic`` to avoid recreating a rng on the inner loop -#[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] -#[derive(Clone, Debug)] -pub struct SabreRng { - pub rng: Pcg64Mcg, -} - -#[pymethods] -impl SabreRng { - #[new] - pub fn new(seed: u64) -> Self { - SabreRng { - rng: Pcg64Mcg::seed_from_u64(seed), - } - } -} diff --git a/src/sabre_swap/swap_map.rs b/src/sabre_swap/swap_map.rs new file mode 100644 index 000000000000..b14d9c72ecdc --- /dev/null +++ b/src/sabre_swap/swap_map.rs @@ -0,0 +1,48 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2022 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use hashbrown::HashMap; +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; + +/// A container for required swaps before a gate qubit +#[pyclass(module = "qiskit._accelerate.sabre_swap")] +#[derive(Clone, Debug)] +pub struct SwapMap { + pub map: HashMap>, +} + +#[pymethods] +impl SwapMap { + // Mapping Protocol + pub fn __len__(&self) -> usize { + self.map.len() + } + + pub fn __contains__(&self, object: usize) -> bool { + self.map.contains_key(&object) + } + + pub fn __getitem__(&self, object: usize) -> PyResult> { + match self.map.get(&object) { + Some(val) => Ok(val.clone()), + None => Err(PyIndexError::new_err(format!( + "Node index {} not in swap mapping", + object + ))), + } + } + + pub fn __str__(&self) -> PyResult { + Ok(format!("{:?}", self.map)) + } +} diff --git a/test/ipynb/mpl/circuit/references/barrier_label.png b/test/ipynb/mpl/circuit/references/barrier_label.png new file mode 100644 index 000000000000..c1db46dccfdd Binary files /dev/null and b/test/ipynb/mpl/circuit/references/barrier_label.png differ diff --git a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py index 68ecb4455e58..656d0a584a60 100644 --- a/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/ipynb/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -920,6 +920,17 @@ def test_wire_order(self): filename="wire_order.png", ) + def test_barrier_label(self): + """Test the barrier label""" + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + self.circuit_drawer(circuit, filename="barrier_label.png") + if __name__ == "__main__": unittest.main(verbosity=1) diff --git a/test/python/algorithms/evolvers/test_evolution_problem.py b/test/python/algorithms/evolvers/test_evolution_problem.py index 0d1d18951039..d3dd2f7bc9b2 100644 --- a/test/python/algorithms/evolvers/test_evolution_problem.py +++ b/test/python/algorithms/evolvers/test_evolution_problem.py @@ -38,14 +38,14 @@ def test_init_default(self): expected_initial_state = One expected_aux_operators = None expected_t_param = None - expected_hamiltonian_value_dict = None + expected_param_value_dict = None self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) self.assertEqual(evo_problem.time, expected_time) self.assertEqual(evo_problem.initial_state, expected_initial_state) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.hamiltonian_value_dict, expected_hamiltonian_value_dict) + self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) def test_init_all(self): """Tests that all fields are initialized correctly.""" @@ -54,7 +54,7 @@ def test_init_all(self): time = 2 initial_state = One aux_operators = [X, Y] - hamiltonian_value_dict = {t_parameter: 3.2} + param_value_dict = {t_parameter: 3.2} evo_problem = EvolutionProblem( hamiltonian, @@ -62,7 +62,7 @@ def test_init_all(self): initial_state, aux_operators, t_param=t_parameter, - hamiltonian_value_dict=hamiltonian_value_dict, + param_value_dict=param_value_dict, ) expected_hamiltonian = Y + t_parameter * Z @@ -70,14 +70,14 @@ def test_init_all(self): expected_initial_state = One expected_aux_operators = [X, Y] expected_t_param = t_parameter - expected_hamiltonian_value_dict = {t_parameter: 3.2} + expected_param_value_dict = {t_parameter: 3.2} self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) self.assertEqual(evo_problem.time, expected_time) self.assertEqual(evo_problem.initial_state, expected_initial_state) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) self.assertEqual(evo_problem.t_param, expected_t_param) - self.assertEqual(evo_problem.hamiltonian_value_dict, expected_hamiltonian_value_dict) + self.assertEqual(evo_problem.param_value_dict, expected_param_value_dict) @data([Y, -1, One], [Y, -1.2, One], [Y, 0, One]) @unpack @@ -93,27 +93,21 @@ def test_validate_params(self): with self.subTest(msg="Parameter missing in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Empty dict."): hamiltonian = param_x * X + param_y * Y param_dict = {} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Extra parameter in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} - evolution_problem = EvolutionProblem( - hamiltonian, 2, Zero, hamiltonian_value_dict=param_dict - ) + evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py index 7baad84fe59b..fe53f929f5f6 100644 --- a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py +++ b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py @@ -185,7 +185,7 @@ def test_trotter_qrte_trotter_two_qubits_with_params(self): operator = w_param * (Z ^ Z) / 2.0 + (Z ^ I) + u_param * (I ^ Z) / 3.0 time = 1 evolution_problem = EvolutionProblem( - operator, time, initial_state, hamiltonian_value_dict=params_dict + operator, time, initial_state, param_value_dict=params_dict ) expected_state = VectorStateFn( Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) @@ -222,7 +222,7 @@ def test_trotter_qrte_qdrift(self, initial_state, expected_state): @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) @unpack - def test_trotter_qrte_trotter_errors(self, t_param, hamiltonian_value_dict): + def test_trotter_qrte_trotter_errors(self, t_param, param_value_dict): """Test TrotterQRTE with raising errors.""" operator = X * Parameter("t") + Z initial_state = Zero @@ -235,7 +235,7 @@ def test_trotter_qrte_trotter_errors(self, t_param, hamiltonian_value_dict): time, initial_state, t_param=t_param, - hamiltonian_value_dict=hamiltonian_value_dict, + param_value_dict=param_value_dict, ) _ = trotter_qrte.evolve(evolution_problem) diff --git a/test/python/algorithms/evolvers/variational/__init__.py b/test/python/algorithms/evolvers/variational/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/solvers/__init__.py b/test/python/algorithms/evolvers/variational/solvers/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py b/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py new file mode 100644 index 000000000000..9c3165f57a2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/expected_results/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py b/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py new file mode 100644 index 000000000000..c6dcf903673f --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/expected_results/test_varqte_linear_solver_expected_1.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" +expected_metric_res_1 = [ + [ + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + 4.77630626e-32 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 0.00000000e00 + 0.0j, + 4.85334346e-32 + 0.0j, + -7.00000000e-18 + 0.0j, + ], + [ + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 0.00000000e00 + 0.0j, + -1.38777878e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 1.38006319e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + ], + [ + 2.50000000e-01 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + -3.85185989e-33 + 0.0j, + -3.85185989e-33 + 0.0j, + -1.38777878e-17 + 0.0j, + -1.38777878e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + ], + [ + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + -7.00000000e-18 + 0.0j, + -7.00000000e-18 + 0.0j, + 0.00000000e00 + 0.0j, + 2.50000000e-01 + 0.0j, + -7.00000000e-18 + 0.0j, + -7.00000000e-18 + 0.0j, + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + 0.00000000e00 + 0.0j, + 4.17500000e-17 + 0.0j, + ], + [ + 4.85000000e-17 + 0.0j, + 4.85334346e-32 + 0.0j, + 1.38006319e-17 + 0.0j, + 1.38006319e-17 + 0.0j, + 4.85334346e-32 + 0.0j, + 4.85334346e-32 + 0.0j, + 1.38006319e-17 + 0.0j, + 1.38006319e-17 + 0.0j, + 4.85000000e-17 + 0.0j, + 0.00000000e00 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.77500000e-17 + 0.0j, + ], + [ + 4.77630626e-32 + 0.0j, + 4.17500000e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 4.17500000e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -1.39493681e-17 + 0.0j, + -7.00000000e-18 + 0.0j, + 4.17500000e-17 + 0.0j, + -2.77500000e-17 + 0.0j, + 2.50000000e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py b/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py new file mode 100644 index 000000000000..08d13233c76e --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_forward_euler_solver.py @@ -0,0 +1,47 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Forward Euler solver.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt, data, unpack +from scipy.integrate import solve_ivp + +from qiskit.algorithms.evolvers.variational.solvers.ode.forward_euler_solver import ( + ForwardEulerSolver, +) + + +@ddt +class TestForwardEulerSolver(QiskitAlgorithmsTestCase): + """Test Forward Euler solver.""" + + @unpack + @data((4, 16), (16, 35.52713678800501), (320, 53.261108839604795)) + def test_solve(self, timesteps, expected_result): + """Test Forward Euler solver for a simple ODE.""" + + y0 = [1] + + # pylint: disable=unused-argument + def func(time, y): + return y + + t_span = [0.0, 4.0] + sol1 = solve_ivp(func, t_span, y0, method=ForwardEulerSolver, num_t_steps=timesteps) + np.testing.assert_equal(sol1.y[-1][-1], expected_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py new file mode 100644 index 000000000000..1ef84204f2cc --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_ode_function.py @@ -0,0 +1,165 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test ODE function generator.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.ode_function import ( + OdeFunction, +) +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, +) + + +class TestOdeFunctionGenerator(QiskitAlgorithmsTestCase): + """Test ODE function generator.""" + + def test_var_qte_ode_function(self): + """Test ODE function generator.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + param_dict = {param: np.pi / 4 for param in parameters} + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + t_param = None + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + + time = 2 + ode_function_generator = OdeFunction( + linear_solver, error_calculator=None, t_param=None, param_dict=param_dict + ) + + qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) + + expected_qte_ode_function = [ + 0.442145, + -0.022081, + 0.106223, + -0.117468, + 0.251233, + 0.321256, + -0.062728, + -0.036209, + -0.509219, + -0.183459, + -0.050739, + -0.093163, + ] + + np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function) + + def test_var_qte_ode_function_time_param(self): + """Test ODE function generator with time param.""" + t_param = Parameter("t") + observable = SummedOp( + [ + 0.2252 * t_param * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + param_dict = {param: np.pi / 4 for param in parameters} + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + time = 2 + + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + ode_function_generator = OdeFunction( + linear_solver, error_calculator=None, t_param=t_param, param_dict=param_dict + ) + + qte_ode_function = ode_function_generator.var_qte_ode_function(time, param_dict.values()) + + expected_qte_ode_function = [ + 0.442145, + -0.022081, + 0.106223, + -0.117468, + 0.251233, + 0.321256, + -0.062728, + -0.036209, + -0.509219, + -0.183459, + -0.050739, + -0.093163, + ] + + np.testing.assert_array_almost_equal(expected_qte_ode_function, qte_ode_function, decimal=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py b/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py new file mode 100644 index 000000000000..5b39229ba502 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/ode/test_var_qte_ode_solver.py @@ -0,0 +1,136 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test solver of ODEs.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import ddt, data, unpack +import numpy as np +from qiskit.algorithms.evolvers.variational.solvers.ode.forward_euler_solver import ( + ForwardEulerSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.var_qte_ode_solver import ( + VarQTEOdeSolver, +) +from qiskit.algorithms.evolvers.variational.solvers.ode.ode_function import ( + OdeFunction, +) +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, +) + + +@ddt +class TestVarQTEOdeSolver(QiskitAlgorithmsTestCase): + """Test solver of ODEs.""" + + @data( + ( + "RK45", + [ + -0.30076755873631345, + -0.8032811383782005, + 1.1674108371914734e-15, + 3.2293849116821145e-16, + 2.541585055586039, + 1.155475184255733, + -2.966331417968169e-16, + 9.604292449638343e-17, + ], + ), + ( + ForwardEulerSolver, + [ + -3.2707e-01, + -8.0960e-01, + 3.4323e-16, + 8.9034e-17, + 2.5290e00, + 1.1563e00, + 3.0227e-16, + -2.2769e-16, + ], + ), + ) + @unpack + def test_run_no_backend(self, ode_solver, expected_result): + """Test ODE solver with no backend.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + + init_param_values = np.zeros(len(parameters)) + for i in range(ansatz.num_qubits): + init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + var_principle = ImaginaryMcLachlanPrinciple() + + time = 1 + + t_param = None + + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + ode_function_generator = OdeFunction(linear_solver, None, param_dict, t_param) + + var_qte_ode_solver = VarQTEOdeSolver( + list(param_dict.values()), + ode_function_generator, + ode_solver=ode_solver, + num_timesteps=25, + ) + + result = var_qte_ode_solver.run(time) + + np.testing.assert_array_almost_equal(result, expected_result, decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py b/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py new file mode 100644 index 000000000000..c5442447bf9c --- /dev/null +++ b/test/python/algorithms/evolvers/variational/solvers/test_varqte_linear_solver.py @@ -0,0 +1,115 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test solver of linear equations.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import ddt, data +import numpy as np + +from qiskit import BasicAer +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.algorithms.evolvers.variational.solvers.var_qte_linear_solver import ( + VarQTELinearSolver, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from .expected_results.test_varqte_linear_solver_expected_1 import ( + expected_metric_res_1, +) + + +@ddt +class TestVarQTELinearSolver(QiskitAlgorithmsTestCase): + """Test solver of linear equations.""" + + @data(BasicAer.get_backend("statevector_simulator"), None) + def test_solve_lse(self, backend): + """Test SLE solver.""" + + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(ansatz.num_qubits): + init_param_values[-(ansatz.num_qubits + i + 1)] = np.pi / 2 + + param_dict = dict(zip(parameters, init_param_values)) + + var_principle = ImaginaryMcLachlanPrinciple() + t_param = None + linear_solver = None + linear_solver = VarQTELinearSolver( + var_principle, + observable, + ansatz, + parameters, + t_param, + linear_solver, + quantum_instance=backend, + ) + + nat_grad_res, metric_res, grad_res = linear_solver.solve_lse(param_dict) + + expected_nat_grad_res = [ + 3.43500000e-01, + -2.89800000e-01, + 2.43575264e-16, + 1.31792695e-16, + -9.61200000e-01, + -2.89800000e-01, + 1.27493709e-17, + 1.12587456e-16, + 3.43500000e-01, + -2.89800000e-01, + 3.69914720e-17, + 1.95052083e-17, + ] + + expected_grad_res = [ + (0.17174999999999926 - 0j), + (-0.21735000000000085 + 0j), + (4.114902862895087e-17 - 0j), + (4.114902862895087e-17 - 0j), + (-0.24030000000000012 + 0j), + (-0.21735000000000085 + 0j), + (4.114902862895087e-17 - 0j), + (4.114902862895087e-17 - 0j), + (0.17174999999999918 - 0j), + (-0.21735000000000076 + 0j), + (1.7789936190837538e-17 - 0j), + (-8.319872568662832e-17 + 0j), + ] + + np.testing.assert_array_almost_equal(nat_grad_res, expected_nat_grad_res, decimal=4) + np.testing.assert_array_almost_equal(grad_res, expected_grad_res, decimal=4) + np.testing.assert_array_almost_equal(metric_res, expected_metric_res_1, decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qite.py b/test/python/algorithms/evolvers/variational/test_var_qite.py new file mode 100644 index 000000000000..6c4a26e13f8c --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qite.py @@ -0,0 +1,287 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Variational Quantum Imaginary Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import data, ddt +import numpy as np +from qiskit.test import slow_test +from qiskit.utils import algorithm_globals, QuantumInstance +from qiskit import BasicAer +from qiskit.algorithms import EvolutionProblem, VarQITE +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, + ExpectationFactory, +) + + +@ddt +class TestVarQITE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Imaginary Time Evolution algorithm.""" + + def setUp(self): + super().setUp() + self.seed = 11 + np.random.seed(self.seed) + backend_statevector = BasicAer.get_backend("statevector_simulator") + backend_qasm = BasicAer.get_backend("qasm_simulator") + self.quantum_instance = QuantumInstance( + backend=backend_statevector, + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.quantum_instance_qasm = QuantumInstance( + backend=backend_qasm, + shots=4000, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.backends_dict = { + "qi_sv": self.quantum_instance, + "qi_qasm": self.quantum_instance_qasm, + "b_sv": backend_statevector, + } + + self.backends_names = ["qi_qasm", "b_sv", "qi_sv"] + + @slow_test + def test_run_d_1_with_aux_ops(self): + """Test VarQITE for d = 1 and t = 1 with evaluating auxiliary operator and the Forward + Euler solver..""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + aux_ops = [X ^ X, Y ^ Z] + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = ImaginaryMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + time = 1 + + evolution_problem = EvolutionProblem(observable, time, aux_operators=aux_ops) + + thetas_expected_sv = [ + 1.03612467538419, + 1.91891042963193, + 2.81129500883365, + 2.78938736703301, + 2.2215151699331, + 1.61953721158502, + 2.23490753161058, + 1.97145113701782, + ] + + thetas_expected_qasm = [ + 1.03612467538419, + 1.91891042963193, + 2.81129500883365, + 2.78938736703301, + 2.2215151699331, + 1.61953721158502, + 2.23490753161058, + 1.97145113701782, + ] + + expected_aux_ops_evaluated_sv = [(-0.160899, 0.0), (0.26207, 0.0)] + expected_aux_ops_evaluated_qasm = [ + (-0.1765, 0.015563), + (0.2555, 0.015287), + ] + + for backend_name in self.backends_names: + with self.subTest(msg=f"Test {backend_name} backend."): + algorithm_globals.random_seed = self.seed + backend = self.backends_dict[backend_name] + expectation = ExpectationFactory.build( + operator=observable, + backend=backend, + ) + var_qite = VarQITE( + ansatz, + var_principle, + param_dict, + expectation=expectation, + num_timesteps=25, + quantum_instance=backend, + ) + evolution_result = var_qite.evolve(evolution_problem) + + evolved_state = evolution_result.evolved_state + aux_ops = evolution_result.aux_ops_evaluated + + parameter_values = evolved_state.data[0][0].params + + if backend_name == "qi_qasm": + thetas_expected = thetas_expected_qasm + expected_aux_ops = expected_aux_ops_evaluated_qasm + else: + thetas_expected = thetas_expected_sv + expected_aux_ops = expected_aux_ops_evaluated_sv + + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=3 + ) + + np.testing.assert_array_almost_equal(aux_ops, expected_aux_ops) + + def test_run_d_1_t_7(self): + """Test VarQITE for d = 1 and t = 7 with RK45 ODE solver.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = ImaginaryMcLachlanPrinciple() + + backend = BasicAer.get_backend("statevector_simulator") + + time = 7 + var_qite = VarQITE( + ansatz, + var_principle, + init_param_values, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 0.828917365718767, + 1.88481074798033, + 3.14111335991238, + 3.14125849601269, + 2.33768562678401, + 1.78670990729437, + 2.04214275514208, + 2.04009918594422, + ] + + self._test_helper(observable, thetas_expected, time, var_qite, 2) + + @slow_test + @data( + SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ), + 0.2252 * (I ^ I) + + 0.5716 * (Z ^ Z) + + 0.3435 * (I ^ Z) + + -0.4347 * (Z ^ I) + + 0.091 * (Y ^ Y) + + 0.091 * (X ^ X), + ) + def test_run_d_2(self, observable): + """Test VarQITE for d = 2 and t = 1 with RK45 ODE solver.""" + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 4 + + var_principle = ImaginaryMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + time = 1 + var_qite = VarQITE( + ansatz, + var_principle, + param_dict, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 1.29495364023786, + 1.08970061333559, + 0.667488228710748, + 0.500122687902944, + 1.4377736672043, + 1.22881086103085, + 0.729773048146251, + 1.01698854755226, + 0.050807780587492, + 0.294828474947149, + 0.839305697704923, + 0.663689581255428, + ] + + self._test_helper(observable, thetas_expected, time, var_qite, 4) + + def _test_helper(self, observable, thetas_expected, time, var_qite, decimal): + evolution_problem = EvolutionProblem(observable, time) + evolution_result = var_qite.evolve(evolution_problem) + evolved_state = evolution_result.evolved_state + + parameter_values = evolved_state.data[0][0].params + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=decimal + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qrte.py b/test/python/algorithms/evolvers/variational/test_var_qrte.py new file mode 100644 index 000000000000..22ab25394e14 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qrte.py @@ -0,0 +1,234 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Variational Quantum Real Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from ddt import data, ddt +import numpy as np +from qiskit.test import slow_test +from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit.algorithms import EvolutionProblem, VarQRTE +from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, +) +from qiskit import BasicAer +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + SummedOp, + X, + Y, + I, + Z, + ExpectationFactory, +) + + +@ddt +class TestVarQRTE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Real Time Evolution algorithm.""" + + def setUp(self): + super().setUp() + self.seed = 11 + np.random.seed(self.seed) + backend_statevector = BasicAer.get_backend("statevector_simulator") + backend_qasm = BasicAer.get_backend("qasm_simulator") + self.quantum_instance = QuantumInstance( + backend=backend_statevector, + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.quantum_instance_qasm = QuantumInstance( + backend=backend_qasm, + shots=4000, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.backends_dict = { + "qi_sv": self.quantum_instance, + "qi_qasm": self.quantum_instance_qasm, + "b_sv": backend_statevector, + } + + self.backends_names = ["qi_qasm", "b_sv", "qi_sv"] + + @slow_test + def test_run_d_1_with_aux_ops(self): + """Test VarQRTE for d = 1 and t = 0.1 with evaluating auxiliary operators and the Forward + Euler solver.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + aux_ops = [X ^ X, Y ^ Z] + d = 1 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 2 + init_param_values[0] = 1 + var_principle = RealMcLachlanPrinciple() + + time = 0.1 + + evolution_problem = EvolutionProblem(observable, time, aux_operators=aux_ops) + + thetas_expected_sv = [ + 0.88967020378258, + 1.53740751016451, + 1.57076759018861, + 1.58893301221363, + 1.60100970594142, + 1.57008242207638, + 1.63791241090936, + 1.53741371076912, + ] + + thetas_expected_qasm = [ + 0.88967811203145, + 1.53745130248168, + 1.57206794045495, + 1.58901347342829, + 1.60101431615503, + 1.57138020823337, + 1.63796000651177, + 1.53742227084076, + ] + + expected_aux_ops_evaluated_sv = [(0.06675, 0.0), (0.772636, 0.0)] + + expected_aux_ops_evaluated_qasm = [ + (0.06450000000000006, 0.01577846435810532), + (0.7895000000000001, 0.009704248425303218), + ] + + for backend_name in self.backends_names: + with self.subTest(msg=f"Test {backend_name} backend."): + algorithm_globals.random_seed = self.seed + backend = self.backends_dict[backend_name] + expectation = ExpectationFactory.build( + operator=observable, + backend=backend, + ) + var_qrte = VarQRTE( + ansatz, + var_principle, + init_param_values, + expectation=expectation, + num_timesteps=25, + quantum_instance=backend, + ) + evolution_result = var_qrte.evolve(evolution_problem) + + evolved_state = evolution_result.evolved_state + aux_ops = evolution_result.aux_ops_evaluated + + parameter_values = evolved_state.data[0][0].params + if backend_name == "qi_qasm": + thetas_expected = thetas_expected_qasm + expected_aux_ops = expected_aux_ops_evaluated_qasm + else: + thetas_expected = thetas_expected_sv + expected_aux_ops = expected_aux_ops_evaluated_sv + + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal( + float(parameter_value), thetas_expected[i], decimal=3 + ) + np.testing.assert_array_almost_equal(aux_ops, expected_aux_ops) + + @slow_test + @data( + SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ), + 0.2252 * (I ^ I) + + 0.5716 * (Z ^ Z) + + 0.3435 * (I ^ Z) + + -0.4347 * (Z ^ I) + + 0.091 * (Y ^ Y) + + 0.091 * (X ^ X), + ) + def test_run_d_2(self, observable): + """Test VarQRTE for d = 2 and t = 1 with RK45 ODE solver.""" + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + parameters = list(ansatz.parameters) + init_param_values = np.zeros(len(parameters)) + for i in range(len(parameters)): + init_param_values[i] = np.pi / 4 + + var_principle = RealMcLachlanPrinciple() + + param_dict = dict(zip(parameters, init_param_values)) + + backend = BasicAer.get_backend("statevector_simulator") + + time = 1 + var_qrte = VarQRTE( + ansatz, + var_principle, + param_dict, + ode_solver="RK45", + num_timesteps=25, + quantum_instance=backend, + ) + + thetas_expected = [ + 0.348407744196573, + 0.919404626262464, + 1.18189219371626, + 0.771011177789998, + 0.734384256533924, + 0.965289520781899, + 1.14441687204195, + 1.17231927568571, + 1.03014771379412, + 0.867266309056347, + 0.699606368428206, + 0.610788576398685, + ] + + self._test_helper(observable, thetas_expected, time, var_qrte) + + def _test_helper(self, observable, thetas_expected, time, var_qrte): + evolution_problem = EvolutionProblem(observable, time) + evolution_result = var_qrte.evolve(evolution_problem) + evolved_state = evolution_result.evolved_state + + parameter_values = evolved_state.data[0][0].params + for i, parameter_value in enumerate(parameter_values): + np.testing.assert_almost_equal(float(parameter_value), thetas_expected[i], decimal=4) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/test_var_qte.py b/test/python/algorithms/evolvers/variational/test_var_qte.py new file mode 100644 index 000000000000..1083a1564f68 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/test_var_qte.py @@ -0,0 +1,78 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Test Variational Quantum Real Time Evolution algorithm.""" + +import unittest + +from test.python.algorithms import QiskitAlgorithmsTestCase +from numpy.testing import assert_raises +from ddt import data, ddt +import numpy as np + +from qiskit.algorithms.evolvers.variational.var_qte import VarQTE +from qiskit.circuit import Parameter + + +@ddt +class TestVarQTE(QiskitAlgorithmsTestCase): + """Test Variational Quantum Time Evolution class methods.""" + + def setUp(self): + super().setUp() + self._parameters1 = [Parameter("a"), Parameter("b"), Parameter("c")] + + @data([1.4, 2, 3], np.asarray([1.4, 2, 3])) + def test_create_init_state_param_dict(self, param_values): + """Tests if a correct dictionary is created.""" + expected = dict(zip(self._parameters1, param_values)) + with self.subTest("Parameters values given as a list test."): + result = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + np.testing.assert_equal(result, expected) + with self.subTest("Parameters values given as a dictionary test."): + result = VarQTE._create_init_state_param_dict( + dict(zip(self._parameters1, param_values)), self._parameters1 + ) + np.testing.assert_equal(result, expected) + with self.subTest("Parameters values given as a superset dictionary test."): + expected = dict( + zip( + [self._parameters1[0], self._parameters1[2]], [param_values[0], param_values[2]] + ) + ) + result = VarQTE._create_init_state_param_dict( + dict(zip(self._parameters1, param_values)), + [self._parameters1[0], self._parameters1[2]], + ) + np.testing.assert_equal(result, expected) + + @data([1.4, 2], np.asarray([1.4, 3]), {}, []) + def test_create_init_state_param_dict_errors_list(self, param_values): + """Tests if an error is raised.""" + with assert_raises(ValueError): + _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + + @data([1.4, 2], np.asarray([1.4, 3])) + def test_create_init_state_param_dict_errors_subset(self, param_values): + """Tests if an error is raised if subset of parameters provided.""" + param_values_dict = dict(zip([self._parameters1[0], self._parameters1[2]], param_values)) + with assert_raises(ValueError): + _ = VarQTE._create_init_state_param_dict(param_values_dict, self._parameters1) + + @data(5, "s", Parameter("x")) + def test_create_init_state_param_dict_errors_type(self, param_values): + """Tests if an error is raised if wrong input type.""" + with assert_raises(TypeError): + _ = VarQTE._create_init_state_param_dict(param_values, self._parameters1) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/variational_principles/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py new file mode 100644 index 000000000000..9c3165f57a2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py new file mode 100644 index 000000000000..231cbac4dba4 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected1.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_1 = [ + [ + 2.50000000e-01 + 0.0j, + 1.59600000e-33 + 0.0j, + 5.90075760e-18 + 0.0j, + -8.49242405e-19 + 0.0j, + 8.83883476e-02 + 0.0j, + 1.33253788e-17 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.40000000e-17 + 0.0j, + -1.41735435e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.00222087e-01 + 0.0j, + -3.12500000e-02 + 0.0j, + ], + [ + 1.59600000e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 1.34350288e-17 + 0.0j, + 6.43502884e-18 + 0.0j, + -8.83883476e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 1.48207521e-01 + 0.0j, + 2.00444174e-01 + 0.0j, + ], + [ + 5.90075760e-18 + 0.0j, + 1.34350288e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -4.41941738e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + 1.04933262e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + ], + [ + -8.49242405e-19 + 0.0j, + 6.43502884e-18 + 0.0j, + -1.38777878e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 5.14514565e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + 1.94162607e-02 + 0.0j, + ], + [ + 8.83883476e-02 + 0.0j, + -8.83883476e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 2.34375000e-01 + 0.0j, + -1.10485435e-01 + 0.0j, + -2.02014565e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + -1.42172278e-03 + 0.0j, + -1.23822206e-01 + 0.0j, + ], + [ + 1.33253788e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + -1.10485435e-01 + 0.0j, + 2.18750000e-01 + 0.0j, + -2.68082618e-03 + 0.0j, + -1.59099026e-17 + 0.0j, + -1.57197815e-01 + 0.0j, + 2.53331304e-02 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.06138957e-01 + 0.0j, + ], + [ + 6.25000000e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + -2.02014565e-02 + 0.0j, + -2.68082618e-03 + 0.0j, + 2.23881674e-01 + 0.0j, + 1.37944174e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.34535646e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + ], + [ + 1.40000000e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -1.59099026e-17 + 0.0j, + 1.37944174e-01 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.15574269e-17 + 0.0j, + 9.75412607e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + ], + [ + -1.41735435e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 5.14514565e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -1.57197815e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.95283753e-01 + 0.0j, + -3.82941440e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + ], + [ + 3.12500000e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + 2.53331304e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.15574269e-17 + 0.0j, + -3.82941440e-02 + 0.0j, + 2.17629701e-01 + 0.0j, + 1.32431810e-01 + 0.0j, + -1.91961467e-02 + 0.0j, + ], + [ + 1.00222087e-01 + 0.0j, + 1.48207521e-01 + 0.0j, + 1.04933262e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + -1.42172278e-03 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.34535646e-01 + 0.0j, + 9.75412607e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + 1.32431810e-01 + 0.0j, + 1.81683746e-01 + 0.0j, + 7.28902444e-02 + 0.0j, + ], + [ + -3.12500000e-02 + 0.0j, + 2.00444174e-01 + 0.0j, + -6.89720869e-02 + 0.0j, + 1.94162607e-02 + 0.0j, + -1.23822206e-01 + 0.0j, + 1.06138957e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + -1.91961467e-02 + 0.0j, + 7.28902444e-02 + 0.0j, + 2.38616353e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py new file mode 100644 index 000000000000..386e3196ea4e --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected2.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_2 = [ + [ + 2.50000000e-01 + 0.0j, + 1.59600000e-33 + 0.0j, + 5.90075760e-18 + 0.0j, + -8.49242405e-19 + 0.0j, + 8.83883476e-02 + 0.0j, + 1.33253788e-17 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.40000000e-17 + 0.0j, + -1.41735435e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.00222087e-01 + 0.0j, + -3.12500000e-02 + 0.0j, + ], + [ + 1.59600000e-33 + 0.0j, + 2.50000000e-01 + 0.0j, + 1.34350288e-17 + 0.0j, + 6.43502884e-18 + 0.0j, + -8.83883476e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 1.48207521e-01 + 0.0j, + 2.00444174e-01 + 0.0j, + ], + [ + 5.90075760e-18 + 0.0j, + 1.34350288e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -1.38777878e-17 + 0.0j, + -4.41941738e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + 1.04933262e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + ], + [ + -8.49242405e-19 + 0.0j, + 6.43502884e-18 + 0.0j, + -1.38777878e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + 3.12500000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + 5.14514565e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + 1.94162607e-02 + 0.0j, + ], + [ + 8.83883476e-02 + 0.0j, + -8.83883476e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 2.34375000e-01 + 0.0j, + -1.10485435e-01 + 0.0j, + -2.02014565e-02 + 0.0j, + -4.41941738e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + -1.42172278e-03 + 0.0j, + -1.23822206e-01 + 0.0j, + ], + [ + 1.33253788e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + -6.25000000e-02 + 0.0j, + -1.10485435e-01 + 0.0j, + 2.18750000e-01 + 0.0j, + -2.68082618e-03 + 0.0j, + -1.59099026e-17 + 0.0j, + -1.57197815e-01 + 0.0j, + 2.53331304e-02 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.06138957e-01 + 0.0j, + ], + [ + 6.25000000e-02 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.19638348e-01 + 0.0j, + 3.12500000e-02 + 0.0j, + -2.02014565e-02 + 0.0j, + -2.68082618e-03 + 0.0j, + 2.23881674e-01 + 0.0j, + 1.37944174e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.34535646e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + ], + [ + 1.40000000e-17 + 0.0j, + 1.25000000e-01 + 0.0j, + 6.25000000e-02 + 0.0j, + 1.25000000e-01 + 0.0j, + -4.41941738e-02 + 0.0j, + -1.59099026e-17 + 0.0j, + 1.37944174e-01 + 0.0j, + 2.50000000e-01 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.15574269e-17 + 0.0j, + 9.75412607e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + ], + [ + -1.41735435e-01 + 0.0j, + -8.45970869e-02 + 0.0j, + -5.14514565e-02 + 0.0j, + 5.14514565e-02 + 0.0j, + 1.49547935e-02 + 0.0j, + -1.57197815e-01 + 0.0j, + -3.78033966e-02 + 0.0j, + -2.10523539e-17 + 0.0j, + 1.95283753e-01 + 0.0j, + -3.82941440e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + ], + [ + 3.12500000e-02 + 0.0j, + 7.54441738e-02 + 0.0j, + 6.89720869e-02 + 0.0j, + -6.89720869e-02 + 0.0j, + -2.24896848e-02 + 0.0j, + 2.53331304e-02 + 0.0j, + 1.58423239e-01 + 0.0j, + 1.15574269e-17 + 0.0j, + -3.82941440e-02 + 0.0j, + 2.17629701e-01 + 0.0j, + 1.32431810e-01 + 0.0j, + -1.91961467e-02 + 0.0j, + ], + [ + 1.00222087e-01 + 0.0j, + 1.48207521e-01 + 0.0j, + 1.04933262e-02 + 0.0j, + 7.81250000e-03 + 0.0j, + -1.42172278e-03 + 0.0j, + 9.82311963e-03 + 0.0j, + 1.34535646e-01 + 0.0j, + 9.75412607e-02 + 0.0j, + -6.11392595e-02 + 0.0j, + 1.32431810e-01 + 0.0j, + 1.81683746e-01 + 0.0j, + 7.28902444e-02 + 0.0j, + ], + [ + -3.12500000e-02 + 0.0j, + 2.00444174e-01 + 0.0j, + -6.89720869e-02 + 0.0j, + 1.94162607e-02 + 0.0j, + -1.23822206e-01 + 0.0j, + 1.06138957e-01 + 0.0j, + -5.49651086e-02 + 0.0j, + 5.71383476e-02 + 0.0j, + -4.51588288e-02 + 0.0j, + -1.91961467e-02 + 0.0j, + 7.28902444e-02 + 0.0j, + 2.38616353e-01 + 0.0j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py new file mode 100644 index 000000000000..5c295c0c6f2a --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/expected_results/test_imaginary_mc_lachlan_variational_principle_expected3.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Stores expected results that are lengthy.""" +expected_bound_metric_tensor_3 = [ + [ + -1.21000000e-34 + 0.00e00j, + 1.21000000e-34 + 2.50e-19j, + 1.76776695e-01 - 1.00e-18j, + -1.40000000e-17 + 0.00e00j, + -6.25000000e-02 + 0.00e00j, + 8.83883476e-02 - 1.25e-18j, + 1.69194174e-01 + 2.25e-18j, + 8.83883476e-02 - 2.50e-19j, + -7.27633476e-02 + 0.00e00j, + 9.75412607e-02 + 7.50e-19j, + 1.48398042e-02 - 1.75e-18j, + -9.75412607e-02 + 3.75e-18j, + ], + [ + 1.21000000e-34 + 2.50e-19j, + -1.21000000e-34 + 0.00e00j, + 1.10000000e-34 + 2.75e-18j, + 1.76776695e-01 - 2.25e-18j, + -6.25000000e-02 + 0.00e00j, + -8.83883476e-02 + 4.00e-18j, + 4.41941738e-02 - 1.25e-18j, + 1.76776695e-01 - 2.50e-19j, + 7.27633476e-02 - 7.50e-19j, + -9.75412607e-02 - 7.50e-19j, + 1.10485435e-02 - 7.50e-19j, + 2.74587393e-02 + 2.50e-19j, + ], + [ + 1.76776695e-01 - 1.00e-18j, + 1.10000000e-34 + 2.75e-18j, + -1.25000000e-01 + 0.00e00j, + -1.25000000e-01 + 0.00e00j, + -1.06694174e-01 + 1.25e-18j, + -6.25000000e-02 + 1.75e-18j, + -1.01332521e-01 + 7.50e-19j, + 4.67500000e-17 - 7.50e-19j, + 1.75206304e-02 + 5.00e-19j, + -8.57075215e-02 - 1.00e-18j, + -1.63277304e-01 + 1.00e-18j, + -1.56250000e-02 + 0.00e00j, + ], + [ + -1.40000000e-17 + 0.00e00j, + 1.76776695e-01 - 2.25e-18j, + -1.25000000e-01 + 0.00e00j, + -1.25000000e-01 + 0.00e00j, + 1.83058262e-02 - 1.50e-18j, + -1.50888348e-01 - 1.50e-18j, + -1.01332521e-01 + 2.50e-19j, + -8.83883476e-02 - 1.00e-18j, + -2.28822827e-02 - 1.00e-18j, + -1.16957521e-01 + 1.00e-18j, + -1.97208130e-01 + 0.00e00j, + -1.79457521e-01 + 1.25e-18j, + ], + [ + -6.25000000e-02 + 0.00e00j, + -6.25000000e-02 + 0.00e00j, + -1.06694174e-01 + 1.25e-18j, + 1.83058262e-02 - 1.50e-18j, + -1.56250000e-02 + 0.00e00j, + -2.20970869e-02 - 2.00e-18j, + 1.48992717e-01 - 1.00e-18j, + 2.60000000e-17 - 1.50e-18j, + -6.69614673e-02 - 5.00e-19j, + 2.00051576e-01 + 5.00e-19j, + 1.13640168e-01 + 1.25e-18j, + -4.83780325e-02 - 1.00e-18j, + ], + [ + 8.83883476e-02 - 1.25e-18j, + -8.83883476e-02 + 4.00e-18j, + -6.25000000e-02 + 1.75e-18j, + -1.50888348e-01 - 1.50e-18j, + -2.20970869e-02 - 2.00e-18j, + -3.12500000e-02 + 0.00e00j, + -2.85691738e-02 + 4.25e-18j, + 1.76776695e-01 + 0.00e00j, + 5.52427173e-03 + 1.00e-18j, + -1.29346478e-01 + 5.00e-19j, + -4.81004238e-02 + 4.25e-18j, + 5.27918696e-02 + 2.50e-19j, + ], + [ + 1.69194174e-01 + 2.25e-18j, + 4.41941738e-02 - 1.25e-18j, + -1.01332521e-01 + 7.50e-19j, + -1.01332521e-01 + 2.50e-19j, + 1.48992717e-01 - 1.00e-18j, + -2.85691738e-02 + 4.25e-18j, + -2.61183262e-02 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + 6.62099510e-02 - 1.00e-18j, + -2.90767610e-02 + 1.75e-18j, + -1.24942505e-01 + 0.00e00j, + -1.72430217e-02 + 2.50e-19j, + ], + [ + 8.83883476e-02 - 2.50e-19j, + 1.76776695e-01 - 2.50e-19j, + 4.67500000e-17 - 7.50e-19j, + -8.83883476e-02 - 1.00e-18j, + 2.60000000e-17 - 1.50e-18j, + 1.76776695e-01 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + -6.88900000e-33 + 0.00e00j, + 1.79457521e-01 - 1.75e-18j, + -5.33470869e-02 + 2.00e-18j, + -9.56456304e-02 + 3.00e-18j, + -1.32582521e-01 + 2.50e-19j, + ], + [ + -7.27633476e-02 + 0.00e00j, + 7.27633476e-02 - 7.50e-19j, + 1.75206304e-02 + 5.00e-19j, + -2.28822827e-02 - 1.00e-18j, + -6.69614673e-02 - 5.00e-19j, + 5.52427173e-03 + 1.00e-18j, + 6.62099510e-02 - 1.00e-18j, + 1.79457521e-01 - 1.75e-18j, + -5.47162473e-02 + 0.00e00j, + -4.20854047e-02 + 4.00e-18j, + -7.75494553e-02 - 2.50e-18j, + -2.49573723e-02 + 7.50e-19j, + ], + [ + 9.75412607e-02 + 7.50e-19j, + -9.75412607e-02 - 7.50e-19j, + -8.57075215e-02 - 1.00e-18j, + -1.16957521e-01 + 1.00e-18j, + 2.00051576e-01 + 5.00e-19j, + -1.29346478e-01 + 5.00e-19j, + -2.90767610e-02 + 1.75e-18j, + -5.33470869e-02 + 2.00e-18j, + -4.20854047e-02 + 4.00e-18j, + -3.23702991e-02 + 0.00e00j, + -4.70257118e-02 + 0.00e00j, + 1.22539288e-01 - 2.25e-18j, + ], + [ + 1.48398042e-02 - 1.75e-18j, + 1.10485435e-02 - 7.50e-19j, + -1.63277304e-01 + 1.00e-18j, + -1.97208130e-01 + 0.00e00j, + 1.13640168e-01 + 1.25e-18j, + -4.81004238e-02 + 4.25e-18j, + -1.24942505e-01 + 0.00e00j, + -9.56456304e-02 + 3.00e-18j, + -7.75494553e-02 - 2.50e-18j, + -4.70257118e-02 + 0.00e00j, + -6.83162540e-02 + 0.00e00j, + -2.78870598e-02 + 0.00e00j, + ], + [ + -9.75412607e-02 + 3.75e-18j, + 2.74587393e-02 + 2.50e-19j, + -1.56250000e-02 + 0.00e00j, + -1.79457521e-01 + 1.25e-18j, + -4.83780325e-02 - 1.00e-18j, + 5.27918696e-02 + 2.50e-19j, + -1.72430217e-02 + 2.50e-19j, + -1.32582521e-01 + 2.50e-19j, + -2.49573723e-02 + 7.50e-19j, + 1.22539288e-01 - 2.25e-18j, + -2.78870598e-02 + 0.00e00j, + -1.13836467e-02 + 0.00e00j, + ], +] diff --git a/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py new file mode 100644 index 000000000000..5118a9a699a4 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/imaginary/test_imaginary_mc_lachlan_principle.py @@ -0,0 +1,111 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test imaginary McLachlan's variational principle.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np + +from qiskit.algorithms.evolvers.variational import ( + ImaginaryMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from ..expected_results.test_imaginary_mc_lachlan_variational_principle_expected1 import ( + expected_bound_metric_tensor_1, +) + + +class TestImaginaryMcLachlanPrinciple(QiskitAlgorithmsTestCase): + """Test imaginary McLachlan's variational principle.""" + + def test_calc_metric_tensor(self): + """Test calculating a metric tensor.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = ImaginaryMcLachlanPrinciple() + + bound_metric_tensor = var_principle.metric_tensor( + ansatz, parameters, parameters, param_dict.values(), None, None + ) + + np.testing.assert_almost_equal(bound_metric_tensor, expected_bound_metric_tensor_1) + + def test_calc_calc_evolution_grad(self): + """Test calculating evolution gradient.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = ImaginaryMcLachlanPrinciple() + + bound_evolution_grad = var_principle.evolution_grad( + observable, + ansatz, + None, + param_dict, + parameters, + parameters, + param_dict.values(), + None, + None, + ) + + expected_bound_evolution_grad = [ + (0.19308934095957098 - 1.4e-17j), + (0.007027674650099142 - 0j), + (0.03192524520091862 - 0j), + (-0.06810314606309673 - 1e-18j), + (0.07590371669521798 - 7e-18j), + (0.11891968269385343 + 1.5e-18j), + (-0.0012030273438232639 + 0j), + (-0.049885258804562266 + 1.8500000000000002e-17j), + (-0.20178860797540302 - 5e-19j), + (-0.0052269232310933195 + 1e-18j), + (0.022892905637005266 - 3e-18j), + (-0.022892905637005294 + 3.5e-18j), + ] + + np.testing.assert_almost_equal(bound_evolution_grad, expected_bound_evolution_grad) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py b/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py new file mode 100644 index 000000000000..b3ac36d2a6d9 --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/real/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py b/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py new file mode 100644 index 000000000000..13c126928bdb --- /dev/null +++ b/test/python/algorithms/evolvers/variational/variational_principles/real/test_real_mc_lachlan_principle.py @@ -0,0 +1,114 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test real McLachlan's variational principle.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.evolvers.variational import ( + RealMcLachlanPrinciple, +) +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import SummedOp, X, Y, I, Z +from ..expected_results.test_imaginary_mc_lachlan_variational_principle_expected2 import ( + expected_bound_metric_tensor_2, +) + + +class TestRealMcLachlanPrinciple(QiskitAlgorithmsTestCase): + """Test real McLachlan's variational principle.""" + + def test_calc_calc_metric_tensor(self): + """Test calculating a metric tensor.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = RealMcLachlanPrinciple() + + bound_metric_tensor = var_principle.metric_tensor( + ansatz, parameters, parameters, list(param_dict.values()), None, None + ) + + np.testing.assert_almost_equal( + bound_metric_tensor, expected_bound_metric_tensor_2, decimal=5 + ) + + def test_calc_evolution_grad(self): + """Test calculating evolution gradient.""" + observable = SummedOp( + [ + 0.2252 * (I ^ I), + 0.5716 * (Z ^ Z), + 0.3435 * (I ^ Z), + -0.4347 * (Z ^ I), + 0.091 * (Y ^ Y), + 0.091 * (X ^ X), + ] + ) + + d = 2 + ansatz = EfficientSU2(observable.num_qubits, reps=d) + + # Define a set of initial parameters + parameters = list(ansatz.parameters) + param_dict = {param: np.pi / 4 for param in parameters} + var_principle = RealMcLachlanPrinciple() + + bound_evolution_grad = var_principle.evolution_grad( + observable, + ansatz, + None, + param_dict, + parameters, + parameters, + list(param_dict.values()), + None, + None, + ) + + expected_bound_evolution_grad = [ + (-0.04514911474522546 + 4e-18j), + (0.0963123928027075 - 1.5e-18j), + (0.1365347823673539 - 7e-18j), + (0.004969316401057883 - 4.9999999999999996e-18j), + (-0.003843833929692342 - 4.999999999999998e-19j), + (0.07036988622493834 - 7e-18j), + (0.16560609099860682 - 3.5e-18j), + (0.16674183768051887 + 1e-18j), + (-0.03843296670360974 - 6e-18j), + (0.08891074158680243 - 6e-18j), + (0.06425681697616654 + 7e-18j), + (-0.03172376682078948 - 7e-18j), + ] + + np.testing.assert_almost_equal( + bound_evolution_grad, expected_bound_evolution_grad, decimal=5 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/optimizers/test_gradient_descent.py b/test/python/algorithms/optimizers/test_gradient_descent.py index 0391c3a714c6..abe15f4b5362 100644 --- a/test/python/algorithms/optimizers/test_gradient_descent.py +++ b/test/python/algorithms/optimizers/test_gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,10 +13,9 @@ """Tests for the Gradient Descent optimizer.""" from test.python.algorithms import QiskitAlgorithmsTestCase - import numpy as np - -from qiskit.algorithms.optimizers import GradientDescent +from qiskit.algorithms.optimizers import GradientDescent, GradientDescentState +from qiskit.algorithms.optimizers.steppable_optimizer import TellData, AskData from qiskit.circuit.library import PauliTwoDesign from qiskit.opflow import I, Z, StateFn from qiskit.test.decorators import slow_test @@ -28,6 +27,15 @@ class TestGradientDescent(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def grad(self, x): + """Gradient of the objective function""" + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) @slow_test def test_pauli_two_design(self): @@ -54,15 +62,15 @@ def test_pauli_two_design(self): ] ) - def objective(x): + def objective_pauli(x): return expr.bind_parameters(dict(zip(parameters, x))).eval().real optimizer = GradientDescent(maxiter=100, learning_rate=0.1, perturbation=0.1) - result = optimizer.minimize(objective, x0=initial_point) + result = optimizer.minimize(objective_pauli, x0=initial_point) self.assertLess(result.fun, -0.95) # final loss - self.assertEqual(result.nfev, 100) # function evaluations + self.assertEqual(result.nfev, 1300) # function evaluations def test_callback(self): """Test the callback.""" @@ -74,10 +82,7 @@ def callback(*args): optimizer = GradientDescent(maxiter=1, callback=callback) - def objective(x): - return np.linalg.norm(x) - - _ = optimizer.minimize(objective, np.array([1, -1])) + _ = optimizer.minimize(self.objective, np.array([1, -1])) self.assertEqual(len(history), 1) self.assertIsInstance(history[0][0], int) # nfevs @@ -85,8 +90,8 @@ def objective(x): self.assertIsInstance(history[0][2], float) # function value self.assertIsInstance(history[0][3], float) # norm of the gradient - def test_iterator_learning_rate(self): - """Test setting the learning rate as iterator.""" + def test_minimize(self): + """Test setting the learning rate as iterator and minimizing the funciton.""" def learning_rate(): power = 0.6 @@ -100,15 +105,93 @@ def powerlaw(): return powerlaw() - def objective(x): - return (np.linalg.norm(x) - 1) ** 2 + optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) + result = optimizer.minimize(self.objective, self.initial_point, self.grad) - def grad(x): - return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + self.assertLess(result.fun, 1e-5) - initial_point = np.array([1, 0.5, -2]) + def test_no_start(self): + """Tests that making a step without having started the optimizer raises an error.""" + optimizer = GradientDescent() + with self.assertRaises(AttributeError): + optimizer.step() + + def test_start(self): + """Tests if the start method initializes the state properly.""" + optimizer = GradientDescent() + self.assertIsNone(optimizer.state) + self.assertIsNone(optimizer.perturbation) + optimizer.start(x0=self.initial_point, fun=self.objective) + + test_state = GradientDescentState( + x=self.initial_point, + fun=self.objective, + jac=None, + nfev=0, + njev=0, + nit=0, + learning_rate=1, + stepsize=None, + ) - optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) - result = optimizer.minimize(objective, initial_point, grad) + self.assertEqual(test_state, optimizer.state) + + def test_ask(self): + """Test the ask method.""" + optimizer = GradientDescent() + optimizer.start(fun=self.objective, x0=self.initial_point) + + ask_data = optimizer.ask() + np.testing.assert_equal(ask_data.x_jac, self.initial_point) + self.assertIsNone(ask_data.x_fun) + + def test_evaluate(self): + """Test the evaluate method.""" + optimizer = GradientDescent(perturbation=1e-10) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = optimizer.evaluate(ask_data=ask_data) + np.testing.assert_almost_equal(tell_data.eval_jac, self.grad(self.initial_point), decimal=2) + + def test_tell(self): + """Test the tell method.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=self.initial_point) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + np.testing.assert_equal(optimizer.state.x, np.zeros(optimizer.state.x.shape)) + + def test_continue_condition(self): + """Test if the continue condition is working properly.""" + optimizer = GradientDescent(tol=1) + optimizer.start(fun=self.objective, x0=self.initial_point) + self.assertTrue(optimizer.continue_condition()) + optimizer.state.stepsize = 0.1 + self.assertFalse(optimizer.continue_condition()) + optimizer.state.stepsize = 10 + optimizer.state.nit = 1000 + self.assertFalse(optimizer.continue_condition()) + + def test_step(self): + """Tests if performing one step yields the desired result.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, jac=self.grad, x0=self.initial_point) + optimizer.step() + np.testing.assert_almost_equal( + optimizer.state.x, self.initial_point - self.grad(self.initial_point), 6 + ) - self.assertLess(result.fun, 1e-5) + def test_wrong_dimension_gradient(self): + """Tests if an error is raised when a gradient of the wrong dimension is passed.""" + + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=np.array([1.0, 5])) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + tell_data = TellData(eval_jac=np.array(1)) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/test/python/algorithms/optimizers/test_spsa.py b/test/python/algorithms/optimizers/test_spsa.py index 55e4b7b3c068..5f31d69d463f 100644 --- a/test/python/algorithms/optimizers/test_spsa.py +++ b/test/python/algorithms/optimizers/test_spsa.py @@ -181,3 +181,17 @@ def callback(nfev, point, fval, update, accepted): for i, (key, values) in enumerate(history.items()): self.assertTrue(all(isinstance(value, expected_types[i]) for value in values)) self.assertEqual(len(history[key]), maxiter) + + @data(1, 2, 3, 4) + def test_estimate_stddev(self, max_evals_grouped): + """Test the estimate_stddev + See https://github.com/Qiskit/qiskit-nature/issues/797""" + + def objective(x): + if len(x.shape) == 2: + return np.array([sum(x_i) for x_i in x]) + return sum(x) + + point = np.ones(5) + result = SPSA.estimate_stddev(objective, point, avg=10, max_evals_grouped=max_evals_grouped) + self.assertAlmostEqual(result, 0) diff --git a/test/python/algorithms/optimizers/utils/__init__.py b/test/python/algorithms/optimizers/utils/__init__.py new file mode 100644 index 000000000000..f3adc3e3b4da --- /dev/null +++ b/test/python/algorithms/optimizers/utils/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Tests for Optimizer Utils.""" diff --git a/test/python/algorithms/optimizers/utils/test_learning_rate.py b/test/python/algorithms/optimizers/utils/test_learning_rate.py new file mode 100644 index 000000000000..52acdbf98aaa --- /dev/null +++ b/test/python/algorithms/optimizers/utils/test_learning_rate.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for LearningRate.""" + +from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.algorithms.optimizers.optimizer_utils import LearningRate + + +class TestLearningRate(QiskitAlgorithmsTestCase): + """Tests for the LearningRate class.""" + + def setUp(self): + super().setUp() + np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def test_learning_rate(self): + """ + Tests if the learning rate is initialized properly for each kind of input: + float, list and iterator. + """ + constant_learning_rate_input = 0.01 + list_learning_rate_input = [0.01 * n for n in range(10)] + generator_learning_rate_input = lambda: (el for el in list_learning_rate_input) + + with self.subTest("Check constant learning rate."): + constant_learning_rate = LearningRate(learning_rate=constant_learning_rate_input) + for _ in range(5): + self.assertEqual(constant_learning_rate_input, next(constant_learning_rate)) + + with self.subTest("Check learning rate list."): + list_learning_rate = LearningRate(learning_rate=list_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(list_learning_rate)) + + with self.subTest("Check learning rate generator."): + generator_learning_rate = LearningRate(generator_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(generator_learning_rate)) diff --git a/test/python/algorithms/state_fidelities/__init__.py b/test/python/algorithms/state_fidelities/__init__.py new file mode 100644 index 000000000000..d8b7d587c4cc --- /dev/null +++ b/test/python/algorithms/state_fidelities/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the primitive-based fidelity interfaces.""" diff --git a/test/python/algorithms/state_fidelities/test_compute_uncompute.py b/test/python/algorithms/state_fidelities/test_compute_uncompute.py new file mode 100644 index 000000000000..d4a605dbb964 --- /dev/null +++ b/test/python/algorithms/state_fidelities/test_compute_uncompute.py @@ -0,0 +1,217 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for Fidelity.""" + +import unittest + +import numpy as np + +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Sampler +from qiskit.algorithms.state_fidelities import ComputeUncompute +from qiskit.test import QiskitTestCase +from qiskit import QiskitError + + +class TestComputeUncompute(QiskitTestCase): + """Test Compute-Uncompute Fidelity class""" + + def setUp(self): + super().setUp() + parameters = ParameterVector("x", 2) + + rx_rotations = QuantumCircuit(2) + rx_rotations.rx(parameters[0], 0) + rx_rotations.rx(parameters[1], 1) + + ry_rotations = QuantumCircuit(2) + ry_rotations.ry(parameters[0], 0) + ry_rotations.ry(parameters[1], 1) + + plus = QuantumCircuit(2) + plus.h([0, 1]) + + zero = QuantumCircuit(2) + + rx_rotation = QuantumCircuit(2) + rx_rotation.rx(parameters[0], 0) + rx_rotation.h(1) + + self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] + self._sampler = Sampler() + self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) + self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) + + def test_1param_pair(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_4param_pairs(self): + """test for fidelity with four pairs of parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-16) + + def test_symmetry(self): + """test for fidelity with the same circuit""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job_1 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params + ) + job_2 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params + ) + results_1 = job_1.result() + results_2 = job_2.result() + np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-16) + + def test_no_params(self): + """test for fidelity without parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run([self._circuit[2]], [self._circuit[3]]) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + + def test_left_param(self): + """test for fidelity with only left parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_right_param(self): + """test for fidelity with only right parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_not_set_circuits(self): + """test for fidelity with no circuits.""" + fidelity = ComputeUncompute(self._sampler) + with self.assertRaises(TypeError): + job = fidelity.run( + circuits_1=None, + circuits_2=None, + values_1=self._left_params, + values_2=self._right_params, + ) + job.result() + + def test_circuit_mismatch(self): + """test for fidelity with different number of left/right circuits.""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + with self.assertRaises(ValueError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * (n + 1), + self._left_params, + self._right_params, + ) + job.result() + + def test_param_mismatch(self): + """test for fidelity with different number of left/right parameters that + do not match the circuits'.""" + + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + with self.assertRaises(QiskitError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * n, + self._left_params, + self._right_params[:-2], + ) + job.result() + + with self.assertRaises(QiskitError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * n, + self._left_params[:-2], + self._right_params[:-2], + ) + job.result() + + with self.assertRaises(ValueError): + job = fidelity.run([self._circuit[0]] * n, [self._circuit[1]] * n) + job.result() + + def test_asymmetric_params(self): + """test for fidelity when the 2 circuits have different number of + left/right parameters.""" + + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + right_params = [[p] for p in self._right_params[:, 0]] + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[4]] * n, + self._left_params, + right_params, + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-16) + + def test_input_format(self): + """test for different input format variations""" + + fidelity = ComputeUncompute(self._sampler) + circuit = RealAmplitudes(2) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + # lists of circuits, lists of numpy arrays + job = fidelity.run([circuit], [circuit], [values], [values + shift]) + result_1 = job.result() + + # lists of circuits, lists of lists + shift_val = values + shift + job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) + result_2 = job.result() + + # circuits, lists + shift_val = values + shift + job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) + result_3 = job.result() + + # circuits, np.arrays + job = fidelity.run(circuit, circuit, values, values + shift) + result_4 = job.result() + + np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-16) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py index ce7bb6ecb599..0b41e863aed2 100644 --- a/test/python/algorithms/test_amplitude_estimators.py +++ b/test/python/algorithms/test_amplitude_estimators.py @@ -130,7 +130,6 @@ def test_statevector(self, prob, qae, expect): problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) result = qae.estimate(problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -350,7 +349,6 @@ def test_statevector(self, n, qae, expect): # result = qae.run(self._statevector) result = qae.estimate(estimation_problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -409,7 +407,6 @@ def test_confidence_intervals(self, qae, key, expect): # statevector simulator result = qae.estimate(estimation_problem) - self.assertGreater(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() methods = ["lr", "fi", "oi"] # short for likelihood_ratio, fisher, observed_fisher alphas = [0.1, 0.00001, 0.9] # alpha shouldn't matter in statevector @@ -438,7 +435,6 @@ def test_iqae_confidence_intervals(self): # statevector simulator result = qae.estimate(estimation_problem) - self.assertGreaterEqual(self._statevector.time_taken, 0.0) self._statevector.reset_execution_results() confint = result.confidence_interval # confidence interval based on statevector should be empty, as we are sure of the result diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py index aa251841e744..21674d1c953e 100644 --- a/test/python/algorithms/test_backendv1.py +++ b/test/python/algorithms/test_backendv1.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,6 +13,7 @@ """ Test Providers that support BackendV1 interface """ import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit import QuantumCircuit from qiskit.providers.fake_provider import FakeProvider @@ -40,7 +41,14 @@ def test_shor_factoring(self): qasm_simulator = QuantumInstance( self._qasm, shots=1000, seed_simulator=self.seed, seed_transpiler=self.seed ) - shor = Shor(quantum_instance=qasm_simulator) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + shor = Shor(quantum_instance=qasm_simulator) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) + result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts) diff --git a/test/python/algorithms/test_backendv2.py b/test/python/algorithms/test_backendv2.py index 27cf0f7cfb86..4b86bfc35139 100644 --- a/test/python/algorithms/test_backendv2.py +++ b/test/python/algorithms/test_backendv2.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,6 +13,7 @@ """ Test Providers that support BackendV2 interface """ import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit import QuantumCircuit from qiskit.providers.fake_provider import FakeProvider @@ -40,7 +41,14 @@ def test_shor_factoring(self): qasm_simulator = QuantumInstance( self._qasm, shots=1000, seed_simulator=self.seed, seed_transpiler=self.seed ) - shor = Shor(quantum_instance=qasm_simulator) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + shor = Shor(quantum_instance=qasm_simulator) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) + result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts) diff --git a/test/python/algorithms/test_linear_solvers.py b/test/python/algorithms/test_linear_solvers.py index ea16bf5ee0e9..b013ed5c32e2 100644 --- a/test/python/algorithms/test_linear_solvers.py +++ b/test/python/algorithms/test_linear_solvers.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,6 +13,7 @@ """Test the quantum linear system solver algorithm.""" import unittest +import warnings from test.python.algorithms import QiskitAlgorithmsTestCase from scipy.linalg import expm import numpy as np @@ -30,6 +31,22 @@ from qiskit import quantum_info +def _factory_tridiagonal_toeplitz( + num_state_qubits: int, main_diag: float, off_diag: float, trotter_steps: int = 1 +): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return TridiagonalToeplitz( + num_state_qubits, main_diag, off_diag, trotter_steps=trotter_steps + ) + + +def _factory_numpy_matrix(matrix: np.ndarray): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return NumPyMatrix(matrix) + + @ddt class TestMatrices(QiskitAlgorithmsTestCase): """Tests based on the matrices classes. @@ -40,10 +57,10 @@ class TestMatrices(QiskitAlgorithmsTestCase): @idata( [ - [TridiagonalToeplitz(2, 1, -1 / 3)], - [TridiagonalToeplitz(3, 2, 1), 1.1, 3], + [_factory_tridiagonal_toeplitz(2, 1, -1 / 3)], + [_factory_tridiagonal_toeplitz(3, 2, 1), 1.1, 3], [ - NumPyMatrix( + _factory_numpy_matrix( np.array( [ [1 / 2, 1 / 6, 0, 0], @@ -80,8 +97,8 @@ def test_matrices(self, matrix, time=1.0, power=1): @idata( [ - [TridiagonalToeplitz(2, 1.5, 2.5)], - [TridiagonalToeplitz(4, -1, 1.6)], + [_factory_tridiagonal_toeplitz(2, 1.5, 2.5)], + [_factory_tridiagonal_toeplitz(4, -1, 1.6)], ] ) @unpack @@ -101,6 +118,18 @@ def test_eigs_bounds(self, matrix): np.testing.assert_almost_equal(matrix_lambda_max, exact_lambda_max, decimal=6) +def _factory_absolute_average(): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return AbsoluteAverage() + + +def _factory_matrix_functional(main_diag: float, off_diag: int): + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore") + return MatrixFunctional(main_diag, off_diag) + + @ddt class TestObservables(QiskitAlgorithmsTestCase): """Tests based on the observables classes. @@ -111,8 +140,8 @@ class TestObservables(QiskitAlgorithmsTestCase): @idata( [ - [AbsoluteAverage(), [1.0, -2.1, 3.2, -4.3]], - [AbsoluteAverage(), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12]], + [_factory_absolute_average(), [1.0, -2.1, 3.2, -4.3]], + [_factory_absolute_average(), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12]], ] ) @unpack @@ -139,9 +168,9 @@ def test_absolute_average(self, observable, vector): @idata( [ - [MatrixFunctional(1, -1 / 3), [1.0, -2.1, 3.2, -4.3]], + [_factory_matrix_functional(1, -1 / 3), [1.0, -2.1, 3.2, -4.3]], [ - MatrixFunctional(2 / 3, 11 / 7), + _factory_matrix_functional(2 / 3, 11 / 7), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12], ], ] @@ -237,16 +266,16 @@ class TestLinearSolver(QiskitAlgorithmsTestCase): @idata( [ [ - TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2), + _factory_tridiagonal_toeplitz(2, 1, 1 / 3, trotter_steps=2), [1.0, -2.1, 3.2, -4.3], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ np.array( [[0, 0, 1.585, 0], [0, 0, -0.585, 1], [1.585, -0.585, 0, 0], [0, 1, 0, 0]] ), [1.0, 0, 0, 0], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ [ @@ -256,18 +285,18 @@ class TestLinearSolver(QiskitAlgorithmsTestCase): [0, 0, 1 / 6, 1 / 2], ], [1.0, -2.1, 3.2, -4.3], - MatrixFunctional(1, 1 / 2), + _factory_matrix_functional(1, 1 / 2), ], [ np.array([[82, 34], [34, 58]]), np.array([[1], [0]]), - AbsoluteAverage(), + _factory_absolute_average(), 3, ], [ - TridiagonalToeplitz(3, 1, -1 / 2, trotter_steps=2), + _factory_tridiagonal_toeplitz(3, 1, -1 / 2, trotter_steps=2), [-9 / 4, -0.3, 8 / 7, 10, -5, 11.1, 13 / 11, -27 / 12], - AbsoluteAverage(), + _factory_absolute_average(), ], ] ) @@ -287,8 +316,11 @@ def test_hhl(self, matrix, right_hand_side, observable, decimal=1): qc = QuantumCircuit(num_qubits) qc.isometry(rhs, list(range(num_qubits)), None) - hhl = HHL() - solution = hhl.solve(matrix, qc, observable) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + hhl = HHL() + self.assertTrue("HHL class is deprecated" in str(caught_warnings[0].message)) + solution = hhl.solve(matrix, qc, observable) approx_result = solution.observable # Calculate analytical value @@ -304,7 +336,10 @@ def test_hhl(self, matrix, right_hand_side, observable, decimal=1): def test_hhl_qi(self): """Test the HHL quantum instance getter and setter.""" - hhl = HHL() + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + hhl = HHL() + self.assertTrue("HHL class is deprecated" in str(caught_warnings[0].message)) self.assertIsNone(hhl.quantum_instance) # Defaults to None # First set a valid quantum instance and check via getter diff --git a/test/python/algorithms/test_shor.py b/test/python/algorithms/test_shor.py index 0ecc5886fe54..811680b1799a 100644 --- a/test/python/algorithms/test_shor.py +++ b/test/python/algorithms/test_shor.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,6 +13,7 @@ """ Test Shor """ import unittest +import warnings import math from test.python.algorithms import QiskitAlgorithmsTestCase from ddt import ddt, data, idata, unpack @@ -30,12 +31,18 @@ class TestShor(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - backend = Aer.get_backend("qasm_simulator") - self.instance = Shor(quantum_instance=QuantumInstance(backend, shots=1000)) + backend = Aer.get_backend("aer_simulator") + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.filterwarnings( + "always", + category=DeprecationWarning, + ) + self.instance = Shor(quantum_instance=QuantumInstance(backend, shots=1000)) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) @idata( [ - [15, "qasm_simulator", [3, 5]], + [15, "aer_simulator", [3, 5]], ] ) @unpack @@ -46,7 +53,7 @@ def test_shor_factoring(self, n_v, backend, factors): @slow_test @idata( [ - [21, "qasm_simulator", [3, 7]], + [21, "aer_simulator", [3, 7]], ] ) @unpack @@ -56,7 +63,10 @@ def test_shor_factoring_5_bit_number(self, n_v, backend, factors): def _test_shor_factoring(self, backend, factors, n_v): """shor factoring test""" - shor = Shor(quantum_instance=QuantumInstance(Aer.get_backend(backend), shots=1000)) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + shor = Shor(quantum_instance=QuantumInstance(Aer.get_backend(backend), shots=1000)) + self.assertTrue("Shor class is deprecated" in str(caught_warnings[0].message)) result = shor.factor(N=n_v) self.assertListEqual(result.factors[0], factors) self.assertTrue(result.total_counts >= result.successful_counts) diff --git a/test/python/algorithms/test_vqd.py b/test/python/algorithms/test_vqd.py index 665d73d687dd..286f9422daad 100644 --- a/test/python/algorithms/test_vqd.py +++ b/test/python/algorithms/test_vqd.py @@ -257,13 +257,14 @@ def test_with_aer_qasm_snapshot_mode(self): def test_callback(self): """Test the callback on VQD.""" - history = {"eval_count": [], "parameters": [], "mean": [], "std": []} + history = {"eval_count": [], "parameters": [], "mean": [], "std": [], "step": []} - def store_intermediate_result(eval_count, parameters, mean, std): + def store_intermediate_result(eval_count, parameters, mean, std, step): history["eval_count"].append(eval_count) history["parameters"].append(parameters) history["mean"].append(mean) history["std"].append(std) + history["step"].append(step) optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction @@ -279,9 +280,20 @@ def store_intermediate_result(eval_count, parameters, mean, std): self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) self.assertTrue(all(isinstance(std, float) for std in history["std"])) + self.assertTrue(all(isinstance(count, int) for count in history["step"])) for params in history["parameters"]: self.assertTrue(all(isinstance(param, float) for param in params)) + ref_eval_count = [1, 2, 3, 1, 2, 3] + ref_mean = [-1.063, -1.457, -1.360, 37.340, 48.543, 28.586] + ref_std = [0.011, 0.010, 0.014, 0.011, 0.010, 0.015] + ref_step = [1, 1, 1, 2, 2, 2] + + np.testing.assert_array_almost_equal(history["eval_count"], ref_eval_count, decimal=0) + np.testing.assert_array_almost_equal(history["mean"], ref_mean, decimal=2) + np.testing.assert_array_almost_equal(history["std"], ref_std, decimal=2) + np.testing.assert_array_almost_equal(history["step"], ref_step, decimal=0) + def test_reuse(self): """Test re-using a VQD algorithm instance.""" vqd = VQD(k=1) diff --git a/test/python/circuit/library/test_state_preparation.py b/test/python/circuit/library/test_state_preparation.py index 430d462fa745..f34141ce596c 100644 --- a/test/python/circuit/library/test_state_preparation.py +++ b/test/python/circuit/library/test_state_preparation.py @@ -19,7 +19,7 @@ import numpy as np from ddt import ddt, data -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Statevector, Operator from qiskit.test import QiskitTestCase from qiskit.exceptions import QiskitError @@ -54,6 +54,15 @@ def test_prepare_from_list(self): actual_sv = Statevector(qc) self.assertTrue(desired_sv == actual_sv) + def test_prepare_single_qubit(self): + """Prepare state in single qubit.""" + qreg = QuantumRegister(2) + circuit = QuantumCircuit(qreg) + circuit.prepare_state([1 / math.sqrt(2), 1 / math.sqrt(2)], qreg[1]) + expected = QuantumCircuit(qreg) + expected.prepare_state([1 / math.sqrt(2), 1 / math.sqrt(2)], [qreg[1]]) + self.assertEqual(circuit, expected) + def test_nonzero_state_incorrect(self): """Test final state incorrect if initial state not zero""" desired_sv = Statevector([1 / math.sqrt(2), 0, 0, 1 / math.sqrt(2)]) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 8292cee07780..9cc5f277580c 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -998,6 +998,17 @@ def test_controlled_gate(self): new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + def test_controlled_gate_open_controls(self): + """Test a controlled gate with open controls round-trips exactly.""" + qc = QuantumCircuit(3) + controlled_gate = DCXGate().control(1, ctrl_state=0) + qc.append(controlled_gate, [0, 1, 2]) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circuit = load(qpy_file)[0] + self.assertEqual(qc, new_circuit) + def test_nested_controlled_gate(self): """Test a custom nested controlled gate.""" custom_gate = Gate("black_box", 1, []) diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 9db5de33ff0f..c44252a64f3e 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -311,15 +311,13 @@ def test_conditional_gates(self): qr = QuantumRegister(3) cr = ClassicalRegister(2) - # Different quantum bits (and empty classical bits). - # We should be able to swap these. + # Currently, in all cases commutativity checker should returns False. + # This is definitely suboptimal. res = comm_checker.commute( CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[2]], [] ) - self.assertTrue(res) + self.assertFalse(res) - # In all other cases, commutativity checker currently returns False. - # This is definitely suboptimal. res = comm_checker.commute( CXGate().c_if(cr[0], 0), [qr[0], qr[1]], [], XGate(), [qr[1]], [] ) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 369237684aac..0452ef8a701e 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -2062,6 +2062,64 @@ def test_copy_of_instruction_parameters(self): self.assertEqual(while_body, copy.copy(while_body)) self.assertEqual(while_body, copy.deepcopy(while_body)) + def test_inplace_compose_within_builder(self): + """Test that QuantumCircuit.compose used in-place works as expected within control-flow + scopes.""" + inner = QuantumCircuit(1) + inner.x(0) + + base = QuantumCircuit(1, 1) + base.h(0) + base.measure(0, 0) + + with self.subTest("if"): + outer = base.copy() + with outer.if_test((outer.clbits[0], 1)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.if_test((expected.clbits[0], 1)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("else"): + outer = base.copy() + with outer.if_test((outer.clbits[0], 1)) as else_: + outer.compose(inner, inplace=True) + with else_: + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.if_test((expected.clbits[0], 1)) as else_: + expected.x(0) + with else_: + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("for"): + outer = base.copy() + with outer.for_loop(range(3)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.for_loop(range(3)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + + with self.subTest("while"): + outer = base.copy() + with outer.while_loop((outer.clbits[0], 0)): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.while_loop((outer.clbits[0], 0)): + expected.x(0) + + self.assertCircuitsEquivalent(outer, expected) + @ddt.ddt class TestControlFlowBuildersFailurePaths(QiskitTestCase): @@ -2458,3 +2516,28 @@ def dummy_requester(resource): ) with self.assertRaisesRegex(TypeError, r"Can only add qubits or classical bits.*"): builder_block.add_bits([bit]) + + def test_compose_front_inplace_invalid_within_builder(self): + """Test that `QuantumCircuit.compose` raises a sensible error when called within a + control-flow builder block.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.measure(0, 0) + outer.compose(inner, front=True, inplace=True) + with outer.if_test((outer.clbits[0], 1)): + with self.assertRaisesRegex(CircuitError, r"Cannot compose to the front.*"): + outer.compose(inner, front=True, inplace=True) + + def test_compose_new_invalid_within_builder(self): + """Test that `QuantumCircuit.compose` raises a sensible error when called within a + control-flow builder block if trying to emit a new circuit.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.measure(0, 0) + with outer.if_test((outer.clbits[0], 1)): + with self.assertRaisesRegex(CircuitError, r"Cannot emit a new composed circuit.*"): + outer.compose(inner, inplace=False) diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py index 69f23a585ecf..f83a9f150866 100644 --- a/test/python/circuit/test_equivalence.py +++ b/test/python/circuit/test_equivalence.py @@ -13,6 +13,7 @@ """Test Qiskit's EquivalenceLibrary class.""" +import unittest import numpy as np from qiskit.test import QiskitTestCase @@ -21,8 +22,10 @@ from qiskit.circuit.library import U2Gate from qiskit.circuit.exceptions import CircuitError from qiskit.converters import circuit_to_instruction, circuit_to_gate - from qiskit.circuit import EquivalenceLibrary +from qiskit.utils import optionals + +from ..visualization.visualization import QiskitVisualizationTestCase, path_to_diagram_reference class OneQubitZeroParamGate(Gate): @@ -461,3 +464,26 @@ def test_gate_decomposition_properties(self): self.assertEqual(len(decomps), 1) self.assertEqual(decomps[0], qc2) + + +class TestEquivalenceLibraryVisualization(QiskitVisualizationTestCase): + """Test cases for EquivalenceLibrary visualization.""" + + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + def test_equivalence_draw(self): + """Verify EquivalenceLibrary drawing with reference image.""" + sel = EquivalenceLibrary() + gate = OneQubitZeroParamGate() + first_equiv = QuantumCircuit(1) + first_equiv.h(0) + + sel.add_equivalence(gate, first_equiv) + + second_equiv = QuantumCircuit(1) + second_equiv.append(U2Gate(0, np.pi), [0]) + + sel.add_equivalence(gate, second_equiv) + + image = sel.draw() + image_ref = path_to_diagram_reference("equivalence_library.png") + self.assertImagesAreEqual(image, image_ref, 0.04) diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index c411836da5fb..bffd9c69bac3 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -24,7 +24,6 @@ from qiskit import execute from qiskit.test import QiskitTestCase from qiskit.compiler import transpile -from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.quantum_info import Operator from qiskit.extensions.quantum_initializer.isometry import Isometry @@ -69,7 +68,7 @@ def test_isometry(self, iso): unitary = result.get_unitary(qc) iso_from_circuit = unitary[::, 0 : 2**num_q_input] iso_desired = iso - self.assertTrue(matrix_equal(iso_from_circuit, iso_desired, ignore_phase=True)) + self.assertTrue(np.allclose(iso_from_circuit, iso_desired)) @data( np.eye(2, 2), @@ -108,7 +107,7 @@ def test_isometry_tolerance(self, iso): result = execute(qc, simulator).result() unitary = result.get_unitary(qc) iso_from_circuit = unitary[::, 0 : 2**num_q_input] - self.assertTrue(matrix_equal(iso_from_circuit, iso, ignore_phase=True)) + self.assertTrue(np.allclose(iso_from_circuit, iso)) @data( np.eye(2, 2), diff --git a/test/python/circuit/test_registerless_circuit.py b/test/python/circuit/test_registerless_circuit.py index 6a4d0a401744..865b80e78034 100644 --- a/test/python/circuit/test_registerless_circuit.py +++ b/test/python/circuit/test_registerless_circuit.py @@ -230,6 +230,18 @@ def test_circuit_initialize(self): self.assertEqual(circuit, expected) + def test_circuit_initialize_single_qubit(self): + """Test initialize on single qubit.""" + init_vector = [numpy.sqrt(0.5), numpy.sqrt(0.5)] + qreg = QuantumRegister(2) + circuit = QuantumCircuit(qreg) + circuit.initialize(init_vector, qreg[0]) + + expected = QuantumCircuit(qreg) + expected.initialize(init_vector, [qreg[0]]) + + self.assertEqual(circuit, expected) + def test_mixed_register_and_registerless_indexing(self): """Test indexing if circuit contains bits in and out of registers.""" diff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py index cedff9f4ac41..1f50b650623c 100644 --- a/test/python/circuit/test_uc.py +++ b/test/python/circuit/test_uc.py @@ -30,6 +30,7 @@ from qiskit.quantum_info.random import random_unitary from qiskit.compiler import transpile from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.quantum_info import Operator _id = np.eye(2, 2) _not = np.matrix([[0, 1], [1, 0]]) @@ -71,7 +72,7 @@ def test_ucg(self, squs, up_to_diagonal): self.assertTrue(matrix_equal(unitary_desired, unitary, ignore_phase=True)) def test_global_phase_ucg(self): - """ "Test global phase of uniformly controlled gates""" + """Test global phase of uniformly controlled gates""" gates = [random_unitary(2).data for _ in range(2**2)] num_con = int(np.log2(len(gates))) q = QuantumRegister(num_con + 1) @@ -85,6 +86,21 @@ def test_global_phase_ucg(self): self.assertTrue(np.allclose(unitary_desired, unitary)) + def test_inverse_ucg(self): + """Test inverse function of uniformly controlled gates""" + gates = [random_unitary(2, seed=42 + s).data for s in range(2**2)] + num_con = int(np.log2(len(gates))) + q = QuantumRegister(num_con + 1) + qc = QuantumCircuit(q) + + qc.uc(gates, q[1:], q[0], up_to_diagonal=False) + qc.append(qc.inverse(), qc.qubits) + + unitary = Operator(qc).data + unitary_desired = np.identity(2**qc.num_qubits) + + self.assertTrue(np.allclose(unitary_desired, unitary)) + def _get_ucg_matrix(squs): return block_diag(*squs) diff --git a/test/python/opflow/test_z2_symmetries.py b/test/python/opflow/test_z2_symmetries.py index c7335f6425bd..911cc71db080 100644 --- a/test/python/opflow/test_z2_symmetries.py +++ b/test/python/opflow/test_z2_symmetries.py @@ -92,3 +92,21 @@ def test_truncate_tapered_op(self): ) expected_op = TaperedPauliSumOp(primitive, z2_symmetries) self.assertEqual(tapered_op, expected_op) + + def test_twostep_tapering(self): + """Test the two-step tapering""" + qubit_op = PauliSumOp.from_list( + [ + ("II", -1.0537076071291125), + ("IZ", 0.393983679438514), + ("ZI", -0.39398367943851387), + ("ZZ", -0.01123658523318205), + ("XX", 0.1812888082114961), + ] + ) + z2_symmetries = Z2Symmetries.find_Z2_symmetries(qubit_op) + tapered_op = z2_symmetries.taper(qubit_op) + + tapered_op_firststep = z2_symmetries.convert_clifford(qubit_op) + tapered_op_secondstep = z2_symmetries.taper_clifford(tapered_op_firststep) + self.assertEqual(tapered_op, tapered_op_secondstep) diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 1be5c96950b3..b3a45d7e1f53 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -31,7 +31,9 @@ FakeBackendSimple, FakeBackendV2LegacyQubitProps, ) +from qiskit.providers.fake_provider.backends import FakeBogotaV2 from qiskit.quantum_info import Operator +from qiskit.pulse import channels @ddt @@ -86,17 +88,7 @@ def test_transpile(self, opt_level): qc = QuantumCircuit(2) qc.h(1) qc.cz(1, 0) - with self.assertLogs("qiskit.providers.backend", level="WARN") as log: - tqc = transpile(qc, self.backend, optimization_level=opt_level) - self.assertEqual( - log.output, - [ - "WARNING:qiskit.providers.backend:This backend's operations: " - "cx,ecr only apply to a subset of qubits. Using this property to " - "get 'basis_gates' for the transpiler may potentially create " - "invalid output" - ], - ) + tqc = transpile(qc, self.backend, optimization_level=opt_level) self.assertTrue(Operator.from_circuit(tqc).equiv(qc)) self.assertMatchesTargetConstraints(tqc, self.backend.target) @@ -190,3 +182,45 @@ def test_transpile_parse_inst_map(self): """Test that transpiler._parse_inst_map() supports BackendV2.""" inst_map = _parse_inst_map(inst_map=None, backend=self.backend) self.assertIsInstance(inst_map, InstructionScheduleMap) + + @data(0, 1, 2, 3, 4) + def test_drive_channel(self, qubit): + """Test getting drive channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.drive_channel(qubit) + ref = channels.DriveChannel(qubit) + self.assertEqual(chan, ref) + + @data(0, 1, 2, 3, 4) + def test_measure_channel(self, qubit): + """Test getting measure channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.measure_channel(qubit) + ref = channels.MeasureChannel(qubit) + self.assertEqual(chan, ref) + + @data(0, 1, 2, 3, 4) + def test_acquire_channel(self, qubit): + """Test getting acquire channel with qubit index.""" + backend = FakeBogotaV2() + chan = backend.acquire_channel(qubit) + ref = channels.AcquireChannel(qubit) + self.assertEqual(chan, ref) + + @data((4, 3), (3, 4), (3, 2), (2, 3), (1, 2), (2, 1), (1, 0), (0, 1)) + def test_control_channel(self, qubits): + """Test getting acquire channel with qubit index.""" + bogota_cr_channels_map = { + (4, 3): 7, + (3, 4): 6, + (3, 2): 5, + (2, 3): 4, + (1, 2): 2, + (2, 1): 3, + (1, 0): 1, + (0, 1): 0, + } + backend = FakeBogotaV2() + chan = backend.control_channel(qubits)[0] + ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) + self.assertEqual(chan, ref) diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py index 86e166ffded9..2d513951a2de 100644 --- a/test/python/transpiler/test_commutative_cancellation.py +++ b/test/python/transpiler/test_commutative_cancellation.py @@ -621,6 +621,17 @@ def test_basis_global_phase_03(self): ccirc = passmanager.run(circ) self.assertEqual(Operator(circ), Operator(ccirc)) + 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) + # 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) + self.assertEqual(original, transpiled) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index bf210178abae..c81916fa2618 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -12,10 +12,15 @@ # pylint: disable=missing-docstring +import unittest + from qiskit.transpiler import CouplingMap from qiskit.transpiler.exceptions import CouplingError from qiskit.providers.fake_provider import FakeRueschlikon from qiskit.test import QiskitTestCase +from qiskit.utils import optionals + +from ..visualization.visualization import QiskitVisualizationTestCase, path_to_diagram_reference class CouplingTest(QiskitTestCase): @@ -436,3 +441,13 @@ def test_subgraph(self): edge_list = subgraph.get_edges() expected = [(0, 1), (1, 2), (2, 3)] self.assertEqual(expected, edge_list, f"{edge_list} does not match {expected}") + + +class CouplingVisualizationTest(QiskitVisualizationTestCase): + @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + def test_coupling_draw(self): + """Test that the coupling map drawing with respect to the reference file is correct.""" + cmap = CouplingMap([[0, 1], [1, 2], [2, 3], [2, 4], [2, 5], [2, 6]]) + image_ref = path_to_diagram_reference("coupling_map.png") + image = cmap.draw() + self.assertImagesAreEqual(image, image_ref, diff_tolerance=0.01) diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 1a0aaf4a4b68..2664a00c7fbb 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -300,3 +300,18 @@ def test_decompose_reps(self): decom_circ = self.complex_circuit.decompose(reps=2) decomposed = self.complex_circuit.decompose().decompose() self.assertEqual(decom_circ, decomposed) + + def test_decompose_single_qubit_clbit(self): + """Test the decomposition of a block with a single qubit and clbit works. + + Regression test of Qiskit/qiskit-terra#8591. + """ + block = QuantumCircuit(1, 1) + block.h(0) + + circuit = QuantumCircuit(1, 1) + circuit.append(block, [0], [0]) + + decomposed = circuit.decompose() + + self.assertEqual(decomposed, block) diff --git a/test/python/transpiler/test_mappers.py b/test/python/transpiler/test_mappers.py index ef061f7c7d28..6a3d0d1748e3 100644 --- a/test/python/transpiler/test_mappers.py +++ b/test/python/transpiler/test_mappers.py @@ -294,7 +294,7 @@ class TestsSabreSwap(SwapperCommonTestCases, QiskitTestCase): """Test SwapperCommonTestCases using SabreSwap.""" pass_class = SabreSwap - additional_args = {"seed": 4242} + additional_args = {"seed": 1242} if __name__ == "__main__": diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 2ce8ef1d0343..e954932e2f1c 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -23,9 +23,10 @@ from qiskit.circuit import Qubit from qiskit.compiler import transpile, assemble from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError -from qiskit.circuit.library import U2Gate, U3Gate +from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( + FakeBelem, FakeTenerife, FakeMelbourne, FakeJohannesburg, @@ -41,6 +42,7 @@ from qiskit.quantum_info import random_unitary from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.utils.optionals import HAS_TOQM +from qiskit.transpiler.passes import Collect2qBlocks, GatesInBasis def emptycircuit(): @@ -207,6 +209,31 @@ def test_alignment_constraints_called_with_delay_in_circuit(self, level): transpile(circuit, backend=FakeJohannesburg(), optimization_level=level) mock.assert_called_once() + def test_unroll_only_if_not_gates_in_basis(self): + """Test that the list of passes _unroll only runs if a gate is not in the basis.""" + qcomp = FakeBelem() + qv_circuit = QuantumVolume(3) + gates_in_basis_true_count = 0 + collect_2q_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 + 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 + + transpile( + qv_circuit, + backend=qcomp, + optimization_level=3, + callback=counting_callback_func, + translation_method="synthesis", + ) + self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count) + @ddt @unittest.skipUnless(HAS_TOQM, "qiskit-toqm needs to be installed") @@ -698,25 +725,25 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { - 6: qr[0], - 11: qr[1], - 10: qr[2], - 5: qr[3], - 16: qr[4], + 11: qr[0], + 17: qr[1], + 16: qr[2], + 6: qr[3], + 18: qr[4], 0: ancilla[0], 1: ancilla[1], 2: ancilla[2], 3: ancilla[3], 4: ancilla[4], - 7: ancilla[5], - 8: ancilla[6], - 9: ancilla[7], - 12: ancilla[8], - 13: ancilla[9], - 14: ancilla[10], - 15: ancilla[11], - 17: ancilla[12], - 18: ancilla[13], + 5: ancilla[5], + 7: ancilla[6], + 8: ancilla[7], + 9: ancilla[8], + 10: ancilla[9], + 12: ancilla[10], + 13: ancilla[11], + 14: ancilla[12], + 15: ancilla[13], 19: ancilla[14], } @@ -913,7 +940,7 @@ def test_2(self, level): optimization_level=level, basis_gates=basis, coupling_map=coupling_map, - seed_transpiler=42, + seed_transpiler=42123, ) self.assertIsInstance(result, QuantumCircuit) resulting_basis = {node.name for node in circuit_to_dag(result).op_nodes()} diff --git a/test/python/transpiler/test_reset_after_measure_simplification.py b/test/python/transpiler/test_reset_after_measure_simplification.py new file mode 100644 index 000000000000..86b810299011 --- /dev/null +++ b/test/python/transpiler/test_reset_after_measure_simplification.py @@ -0,0 +1,140 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test the ResetAfterMeasureSimplification pass""" + +from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister +from qiskit.circuit.classicalregister import Clbit +from qiskit.transpiler.passes.optimization import ResetAfterMeasureSimplification +from qiskit.test import QiskitTestCase + + +class TestResetAfterMeasureSimplificationt(QiskitTestCase): + """Test ResetAfterMeasureSimplification transpiler pass.""" + + def test_simple(self): + """Test simple""" + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.reset(0) + + 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) + self.assertEqual(new_qc, ans_qc) + + def test_simple_null(self): + """Test simple no change in circuit""" + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0) + qc.reset(0) + new_qc = ResetAfterMeasureSimplification()(qc) + + self.assertEqual(new_qc, qc) + + def test_simple_multi_reg(self): + """Test simple, multiple registers""" + cr1 = ClassicalRegister(1, "c1") + cr2 = ClassicalRegister(1, "c2") + qr = QuantumRegister(1, "q") + qc = QuantumCircuit(qr, cr1, cr2) + qc.measure(0, 1) + qc.reset(0) + + new_qc = ResetAfterMeasureSimplification()(qc) + + ans_qc = QuantumCircuit(qr, cr1, cr2) + ans_qc.measure(0, 1) + ans_qc.x(0).c_if(cr2[0], 1) + + self.assertEqual(new_qc, ans_qc) + + def test_simple_multi_reg_null(self): + """Test simple, multiple registers, null change""" + cr1 = ClassicalRegister(1, "c1") + cr2 = ClassicalRegister(1, "c2") + qr = QuantumRegister(2, "q") + 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) + + def test_simple_multi_resets(self): + """Only first reset is collapsed""" + qc = QuantumCircuit(1, 2) + qc.measure(0, 0) + qc.reset(0) + qc.reset(0) + + 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) + ans_qc.reset(0) + self.assertEqual(new_qc, ans_qc) + + def test_simple_multi_resets_with_resets_before_measure(self): + """Reset BEFORE measurement not collapsed""" + qc = QuantumCircuit(2, 2) + qc.measure(0, 0) + qc.reset(0) + qc.reset(1) + qc.measure(1, 1) + + 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) + ans_qc.reset(1) + ans_qc.measure(1, 1) + + self.assertEqual(new_qc, ans_qc) + + def test_barriers_work(self): + """Test that barriers block consolidation""" + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.barrier(0) + qc.reset(0) + + new_qc = ResetAfterMeasureSimplification()(qc) + self.assertEqual(new_qc, qc) + + def test_bv_circuit(self): + """Test Bernstein Vazirani circuit with midcircuit measurement.""" + bitstring = "11111" + qc = QuantumCircuit(2, len(bitstring)) + qc.x(1) + qc.h(1) + for idx, bit in enumerate(bitstring[::-1]): + qc.h(0) + if int(bit): + qc.cx(0, 1) + qc.h(0) + qc.measure(0, idx) + if idx != len(bitstring) - 1: + qc.reset(0) + # reset control + qc.reset(1) + qc.x(1) + qc.h(1) + new_qc = ResetAfterMeasureSimplification()(qc) + for op in new_qc.data: + if op.operation.name == "reset": + self.assertEqual(op.qubits[0], new_qc.qubits[1]) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 85a975dd6a48..77aa5af050b3 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -19,7 +19,10 @@ from qiskit.transpiler.passes import SabreLayout from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase +from qiskit.compiler.transpiler import transpile from qiskit.providers.fake_provider import FakeAlmaden +from qiskit.providers.fake_provider import FakeKolkata +from qiskit.providers.fake_provider import FakeMontreal class TestSabreLayout(QiskitTestCase): @@ -52,14 +55,14 @@ def test_5q_circuit_20q_coupling(self): circuit.cx(qr[1], qr[2]) dag = circuit_to_dag(circuit) - pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0) + pass_ = SabreLayout(CouplingMap(self.cmap20), seed=0, swap_trials=32) pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr[0]], 11) - self.assertEqual(layout[qr[1]], 6) - self.assertEqual(layout[qr[2]], 12) - self.assertEqual(layout[qr[3]], 5) + self.assertEqual(layout[qr[0]], 10) + self.assertEqual(layout[qr[1]], 12) + self.assertEqual(layout[qr[2]], 7) + self.assertEqual(layout[qr[3]], 11) self.assertEqual(layout[qr[4]], 13) def test_6q_circuit_20q_coupling(self): @@ -92,12 +95,118 @@ def test_6q_circuit_20q_coupling(self): pass_.run(dag) layout = pass_.property_set["layout"] - self.assertEqual(layout[qr0[0]], 8) - self.assertEqual(layout[qr0[1]], 2) + self.assertEqual(layout[qr0[0]], 2) + self.assertEqual(layout[qr0[1]], 3) self.assertEqual(layout[qr0[2]], 10) - self.assertEqual(layout[qr1[0]], 3) - self.assertEqual(layout[qr1[1]], 12) - self.assertEqual(layout[qr1[2]], 11) + self.assertEqual(layout[qr1[0]], 1) + self.assertEqual(layout[qr1[1]], 7) + self.assertEqual(layout[qr1[2]], 5) + + def test_layout_with_classical_bits(self): + """Test sabre layout with classical bits recreate from issue #8635.""" + qc = QuantumCircuit.from_qasm_str( + """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q4833[1]; +qreg q4834[6]; +qreg q4835[7]; +creg c982[2]; +creg c983[2]; +creg c984[2]; +rzz(0) q4833[0],q4834[4]; +cu(0,-6.1035156e-05,0,1e-05) q4834[1],q4835[2]; +swap q4834[0],q4834[2]; +cu(-1.1920929e-07,0,-0.33333333,0) q4833[0],q4834[2]; +ccx q4835[2],q4834[5],q4835[4]; +measure q4835[4] -> c984[0]; +ccx q4835[2],q4835[5],q4833[0]; +measure q4835[5] -> c984[1]; +measure q4834[0] -> c982[1]; +u(10*pi,0,1.9) q4834[5]; +measure q4834[3] -> c984[1]; +measure q4835[0] -> c982[0]; +rz(0) q4835[1]; +""" + ) + res = transpile(qc, FakeKolkata(), layout_method="sabre", seed_transpiler=1234) + self.assertIsInstance(res, QuantumCircuit) + layout = res._layout + self.assertEqual(layout[qc.qubits[0]], 14) + self.assertEqual(layout[qc.qubits[1]], 19) + self.assertEqual(layout[qc.qubits[2]], 7) + self.assertEqual(layout[qc.qubits[3]], 13) + self.assertEqual(layout[qc.qubits[4]], 6) + self.assertEqual(layout[qc.qubits[5]], 16) + self.assertEqual(layout[qc.qubits[6]], 18) + self.assertEqual(layout[qc.qubits[7]], 26) + + # pylint: disable=line-too-long + def test_layout_many_search_trials(self): + """Test recreate failure from randomized testing that overflowed.""" + qc = QuantumCircuit.from_qasm_str( + """ + OPENQASM 2.0; +include "qelib1.inc"; +qreg q18585[14]; +creg c1423[5]; +creg c1424[4]; +creg c1425[3]; +barrier q18585[4],q18585[5],q18585[12],q18585[1]; +cz q18585[11],q18585[3]; +cswap q18585[8],q18585[10],q18585[6]; +u(-2.00001,6.1035156e-05,-1.9) q18585[2]; +barrier q18585[3],q18585[6],q18585[5],q18585[8],q18585[10],q18585[9],q18585[11],q18585[2],q18585[12],q18585[7],q18585[13],q18585[4],q18585[0],q18585[1]; +cp(0) q18585[2],q18585[4]; +cu(-0.99999,0,0,0) q18585[7],q18585[1]; +cu(0,0,0,2.1507119) q18585[6],q18585[3]; +barrier q18585[13],q18585[0],q18585[12],q18585[3],q18585[2],q18585[10]; +ry(-1.1044662) q18585[13]; +barrier q18585[13]; +id q18585[12]; +barrier q18585[12],q18585[6]; +cu(-1.9,1.9,-1.5,0) q18585[10],q18585[0]; +barrier q18585[13]; +id q18585[8]; +barrier q18585[12]; +barrier q18585[12],q18585[1],q18585[9]; +sdg q18585[2]; +rz(-10*pi) q18585[6]; +u(0,27.566433,1.9) q18585[1]; +barrier q18585[12],q18585[11],q18585[9],q18585[4],q18585[7],q18585[0],q18585[13],q18585[3]; +cu(-0.99999,-5.9604645e-08,-0.5,2.00001) q18585[3],q18585[13]; +rx(-5.9604645e-08) q18585[7]; +p(1.1) q18585[13]; +barrier q18585[12],q18585[13],q18585[10],q18585[9],q18585[7],q18585[4]; +z q18585[10]; +measure q18585[7] -> c1423[2]; +barrier q18585[0],q18585[3],q18585[7],q18585[4],q18585[1],q18585[8],q18585[6],q18585[11],q18585[5]; +barrier q18585[5],q18585[2],q18585[8],q18585[3],q18585[6]; +""" + ) + res = transpile( + qc, + FakeMontreal(), + layout_method="sabre", + routing_method="stochastic", + seed_transpiler=12345, + ) + self.assertIsInstance(res, QuantumCircuit) + layout = res._layout + self.assertEqual(layout[qc.qubits[0]], 19) + self.assertEqual(layout[qc.qubits[1]], 22) + self.assertEqual(layout[qc.qubits[2]], 17) + self.assertEqual(layout[qc.qubits[3]], 14) + self.assertEqual(layout[qc.qubits[4]], 18) + self.assertEqual(layout[qc.qubits[5]], 9) + self.assertEqual(layout[qc.qubits[6]], 11) + self.assertEqual(layout[qc.qubits[7]], 25) + self.assertEqual(layout[qc.qubits[8]], 16) + self.assertEqual(layout[qc.qubits[9]], 3) + self.assertEqual(layout[qc.qubits[10]], 12) + self.assertEqual(layout[qc.qubits[11]], 13) + self.assertEqual(layout[qc.qubits[12]], 20) + self.assertEqual(layout[qc.qubits[13]], 8) if __name__ == "__main__": diff --git a/test/python/transpiler/test_stage_plugin.py b/test/python/transpiler/test_stage_plugin.py new file mode 100644 index 000000000000..1242905e9f4e --- /dev/null +++ b/test/python/transpiler/test_stage_plugin.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Tests for the staged transpiler plugins. +""" + +from test import combine + +import ddt + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.compiler.transpiler import transpile +from qiskit.test import QiskitTestCase +from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap +from qiskit.transpiler.preset_passmanagers.plugin import ( + PassManagerStagePluginManager, + list_stage_plugins, +) +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers.basicaer import QasmSimulatorPy + + +class TestStagePassManagerPlugin(QiskitTestCase): + """Tests for the transpiler stage plugin interface.""" + + def test_list_stage_plugins(self): + """Test list stage plugin function.""" + routing_passes = list_stage_plugins("routing") + self.assertIn("basic", routing_passes) + self.assertIn("sabre", routing_passes) + self.assertIn("lookahead", routing_passes) + self.assertIn("stochastic", routing_passes) + self.assertIsInstance(list_stage_plugins("init"), list) + self.assertIsInstance(list_stage_plugins("layout"), list) + self.assertIsInstance(list_stage_plugins("translation"), list) + self.assertIsInstance(list_stage_plugins("optimization"), list) + self.assertIsInstance(list_stage_plugins("scheduling"), list) + + def test_list_stage_plugins_invalid_stage_name(self): + """Test list stage plugin function with invalid stage name.""" + with self.assertRaises(TranspilerError): + list_stage_plugins("not_a_stage") + + def test_build_pm_invalid_plugin_name_valid_stage(self): + """Test get pm from plugin with invalid plugin name and valid stage.""" + plugin_manager = PassManagerStagePluginManager() + with self.assertRaises(TranspilerError): + plugin_manager.get_passmanager_stage("init", "empty_plugin", PassManagerConfig()) + + def test_build_pm_invalid_stage(self): + """Test get pm from plugin with invalid stage.""" + plugin_manager = PassManagerStagePluginManager() + with self.assertRaises(TranspilerError): + plugin_manager.get_passmanager_stage( + "not_a_sage", "fake_plugin_not_real", PassManagerConfig() + ) + + def test_build_pm(self): + """Test get pm from plugin.""" + plugin_manager = PassManagerStagePluginManager() + pm_config = PassManagerConfig() + pm = plugin_manager.get_passmanager_stage( + "routing", "sabre", pm_config, optimization_level=3 + ) + self.assertIsInstance(pm, PassManager) + + +@ddt.ddt +class TestBuiltinPlugins(QiskitTestCase): + """Test that all built-in plugins work in transpile().""" + + @combine( + optimization_level=list(range(4)), + routing_method=["basic", "lookahead", "sabre", "stochastic"], + ) + def test_routing_plugins(self, optimization_level, routing_method): + """Test all routing plugins (excluding error).""" + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.measure_all() + tqc = transpile( + qc, + basis_gates=["cx", "sx", "x", "rz"], + coupling_map=CouplingMap.from_line(4), + optimization_level=optimization_level, + routing_method=routing_method, + ) + backend = QasmSimulatorPy() + counts = backend.run(tqc, shots=1000).result().get_counts() + self.assertDictAlmostEqual(counts, {"0000": 500, "1111": 500}, delta=100) diff --git a/test/python/visualization/references/coupling_map.png b/test/python/visualization/references/coupling_map.png new file mode 100644 index 000000000000..57ab05e1baaa Binary files /dev/null and b/test/python/visualization/references/coupling_map.png differ diff --git a/test/python/visualization/references/equivalence_library.png b/test/python/visualization/references/equivalence_library.png new file mode 100644 index 000000000000..081204bf40fc Binary files /dev/null and b/test/python/visualization/references/equivalence_library.png differ diff --git a/test/python/visualization/references/test_latex_barrier_label.tex b/test/python/visualization/references/test_latex_barrier_label.tex new file mode 100644 index 000000000000..c5f434fa903c --- /dev/null +++ b/test/python/visualization/references/test_latex_barrier_label.tex @@ -0,0 +1,12 @@ +\documentclass[border=2px]{standalone} + +\usepackage[braket, qm]{qcircuit} +\usepackage{graphicx} + +\begin{document} +\scalebox{1.0}{ +\Qcircuit @C=1.0em @R=0.2em @!R { \\ + \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{X}} \barrier[0em]{1} & \qw & \gate{\mathrm{Y}} \barrier[0em]{1} & \cds{0}{^{\mathrm{End\,Y/X}}} & \qw & \qw\\ + \nghost{{q}_{1} : } & \lstick{{q}_{1} : } & \gate{\mathrm{Y}} & \qw & \gate{\mathrm{X}} & \qw & \qw & \qw\\ +\\ }} +\end{document} \ No newline at end of file diff --git a/test/python/visualization/references/test_latex_plot_barriers_true.tex b/test/python/visualization/references/test_latex_plot_barriers_true.tex index 67618f16e7f8..b7679d27cd39 100644 --- a/test/python/visualization/references/test_latex_plot_barriers_true.tex +++ b/test/python/visualization/references/test_latex_plot_barriers_true.tex @@ -6,7 +6,7 @@ \begin{document} \scalebox{1.0}{ \Qcircuit @C=1.0em @R=0.2em @!R { \\ - \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{H}} \barrier[0em]{1} & \qw & \qw \barrier[0em]{1} & \qw & \qw & \qw\\ + \nghost{{q}_{0} : } & \lstick{{q}_{0} : } & \gate{\mathrm{H}} \barrier[0em]{1} & \qw & \qw \barrier[0em]{1} & \cds{0}{^{\mathrm{sn\,1}}} & \qw & \qw\\ \nghost{{q}_{1} : } & \lstick{{q}_{1} : } & \qw & \qw & \gate{\mathrm{H}} & \qw & \qw & \qw\\ \nghost{\mathrm{{c} : }} & \lstick{\mathrm{{c} : }} & \lstick{/_{_{2}}} \cw & \cw & \cw & \cw & \cw & \cw\\ \\ }} diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index 3930725a5f38..447a173392b8 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -241,7 +241,7 @@ def test_plot_barriers(self): # this import appears to be unused, but is actually needed to get snapshot instruction import qiskit.extensions.simulator # pylint: disable=unused-import - circuit.snapshot("1") + circuit.snapshot("sn 1") # check the barriers plot properly when plot_barriers= True circuit_drawer(circuit, filename=filename1, output="latex_source", plot_barriers=True) @@ -265,6 +265,22 @@ def test_no_barriers_false(self): self.assertEqualToReference(filename) + def test_barrier_label(self): + """Test the barrier label""" + filename = self._get_resource_path("test_latex_barrier_label.tex") + qr = QuantumRegister(2, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + + circuit_drawer(circuit, filename=filename, output="latex_source") + + self.assertEqualToReference(filename) + def test_big_gates(self): """Test large gates with params""" filename = self._get_resource_path("test_latex_big_gates.tex") diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 25a2d79ce952..3f2e4611b173 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -1052,6 +1052,28 @@ def test_text_justify_right_barrier(self): circuit.h(qr1[1]) self.assertEqual(str(_text_circuit_drawer(circuit, justify="right")), expected) + def test_text_barrier_label(self): + """Show barrier label""" + expected = "\n".join( + [ + " ┌───┐ ░ ┌───┐ End Y/X ", + "q_0: |0>┤ X ├─░─┤ Y ├────░────", + " ├───┤ ░ ├───┤ ░ ", + "q_1: |0>┤ Y ├─░─┤ X ├────░────", + " └───┘ ░ └───┘ ░ ", + ] + ) + + qr = QuantumRegister(2, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + circuit.y(1) + circuit.barrier() + circuit.y(0) + circuit.x(1) + circuit.barrier(label="End Y/X") + self.assertEqual(str(_text_circuit_drawer(circuit)), expected) + def test_text_overlap_cx(self): """Overlapping CX gates are drawn not overlapping""" expected = "\n".join( @@ -1661,7 +1683,7 @@ def test_control_gate_label_with_cond_1_high_cregbundle(self): str(_text_circuit_drawer(circ, vertical_compression="high", cregbundle=True)), expected ) - def test_control_gate_label_with_cond_2_med(self): + def test_control_gate_label_with_cond_2_med_space(self): """Control gate has a label and a conditional (on label, compression=med) See https://github.com/Qiskit/qiskit-terra/issues/4361""" expected = "\n".join( @@ -1670,8 +1692,7 @@ def test_control_gate_label_with_cond_2_med(self): "q_0: |0>┤ my h ├", " └──┬───┘", "q_1: |0>───■────", - " my ch ", - " ║ ", + " my║ch ", " c: 0 ═══■════", " 0x1 ", ] @@ -1686,6 +1707,31 @@ def test_control_gate_label_with_cond_2_med(self): self.assertEqual(str(_text_circuit_drawer(circ, vertical_compression="medium")), expected) + def test_control_gate_label_with_cond_2_med(self): + """Control gate has a label and a conditional (on label, compression=med) + See https://github.com/Qiskit/qiskit-terra/issues/4361""" + expected = "\n".join( + [ + " ┌──────┐ ", + "q_0: |0>──┤ my h ├─", + " └──┬───┘ ", + "q_1: |0>─────■─────", + " my ctrl-h ", + " ║ ", + " c: 0 ═════■═════", + " 0x1 ", + ] + ) + + qr = QuantumRegister(2, "q") + cr = ClassicalRegister(1, "c") + circ = QuantumCircuit(qr, cr) + hgate = HGate(label="my h") + controlh = hgate.control(label="my ctrl-h").c_if(cr, 1) + circ.append(controlh, [1, 0]) + + self.assertEqual(str(_text_circuit_drawer(circ, vertical_compression="medium")), expected) + def test_control_gate_label_with_cond_2_med_cregbundle(self): """Control gate has a label and a conditional (on label, compression=med) with cregbundle See https://github.com/Qiskit/qiskit-terra/issues/4361""" @@ -2024,7 +2070,6 @@ def test_text_conditional_1(self): " └─╥─┘└─╥─┘", "c0: 0 ══■════╬══", " 0x1 ║ ", - " ║ ", "c1: 0 ═══════■══", " 0x1 ", ] @@ -2126,6 +2171,98 @@ def test_text_measure_with_spaces_bundle(self): expected, ) + def test_text_barrier_med_compress_1(self): + """Medium vertical compression avoids connection break.""" + circuit = QuantumCircuit(4) + circuit.cx(1, 3) + circuit.x(1) + circuit.barrier((2, 3), label="Bar 1") + + expected = "\n".join( + [ + " ", + "q_0: |0>────────────", + " ┌───┐ ", + "q_1: |0>──■───┤ X ├─", + " │ └───┘ ", + " │ Bar 1 ", + "q_2: |0>──┼─────░───", + " ┌─┴─┐ ░ ", + "q_3: |0>┤ X ├───░───", + " └───┘ ░ ", + ] + ) + + self.assertEqual( + str(_text_circuit_drawer(circuit, vertical_compression="medium", cregbundle=False)), + expected, + ) + + def test_text_barrier_med_compress_2(self): + """Medium vertical compression avoids overprint.""" + circuit = QuantumCircuit(4) + circuit.barrier((0, 1, 2), label="a") + circuit.cx(1, 3) + circuit.x(1) + circuit.barrier((2, 3), label="Bar 1") + + expected = "\n".join( + [ + " a ", + "q_0: |0>─░─────────────", + " ░ ┌───┐ ", + "q_1: |0>─░───■───┤ X ├─", + " ░ │ └───┘ ", + " ░ │ Bar 1 ", + "q_2: |0>─░───┼─────░───", + " ░ ┌─┴─┐ ░ ", + "q_3: |0>───┤ X ├───░───", + " └───┘ ░ ", + ] + ) + + self.assertEqual( + str(_text_circuit_drawer(circuit, vertical_compression="medium", cregbundle=False)), + expected, + ) + + def test_text_barrier_med_compress_3(self): + """Medium vertical compression avoids conditional connection break.""" + qr = QuantumRegister(1, "qr") + 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) + + expected = "\n".join( + [ + " ┌───┐┌───┐", + " qr: |0>┤ X ├┤ X ├", + " └─╥─┘└─╥─┘", + "cr_0: 0 ══■════╬══", + " ║ ║ ", + "cr_2: 0 ══o════╬══", + " ║ ║ ", + " cr2: 0 ══╬════■══", + " ║ ", + "cr_1: 0 ══■═══════", + " 0x3 ", + ] + ) + + self.assertEqual( + str( + _text_circuit_drawer( + circuit, + vertical_compression="medium", + wire_order=[0, 1, 3, 4, 2], + cregbundle=False, + ) + ), + expected, + ) + class TestTextConditional(QiskitTestCase): """Gates with conditionals""" diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 8c23af7fdb0f..310ba871a771 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -446,14 +446,14 @@ def generate_calibrated_circuits(): # custom gate mygate = Gate("mygate", 1, []) - qc = QuantumCircuit(1) + qc = QuantumCircuit(1, name="calibrated_circuit_1") qc.append(mygate, [0]) with builder.build() as caldef: builder.play(Constant(100, 0.1), DriveChannel(0)) qc.add_calibration(mygate, (0,), caldef) circuits.append(qc) # override instruction - qc = QuantumCircuit(1) + qc = QuantumCircuit(1, name="calibrated_circuit_2") qc.x(0) with builder.build() as caldef: builder.play(Constant(100, 0.1), DriveChannel(0)) @@ -466,7 +466,7 @@ def generate_calibrated_circuits(): def generate_controlled_gates(): """Test QPY serialization with custom ControlledGates.""" circuits = [] - qc = QuantumCircuit(3) + qc = QuantumCircuit(3, name="custom_controlled_gates") controlled_gate = DCXGate().control(1) qc.append(controlled_gate, [0, 1, 2]) circuits.append(qc) @@ -476,18 +476,42 @@ def generate_controlled_gates(): custom_definition.rz(1.5, 0) custom_definition.sdg(0) custom_gate.definition = custom_definition - nested_qc = QuantumCircuit(3) + nested_qc = QuantumCircuit(3, name="nested_qc") qc.append(custom_gate, [0]) controlled_gate = custom_gate.control(2) nested_qc.append(controlled_gate, [0, 1, 2]) nested_qc.measure_all() circuits.append(nested_qc) - qc_open = QuantumCircuit(2) + qc_open = QuantumCircuit(2, name="open_cx") qc_open.cx(0, 1, ctrl_state=0) circuits.append(qc_open) return circuits +def generate_open_controlled_gates(): + """Test QPY serialization with custom ControlledGates with open controls.""" + circuits = [] + qc = QuantumCircuit(3, name="open_controls_simple") + controlled_gate = DCXGate().control(1, ctrl_state=0) + qc.append(controlled_gate, [0, 1, 2]) + circuits.append(qc) + + custom_gate = Gate("black_box", 1, []) + custom_definition = QuantumCircuit(1) + custom_definition.h(0) + custom_definition.rz(1.5, 0) + custom_definition.sdg(0) + custom_gate.definition = custom_definition + nested_qc = QuantumCircuit(3, name="open_controls_nested") + nested_qc.append(custom_gate, [0]) + controlled_gate = custom_gate.control(2, ctrl_state=1) + nested_qc.append(controlled_gate, [0, 1, 2]) + nested_qc.measure_all() + circuits.append(nested_qc) + + return circuits + + def generate_circuits(version_str=None): """Generate reference circuits.""" version_parts = None @@ -525,6 +549,8 @@ def generate_circuits(version_str=None): output_circuits["controlled_gates.qpy"] = generate_controlled_gates() output_circuits["schedule_blocks.qpy"] = generate_schedule_blocks() output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() + if version_parts >= (0, 21, 2): + output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() return output_circuits diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index bfb48d0068de..1c678ddda5fc 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -229,18 +229,22 @@ def _fully_supports_scheduling(backend): @st.composite def transpiler_conf(draw): """Composite search strategy to pick a valid transpiler config.""" - opt_level = draw(st.integers(min_value=0, max_value=3)) - layout_method = draw(st.sampled_from(layout_methods)) - routing_method = draw(st.sampled_from(routing_methods)) + all_backends = st.one_of(st.none(), st.sampled_from(mock_backends)) + scheduling_backends = st.sampled_from(mock_backends_with_scheduling) scheduling_method = draw(st.sampled_from(scheduling_methods)) - - compatible_backends = st.one_of(st.none(), st.sampled_from(mock_backends)) - if scheduling_method is not None or backend_needs_durations: - compatible_backends = st.sampled_from(mock_backends_with_scheduling) - - backend = draw(st.one_of(compatible_backends)) - - return (backend, opt_level, layout_method, routing_method, scheduling_method) + backend = ( + draw(scheduling_backends) + if scheduling_method or backend_needs_durations + else draw(all_backends) + ) + return { + "backend": backend, + "optimization_level": draw(st.integers(min_value=0, max_value=3)), + "layout_method": draw(st.sampled_from(layout_methods)), + "routing_method": draw(st.sampled_from(routing_methods)), + "scheduling_method": scheduling_method, + "seed_transpiler": draw(st.integers(min_value=0, max_value=1_000_000)), + } class QCircuitMachine(RuleBasedStateMachine): @@ -337,21 +341,23 @@ def qasm(self): self.qc.qasm() @precondition(lambda self: any(isinstance(d[0], Measure) for d in self.qc.data)) - @rule(conf=transpiler_conf()) - def equivalent_transpile(self, conf): + @rule(kwargs=transpiler_conf()) + def equivalent_transpile(self, kwargs): """Simulate, transpile and simulate the present circuit. Verify that the counts are not significantly different before and after transpilation. """ - backend, opt_level, layout_method, routing_method, scheduling_method = conf - - assume(backend is None or backend.configuration().n_qubits >= len(self.qc.qubits)) + assume( + kwargs["backend"] is None + or kwargs["backend"].configuration().n_qubits >= len(self.qc.qubits) + ) - print( - f"Evaluating circuit at level {opt_level} on {backend} " - f"using layout_method={layout_method} routing_method={routing_method} " - f"and scheduling_method={scheduling_method}:\n{self.qc.qasm()}" + call = ( + "transpile(qc, " + + ", ".join(f"{key:s}={value!r}" for key, value in kwargs.items() if value is not None) + + ")" ) + print(f"Evaluating {call} for:\n{self.qc.qasm()}") shots = 4096 @@ -360,18 +366,9 @@ def equivalent_transpile(self, conf): aer_counts = self.backend.run(self.qc, shots=shots).result().get_counts() try: - xpiled_qc = transpile( - self.qc, - backend=backend, - optimization_level=opt_level, - layout_method=layout_method, - routing_method=routing_method, - scheduling_method=scheduling_method, - ) + xpiled_qc = transpile(self.qc, **kwargs) except Exception as e: - failed_qasm = "Exception caught during transpilation of circuit: \n{}".format( - self.qc.qasm() - ) + failed_qasm = f"Exception caught during transpilation of circuit: \n{self.qc.qasm()}" raise RuntimeError(failed_qasm) from e xpiled_aer_counts = self.backend.run(xpiled_qc, shots=shots).result().get_counts() diff --git a/tools/report_ci_failure.py b/tools/report_ci_failure.py deleted file mode 100644 index b9be8243560d..000000000000 --- a/tools/report_ci_failure.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. -"""Utility module to open an issue on the repository when CIs fail.""" - -import os -import re - -from github import Github - - -class CIFailureReporter: - """Instances of this class can report to GitHub that the CI is failing.""" - - stable_branch_regex = re.compile(r"^stable/\d+\.\d+") - - def __init__(self, repository, token): - """ - Args: - repository (str): a string in the form 'owner/repository-name' - indicating the GitHub repository to report against. - token (str): a GitHub token obtained following: - https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - """ - self._repo = repository - self._api = Github(token) - - def report(self, branch, commit, infourl=None, job_name=None): - """Report on GitHub that the specified branch is failing to build at - the specified commit. The method will open an issue indicating that - the branch is failing. If there is an issue already open, it will add a - comment avoiding to report twice about the same failure. - - Args: - branch (str): branch name to report about. - commit (str): commit hash at which the build fails. - infourl (str): URL with extra info about the failure such as the - build logs. - job_name (str): name of the failed ci job. - """ - if branch != "main" and not self.stable_branch_regex.search(branch): - return - key_label = self._key_label(branch, job_name) - issue_number = self._get_report_issue_number(key_label) - if issue_number: - self._report_as_comment(issue_number, branch, commit, infourl) - else: - self._report_as_issue(branch, commit, infourl, job_name) - - def _key_label(self, branch_name, job_name): - if job_name == "Randomized tests": - return "randomized test" - elif job_name == "Benchmarks": - return "benchmarks failing" - elif branch_name == "main": - return "main failing" - elif branch_name.startswith("stable/"): - return "stable failing" - else: - return "" - - def _get_report_issue_number(self, key_label): - query = f'state:open label:"{key_label}" repo:{self._repo}' - results = self._api.search_issues(query=query) - try: - return results[0].number - except IndexError: - return None - - def _report_as_comment(self, issue_number, branch, commit, infourl): - stamp = _branch_is_failing_stamp(branch, commit) - report_exists = self._check_report_existence(issue_number, stamp) - if not report_exists: - _, body = _branch_is_failing_template(branch, commit, infourl) - message_body = f"{stamp}\n{body}" - self._post_new_comment(issue_number, message_body) - - def _check_report_existence(self, issue_number, target): - repo = self._api.get_repo(self._repo) - issue = repo.get_issue(issue_number) - if target in issue.body: - return True - - for comment in issue.get_comments(): - if target in comment.body: - return True - - return False - - def _report_as_issue(self, branch, commit, infourl, key_label): - repo = self._api.get_repo(self._repo) - stamp = _branch_is_failing_stamp(branch, commit) - title, body = _branch_is_failing_template(branch, commit, infourl) - message_body = f"{stamp}\n{body}" - repo.create_issue(title=title, body=message_body, labels=[key_label]) - - def _post_new_comment(self, issue_number, body): - repo = self._api.get_repo(self._repo) - issue = repo.get_issue(issue_number) - issue.create_comment(body) - - -def _branch_is_failing_template(branch, commit, infourl): - title = f"Branch `{branch}` is failing" - body = f"Trying to build `{branch}` at commit {commit} failed." - if infourl: - body += f"\nMore info at: {infourl}" - return title, body - - -def _branch_is_failing_stamp(branch, commit): - return f"" - - -_REPOSITORY = "Qiskit/qiskit-terra" -_GH_TOKEN = os.getenv("GH_TOKEN") - - -def _get_repo_name(): - return os.getenv("TRAVIS_REPO_SLUG") or os.getenv("APPVEYOR_REPO_NAME") - - -def _get_branch_name(): - return os.getenv("TRAVIS_BRANCH") or os.getenv("APPVEYOR_REPO_BRANCH") - - -def _get_commit_hash(): - return os.getenv("TRAVIS_COMMIT") or os.getenv("APPVEYOR_REPO_COMMIT") - - -def _get_job_name(): - return os.getenv("TRAVIS_JOB_NAME") or os.getenv("APPVEYOR_JOB_NAME") - - -def _get_info_url(): - if os.getenv("TRAVIS"): - job_id = os.getenv("TRAVIS_JOB_ID") - return f"https://travis-ci.com/{_REPOSITORY}/jobs/{job_id}" - - if os.getenv("APPVEYOR"): - build_id = os.getenv("APPVEYOR_BUILD_ID") - return f"https://ci.appveyor.com/project/{_REPOSITORY}/build/{build_id}" - - return None - - -if __name__ == "__main__": - if os.getenv("TRAVIS_EVENT_TYPE", "") == "push": - _REPORTER = CIFailureReporter(_get_repo_name(), _GH_TOKEN) - _REPORTER.report(_get_branch_name(), _get_commit_hash(), _get_info_url(), _get_job_name()) diff --git a/tox.ini b/tox.ini index 0561b10732a7..ef11d313352e 100644 --- a/tox.ini +++ b/tox.ini @@ -14,8 +14,9 @@ setenv = QISKIT_SUPRESS_PACKAGING_WARNINGS=Y QISKIT_TEST_CAPTURE_STREAMS=1 QISKIT_PARALLEL=FALSE -passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL SETUPTOOLS_ENABLE_FEATURES -deps = -r{toxinidir}/requirements.txt +passenv = RAYON_NUM_THREADS OMP_NUM_THREADS QISKIT_PARALLEL RUST_BACKTRACE SETUPTOOLS_ENABLE_FEATURES +deps = setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) + -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands = stestr run {posargs} @@ -73,6 +74,7 @@ setenv = {[testenv]setenv} QISKIT_SUPPRESS_PACKAGING_WARNINGS=Y deps = + setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) -r{toxinidir}/requirements-dev.txt qiskit-aer commands =