From d3b08adf692f54b70b809264d3798ab4b81527ff Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Mon, 27 Nov 2023 13:00:45 -0500 Subject: [PATCH 001/124] more transpiler docs improvements (#11300) * more transpiler docs improvements * Update qiskit/transpiler/__init__.py Co-authored-by: Matthew Treinish * use double ticks and remove extra line of code --------- Co-authored-by: Matthew Treinish --- qiskit/transpiler/__init__.py | 43 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index b91662c26f82..d6ce7b31ca4c 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -105,28 +105,35 @@ .. code-block:: python - from qiskit.circuit.library import XGate, HGate, RXGate, PhaseGate, TGate, TdgGate + import numpy as np + from qiskit.circuit.library import HGate, PhaseGate, RXGate, TdgGate, TGate, XGate from qiskit.transpiler import PassManager - from qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling - from qiskit.transpiler.passes import CXCancellation, InverseCancellation + from qiskit.transpiler.passes import ( + ALAPScheduleAnalysis, + CXCancellation, + InverseCancellation, + PadDynamicalDecoupling, + ) - backend_durations = backend.target.durations() dd_sequence = [XGate(), XGate()] - scheduling_pm = PassManager([ - ALAPScheduleAnalysis(backend_durations), - PadDynamicalDecoupling(backend_durations, dd_sequence), - ]) + scheduling_pm = PassManager( + [ + ALAPScheduleAnalysis(target=backend.target), + PadDynamicalDecoupling(target=backend.target, dd_sequence=dd_sequence), + ] + ) inverse_gate_list = [ HGate(), (RXGate(np.pi / 4), RXGate(-np.pi / 4)), (PhaseGate(np.pi / 4), PhaseGate(-np.pi / 4)), (TGate(), TdgGate()), - - ]) - logical_opt = PassManager([ - CXCancellation(), - InverseCancellation([HGate(), (RXGate(np.pi / 4), RXGate(-np.pi / 4)) - ]) + ] + logical_opt = PassManager( + [ + CXCancellation(), + InverseCancellation(inverse_gate_list), + ] + ) # Add pre-layout stage to run extra logical optimization @@ -134,11 +141,9 @@ # Set scheduling stage to custom pass manager pass_manager.scheduling = scheduling_pm - -Then when :meth:`~.StagedPassManager.run` is called on ``pass_manager`` the -``logical_opt`` :class:`~.PassManager` will be called prior to the ``layout`` stage -and for the ``scheduling`` stage our custom :class:`~.PassManager` -``scheduling_pm`` will be used. +Now, when the staged pass manager is run via the :meth:`~.StagedPassManager.run` method, +the ``logical_opt`` pass manager will be called before the ``layout`` stage, and the +``scheduling_pm`` pass manager will be used for the ``scheduling`` stage instead of the default. Custom Pass Managers ==================== From 5bbda4a263121671f0a277d29487cc0544987f19 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 27 Nov 2023 18:14:19 +0000 Subject: [PATCH 002/124] Update `Instruction.condition_bits` for runtime classical expressions (#11325) I didn't even know this property existed and it wasn't tested directly, but the IBM provider uses it during its custom scheduling passes, so until removal, it should be kept updated. --- qiskit/circuit/instruction.py | 7 ++- ...ction-condition-bits-17694f98628b30ad.yaml | 5 +++ test/python/circuit/test_instructions.py | 44 ++++++++++++++----- 3 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/fix-instruction-condition-bits-17694f98628b30ad.yaml diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 84e3f344bf07..9fbe77a04f82 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -618,12 +618,11 @@ def repeat(self, n): @property def condition_bits(self) -> List[Clbit]: """Get Clbits in condition.""" + from qiskit.circuit.controlflow import condition_resources # pylint: disable=cyclic-import + if self.condition is None: return [] - if isinstance(self.condition[0], Clbit): - return [self.condition[0]] - else: # ClassicalRegister - return list(self.condition[0]) + return list(condition_resources(self.condition).clbits) @property def name(self): diff --git a/releasenotes/notes/fix-instruction-condition-bits-17694f98628b30ad.yaml b/releasenotes/notes/fix-instruction-condition-bits-17694f98628b30ad.yaml new file mode 100644 index 000000000000..4f044b6a468f --- /dev/null +++ b/releasenotes/notes/fix-instruction-condition-bits-17694f98628b30ad.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The property :attr:`.Instruction.condition_bits` will now correctly handle runtime classical + expressions (:mod:`qiskit.circuit.classical`). diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index fc60be60fdc1..d111d2d21181 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -18,16 +18,20 @@ import numpy as np -from qiskit.circuit import Gate -from qiskit.circuit import Parameter -from qiskit.circuit import Instruction, InstructionSet -from qiskit.circuit import QuantumCircuit -from qiskit.circuit import QuantumRegister, ClassicalRegister, Qubit, Clbit -from qiskit.circuit.library.standard_gates.h import HGate -from qiskit.circuit.library.standard_gates.rz import RZGate -from qiskit.circuit.library.standard_gates.x import CXGate -from qiskit.circuit.library.standard_gates.s import SGate -from qiskit.circuit.library.standard_gates.t import TGate +from qiskit.circuit import ( + Gate, + Parameter, + Instruction, + InstructionSet, + QuantumCircuit, + QuantumRegister, + ClassicalRegister, + Qubit, + Clbit, + IfElseOp, +) +from qiskit.circuit.library import HGate, RZGate, CXGate, SGate, TGate +from qiskit.circuit.classical import expr from qiskit.test import QiskitTestCase from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.random import random_circuit @@ -426,6 +430,26 @@ def test_repr_of_instructions(self): ), ) + def test_instruction_condition_bits(self): + """Test that the ``condition_bits`` property behaves correctly until it is deprecated and + removed.""" + bits = [Clbit(), Clbit()] + cr1 = ClassicalRegister(2, "cr1") + cr2 = ClassicalRegister(2, "cr2") + body = QuantumCircuit(cr1, cr2, bits) + + def key(bit): + return body.find_bit(bit).index + + op = IfElseOp((bits[0], False), body) + self.assertEqual(op.condition_bits, [bits[0]]) + + op = IfElseOp((cr1, 3), body) + self.assertEqual(op.condition_bits, list(cr1)) + + op = IfElseOp(expr.logic_and(bits[1], expr.equal(cr2, 3)), body) + self.assertEqual(sorted(op.condition_bits, key=key), sorted([bits[1]] + list(cr2), key=key)) + def test_instructionset_c_if_direct_resource(self): """Test that using :meth:`.InstructionSet.c_if` with an exact classical resource always works, and produces the expected condition.""" From e91947e1b55f6e661c683fba72ab66c5f92d3aec Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 27 Nov 2023 20:54:38 +0000 Subject: [PATCH 003/124] Replace `qiskit` metapackage with `qiskit-terra` (#11271) * Replace `qiskit` metapackage with `qiskit-terra` This commit completely removes the concept of the `qiskit-terra` package from Qiskit main. The hitherto metapackage `qiskit` is promoted to be the concrete code package. This is a completely breaking change for packaging purposes for users, as there is no clean upgrade path from `qiskit-terra` to `qiskit`; PyPI and pip do not give us the tools to mark that the former obsoletes and supersedes the latter. We intend to follow this PR with a technical blog post somewhere explaining the changes, how users should adapt ("to install Qiskit 1.0, you must start a new venv"), and why we needed to do this. The "why", in part, is: - the metapackage legacy causes awkward upgrade paths on every release; some packages still depend on `qiskit-terra`, some on `qiskit`, and pip will happily make those two get out-of-sync when upgrading a transitive dependency with only a warning that users are used to ignoring. - with the 1.0 release, we (believe we) will have some leeway from users to make such a breaking change. - having the metapackage split makes it difficult for downstream projects and developers to test against `main` - they always must install both `qiskit-terra` and `qiskit`, and the latter has no meaning in editable installs, so needs re-installing after each version bump. Problems surrounding this have already broken CI for Qiskit, for at least one internal IBM repo, and at least my dev install of Qiskit. This could be improved a bit with more education, but it's still always going to increase the barrier to entry and make it much harder to do the right thing. We will not publish any 1.0 or above version of `qiskit-terra`. All dependents on Qiskit should switch their requirements to `qiskit`. * Add manual uninstall for Neko * Fix Windows paths Co-authored-by: Matthew Treinish * Refer to stdlib documentation for odd shells --------- Co-authored-by: Matthew Treinish --- .azure/lint-linux.yml | 5 +- .azure/test-linux.yml | 4 +- .azure/test-macos.yml | 1 - .azure/test-windows.yml | 1 - .github/workflows/coverage.yml | 2 +- .github/workflows/neko.yml | 4 +- .github/workflows/slow.yml | 2 +- .github/workflows/wheels.yml | 30 +------- qiskit_pkg/LICENSE.txt | 1 - qiskit_pkg/MANIFEST.in | 1 - qiskit_pkg/README.md | 1 - qiskit_pkg/setup.py | 74 ------------------- .../notes/terra-nullius-7ef598626d8118c1.yaml | 30 ++++++++ setup.py | 2 +- tools/deploy_translatable_strings.sh | 5 +- tox.ini | 16 ++-- 16 files changed, 48 insertions(+), 131 deletions(-) delete mode 120000 qiskit_pkg/LICENSE.txt delete mode 100644 qiskit_pkg/MANIFEST.in delete mode 120000 qiskit_pkg/README.md delete mode 100644 qiskit_pkg/setup.py create mode 100644 releasenotes/notes/terra-nullius-7ef598626d8118c1.yaml diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index 5855fe1e6e67..a1809bd829bd 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -26,7 +26,6 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - ./qiskit_pkg \ -e . # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. @@ -38,7 +37,7 @@ jobs: set -e source test-job/bin/activate echo "Running black, any errors reported can be fixed with 'tox -eblack'" - black --check qiskit test tools examples setup.py qiskit_pkg + black --check qiskit test tools examples setup.py echo "Running rustfmt check, any errors reported can be fixed with 'cargo fmt'" cargo fmt --check displayName: "Formatting" @@ -47,7 +46,7 @@ jobs: set -e source test-job/bin/activate echo "Running ruff" - ruff qiskit test tools examples setup.py qiskit_pkg/setup.py + ruff qiskit test tools examples setup.py echo "Running pylint" pylint -rn qiskit test tools echo "Running Cargo Clippy" diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index eb456f8497ad..512e1d773c35 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -74,12 +74,11 @@ jobs: python -m pip install -U pip python -m pip install -U build python -m build --sdist . - python -m build --sdist qiskit_pkg python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - dist/qiskit*.tar.gz + dist/qiskit-*.tar.gz # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. displayName: "Install Terra from sdist" @@ -92,7 +91,6 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - ./qiskit_pkg \ -e . # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index cbf2fc8b0e08..c241f51f57e9 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -43,7 +43,6 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - ./qiskit_pkg \ -e . # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index 6ba2e442946c..f546fdb41dc3 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -42,7 +42,6 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - ./qiskit_pkg \ -e . # Build and install both qiskit and qiskit-terra so that any optionals # depending on `qiskit` will resolve correctly. diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 221840b4e7c6..de3485c5fdce 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -42,7 +42,7 @@ jobs: run: python -m pip install -c constraints.txt --upgrade pip setuptools wheel - name: Build and install qiskit-terra - run: python -m pip install -c constraints.txt -e . ./qiskit_pkg + run: python -m pip install -c constraints.txt -e . env: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Cinstrument-coverage" diff --git a/.github/workflows/neko.yml b/.github/workflows/neko.yml index bda60fd39790..30921c8b051b 100644 --- a/.github/workflows/neko.yml +++ b/.github/workflows/neko.yml @@ -16,4 +16,6 @@ jobs: - uses: Qiskit/qiskit-neko@main with: test_selection: terra - repo_install_command: "pip install -c constraints.txt ." + # We have to forcibly uninstall any old version of qiskit or qiskit-terra during the + # changeover, because it's not possible to safely upgrade an existing installation to 1.0. + repo_install_command: "pip uninstall qiskit qiskit-terra && pip install -c constraints.txt ." diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 528305740d95..62fa9ec19c62 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -19,7 +19,7 @@ jobs: python -m pip install -U pip setuptools wheel python -m pip install -U -r requirements.txt -c constraints.txt python -m pip install -U -r requirements-dev.txt -c constraints.txt - python -m pip install -c constraints.txt -e . ./qiskit_pkg + python -m pip install -c constraints.txt -e . python -m pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 74f4f4a63daf..99a46a97b63e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,7 @@ jobs: with: packages-dir: wheelhouse/ sdist: - name: Build and publish terra sdist + name: Build and publish sdist runs-on: ${{ matrix.os }} needs: ["upload_shared_wheels"] environment: release @@ -190,31 +190,3 @@ jobs: run: python -m build . --sdist - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - metapackage: - name: Build and publish terra sdist - runs-on: ${{ matrix.os }} - needs: ["sdist"] - environment: release - permissions: - id-token: write - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - name: Install Python - with: - python-version: '3.10' - - name: Install deps - run: pip install -U build - - name: Build packages - run: | - set -e - cd qiskit_pkg - python -m build . - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: qiskit_pkg/dist diff --git a/qiskit_pkg/LICENSE.txt b/qiskit_pkg/LICENSE.txt deleted file mode 120000 index 4ab43736a839..000000000000 --- a/qiskit_pkg/LICENSE.txt +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.txt \ No newline at end of file diff --git a/qiskit_pkg/MANIFEST.in b/qiskit_pkg/MANIFEST.in deleted file mode 100644 index 42eb4101e514..000000000000 --- a/qiskit_pkg/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE.txt diff --git a/qiskit_pkg/README.md b/qiskit_pkg/README.md deleted file mode 120000 index 32d46ee883b5..000000000000 --- a/qiskit_pkg/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/qiskit_pkg/setup.py b/qiskit_pkg/setup.py deleted file mode 100644 index ecff044d2ac5..000000000000 --- a/qiskit_pkg/setup.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 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. - -# This file is the setup.py file for the qiskit package. Because python -# packaging doesn't offer a mechanism to have qiskit supersede qiskit-terra -# and cleanly upgrade from one to the other, there needs to be a separate -# package shim to ensure no matter how people installed qiskit < 0.45.0 the -# upgrade works. - -import os - -from setuptools import setup - -README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md") -with open(README_PATH) as readme_file: - README = readme_file.read() - -requirements = ["qiskit-terra==1.0.0"] - -setup( - name="qiskit", - version="1.0.0", - description="Software for developing quantum computing programs", - long_description=README, - long_description_content_type="text/markdown", - url="https://qiskit.org/", - author="Qiskit Development Team", - author_email="hello@qiskit.org", - license="Apache 2.0", - py_modules=[], - packages=[], - classifiers=[ - "Environment :: Console", - "License :: OSI Approved :: Apache Software License", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering", - ], - keywords="qiskit sdk quantum", - install_requires=requirements, - project_urls={ - "Bug Tracker": "https://github.com/Qiskit/qiskit/issues", - "Documentation": "https://qiskit.org/documentation/", - "Source Code": "https://github.com/Qiskit/qiskit", - }, - include_package_data=True, - python_requires=">=3.8", - extras_require={ - "qasm3-import": ["qiskit-terra[qasm3-import]"], - "visualization": ["qiskit-terra[visualization]"], - "crosstalk-pass": ["qiskit-terra[crosstalk-pass]"], - "csp-layout-pass": ["qiskit-terra[csp-layout-pass]"], - "all": ["qiskit-terra[all]"], - }, -) diff --git a/releasenotes/notes/terra-nullius-7ef598626d8118c1.yaml b/releasenotes/notes/terra-nullius-7ef598626d8118c1.yaml new file mode 100644 index 000000000000..562929a17b9f --- /dev/null +++ b/releasenotes/notes/terra-nullius-7ef598626d8118c1.yaml @@ -0,0 +1,30 @@ +--- +critical: + - | + You cannot upgrade in place to Qiskit 1.0. You must begin a new virtual environment. + + From Qiskit 1.0, Qiskit is comprised of exactly one Python package: ``qiskit``. Previously, + as a legacy of the "component elements" of early Qiskit, the ``qiskit`` package was a + dependency-only "metapackage", and the core code of Qiskit was in a package called ``qiskit-terra``. + As Qiskit grew, the other elements split off into their own packages (such as ``qiskit-aer``) + until only the core was left in the metapackage. For Qiskit 1.0, we are removing the metapackage + entirely, and replacing it with the actual Qiskit code. + + This means that you cannot upgrade an existing installation to Qiskit 1.0. Instead, you must + create a new Python virtual environment. Using the built-in ``venv`` module, you can do (Linux + and Mac): + + .. code-block:: bash + + # Create the new environment (only once). + python -m venv ~/qiskit-1.0-venv + # Activate the environment (every session). + source ~/qiskit-1.0-venv/bin/activate + # Install Qiskit (only once). + pip install 'qiskit>=1.0' + + For other platforms, or more unusual shells, refer to `the Python standard-library documentation + on activating virtual environments `__. + + If you are a library author, or have code that depends on Qiskit, you should update any old + dependencies on ``qiskit-terra`` to instead depend on ``qiskit``. diff --git a/setup.py b/setup.py index 751a151f21fc..55950efef410 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ setup( - name="qiskit-terra", + name="qiskit", version="1.0.0", description="Software for developing quantum computing programs", long_description=README, diff --git a/tools/deploy_translatable_strings.sh b/tools/deploy_translatable_strings.sh index afac6869447d..1ae60b5f7072 100755 --- a/tools/deploy_translatable_strings.sh +++ b/tools/deploy_translatable_strings.sh @@ -84,8 +84,9 @@ rm -rf \ echo "+ 'cp' wanted files from source to target" # Copy the new rendered files and add them to the commit. cp -r "$SOURCE_PO_DIR/." "$TARGET_PO_DIR" -# Copy files necessary to build the Qiskit metapackage. -cp "$SOURCE_REPO_ROOT/qiskit_pkg/setup.py" "${TARGET_REPO_ROOT}" +# Copy files necessary to build the Qiskit package. +cp "$SOURCE_REPO_ROOT/setup.py" "${TARGET_REPO_ROOT}" +cp "$SOURCE_REPO_ROOT/pyproject.toml" "${TARGET_REPO_ROOT}" cat "$SOURCE_REPO_ROOT/requirements-dev.txt" "$SOURCE_REPO_ROOT/requirements-optional.txt" \ > "${TARGET_REPO_ROOT}/requirements-dev.txt" cp "$SOURCE_REPO_ROOT/constraints.txt" "${TARGET_REPO_ROOT}" diff --git a/tox.ini b/tox.ini index 5c517b1d2cc9..df46820fc026 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,6 @@ envlist = py38, py39, py310, py311, py312, lint-incr isolated_build = true [testenv] -# We pretend that we're not actually installing the package, because we need tox to let us have two -# packages ('qiskit' and 'qiskit-terra') under test at the same time. For that, we have to stuff -# them into 'deps'. -skip_install = true install_command = pip install -c{toxinidir}/constraints.txt -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} @@ -22,16 +18,14 @@ 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 - -e . - -e ./qiskit_pkg commands = stestr run {posargs} [testenv:lint] basepython = python3 commands = - ruff check qiskit test tools examples setup.py qiskit_pkg - black --check {posargs} qiskit test tools examples setup.py qiskit_pkg + ruff check qiskit test tools examples setup.py + black --check {posargs} qiskit test tools examples setup.py pylint -rn qiskit test tools # This line is commented out until #6649 merges. We can't run this currently # via tox because tox doesn't support globbing @@ -45,8 +39,8 @@ commands = basepython = python3 allowlist_externals = git commands = - ruff check qiskit test tools examples setup.py qiskit_pkg - black --check {posargs} qiskit test tools examples setup.py qiskit_pkg + ruff check qiskit test tools examples setup.py + black --check {posargs} qiskit test tools examples setup.py -git fetch -q https://github.com/Qiskit/qiskit-terra.git :lint_incr_latest python {toxinidir}/tools/pylint_incr.py -rn -j4 -sn --paths :/qiskit/*.py :/test/*.py :/tools/*.py python {toxinidir}/tools/pylint_incr.py -rn -j4 -sn --disable='invalid-name,missing-module-docstring,redefined-outer-name' --paths :(glob,top)examples/python/*.py @@ -59,7 +53,7 @@ commands = skip_install = true deps = -r requirements-dev.txt -commands = black {posargs} qiskit test tools examples setup.py qiskit_pkg +commands = black {posargs} qiskit test tools examples setup.py [testenv:coverage] basepython = python3 From 7f809a9ff8545bf0dc7e22f0c0baed042e2105ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:04:22 +0100 Subject: [PATCH 004/124] Remove leftover algorithm/opflow utils (#11324) * Remove utils * Update reno * Fix test, reno * Update reno --- qiskit/utils/__init__.py | 16 -- qiskit/utils/arithmetic.py | 152 ------------------ qiskit/utils/circuit_utils.py | 69 -------- qiskit/utils/entangler_map.py | 111 ------------- qiskit/utils/name_unnamed_args.py | 73 --------- ...move-algorithm-utils-707648b69af439dc.yaml | 23 ++- test/python/test_util.py | 15 +- 7 files changed, 19 insertions(+), 440 deletions(-) delete mode 100644 qiskit/utils/arithmetic.py delete mode 100644 qiskit/utils/circuit_utils.py delete mode 100644 qiskit/utils/entangler_map.py delete mode 100644 qiskit/utils/name_unnamed_args.py diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index e12d30b5653b..a9b73b85f95d 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -29,14 +29,6 @@ .. autofunction:: detach_prefix .. autofunction:: wrap_method -Algorithm Utilities -=================== - -.. autofunction:: summarize_circuits -.. autofunction:: get_entangler_map -.. autofunction:: validate_entangler_map -.. autofunction:: name_args - Optional Dependency Checkers (:mod:`qiskit.utils.optionals`) ============================================================ @@ -59,18 +51,10 @@ from . import optionals -from .circuit_utils import summarize_circuits -from .entangler_map import get_entangler_map, validate_entangler_map -from .name_unnamed_args import name_args - __all__ = [ "LazyDependencyManager", "LazyImportTester", "LazySubprocessTester", - "summarize_circuits", - "get_entangler_map", - "validate_entangler_map", - "name_args", "add_deprecation_to_docstring", "deprecate_arg", "deprecate_arguments", diff --git a/qiskit/utils/arithmetic.py b/qiskit/utils/arithmetic.py deleted file mode 100644 index 23a838721f9e..000000000000 --- a/qiskit/utils/arithmetic.py +++ /dev/null @@ -1,152 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2020. -# -# 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. - -""" -Arithmetic Utilities -""" - -from typing import List, Tuple -import numpy as np - - -def normalize_vector(vector): - """ - Normalize the input state vector. - """ - return vector / np.linalg.norm(vector) - - -def is_power_of_2(num): - """ - Check if the input number is a power of 2. - """ - return num != 0 and ((num & (num - 1)) == 0) - - -def log2(num): - """ - Compute the log2 of the input number. Use bit operation if the input is a power of 2. - """ - if is_power_of_2(num): - ret = 0 - while True: - if num >> ret == 1: - return ret - else: - ret += 1 - else: - return np.log2(num) - - -def is_power(num, return_decomposition=False): - """ - Check if num is a perfect power in O(n^3) time, n=ceil(logN) - """ - b = 2 - while (2**b) <= num: - a = 1 - c = num - while (c - a) >= 2: - m = int((a + c) / 2) - - if (m**b) < (num + 1): - p = int(m**b) - else: - p = int(num + 1) - - if int(p) == int(num): - if return_decomposition: - return True, int(m), int(b) - else: - return True - - if p < num: - a = int(m) - else: - c = int(m) - b = b + 1 - if return_decomposition: - return False, num, 1 - else: - return False - - -def next_power_of_2_base(n): - """ - Return the base of the smallest power of 2 no less than the input number - """ - base = 0 - if n and not (n & (n - 1)): # pylint: disable=superfluous-parens - return log2(n) - - while n != 0: - n >>= 1 - base += 1 - - return base - - -def transpositions(permutation: List[int]) -> List[Tuple[int, int]]: - """Return a sequence of transpositions, corresponding to the permutation. - - Args: - permutation: The ``List[int]`` defining the permutation. An element at index ``j`` should be - permuted to index ``permutation[j]``. - - Returns: - List of transpositions, corresponding to the permutation. For permutation = [3, 0, 2, 1], - returns [(0,1), (0,3)] - """ - unchecked = [True] * len(permutation) - cyclic_form = [] - for i in range(len(permutation)): - if unchecked[i]: - cycle = [i] - unchecked[i] = False - j = i - while unchecked[permutation[j]]: - j = permutation[j] - cycle.append(j) - unchecked[j] = False - if len(cycle) > 1: - cyclic_form.append(cycle) - cyclic_form.sort() - res = [] - for x in cyclic_form: - len_x = len(x) - if len_x == 2: - res.append((x[0], x[1])) - elif len_x > 2: - first = x[0] - for y in x[len_x - 1 : 0 : -1]: - res.append((first, y)) - return res - - -def triu_to_dense(triu: np.ndarray) -> np.ndarray: - """Converts upper triangular part of matrix to dense matrix. - - Args: - triu: array in the form [[A, B, C], [D, E], [F]] - - Returns: - Array [[A, B, C], [B, D, E], [C, E, F]] - """ - dim = len(triu) - matrix = np.empty((dim, dim), dtype=complex) - for i in range(dim): - for j in range(dim - i): - matrix[i, i + j] = triu[i][j] - if j != 0: - matrix[i + j, i] = triu[i][j] - - return matrix diff --git a/qiskit/utils/circuit_utils.py b/qiskit/utils/circuit_utils.py deleted file mode 100644 index 2fe140d3780d..000000000000 --- a/qiskit/utils/circuit_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Circuit utility functions""" - -import numpy as np - - -def summarize_circuits(circuits): - """Summarize circuits based on QuantumCircuit, and five metrics are summarized. - - Number of qubits - - Number of classical bits - - Number of operations - - Depth of circuits - - Counts of different gate operations - - The average statistic of the first four is provided if multiple circuits are provided. - - Args: - circuits (QuantumCircuit or [QuantumCircuit]): the to-be-summarized circuits - - Returns: - str: a formatted string records the summary - """ - if not isinstance(circuits, list): - circuits = [circuits] - ret = "" - ret += f"Submitting {len(circuits)} circuits.\n" - ret += "============================================================================\n" - stats = np.zeros(4) - for i, circuit in enumerate(circuits): - depth = circuit.depth() - size = circuit.size() - num_qubits = sum(reg.size for reg in circuit.qregs) - num_clbits = sum(reg.size for reg in circuit.cregs) - op_counts = circuit.count_ops() - stats[0] += num_qubits - stats[1] += num_clbits - stats[2] += size - stats[3] += depth - ret = "".join( - [ - ret, - "{}-th circuit: {} qubits, {} classical bits and {} " - "operations with depth {}\nop_counts: {}\n".format( - i, num_qubits, num_clbits, size, depth, op_counts - ), - ] - ) - if len(circuits) > 1: - stats /= len(circuits) - ret = "".join( - [ - ret, - "Average: {:.2f} qubits, {:.2f} classical bits and {:.2f} " - "operations with depth {:.2f}\n".format(stats[0], stats[1], stats[2], stats[3]), - ] - ) - ret += "============================================================================\n" - return ret diff --git a/qiskit/utils/entangler_map.py b/qiskit/utils/entangler_map.py deleted file mode 100644 index 1cd750398ccb..000000000000 --- a/qiskit/utils/entangler_map.py +++ /dev/null @@ -1,111 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -This module contains the definition of creating and validating entangler map -based on the number of qubits. -""" - - -def get_entangler_map(map_type, num_qubits, offset=0): - """Utility method to get an entangler map among qubits. - - Args: - map_type (str): 'full' entangles each qubit with all the subsequent ones - 'linear' entangles each qubit with the next - 'sca' (shifted circular alternating entanglement) is a - circular entanglement where the 'long' entanglement is - shifted by one position every block and every block the - role or control/target qubits alternate - num_qubits (int): Number of qubits for which the map is needed - offset (int): Some map_types (e.g. 'sca') can shift the gates in - the entangler map by the specified integer offset. - - Returns: - list: A map of qubit index to an array of indexes to which this should be entangled - - Raises: - ValueError: if map_type is not valid. - """ - ret = [] - - if num_qubits > 1: - if map_type == "full": - ret = [[i, j] for i in range(num_qubits) for j in range(i + 1, num_qubits)] - elif map_type == "linear": - ret = [[i, i + 1] for i in range(num_qubits - 1)] - elif map_type == "sca": - offset_idx = offset % num_qubits - if offset_idx % 2 == 0: # even block numbers - for i in reversed(range(offset_idx)): - ret += [[i, i + 1]] - - ret += [[num_qubits - 1, 0]] - - for i in reversed(range(offset_idx + 1, num_qubits)): - ret += [[i - 1, i]] - - else: # odd block numbers - for i in range(num_qubits - offset_idx - 1, num_qubits - 1): - ret += [[i + 1, i]] - - ret += [[0, num_qubits - 1]] - - for i in range(num_qubits - offset_idx - 1): - ret += [[i + 1, i]] - else: - raise ValueError("map_type only supports 'full', 'linear' or 'sca' type.") - return ret - - -def validate_entangler_map(entangler_map, num_qubits, allow_double_entanglement=False): - """Validate a user supplied entangler map and converts entries to ints. - - Args: - entangler_map (list[list]) : An entangler map, keys are source qubit index (int), - value is array - of target qubit index(es) (int) - num_qubits (int) : Number of qubits - allow_double_entanglement (bool): If we allow in two qubits can be entangled each other - - Returns: - list: Validated/converted map - - Raises: - TypeError: entangler map is not list type or list of list - ValueError: the index of entangler map is out of range - ValueError: the qubits are cross-entangled. - - """ - - if isinstance(entangler_map, dict): - raise TypeError("The type of entangler map is changed to list of list.") - - if not isinstance(entangler_map, list): - raise TypeError("Entangler map type 'list' expected") - - for src_to_targ in entangler_map: - if not isinstance(src_to_targ, list): - raise TypeError(f"Entangle index list expected but got {type(src_to_targ)}") - - ret_map = [] - ret_map = [[int(src), int(targ)] for src, targ in entangler_map] - - for src, targ in ret_map: - if src < 0 or src >= num_qubits: - raise ValueError(f"Qubit entangle source value {src} invalid for {num_qubits} qubits") - if targ < 0 or targ >= num_qubits: - raise ValueError(f"Qubit entangle target value {targ} invalid for {num_qubits} qubits") - if not allow_double_entanglement and [targ, src] in ret_map: - raise ValueError(f"Qubit {src} and {targ} cross-entangled.") - - return ret_map diff --git a/qiskit/utils/name_unnamed_args.py b/qiskit/utils/name_unnamed_args.py deleted file mode 100644 index 4e153dcfefd2..000000000000 --- a/qiskit/utils/name_unnamed_args.py +++ /dev/null @@ -1,73 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tool to name unnamed arguments.""" - -import functools - - -def name_args(mapping, skip=0): - """Decorator to convert unnamed arguments to named ones. - - Can be used to deprecate old signatures of a function, e.g. - - .. code-block:: - - old_f(a: TypeA, b: TypeB, c: TypeC) - new_f(a: TypeA, d: TypeD, b: TypeB=None, c: TypeC=None) - - Then, to support the old signature this decorator can be used as - - .. code-block:: - - @name_args([ - ('a'), # stays the same - ('d', {TypeB: 'b'}), # if arg is of type TypeB, call if 'b' else 'd' - ('b', {TypeC: 'c'}) - ]) - def new_f(a: TypeA, d: TypeD, b: TypeB=None, c: TypeC=None): - if b is not None: - # raise warning, this is deprecated! - if c is not None: - # raise warning, this is deprecated! - - """ - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - # turn args into kwargs - for arg, replacement in zip(args[skip:], mapping): - default_name = replacement[0] - if len(replacement) == 1: # just renaming, no special cases - if default_name in kwargs: - raise ValueError(f"Name collapse on {default_name}") - kwargs[default_name] = arg - else: - # check if we find a special name - name = None - for special_type, special_name in replacement[1].items(): - if isinstance(arg, special_type): - name = special_name - break - if name is None: - name = default_name - - if name in kwargs: - raise ValueError(f"Name collapse on {default_name}") - kwargs[name] = arg - - return func(*args[:skip], **kwargs) - - return wrapper - - return decorator diff --git a/releasenotes/notes/remove-algorithm-utils-707648b69af439dc.yaml b/releasenotes/notes/remove-algorithm-utils-707648b69af439dc.yaml index 3e0dd0863026..67c551dad70f 100644 --- a/releasenotes/notes/remove-algorithm-utils-707648b69af439dc.yaml +++ b/releasenotes/notes/remove-algorithm-utils-707648b69af439dc.yaml @@ -1,14 +1,27 @@ --- upgrade: - | - The following algorithm utilities in :mod:`qiskit.utils` have been removed - from the codebase: + The following tools in :mod:`qiskit.utils` have been removed from the codebase: + * Utils in ``qiskit.utils.arithmetic`` + * Utils in ``qiskit.utils.circuit_utils`` + * Utils in ``qiskit.utils.entangler_map`` + * Utils in ``qiskit.utils.name_unnamed_args`` + + These functions were used exclusively in the context of ``qiskit.algorithms`` and + ``qiskit.opflow``, and were deprecated in Qiskit 0.46. ``qiskit.algorithms`` and + ``qiskit.opflow`` have been deprecated since Qiskit 0.45 and Qiskit Terra 0.24 + respectively. + + The following utilities have also been removed: + + * Utils in ``qiskit.utils.validation`` * ``algorithm_globals`` - * ``qiskit.utils.validation`` - They were deprecated in Qiskit 0.45 as a consequence of the migration + These were deprecated in Qiskit 0.45 as a consequence of the migration of ``qiskit.algorithms`` to a standalone `package `_, where - these utils have also been migrated. The can be found in the new package + these utils have also been migrated. They can be found in the new package under ``qiskit_algorithms.utils``. + + diff --git a/test/python/test_util.py b/test/python/test_util.py index b574ff8390b0..d60aad284c18 100644 --- a/test/python/test_util.py +++ b/test/python/test_util.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2018. +# (C) Copyright IBM 2017, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,11 +13,9 @@ """Tests for qiskit/utils""" from unittest import mock -import numpy as np from qiskit.utils.multiprocessing import local_hardware_info from qiskit.test import QiskitTestCase -from qiskit.utils.arithmetic import triu_to_dense class TestUtil(QiskitTestCase): @@ -31,14 +29,3 @@ def test_local_hardware_none_cpu_count(self, cpu_count_mock, vmem_mock, platform del cpu_count_mock, vmem_mock, platform_mock # unused result = local_hardware_info() self.assertEqual(1, result["cpus"]) - - def test_triu_to_dense(self): - """Test conversion of upper triangular matrix to dense matrix.""" - np.random.seed(50) - n = np.random.randint(5, 15) - m = np.random.randint(-100, 100, size=(n, n)) - symm = (m + m.T) / 2 - - triu = [[symm[i, j] for i in range(j, n)] for j in range(n)] - - self.assertTrue(np.array_equal(symm, triu_to_dense(triu))) From 50e813746702a27954eeb700e5b43ebf3e1d8244 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 28 Nov 2023 15:11:41 +0000 Subject: [PATCH 005/124] Add representation of storage-owning `Var` nodes (#10944) * Add representation of storage-owning `Var` nodes This adds the representation of `expr.Var` nodes that own their own storage locations, and consequently are not backed by existing Qiskit objects (`Clbit` or `ClassicalRegister`). This is the base of the ability for Qiskit to represent manual classical-value storage in `QuantumCircuit`, and the base for how manual storage will be implemented. * Minor documentation tweaks Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/circuit/classical/expr/__init__.py | 15 +++++-- qiskit/circuit/classical/expr/expr.py | 43 ++++++++++++++++--- .../circuit/classical/test_expr_properties.py | 42 ++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index d2cd4bc5044e..b2b4d138fca7 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -39,10 +39,11 @@ These objects are mutable and should not be reused in a different location without a copy. -The entry point from general circuit objects to the expression system is by wrapping the object -in a :class:`Var` node and associating a :class:`~.types.Type` with it. +The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed runtime +variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. .. autoclass:: Var + :members: var, name Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes with associated types. @@ -86,10 +87,18 @@ The functions and methods described in this section are a more user-friendly way to build the expression tree, while staying close to the internal representation. All these functions will automatically lift valid Python scalar values into corresponding :class:`Var` or :class:`Value` -objects, and will resolve any required implicit casts on your behalf. +objects, and will resolve any required implicit casts on your behalf. If you want to directly use +some scalar value as an :class:`Expr` node, you can manually :func:`lift` it yourself. .. autofunction:: lift +Typically you should create memory-owning :class:`Var` instances by using the +:meth:`.QuantumCircuit.add_var` method to declare them in some circuit context, since a +:class:`.QuantumCircuit` will not accept an :class:`Expr` that contains variables that are not +already declared in it, since it needs to know how to allocate the storage and how the variable will +be initialized. However, should you want to do this manually, you should use the low-level +:meth:`Var.new` call to safely generate a named variable for usage. + You can manually specify casts in cases where the cast is allowed in explicit form, but may be lossy (such as the cast of a higher precision :class:`~.types.Uint` to a lower precision one). diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index b9e9aad4a2b7..3adbacfd6926 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -31,6 +31,7 @@ import abc import enum import typing +import uuid from .. import types @@ -108,24 +109,56 @@ def __repr__(self): @typing.final class Var(Expr): - """A classical variable.""" + """A classical variable. - __slots__ = ("var",) + These variables take two forms: a new-style variable that owns its storage location and has an + associated name; and an old-style variable that wraps a :class:`.Clbit` or + :class:`.ClassicalRegister` instance that is owned by some containing circuit. In general, + construction of variables for use in programs should use :meth:`Var.new` or + :meth:`.QuantumCircuit.add_var`.""" + + __slots__ = ("var", "name") def __init__( - self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister, type: types.Type + self, + var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, + type: types.Type, + *, + name: str | None = None, ): self.type = type self.var = var + """A reference to the backing data storage of the :class:`Var` instance. When lifting + old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`, + this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a + new-style classical variable (one that owns its own storage separate to the old + :class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID` + to uniquely identify it.""" + self.name = name + """The name of the variable. This is required to exist if the backing :attr:`var` attribute + is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is + an old-style variable.""" + + @classmethod + def new(cls, name: str, type: types.Type) -> typing.Self: + """Generate a new named variable that owns its own backing storage.""" + return cls(uuid.uuid4(), type, name=name) def accept(self, visitor, /): return visitor.visit_var(self) def __eq__(self, other): - return isinstance(other, Var) and self.type == other.type and self.var == other.var + return ( + isinstance(other, Var) + and self.type == other.type + and self.var == other.var + and self.name == other.name + ) def __repr__(self): - return f"Var({self.var}, {self.type})" + if self.name is None: + return f"Var({self.var}, {self.type})" + return f"Var({self.var}, {self.type}, name='{self.name}')" @typing.final diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index a6873153c0eb..efda6ba37758 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -56,3 +56,45 @@ def test_expr_can_be_cloned(self, obj): self.assertEqual(obj, copy.copy(obj)) self.assertEqual(obj, copy.deepcopy(obj)) self.assertEqual(obj, pickle.loads(pickle.dumps(obj))) + + def test_var_equality(self): + """Test that various types of :class:`.expr.Var` equality work as expected both in equal and + unequal cases.""" + var_a_bool = expr.Var.new("a", types.Bool()) + self.assertEqual(var_a_bool, var_a_bool) + + # Allocating a new variable should not compare equal, despite the name match. A semantic + # equality checker can choose to key these variables on only their names and types, if it + # knows that that check is valid within the semantic context. + self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool())) + + # Manually constructing the same object with the same UUID should cause it compare equal, + # though, for serialisation ease. + self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a")) + + # This is a badly constructed variable because it's using a different type to refer to the + # same storage location (the UUID) as another variable. It is an IR error to generate this + # sort of thing, but we can't fully be responsible for that and a pass would need to go out + # of its way to do this incorrectly, but we can still ensure that the direct equality check + # would spot the error. + self.assertNotEqual( + var_a_bool, expr.Var(var_a_bool.var, types.Uint(8), name=var_a_bool.name) + ) + + # This is also badly constructed because it uses a different name to refer to the "same" + # storage location. + self.assertNotEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="b")) + + # Obviously, two variables of different types and names should compare unequal. + self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(8))) + # As should two variables of the same name but different storage locations and types. + self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("a", types.Uint(8))) + + def test_var_uuid_clone(self): + """Test that :class:`.expr.Var` instances that have an associated UUID and name roundtrip + through pickle and copy operations to produce values that compare equal.""" + var_a_u8 = expr.Var.new("a", types.Uint(8)) + + self.assertEqual(var_a_u8, pickle.loads(pickle.dumps(var_a_u8))) + self.assertEqual(var_a_u8, copy.copy(var_a_u8)) + self.assertEqual(var_a_u8, copy.deepcopy(var_a_u8)) From 868599a8c4148104765f3dacd1a1e924eca2b3b8 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 28 Nov 2023 19:23:28 +0100 Subject: [PATCH 006/124] update docs link in the README file to new home (#11334) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 739a03ca4e8e..a153957cb221 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It also contains a transpiler that supports optimizing quantum circuits and a qu For more details on how to use Qiskit, refer to the documentation located here: - + ## Installation From 2cd5bbda39da32c778c8a9cd58b54967b5d7500a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 28 Nov 2023 19:12:48 -0500 Subject: [PATCH 007/124] Fix Sabre extended set population order (#10651) * Visit runs of 1Q gates before next 2Q. * Avoid allocation in loop. --- crates/accelerate/src/sabre_swap/mod.rs | 36 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/accelerate/src/sabre_swap/mod.rs b/crates/accelerate/src/sabre_swap/mod.rs index ef5ee9e35a0a..2d4dc40481e1 100644 --- a/crates/accelerate/src/sabre_swap/mod.rs +++ b/crates/accelerate/src/sabre_swap/mod.rs @@ -171,23 +171,35 @@ fn populate_extended_set( let mut decremented: IndexMap = IndexMap::with_hasher(ahash::RandomState::default()); let mut i = 0; + let mut visit_now: Vec = Vec::new(); while i < to_visit.len() && extended_set.len() < EXTENDED_SET_SIZE { - for edge in dag.dag.edges_directed(to_visit[i], Direction::Outgoing) { - let successor_node = edge.target(); - let successor_index = successor_node.index(); - *decremented.entry(successor_index).or_insert(0) += 1; - required_predecessors[successor_index] -= 1; - if required_predecessors[successor_index] == 0 { - if !dag.dag[successor_node].directive - && !dag.node_blocks.contains_key(&successor_index) - { - if let [a, b] = dag.dag[successor_node].qubits[..] { - extended_set.push([a.to_phys(layout), b.to_phys(layout)]); + // Visit runs of non-2Q gates fully before moving on to children + // of 2Q gates. This way, traversal order is a BFS of 2Q gates rather + // than of all gates. + visit_now.push(to_visit[i]); + let mut j = 0; + while let Some(node) = visit_now.get(j) { + for edge in dag.dag.edges_directed(*node, Direction::Outgoing) { + let successor_node = edge.target(); + let successor_index = successor_node.index(); + *decremented.entry(successor_index).or_insert(0) += 1; + required_predecessors[successor_index] -= 1; + if required_predecessors[successor_index] == 0 { + if !dag.dag[successor_node].directive + && !dag.node_blocks.contains_key(&successor_index) + { + if let [a, b] = dag.dag[successor_node].qubits[..] { + extended_set.push([a.to_phys(layout), b.to_phys(layout)]); + to_visit.push(successor_node); + continue; + } } + visit_now.push(successor_node); } - to_visit.push(successor_node); } + j += 1; } + visit_now.clear(); i += 1; } for (node, amount) in decremented.iter() { From c3a7d409323b67dcb66c8e9c7af2b8e33cf8b928 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 28 Nov 2023 20:42:55 -0500 Subject: [PATCH 008/124] Update DAGCircuit.draw() docstring with current requirements (#11337) * Update DAGCircuit.draw() docstring with current requirements Since #8162 the dag drawer hasn't required pydot to run. It now uses rustworkx's graphviz_draw() function which directly calls graphviz. However, in #8162 the docstring for the DAGCircuit.draw() method was not updated to reflect this and the method documentation still said that pydot was required. This commit fixes this oversight and updates the docstring to correctly state that only graphviz is required (as rustworkx is a hard dependency for Qiskit it's not anything that needs to be documented). It also includes more details on how to install graphviz as this is often a potential source of confusion for users. * Update qiskit/dagcircuit/dagcircuit.py Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/dagcircuit/dagcircuit.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 126f19f1bb5d..21e1464d06ed 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -2082,8 +2082,12 @@ def draw(self, scale=0.7, filename=None, style="color"): """ Draws the dag circuit. - This function needs `pydot `_, which in turn needs - `Graphviz `_ to be installed. + This function needs `Graphviz `_ to be + installed. Graphviz is not a python package and can't be pip installed + (the ``graphviz`` package on PyPI is a Python interface library for + Graphviz and does not actually install Graphviz). You can refer to + `the Graphviz documentation `__ on + how to install it. Args: scale (float): scaling factor From b72033e1cd2d37eaeaadde0c23e6ea76e44f7c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <99898527+grossardt@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:26:03 +0100 Subject: [PATCH 009/124] Add pow as supported operation for ParameterExpression (#11235) * Add pow for Parameter Adds powering for qiskit.circuit.ParameterExpression allowing to use a**n, n**a, a**b, pow(a,b) etc. for ParameterExpressions a, b and numbers n. Minimal change using default support of pow by Sympy/Symengine. Added pow to list of operators in TestParameterExpressions test case for unit testing. fixes #8959 Changelog: New Feature * Update test_parameters.py --- qiskit/circuit/parameterexpression.py | 6 ++++ .../add-parameter-pow-ff5f8d10813f5733.yaml | 7 ++++ test/python/circuit/test_parameters.py | 35 +++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-parameter-pow-ff5f8d10813f5733.yaml diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 790fc29b8135..237d6e9d8007 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -341,6 +341,12 @@ def __truediv__(self, other): def __rtruediv__(self, other): return self._apply_operation(operator.truediv, other, reflected=True) + def __pow__(self, other): + return self._apply_operation(pow, other) + + def __rpow__(self, other): + return self._apply_operation(pow, other, reflected=True) + def _call(self, ufunc): return ParameterExpression(self._parameter_symbols, ufunc(self._symbol_expr)) diff --git a/releasenotes/notes/add-parameter-pow-ff5f8d10813f5733.yaml b/releasenotes/notes/add-parameter-pow-ff5f8d10813f5733.yaml new file mode 100644 index 000000000000..094266071990 --- /dev/null +++ b/releasenotes/notes/add-parameter-pow-ff5f8d10813f5733.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + :class:`~qiskit.circuit.ParameterExpression` (and thus also + :class:`~qiskit.circuit.Parameter`) now support powering: :code:`x**y` + where :code:`x` and :code:`y` can be any combination of + :class:`~qiskit.circuit.ParameterExpression` and number types. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c694b39e6e7a..6a5f780a96af 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1332,7 +1332,14 @@ def _paramvec_names(prefix, length): class TestParameterExpressions(QiskitTestCase): """Test expressions of Parameters.""" - supported_operations = [add, sub, mul, truediv] + # supported operations dictionary operation : accuracy (0=exact match) + supported_operations = { + add: 0, + sub: 0, + mul: 0, + truediv: 0, + pow: 1e-12, + } def setUp(self): super().setUp() @@ -1504,12 +1511,19 @@ def test_expressions_of_parameter_with_constant(self): x = Parameter("x") - for op in self.supported_operations: + for op, rel_tol in self.supported_operations.items(): for const in good_constants: expr = op(const, x) bound_expr = expr.bind({x: 2.3}) - self.assertEqual(complex(bound_expr), op(const, 2.3)) + res = complex(bound_expr) + expected = op(const, 2.3) + if rel_tol > 0: + self.assertTrue( + cmath.isclose(res, expected, rel_tol=rel_tol), f"{res} != {expected}" + ) + else: + self.assertEqual(res, expected) # Division by zero will raise. Tested elsewhere. if const == 0 and op == truediv: @@ -1954,6 +1968,21 @@ def test_parameter_expression_grad(self): self.assertEqual(expr.gradient(x), 2 * x) self.assertEqual(expr.gradient(x).gradient(x), 2) + def test_parameter_expression_exp_log_vs_pow(self): + """Test exp, log, pow for ParameterExpressions by asserting x**y = exp(y log(x)).""" + + x = Parameter("x") + y = Parameter("y") + pow1 = x**y + pow2 = (y * x.log()).exp() + for x_val in [2, 1.3, numpy.pi]: + for y_val in [2, 1.3, 0, -1, -1.0, numpy.pi, 1j]: + with self.subTest(msg="with x={x_val}, y={y_val}"): + vals = {x: x_val, y: y_val} + pow1_val = pow1.bind(vals) + pow2_val = pow2.bind(vals) + self.assertTrue(cmath.isclose(pow1_val, pow2_val), f"{pow1_val} != {pow2_val}") + def test_bound_expression_is_real(self): """Test is_real on bound parameters.""" x = Parameter("x") From 2e640b2bae84bc775659a715eb64a6de274111a6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Nov 2023 03:26:22 -0500 Subject: [PATCH 010/124] Remove legacy qasm2 parser (#11308) * Remove legacy qasm2 parser This commit removes the legacy qasm2 parser. This was the original qasm parser in qiskit dating back to the very beginning of the project, but it has been superseded in recent releases by the faster more correct/strict rust parser. We no longer need to keep around two parsers for qasm2. This commit removes the legacy one and it's associated functions (like the ast converter). * Remove api docs too * Remove ast converter benchmark * Remove QuantumCircuit.qasm() exporter method This commit removes the QuantumCircuit.qasm() method as part of the legacy parser removal. The exporter method was tied to some of the internals in the now removed `qiskit.qasm` module. The qasm() method was already queued up for eventual removal, this commit just pushes that forward so we don't have to add more comptaibility code into it to keep it working as is without the qiskit.qasm module existing anymore. * Remove jupyter qasm widget * Update tests * Remove BlueprintCircuit .qasm() override * Fix rst syntax in release note * Apply suggestions from code review Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- docs/apidoc/index.rst | 1 - docs/apidoc/qasm.rst | 6 - qiskit/circuit/library/blueprintcircuit.py | 5 - qiskit/circuit/quantumcircuit.py | 60 - qiskit/converters/__init__.py | 2 - qiskit/converters/ast_to_dag.py | 418 ------ qiskit/qasm/__init__.py | 53 - qiskit/qasm/exceptions.py | 16 - qiskit/qasm/node/__init__.py | 41 - qiskit/qasm/node/barrier.py | 30 - qiskit/qasm/node/binaryop.py | 59 - qiskit/qasm/node/binaryoperator.py | 52 - qiskit/qasm/node/cnot.py | 31 - qiskit/qasm/node/creg.py | 45 - qiskit/qasm/node/customunitary.py | 49 - qiskit/qasm/node/expressionlist.py | 33 - qiskit/qasm/node/external.py | 87 -- qiskit/qasm/node/format.py | 37 - qiskit/qasm/node/gate.py | 62 - qiskit/qasm/node/gatebody.py | 41 - qiskit/qasm/node/id.py | 78 -- qiskit/qasm/node/idlist.py | 33 - qiskit/qasm/node/if_.py | 39 - qiskit/qasm/node/indexedid.py | 41 - qiskit/qasm/node/intnode.py | 51 - qiskit/qasm/node/measure.py | 30 - qiskit/qasm/node/node.py | 59 - qiskit/qasm/node/nodeexception.py | 26 - qiskit/qasm/node/opaque.py | 58 - qiskit/qasm/node/prefix.py | 54 - qiskit/qasm/node/primarylist.py | 33 - qiskit/qasm/node/program.py | 32 - qiskit/qasm/node/qreg.py | 45 - qiskit/qasm/node/real.py | 63 - qiskit/qasm/node/reset.py | 29 - qiskit/qasm/node/unaryoperator.py | 49 - qiskit/qasm/node/universalunitary.py | 32 - qiskit/qasm/pygments/__init__.py | 34 - qiskit/qasm/pygments/lexer.py | 133 -- qiskit/qasm/qasm.py | 53 - qiskit/qasm/qasmlexer.py | 203 --- qiskit/qasm/qasmparser.py | 1156 ----------------- qiskit/qasm2/__init__.py | 13 - qiskit/tools/jupyter/library.py | 66 +- ...-legacy-qasm2-parser-53ad3f1817fd68cc.yaml | 34 + requirements-optional.txt | 1 - requirements.txt | 1 - setup.py | 1 - test/benchmarks/converters.py | 5 - test/python/basicaer/test_qasm_simulator.py | 3 +- .../circuit/library/test_permutation.py | 5 +- test/python/circuit/test_circuit_qasm.py | 160 +-- test/python/circuit/test_circuit_registers.py | 3 +- test/python/circuit/test_unitary.py | 23 +- test/python/compiler/test_compiler.py | 5 +- test/python/converters/test_ast_to_dag.py | 67 - test/python/qasm2/test_circuit_methods.py | 24 +- test/python/qasm2/test_legacy_importer.py | 508 -------- test/python/test_qasm_parser.py | 126 -- 59 files changed, 141 insertions(+), 4363 deletions(-) delete mode 100644 docs/apidoc/qasm.rst delete mode 100644 qiskit/converters/ast_to_dag.py delete mode 100644 qiskit/qasm/__init__.py delete mode 100644 qiskit/qasm/exceptions.py delete mode 100644 qiskit/qasm/node/__init__.py delete mode 100644 qiskit/qasm/node/barrier.py delete mode 100644 qiskit/qasm/node/binaryop.py delete mode 100644 qiskit/qasm/node/binaryoperator.py delete mode 100644 qiskit/qasm/node/cnot.py delete mode 100644 qiskit/qasm/node/creg.py delete mode 100644 qiskit/qasm/node/customunitary.py delete mode 100644 qiskit/qasm/node/expressionlist.py delete mode 100644 qiskit/qasm/node/external.py delete mode 100644 qiskit/qasm/node/format.py delete mode 100644 qiskit/qasm/node/gate.py delete mode 100644 qiskit/qasm/node/gatebody.py delete mode 100644 qiskit/qasm/node/id.py delete mode 100644 qiskit/qasm/node/idlist.py delete mode 100644 qiskit/qasm/node/if_.py delete mode 100644 qiskit/qasm/node/indexedid.py delete mode 100644 qiskit/qasm/node/intnode.py delete mode 100644 qiskit/qasm/node/measure.py delete mode 100644 qiskit/qasm/node/node.py delete mode 100644 qiskit/qasm/node/nodeexception.py delete mode 100644 qiskit/qasm/node/opaque.py delete mode 100644 qiskit/qasm/node/prefix.py delete mode 100644 qiskit/qasm/node/primarylist.py delete mode 100644 qiskit/qasm/node/program.py delete mode 100644 qiskit/qasm/node/qreg.py delete mode 100644 qiskit/qasm/node/real.py delete mode 100644 qiskit/qasm/node/reset.py delete mode 100644 qiskit/qasm/node/unaryoperator.py delete mode 100644 qiskit/qasm/node/universalunitary.py delete mode 100644 qiskit/qasm/pygments/__init__.py delete mode 100644 qiskit/qasm/pygments/lexer.py delete mode 100644 qiskit/qasm/qasm.py delete mode 100644 qiskit/qasm/qasmlexer.py delete mode 100644 qiskit/qasm/qasmparser.py create mode 100644 releasenotes/notes/remove-legacy-qasm2-parser-53ad3f1817fd68cc.yaml delete mode 100644 test/python/converters/test_ast_to_dag.py delete mode 100644 test/python/qasm2/test_legacy_importer.py delete mode 100644 test/python/test_qasm_parser.py diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index eb5ed9e3c2c5..9d1eb2f64960 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -30,7 +30,6 @@ API Reference primitives qasm2 qasm3 - qasm qobj qpy quantum_info diff --git a/docs/apidoc/qasm.rst b/docs/apidoc/qasm.rst deleted file mode 100644 index c1fea25947bb..000000000000 --- a/docs/apidoc/qasm.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-qasm: - -.. automodule:: qiskit.qasm - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 51f6e5826bfc..4244a1f2340f 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -122,11 +122,6 @@ def parameters(self) -> ParameterView: self._build() return super().parameters - def qasm(self, formatted=False, filename=None, encoding=None): - if not self._is_built: - self._build() - return super().qasm(formatted, filename, encoding) - def _append(self, instruction, _qargs=None, _cargs=None): if not self._is_built: self._build() diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4d406daf8189..cb164404ac1e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -44,7 +44,6 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameter import Parameter from qiskit.circuit.exceptions import CircuitError -from qiskit.utils import optionals as _optionals from qiskit.utils.deprecation import deprecate_func from . import _classical_resource_map from ._utils import sort_parameters @@ -1619,65 +1618,6 @@ def decompose( # do not copy operations, this is done in the conversion with circuit_to_dag return dag_to_circuit(dag, copy_operations=False) - def qasm( - self, - formatted: bool = False, - filename: str | None = None, - encoding: str | None = None, - ) -> str | None: - """Return OpenQASM 2.0 string. - - .. seealso:: - - :func:`.qasm2.dump` and :func:`.qasm2.dumps` - The preferred entry points to the OpenQASM 2 export capabilities. These match the - interface for other serialisers in Qiskit. - - Args: - formatted (bool): Return formatted OpenQASM 2.0 string. - filename (str): Save OpenQASM 2.0 to file with name 'filename'. - encoding (str): Optionally specify the encoding to use for the - output file if ``filename`` is specified. By default this is - set to the system's default encoding (ie whatever - ``locale.getpreferredencoding()`` returns) and can be set to - any valid codec or alias from stdlib's - `codec module `__ - - Returns: - str: If formatted=False. - - Raises: - MissingOptionalLibraryError: If pygments is not installed and ``formatted`` is - ``True``. - QASM2ExportError: If circuit has free parameters. - QASM2ExportError: If an operation that has no OpenQASM 2 representation is encountered. - """ - from qiskit import qasm2 # pylint: disable=cyclic-import - - out = qasm2.dumps(self) - if filename is not None: - with open(filename, "w+", encoding=encoding) as file: - print(out, file=file) - - if formatted: - _optionals.HAS_PYGMENTS.require_now("formatted OpenQASM 2.0 output") - - import pygments - from pygments.formatters import ( # pylint: disable=no-name-in-module - Terminal256Formatter, - ) - from qiskit.qasm.pygments import OpenQASMLexer - from qiskit.qasm.pygments import QasmTerminalStyle - - code = pygments.highlight( - out, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle) - ) - print(code) - return None - # The old `QuantumCircuit.qasm()` method included a terminating new line that `qasm2.dumps` - # doesn't, so for full compatibility we add it back here. - return out + "\n" - def draw( self, output: str | None = None, diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index afac4cd2231f..459b739ee011 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -21,7 +21,6 @@ .. autofunction:: dag_to_circuit .. autofunction:: circuit_to_instruction .. autofunction:: circuit_to_gate -.. autofunction:: ast_to_dag .. autofunction:: dagdependency_to_circuit .. autofunction:: circuit_to_dagdependency .. autofunction:: dag_to_dagdependency @@ -32,7 +31,6 @@ from .dag_to_circuit import dag_to_circuit from .circuit_to_instruction import circuit_to_instruction from .circuit_to_gate import circuit_to_gate -from .ast_to_dag import ast_to_dag from .circuit_to_dagdependency import circuit_to_dagdependency from .dagdependency_to_circuit import dagdependency_to_circuit from .dag_to_dagdependency import dag_to_dagdependency diff --git a/qiskit/converters/ast_to_dag.py b/qiskit/converters/ast_to_dag.py deleted file mode 100644 index 4cd60f56573c..000000000000 --- a/qiskit/converters/ast_to_dag.py +++ /dev/null @@ -1,418 +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. - -""" -AST (abstract syntax tree) to DAG (directed acyclic graph) converter. - -Acts as an OpenQASM interpreter. -""" -from collections import OrderedDict -from qiskit.dagcircuit import DAGCircuit -from qiskit.exceptions import QiskitError - -from qiskit.circuit import QuantumRegister, ClassicalRegister, Gate, QuantumCircuit -from qiskit.qasm.node.real import Real -from qiskit.circuit.measure import Measure -from qiskit.circuit.reset import Reset -from qiskit.circuit.barrier import Barrier -from qiskit.circuit.delay import Delay -from qiskit.circuit.library import standard_gates as std - - -def ast_to_dag(ast): - """Build a ``DAGCircuit`` object from an AST ``Node`` object. - - Args: - ast (Program): a Program Node of an AST (parser's output) - - Return: - DAGCircuit: the DAG representing an OpenQASM's AST - - Raises: - QiskitError: if the AST is malformed. - - Example: - .. code-block:: - - from qiskit.converters import ast_to_dag - from qiskit import qasm, QuantumCircuit, ClassicalRegister, QuantumRegister - - q = QuantumRegister(3, 'q') - c = ClassicalRegister(3, 'c') - circ = QuantumCircuit(q, c) - circ.h(q[0]) - circ.cx(q[0], q[1]) - circ.measure(q[0], c[0]) - circ.rz(0.5, q[1]).c_if(c, 2) - qasm_str = circ.qasm() - ast = qasm.Qasm(data=qasm_str).parse() - dag = ast_to_dag(ast) - """ - dag = DAGCircuit() - AstInterpreter(dag)._process_node(ast) - - return dag - - -class AstInterpreter: - """Interprets an OpenQASM by expanding subroutines and unrolling loops.""" - - standard_extension = { - "u1": std.U1Gate, - "u2": std.U2Gate, - "u3": std.U3Gate, - "u": std.UGate, - "p": std.PhaseGate, - "x": std.XGate, - "y": std.YGate, - "z": std.ZGate, - "t": std.TGate, - "tdg": std.TdgGate, - "s": std.SGate, - "sdg": std.SdgGate, - "sx": std.SXGate, - "sxdg": std.SXdgGate, - "swap": std.SwapGate, - "rx": std.RXGate, - "rxx": std.RXXGate, - "ry": std.RYGate, - "rz": std.RZGate, - "rzz": std.RZZGate, - "id": std.IGate, - "h": std.HGate, - "cx": std.CXGate, - "cy": std.CYGate, - "cz": std.CZGate, - "ch": std.CHGate, - "crx": std.CRXGate, - "cry": std.CRYGate, - "crz": std.CRZGate, - "csx": std.CSXGate, - "cu1": std.CU1Gate, - "cp": std.CPhaseGate, - "cu": std.CUGate, - "cu3": std.CU3Gate, - "ccx": std.CCXGate, - "cswap": std.CSwapGate, - "delay": Delay, - "rccx": std.RCCXGate, - "rc3x": std.RC3XGate, - "c3x": std.C3XGate, - "c3sqrtx": std.C3SXGate, - "c4x": std.C4XGate, - } - - def __init__(self, dag): - """Initialize interpreter's data.""" - # DAG object to populate - self.dag = dag - # OPENQASM version number (ignored for now) - self.version = 0.0 - # Dict of gates names and properties - self.gates = OrderedDict() - # Keeping track of conditional gates - self.condition = None - # List of dictionaries mapping local parameter ids to expression Nodes - self.arg_stack = [{}] - # List of dictionaries mapping local bit ids to global ids (name, idx) - self.bit_stack = [{}] - - def _process_bit_id(self, node): - """Process an Id or IndexedId node as a bit or register type. - - Return a list of tuples (Register,index). - """ - reg = None - - if node.name in self.dag.qregs: - reg = self.dag.qregs[node.name] - elif node.name in self.dag.cregs: - reg = self.dag.cregs[node.name] - else: - raise QiskitError( - "expected qreg or creg name:", "line=%s" % node.line, "file=%s" % node.file - ) - - if node.type == "indexed_id": - # An indexed bit or qubit - return [reg[node.index]] - elif node.type == "id": - # A qubit or qreg or creg - if not self.bit_stack[-1]: - # Global scope - return list(reg) - else: - # local scope - if node.name in self.bit_stack[-1]: - return [self.bit_stack[-1][node.name]] - raise QiskitError( - "expected local bit name:", "line=%s" % node.line, "file=%s" % node.file - ) - return None - - def _process_custom_unitary(self, node): - """Process a custom unitary node.""" - name = node.name - if node.arguments is not None: - args = self._process_node(node.arguments) - else: - args = [] - bits = [self._process_bit_id(node_element) for node_element in node.bitlist.children] - - if name in self.gates: - self._arguments(name, bits, args) - else: - raise QiskitError( - "internal error undefined gate:", "line=%s" % node.line, "file=%s" % node.file - ) - - def _process_u(self, node): - """Process a U gate node.""" - args = self._process_node(node.arguments) - bits = [self._process_bit_id(node.bitlist)] - - self._arguments("u", bits, args) - - def _arguments(self, name, bits, args): - gargs = self.gates[name]["args"] - gbits = self.gates[name]["bits"] - - maxidx = max(map(len, bits)) - for idx in range(maxidx): - self.arg_stack.append({gargs[j]: args[j] for j in range(len(gargs))}) - # Only index into register arguments. - element = [idx * x for x in [len(bits[j]) > 1 for j in range(len(bits))]] - self.bit_stack.append({gbits[j]: bits[j][element[j]] for j in range(len(gbits))}) - self._create_dag_op( - name, - [self.arg_stack[-1][s].sym() for s in gargs], - [self.bit_stack[-1][s] for s in gbits], - ) - self.arg_stack.pop() - self.bit_stack.pop() - - def _process_gate(self, node, opaque=False): - """Process a gate node. - - If opaque is True, process the node as an opaque gate node. - """ - self.gates[node.name] = {} - de_gate = self.gates[node.name] - de_gate["print"] = True # default - de_gate["opaque"] = opaque - de_gate["n_args"] = node.n_args() - de_gate["n_bits"] = node.n_bits() - if node.n_args() > 0: - de_gate["args"] = [element.name for element in node.arguments.children] - else: - de_gate["args"] = [] - de_gate["bits"] = [c.name for c in node.bitlist.children] - if node.name in self.standard_extension: - return - if opaque: - de_gate["body"] = None - else: - de_gate["body"] = node.body - - def _process_cnot(self, node): - """Process a CNOT gate node.""" - id0 = self._process_bit_id(node.children[0]) - id1 = self._process_bit_id(node.children[1]) - if not (len(id0) == len(id1) or len(id0) == 1 or len(id1) == 1): - raise QiskitError( - "internal error: qreg size mismatch", "line=%s" % node.line, "file=%s" % node.file - ) - maxidx = max([len(id0), len(id1)]) - for idx in range(maxidx): - cx_gate = std.CXGate() - if self.condition: - cx_gate = cx_gate.c_if(*self.condition) - if len(id0) > 1 and len(id1) > 1: - self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [], check=False) - elif len(id0) > 1: - self.dag.apply_operation_back(cx_gate, [id0[idx], id1[0]], [], check=False) - else: - self.dag.apply_operation_back(cx_gate, [id0[0], id1[idx]], [], check=False) - - def _process_measure(self, node): - """Process a measurement node.""" - id0 = self._process_bit_id(node.children[0]) - id1 = self._process_bit_id(node.children[1]) - if len(id0) != len(id1): - raise QiskitError( - "internal error: reg size mismatch", "line=%s" % node.line, "file=%s" % node.file - ) - for idx, idy in zip(id0, id1): - meas_gate = Measure() - if self.condition: - meas_gate = meas_gate.c_if(*self.condition) - self.dag.apply_operation_back(meas_gate, [idx], [idy], check=False) - - def _process_if(self, node): - """Process an if node.""" - creg_name = node.children[0].name - creg = self.dag.cregs[creg_name] - cval = node.children[1].value - self.condition = (creg, cval) - self._process_node(node.children[2]) - self.condition = None - - def _process_children(self, node): - """Call process_node for all children of node.""" - for kid in node.children: - self._process_node(kid) - - def _process_node(self, node): - """Carry out the action associated with a node.""" - if node.type == "program": - self._process_children(node) - - elif node.type == "qreg": - qreg = QuantumRegister(node.index, node.name) - self.dag.add_qreg(qreg) - - elif node.type == "creg": - creg = ClassicalRegister(node.index, node.name) - self.dag.add_creg(creg) - - elif node.type == "id": - raise QiskitError("internal error: _process_node on id") - - elif node.type == "int": - raise QiskitError("internal error: _process_node on int") - - elif node.type == "real": - raise QiskitError("internal error: _process_node on real") - - elif node.type == "indexed_id": - raise QiskitError("internal error: _process_node on indexed_id") - - elif node.type == "id_list": - # We process id_list nodes when they are leaves of barriers. - return [self._process_bit_id(node_children) for node_children in node.children] - - elif node.type == "primary_list": - # We should only be called for a barrier. - return [self._process_bit_id(m) for m in node.children] - - elif node.type == "gate": - self._process_gate(node) - - elif node.type == "custom_unitary": - self._process_custom_unitary(node) - - elif node.type == "universal_unitary": - self._process_u(node) - - elif node.type == "cnot": - self._process_cnot(node) - - elif node.type == "expression_list": - return node.children - - elif node.type == "binop": - raise QiskitError("internal error: _process_node on binop") - - elif node.type == "prefix": - raise QiskitError("internal error: _process_node on prefix") - - elif node.type == "measure": - self._process_measure(node) - - elif node.type == "format": - self.version = node.version() - - elif node.type == "barrier": - ids = self._process_node(node.children[0]) - qubits = [] - for qubit in ids: - for j, _ in enumerate(qubit): - qubits.append(qubit[j]) - self.dag.apply_operation_back(Barrier(len(qubits)), qubits, [], check=False) - - elif node.type == "reset": - id0 = self._process_bit_id(node.children[0]) - for i, _ in enumerate(id0): - reset = Reset() - if self.condition: - reset = reset.c_if(*self.condition) - self.dag.apply_operation_back(reset, [id0[i]], [], check=False) - - elif node.type == "if": - self._process_if(node) - - elif node.type == "opaque": - self._process_gate(node, opaque=True) - - elif node.type == "external": - raise QiskitError("internal error: _process_node on external") - - else: - raise QiskitError( - "internal error: undefined node type", - node.type, - "line=%s" % node.line, - "file=%s" % node.file, - ) - return None - - def _gate_rules_to_qiskit_circuit(self, node, params): - """From a gate definition in OpenQASM, to a QuantumCircuit format.""" - rules = [] - qreg = QuantumRegister(node["n_bits"]) - bit_args = {node["bits"][i]: q for i, q in enumerate(qreg)} - exp_args = {node["args"][i]: Real(q) for i, q in enumerate(params)} - - for child_op in node["body"].children: - qparams = [] - eparams = [] - for param_list in child_op.children[1:]: - if param_list.type == "id_list": - qparams = [bit_args[param.name] for param in param_list.children] - elif param_list.type == "expression_list": - for param in param_list.children: - eparams.append(param.sym(nested_scope=[exp_args])) - op = self._create_op(child_op.name, params=eparams) - rules.append((op, qparams, [])) - circ = QuantumCircuit(qreg) - for instr, qargs, cargs in rules: - circ._append(instr, qargs, cargs) - return circ - - def _create_dag_op(self, name, params, qargs): - """ - Create a DAG node out of a parsed AST op node. - - Args: - name (str): operation name to apply to the DAG - params (list): op parameters - qargs (list(Qubit)): qubits to attach to - - Raises: - QiskitError: if encountering a non-basis opaque gate - """ - op = self._create_op(name, params) - if self.condition: - op = op.c_if(*self.condition) - self.dag.apply_operation_back(op, qargs, [], check=False) - - def _create_op(self, name, params): - if name in self.standard_extension: - op = self.standard_extension[name](*params) - elif name in self.gates: - op = Gate(name=name, num_qubits=self.gates[name]["n_bits"], params=params) - if not self.gates[name]["opaque"]: - # call a custom gate (otherwise, opaque) - op.definition = self._gate_rules_to_qiskit_circuit(self.gates[name], params=params) - else: - raise QiskitError("unknown operation for ast node name %s" % name) - return op diff --git a/qiskit/qasm/__init__.py b/qiskit/qasm/__init__.py deleted file mode 100644 index 322d230343c6..000000000000 --- a/qiskit/qasm/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -""" -========================= -Qasm (:mod:`qiskit.qasm`) -========================= - -.. currentmodule:: qiskit.qasm - -QASM Routines -============= - -.. autoclass:: Qasm - - -Pygments -======== - -.. autoclass:: OpenQASMLexer - :class-doc-from: class - -.. autoclass:: QasmHTMLStyle - :class-doc-from: class - -.. autoclass:: QasmTerminalStyle - :class-doc-from: class -""" - -from numpy import pi - -from qiskit.utils.optionals import HAS_PYGMENTS - -from .qasm import Qasm -from .exceptions import QasmError - - -def __getattr__(name): - if name in ("OpenQASMLexer", "QasmHTMLStyle", "QasmTerminalStyle"): - import qiskit.qasm.pygments - - return getattr(qiskit.qasm.pygments, name) - - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/qiskit/qasm/exceptions.py b/qiskit/qasm/exceptions.py deleted file mode 100644 index 7bc40db35b8b..000000000000 --- a/qiskit/qasm/exceptions.py +++ /dev/null @@ -1,16 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Exception for errors raised while handling OpenQASM 2.0.""" - -# Re-export from the new place to ensure that old code continues to work. -from qiskit.qasm2.exceptions import QASM2Error as QasmError # pylint: disable=unused-import diff --git a/qiskit/qasm/node/__init__.py b/qiskit/qasm/node/__init__.py deleted file mode 100644 index 09e24db01bf5..000000000000 --- a/qiskit/qasm/node/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""OpenQASM 2 nodes.""" - -from .barrier import Barrier -from .binaryop import BinaryOp -from .binaryoperator import BinaryOperator -from .cnot import Cnot -from .creg import Creg -from .customunitary import CustomUnitary -from .expressionlist import ExpressionList -from .external import External -from .gate import Gate -from .gatebody import GateBody -from .id import Id -from .idlist import IdList -from .if_ import If -from .indexedid import IndexedId -from .intnode import Int -from .format import Format -from .measure import Measure -from .opaque import Opaque -from .prefix import Prefix -from .primarylist import PrimaryList -from .program import Program -from .qreg import Qreg -from .real import Real -from .reset import Reset -from .unaryoperator import UnaryOperator -from .universalunitary import UniversalUnitary -from .nodeexception import NodeException diff --git a/qiskit/qasm/node/barrier.py b/qiskit/qasm/node/barrier.py deleted file mode 100644 index afa16f2f9222..000000000000 --- a/qiskit/qasm/node/barrier.py +++ /dev/null @@ -1,30 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM barrier statement.""" - -from .node import Node - - -class Barrier(Node): - """Node for an OPENQASM barrier statement. - - children[0] is a primarylist node. - """ - - def __init__(self, children): - """Create the barrier node.""" - super().__init__("barrier", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "barrier " + self.children[0].qasm() + ";" diff --git a/qiskit/qasm/node/binaryop.py b/qiskit/qasm/node/binaryop.py deleted file mode 100644 index 45d4de4364d0..000000000000 --- a/qiskit/qasm/node/binaryop.py +++ /dev/null @@ -1,59 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM binary operation expression.""" - -from qiskit.exceptions import MissingOptionalLibraryError -from .node import Node - - -class BinaryOp(Node): - """Node for an OPENQASM binary operation expression. - - children[0] is the operation, as a binary operator node. - children[1] is the left expression. - children[2] is the right expression. - """ - - def __init__(self, children): - """Create the binaryop node.""" - super().__init__("binop", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return ( - "(" + self.children[1].qasm() + self.children[0].value + self.children[2].qasm() + ")" - ) - - def latex(self): - """Return the corresponding math mode latex string.""" - try: - from pylatexenc.latexencode import utf8tolatex - except ImportError as ex: - raise MissingOptionalLibraryError( - "pylatexenc", "latex-from-qasm exporter", "pip install pylatexenc" - ) from ex - return utf8tolatex(self.sym()) - - def real(self): - """Return the correspond floating point number.""" - operation = self.children[0].operation() - lhs = self.children[1].real() - rhs = self.children[2].real() - return operation(lhs, rhs) - - def sym(self, nested_scope=None): - """Return the correspond symbolic number.""" - operation = self.children[0].operation() - lhs = self.children[1].sym(nested_scope) - rhs = self.children[2].sym(nested_scope) - return operation(lhs, rhs) diff --git a/qiskit/qasm/node/binaryoperator.py b/qiskit/qasm/node/binaryoperator.py deleted file mode 100644 index 57e7d883c547..000000000000 --- a/qiskit/qasm/node/binaryoperator.py +++ /dev/null @@ -1,52 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM binary operator.""" - -import operator - -from .node import Node -from .nodeexception import NodeException - - -VALID_OPERATORS = { - "+": operator.add, - "-": operator.sub, - "*": operator.mul, - "/": operator.truediv, - "^": operator.pow, -} - - -class BinaryOperator(Node): - """Node for an OPENQASM binary operator. - - This node has no children. The data is in the value field. - """ - - def __init__(self, operation): - """Create the operator node.""" - super().__init__("operator", None, None) - self.value = operation - - def operation(self): - """ - Return the operator as a function f(left, right). - """ - try: - return VALID_OPERATORS[self.value] - except KeyError as ex: - raise NodeException(f"internal error: undefined operator '{self.value}'") from ex - - def qasm(self): - """Return the OpenQASM 2 representation.""" - return self.value diff --git a/qiskit/qasm/node/cnot.py b/qiskit/qasm/node/cnot.py deleted file mode 100644 index 3034d0ca8e32..000000000000 --- a/qiskit/qasm/node/cnot.py +++ /dev/null @@ -1,31 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM CNOT statement.""" - -from .node import Node - - -class Cnot(Node): - """Node for an OPENQASM CNOT statement. - - children[0], children[1] are id nodes if CX is inside a gate body, - otherwise they are primary nodes. - """ - - def __init__(self, children): - """Create the cnot node.""" - super().__init__("cnot", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "CX " + self.children[0].qasm() + "," + self.children[1].qasm() + ";" diff --git a/qiskit/qasm/node/creg.py b/qiskit/qasm/node/creg.py deleted file mode 100644 index 6a6bac6cb400..000000000000 --- a/qiskit/qasm/node/creg.py +++ /dev/null @@ -1,45 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM creg statement.""" -from .node import Node - - -class Creg(Node): - """Node for an OPENQASM creg statement. - - children[0] is an indexedid node. - """ - - def __init__(self, children): - """Create the creg node.""" - super().__init__("creg", children, None) - # This is the indexed id, the full "id[n]" object - self.id = children[0] # pylint: disable=invalid-name - # Name of the creg - self.name = self.id.name - # Source line number - self.line = self.id.line - # Source file name - self.file = self.id.file - # Size of the register - self.index = self.id.index - - def to_string(self, indent): - """Print the node data, with indent.""" - ind = indent * " " - print(ind, "creg") - self.children[0].to_string(indent + 3) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "creg " + self.id.qasm() + ";" diff --git a/qiskit/qasm/node/customunitary.py b/qiskit/qasm/node/customunitary.py deleted file mode 100644 index 393cf160f3af..000000000000 --- a/qiskit/qasm/node/customunitary.py +++ /dev/null @@ -1,49 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM custom gate statement.""" -from .node import Node - - -class CustomUnitary(Node): - """Node for an OPENQASM custom gate statement. - - children[0] is an id node. - children[1] is an exp_list (if len==3) or primary_list. - children[2], if present, is a primary_list. - - Has properties: - .id = id node - .name = gate name string - .arguments = None or exp_list node - .bitlist = primary_list node - """ - - def __init__(self, children): - """Create the custom gate node.""" - super().__init__("custom_unitary", children, None) - self.id = children[0] # pylint: disable=invalid-name - self.name = self.id.name - if len(children) == 3: - self.arguments = children[1] - self.bitlist = children[2] - else: - self.arguments = None - self.bitlist = children[1] - - def qasm(self): - """Return the corresponding OPENQASM string.""" - string = self.name - if self.arguments is not None: - string += "(" + self.arguments.qasm() + ")" - string += " " + self.bitlist.qasm() + ";" - return string diff --git a/qiskit/qasm/node/expressionlist.py b/qiskit/qasm/node/expressionlist.py deleted file mode 100644 index bef4f7b2e268..000000000000 --- a/qiskit/qasm/node/expressionlist.py +++ /dev/null @@ -1,33 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM expression list.""" -from .node import Node - - -class ExpressionList(Node): - """Node for an OPENQASM expression list. - - children are expression nodes. - """ - - def __init__(self, children): - """Create the expression list node.""" - super().__init__("expression_list", children, None) - - def size(self): - """Return the number of expressions.""" - return len(self.children) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return ",".join([self.children[j].qasm() for j in range(self.size())]) diff --git a/qiskit/qasm/node/external.py b/qiskit/qasm/node/external.py deleted file mode 100644 index 2aecbf26bb62..000000000000 --- a/qiskit/qasm/node/external.py +++ /dev/null @@ -1,87 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM external function.""" - -import numpy as np - -from qiskit.exceptions import MissingOptionalLibraryError -from .node import Node -from .nodeexception import NodeException - - -class External(Node): - """Node for an OPENQASM external function. - - children[0] is an id node with the name of the function. - children[1] is an expression node. - """ - - def __init__(self, children): - """Create the external node.""" - super().__init__("external", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return self.children[0].qasm() + "(" + self.children[1].qasm() + ")" - - def latex(self): - """Return the corresponding math mode latex string.""" - try: - from pylatexenc.latexencode import utf8tolatex - except ImportError as ex: - raise MissingOptionalLibraryError( - "pylatexenc", "latex-from-qasm exporter", "pip install pylatexenc" - ) from ex - return utf8tolatex(self.sym()) - - def real(self, nested_scope=None): - """Return the correspond floating point number.""" - op = self.children[0].name - expr = self.children[1] - dispatch = { - "sin": np.sin, - "cos": np.cos, - "tan": np.tan, - "asin": np.arcsin, - "acos": np.arccos, - "atan": np.arctan, - "exp": np.exp, - "ln": np.log, - "sqrt": np.sqrt, - } - if op in dispatch: - arg = expr.real(nested_scope) - return dispatch[op](arg) - else: - raise NodeException("internal error: undefined external") - - def sym(self, nested_scope=None): - """Return the corresponding symbolic expression.""" - op = self.children[0].name - expr = self.children[1] - dispatch = { - "sin": np.sin, - "cos": np.cos, - "tan": np.tan, - "asin": np.arcsin, - "acos": np.arccos, - "atan": np.arctan, - "exp": np.exp, - "ln": np.log, - "sqrt": np.sqrt, - } - if op in dispatch: - arg = expr.sym(nested_scope) - return dispatch[op](arg) - else: - raise NodeException("internal error: undefined external") diff --git a/qiskit/qasm/node/format.py b/qiskit/qasm/node/format.py deleted file mode 100644 index 3ced9ca08a6a..000000000000 --- a/qiskit/qasm/node/format.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM file identifier/version statement.""" - -import re - -from .node import Node - - -class Format(Node): - """Node for an OPENQASM file identifier/version statement.""" - - def __init__(self, value): - """Create the version node.""" - super().__init__("format", None, None) - parts = re.match(r"(\w+)\s+(\d+)(\.(\d+))?", value) - self.language = parts.group(1) - self.majorversion = parts.group(2) - self.minorversion = parts.group(4) if parts.group(4) is not None else "0" - - def version(self): - """Return the version.""" - return f"{self.majorversion}.{self.minorversion}" - - def qasm(self): - """Return the corresponding format string.""" - return f"{self.language} {self.version()};" diff --git a/qiskit/qasm/node/gate.py b/qiskit/qasm/node/gate.py deleted file mode 100644 index 122bd4f935df..000000000000 --- a/qiskit/qasm/node/gate.py +++ /dev/null @@ -1,62 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM gate definition.""" -from .node import Node - - -class Gate(Node): - """Node for an OPENQASM gate definition. - - children[0] is an id node. - If len(children) is 3, children[1] is an idlist node, - and children[2] is a gatebody node. - Otherwise, children[1] is an expressionlist node, - children[2] is an idlist node, and children[3] is a gatebody node. - """ - - def __init__(self, children): - """Create the gate node.""" - super().__init__("gate", children, None) - self.id = children[0] # pylint: disable=invalid-name - # The next three fields are required by the symbtab - self.name = self.id.name - self.line = self.id.line - self.file = self.id.file - - if len(children) == 3: - self.arguments = None - self.bitlist = children[1] - self.body = children[2] - else: - self.arguments = children[1] - self.bitlist = children[2] - self.body = children[3] - - def n_args(self): - """Return the number of parameter expressions.""" - if self.arguments: - return self.arguments.size() - return 0 - - def n_bits(self): - """Return the number of qubit arguments.""" - return self.bitlist.size() - - def qasm(self): - """Return the corresponding OPENQASM string.""" - string = "gate " + self.name - if self.arguments is not None: - string += "(" + self.arguments.qasm() + ")" - string += " " + self.bitlist.qasm() + "\n" - string += "{\n" + self.body.qasm() + "}" - return string diff --git a/qiskit/qasm/node/gatebody.py b/qiskit/qasm/node/gatebody.py deleted file mode 100644 index a7c591b549b8..000000000000 --- a/qiskit/qasm/node/gatebody.py +++ /dev/null @@ -1,41 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM custom gate body.""" -from .node import Node - - -class GateBody(Node): - """Node for an OPENQASM custom gate body. - - children is a list of gate operation nodes. - These are one of barrier, custom_unitary, U, or CX. - """ - - def __init__(self, children): - """Create the gatebody node.""" - super().__init__("gate_body", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - string = "" - for children in self.children: - string += " " + children.qasm() + "\n" - return string - - def calls(self): - """Return a list of custom gate names in this gate body.""" - lst = [] - for children in self.children: - if children.type == "custom_unitary": - lst.append(children.name) - return lst diff --git a/qiskit/qasm/node/id.py b/qiskit/qasm/node/id.py deleted file mode 100644 index 041ea452a4bf..000000000000 --- a/qiskit/qasm/node/id.py +++ /dev/null @@ -1,78 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM id.""" - -from .node import Node -from .nodeexception import NodeException - - -class Id(Node): - """Node for an OPENQASM id. - - The node has no children but has fields name, line, and file. - There is a flag is_bit that is set when XXXXX to help with scoping. - """ - - def __init__(self, id, line, file): - """Create the id node.""" - # pylint: disable=redefined-builtin - super().__init__("id", None, None) - self.name = id - self.line = line - self.file = file - # To help with scoping rules, so we know the id is a bit, - # this flag is set to True when the id appears in a gate declaration - self.is_bit = False - - def to_string(self, indent): - """Print the node with indent.""" - ind = indent * " " - print(ind, "id", self.name) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return self.name - - def latex(self, nested_scope=None): - """Return the correspond math mode latex string.""" - if not nested_scope: - return "\textrm{" + self.name + "}" - else: - if self.name not in nested_scope[-1]: - raise NodeException( - "Expected local parameter name: ", - "name=%s, " % self.name, - "line=%s, " % self.line, - "file=%s" % self.file, - ) - - return nested_scope[-1][self.name].latex(nested_scope[0:-1]) - - def sym(self, nested_scope=None): - """Return the correspond symbolic number.""" - if not nested_scope or self.name not in nested_scope[-1]: - raise NodeException( - "Expected local parameter name: ", - f"name={self.name}, line={self.line}, file={self.file}", - ) - return nested_scope[-1][self.name].sym(nested_scope[0:-1]) - - def real(self, nested_scope=None): - """Return the correspond floating point number.""" - if not nested_scope or self.name not in nested_scope[-1]: - raise NodeException( - "Expected local parameter name: ", - f"name={self.name}, line={self.line}, file={self.file}", - ) - - return nested_scope[-1][self.name].real(nested_scope[0:-1]) diff --git a/qiskit/qasm/node/idlist.py b/qiskit/qasm/node/idlist.py deleted file mode 100644 index 889fd887f8df..000000000000 --- a/qiskit/qasm/node/idlist.py +++ /dev/null @@ -1,33 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM idlist.""" -from .node import Node - - -class IdList(Node): - """Node for an OPENQASM idlist. - - children is a list of id nodes. - """ - - def __init__(self, children): - """Create the idlist node.""" - super().__init__("id_list", children, None) - - def size(self): - """Return the length of the list.""" - return len(self.children) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return ",".join([self.children[j].qasm() for j in range(self.size())]) diff --git a/qiskit/qasm/node/if_.py b/qiskit/qasm/node/if_.py deleted file mode 100644 index c056b078fe01..000000000000 --- a/qiskit/qasm/node/if_.py +++ /dev/null @@ -1,39 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM if statement.""" -from .node import Node - - -class If(Node): - """Node for an OPENQASM if statement. - - children[0] is an id node. - children[1] is an integer node. - children[2] is quantum operation node, including U, CX, custom_unitary, - measure, reset, (and BUG: barrier, if). - """ - - def __init__(self, children): - """Create the if node.""" - super().__init__("if", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return ( - "if(" - + self.children[0].qasm() - + "==" - + str(self.children[1].value) - + ") " - + self.children[2].qasm() - ) diff --git a/qiskit/qasm/node/indexedid.py b/qiskit/qasm/node/indexedid.py deleted file mode 100644 index ea6aa93444d6..000000000000 --- a/qiskit/qasm/node/indexedid.py +++ /dev/null @@ -1,41 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM indexed id.""" - -from .node import Node - - -class IndexedId(Node): - """Node for an OPENQASM indexed id. - - children[0] is an id node. - children[1] is an Int node. - """ - - def __init__(self, children): - """Create the indexed id node.""" - super().__init__("indexed_id", children, None) - self.id = children[0] # pylint: disable=invalid-name - self.name = self.id.name - self.line = self.id.line - self.file = self.id.file - self.index = children[1].value - - def to_string(self, indent): - """Print with indent.""" - ind = indent * " " - print(ind, "indexed_id", self.name, self.index) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return self.name + "[%d]" % self.index diff --git a/qiskit/qasm/node/intnode.py b/qiskit/qasm/node/intnode.py deleted file mode 100644 index 2b61e670c06b..000000000000 --- a/qiskit/qasm/node/intnode.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM integer.""" - -from .node import Node - - -class Int(Node): - """Node for an OPENQASM integer. - - This node has no children. The data is in the value field. - """ - - def __init__(self, id): - """Create the integer node.""" - # pylint: disable=redefined-builtin - super().__init__("int", None, None) - self.value = id - - def to_string(self, indent): - """Print with indent.""" - ind = indent * " " - print(ind, "int", self.value) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "%d" % self.value - - def latex(self): - """Return the corresponding math mode latex string.""" - return "%d" % self.value - - def sym(self, nested_scope=None): - """Return the correspond symbolic number.""" - del nested_scope - return float(self.value) - - def real(self, nested_scope=None): - """Return the correspond floating point number.""" - del nested_scope # ignored - return float(self.value) diff --git a/qiskit/qasm/node/measure.py b/qiskit/qasm/node/measure.py deleted file mode 100644 index c2045fba54c8..000000000000 --- a/qiskit/qasm/node/measure.py +++ /dev/null @@ -1,30 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM measure statement.""" -from .node import Node - - -class Measure(Node): - """Node for an OPENQASM measure statement. - - children[0] is a primary node (id or indexedid) - children[1] is a primary node (id or indexedid) - """ - - def __init__(self, children): - """Create the measure node.""" - super().__init__("measure", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "measure " + self.children[0].qasm() + " -> " + self.children[1].qasm() + ";" diff --git a/qiskit/qasm/node/node.py b/qiskit/qasm/node/node.py deleted file mode 100644 index 6f64dfb1343f..000000000000 --- a/qiskit/qasm/node/node.py +++ /dev/null @@ -1,59 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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 node object for the OPENQASM syntax tree.""" - - -class Node: - """Base node object for the OPENQASM syntax tree.""" - - def __init__(self, type, children=None, root=None): - """Construct a new node object.""" - # pylint: disable=redefined-builtin - self.type = type - if children: - self.children = children - else: - self.children = [] - self.root = root - # True if this node is an expression node, False otherwise - self.expression = False - - def is_expression(self): - """Return True if this is an expression node.""" - return self.expression - - def add_child(self, node): - """Add a child node.""" - self.children.append(node) - - def to_string(self, indent): - """Print with indent.""" - ind = indent * " " - if self.root: - print(ind, self.type, "---", self.root) - else: - print(ind, self.type) - indent = indent + 3 - ind = indent * " " - for children in self.children: - if children is None: - print("OOPS! type of parent is", type(self)) - print(self.children) - if isinstance(children, str): - print(ind, children) - elif isinstance(children, int): - print(ind, str(children)) - elif isinstance(children, float): - print(ind, str(children)) - else: - children.to_string(indent) diff --git a/qiskit/qasm/node/nodeexception.py b/qiskit/qasm/node/nodeexception.py deleted file mode 100644 index d351f499c929..000000000000 --- a/qiskit/qasm/node/nodeexception.py +++ /dev/null @@ -1,26 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Exception for errors raised while interpreting nodes.""" - - -class NodeException(Exception): - """Base class for errors raised while interpreting nodes.""" - - def __init__(self, *msg): - """Set the error message.""" - super().__init__(*msg) - self.msg = " ".join(msg) - - def __str__(self): - """Return the message.""" - return repr(self.msg) diff --git a/qiskit/qasm/node/opaque.py b/qiskit/qasm/node/opaque.py deleted file mode 100644 index 866ee711840d..000000000000 --- a/qiskit/qasm/node/opaque.py +++ /dev/null @@ -1,58 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM opaque gate declaration.""" - -from .node import Node - - -class Opaque(Node): - """Node for an OPENQASM opaque gate declaration. - - children[0] is an id node. - If len(children) is 3, children[1] is an expressionlist node, - and children[2] is an idlist node. - Otherwise, children[1] is an idlist node. - """ - - def __init__(self, children): - """Create the opaque gate node.""" - super().__init__("opaque", children, None) - self.id = children[0] # pylint: disable=invalid-name - # The next three fields are required by the symbtab - self.name = self.id.name - self.line = self.id.line - self.file = self.id.file - if len(children) == 3: - self.arguments = children[1] - self.bitlist = children[2] - else: - self.arguments = None - self.bitlist = children[1] - - def n_args(self): - """Return the number of parameter expressions.""" - if self.arguments: - return self.arguments.size() - return 0 - - def n_bits(self): - """Return the number of qubit arguments.""" - return self.bitlist.size() - - def qasm(self): - """Return the corresponding OPENQASM string.""" - string = "opaque %s" % self.name - if self.arguments is not None: - string += "(" + self.arguments.qasm() + ")" - string += " " + self.bitlist.qasm() + ";" - return string diff --git a/qiskit/qasm/node/prefix.py b/qiskit/qasm/node/prefix.py deleted file mode 100644 index c144e2b5e280..000000000000 --- a/qiskit/qasm/node/prefix.py +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM prefix expression.""" - -from qiskit.exceptions import MissingOptionalLibraryError -from .node import Node - - -class Prefix(Node): - """Node for an OPENQASM prefix expression. - - children[0] is a unary operator node. - children[1] is an expression node. - """ - - def __init__(self, children): - """Create the prefix node.""" - super().__init__("prefix", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return self.children[0].value + "(" + self.children[1].qasm() + ")" - - def latex(self): - """Return the corresponding math mode latex string.""" - try: - from pylatexenc.latexencode import utf8tolatex - except ImportError as ex: - raise MissingOptionalLibraryError( - "pylatexenc", "latex-from-qasm exporter", "pip install pylatexenc" - ) from ex - return utf8tolatex(self.sym()) - - def real(self): - """Return the correspond floating point number.""" - operation = self.children[0].operation() - expr = self.children[1].real() - return operation(expr) - - def sym(self, nested_scope=None): - """Return the correspond symbolic number.""" - operation = self.children[0].operation() - expr = self.children[1].sym(nested_scope) - return operation(expr) diff --git a/qiskit/qasm/node/primarylist.py b/qiskit/qasm/node/primarylist.py deleted file mode 100644 index 20e20f7b30d3..000000000000 --- a/qiskit/qasm/node/primarylist.py +++ /dev/null @@ -1,33 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM primarylist.""" -from .node import Node - - -class PrimaryList(Node): - """Node for an OPENQASM primarylist. - - children is a list of primary nodes. Primary nodes are indexedid or id. - """ - - def __init__(self, children): - """Create the primarylist node.""" - super().__init__("primary_list", children, None) - - def size(self): - """Return the size of the list.""" - return len(self.children) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return ",".join([self.children[j].qasm() for j in range(self.size())]) diff --git a/qiskit/qasm/node/program.py b/qiskit/qasm/node/program.py deleted file mode 100644 index 4475cb12f3d2..000000000000 --- a/qiskit/qasm/node/program.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM program.""" -from .node import Node - - -class Program(Node): - """Node for an OPENQASM program. - - children is a list of nodes (statements). - """ - - def __init__(self, children): - """Create the program node.""" - super().__init__("program", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - string = "" - for children in self.children: - string += children.qasm() + "\n" - return string diff --git a/qiskit/qasm/node/qreg.py b/qiskit/qasm/node/qreg.py deleted file mode 100644 index fb384bd342e3..000000000000 --- a/qiskit/qasm/node/qreg.py +++ /dev/null @@ -1,45 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM qreg statement.""" -from .node import Node - - -class Qreg(Node): - """Node for an OPENQASM qreg statement. - - children[0] is an indexedid node. - """ - - def __init__(self, children): - """Create the qreg node.""" - super().__init__("qreg", children, None) - # This is the indexed id, the full "id[n]" object - self.id = children[0] # pylint: disable=invalid-name - # Name of the qreg - self.name = self.id.name - # Source line number - self.line = self.id.line - # Source file name - self.file = self.id.file - # Size of the register - self.index = self.id.index - - def to_string(self, indent): - """Print the node data, with indent.""" - ind = indent * " " - print(ind, "qreg") - self.children[0].to_string(indent + 3) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "qreg " + self.id.qasm() + ";" diff --git a/qiskit/qasm/node/real.py b/qiskit/qasm/node/real.py deleted file mode 100644 index f967b7582502..000000000000 --- a/qiskit/qasm/node/real.py +++ /dev/null @@ -1,63 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM real number.""" - -import numpy as np - -from qiskit.exceptions import MissingOptionalLibraryError -from .node import Node - - -class Real(Node): - """Node for an OPENQASM real number. - - This node has no children. The data is in the value field. - """ - - def __init__(self, id): - """Create the real node.""" - # pylint: disable=redefined-builtin - super().__init__("real", None, None) - self.value = id - - def to_string(self, indent): - """Print with indent.""" - ind = indent * " " - print(ind, "real", self.value) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - if self.value == np.pi: - return "pi" - - return str(np.round(float(self.value))) - - def latex(self): - """Return the corresponding math mode latex string.""" - try: - from pylatexenc.latexencode import utf8tolatex - except ImportError as ex: - raise MissingOptionalLibraryError( - "pylatexenc", "latex-from-qasm exporter", "pip install pylatexenc" - ) from ex - return utf8tolatex(self.value) - - def sym(self, nested_scope=None): - """Return the correspond symbolic number.""" - del nested_scope # unused - return float(self.value) - - def real(self, nested_scope=None): - """Return the correspond floating point number.""" - del nested_scope # unused - return float(self.value.evalf()) diff --git a/qiskit/qasm/node/reset.py b/qiskit/qasm/node/reset.py deleted file mode 100644 index 29ccae931d96..000000000000 --- a/qiskit/qasm/node/reset.py +++ /dev/null @@ -1,29 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM reset statement.""" -from .node import Node - - -class Reset(Node): - """Node for an OPENQASM reset statement. - - children[0] is a primary node (id or indexedid) - """ - - def __init__(self, children): - """Create the reset node.""" - super().__init__("reset", children, None) - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "reset " + self.children[0].qasm() + ";" diff --git a/qiskit/qasm/node/unaryoperator.py b/qiskit/qasm/node/unaryoperator.py deleted file mode 100644 index a81e9f0737b1..000000000000 --- a/qiskit/qasm/node/unaryoperator.py +++ /dev/null @@ -1,49 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OpenQASM 2 unary operator.""" - -import operator - -from .node import Node -from .nodeexception import NodeException - - -VALID_OPERATORS = { - "+": operator.pos, - "-": operator.neg, -} - - -class UnaryOperator(Node): - """Node for an OpenQASM 2 unary operator. - - This node has no children. The data is in the value field. - """ - - def __init__(self, operation): - """Create the operator node.""" - super().__init__("unary_operator", None, None) - self.value = operation - - def operation(self): - """ - Return the operator as a function f(left, right). - """ - try: - return VALID_OPERATORS[self.value] - except KeyError as ex: - raise NodeException(f"internal error: undefined prefix '{self.value}'") from ex - - def qasm(self): - """Return OpenQASM 2 representation.""" - return self.value diff --git a/qiskit/qasm/node/universalunitary.py b/qiskit/qasm/node/universalunitary.py deleted file mode 100644 index e00303821273..000000000000 --- a/qiskit/qasm/node/universalunitary.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Node for an OPENQASM U statement.""" -from .node import Node - - -class UniversalUnitary(Node): - """Node for an OPENQASM U statement. - - children[0] is an expressionlist node. - children[1] is a primary node (id or indexedid). - """ - - def __init__(self, children): - """Create the U node.""" - super().__init__("universal_unitary", children) - self.arguments = children[0] - self.bitlist = children[1] - - def qasm(self): - """Return the corresponding OPENQASM string.""" - return "U(" + self.children[0].qasm() + ") " + self.children[1].qasm() + ";" diff --git a/qiskit/qasm/pygments/__init__.py b/qiskit/qasm/pygments/__init__.py deleted file mode 100644 index 686bf5e7800d..000000000000 --- a/qiskit/qasm/pygments/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -================================================= -Qasm Pygments tools (:mod:`qiskit.qasm.pygments`) -================================================= - -.. currentmodule:: qiskit.qasm.pygments - -.. autosummary:: - :toctree: ../stubs/ - - OpenQASMLexer - QasmTerminalStyle - QasmHTMLStyle -""" - -# pylint: disable=wrong-import-position - -from qiskit.utils.optionals import HAS_PYGMENTS - -HAS_PYGMENTS.require_now("built-in OpenQASM 2 syntax highlighting") - -from .lexer import OpenQASMLexer, QasmTerminalStyle, QasmHTMLStyle diff --git a/qiskit/qasm/pygments/lexer.py b/qiskit/qasm/pygments/lexer.py deleted file mode 100644 index cba2163bb0d7..000000000000 --- a/qiskit/qasm/pygments/lexer.py +++ /dev/null @@ -1,133 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. -"""Pygments tools for Qasm. -""" - -from pygments.lexer import RegexLexer -from pygments.token import Comment, String, Keyword, Name, Number, Text -from pygments.style import Style - - -class QasmTerminalStyle(Style): - """A style for OpenQasm in a Terminal env (e.g. Jupyter print).""" - - styles = { - String: "ansibrightred", - Number: "ansibrightcyan", - Keyword.Reserved: "ansibrightgreen", - Keyword.Declaration: "ansibrightgreen", - Keyword.Type: "ansibrightmagenta", - Name.Builtin: "ansibrightblue", - Name.Function: "ansibrightyellow", - } - - -class QasmHTMLStyle(Style): - """A style for OpenQasm in a HTML env (e.g. Jupyter widget).""" - - styles = { - String: "ansired", - Number: "ansicyan", - Keyword.Reserved: "ansigreen", - Keyword.Declaration: "ansigreen", - Keyword.Type: "ansimagenta", - Name.Builtin: "ansiblue", - Name.Function: "ansiyellow", - } - - -class OpenQASMLexer(RegexLexer): - """A pygments lexer for OpenQasm.""" - - name = "OpenQASM" - aliases = ["qasm"] - filenames = ["*.qasm"] - - gates = [ - "id", - "cx", - "x", - "y", - "z", - "s", - "sdg", - "h", - "t", - "tdg", - "ccx", - "c3x", - "c4x", - "c3sqrtx", - "rx", - "ry", - "rz", - "cz", - "cy", - "ch", - "swap", - "cswap", - "crx", - "cry", - "crz", - "cu1", - "cu3", - "rxx", - "rzz", - "rccx", - "rc3x", - "u1", - "u2", - "u3", - ] - - tokens = { - "root": [ - (r"\n", Text), - (r"[^\S\n]+", Text), - (r"//\n", Comment), - (r"//.*?$", Comment.Single), - # Keywords - (r"(OPENQASM|include)\b", Keyword.Reserved, "keywords"), - (r"(qreg|creg)\b", Keyword.Declaration), - # Treat 'if' special - (r"(if)\b", Keyword.Reserved, "if_keywords"), - # Constants - (r"(pi)\b", Name.Constant), - # Special - (r"(barrier|measure|reset)\b", Name.Builtin, "params"), - # Gates (Types) - ("(" + "|".join(gates) + r")\b", Keyword.Type, "params"), - (r"[unitary\d+]", Keyword.Type), - # Functions - (r"(gate)\b", Name.Function, "gate"), - # Generic text - (r"[a-zA-Z_][a-zA-Z0-9_]*", Text, "index"), - ], - "keywords": [ - (r'\s*("([^"]|"")*")', String, "#push"), - (r"\d+", Number, "#push"), - (r".*\(", Text, "params"), - ], - "if_keywords": [ - (r"[a-zA-Z0-9_]*", String, "#pop"), - (r"\d+", Number, "#push"), - (r".*\(", Text, "params"), - ], - "params": [ - (r"[a-zA-Z_][a-zA-Z0-9_]*", Text, "#push"), - (r"\d+", Number, "#push"), - (r"(\d+\.\d*|\d*\.\d+)([eEf][+-]?[0-9]+)?", Number, "#push"), - (r"\)", Text), - ], - "gate": [(r"[unitary\d+]", Keyword.Type, "#push"), (r"p\d+", Text, "#push")], - "index": [(r"\d+", Number, "#pop")], - } diff --git a/qiskit/qasm/qasm.py b/qiskit/qasm/qasm.py deleted file mode 100644 index ded52b32d3fc..000000000000 --- a/qiskit/qasm/qasm.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -""" -OPENQASM circuit object. -""" -from .exceptions import QasmError -from .qasmparser import QasmParser - - -class Qasm: - """OPENQASM circuit object.""" - - def __init__(self, filename=None, data=None): - """Create an OPENQASM circuit object.""" - if filename is None and data is None: - raise QasmError("Missing input file and/or data") - if filename is not None and data is not None: - raise QasmError("File and data must not both be specified initializing OpenQASM 2") - self._filename = filename - self._data = data - - def return_filename(self): - """Return the filename.""" - return self._filename - - def generate_tokens(self): - """Returns a generator of the tokens.""" - if self._filename: - with open(self._filename) as ifile: - self._data = ifile.read() - - with QasmParser(self._filename) as qasm_p: - return qasm_p.read_tokens() - - def parse(self): - """Parse the data.""" - if self._filename: - with open(self._filename) as ifile: - self._data = ifile.read() - - with QasmParser(self._filename) as qasm_p: - qasm_p.parse_debug(False) - return qasm_p.parse(self._data) diff --git a/qiskit/qasm/qasmlexer.py b/qiskit/qasm/qasmlexer.py deleted file mode 100644 index 7766d81c2eec..000000000000 --- a/qiskit/qasm/qasmlexer.py +++ /dev/null @@ -1,203 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -""" -OPENQASM Lexer. - -This is a wrapper around the PLY lexer to support the "include" statement -by creating a stack of lexers. -""" - -import os - -import numpy as np -from ply import lex - -from . import node -from .exceptions import QasmError - -CORE_LIBS_PATH = os.path.join(os.path.dirname(__file__), "libs") -CORE_LIBS = os.listdir(CORE_LIBS_PATH) - - -class QasmLexer: - """OPENQASM Lexer. - - This is a wrapper around the PLY lexer to support the "include" statement - by creating a stack of lexers. - """ - - # pylint: disable=invalid-name,missing-function-docstring - # pylint: disable=attribute-defined-outside-init,bad-docstring-quotes - - def __mklexer__(self, filename): - """Create a PLY lexer.""" - self.lexer = lex.lex(module=self, debug=False) - self.filename = filename - self.lineno = 1 - - if filename: - with open(filename) as ifile: - self.data = ifile.read() - self.lexer.input(self.data) - - def __init__(self, filename): - """Create the OPENQASM lexer.""" - self.__mklexer__(filename) - self.stack = [] - - def input(self, data): - """Set the input text data.""" - self.data = data - self.lexer.input(data) - - def token(self): - """Return the next token.""" - ret = self.lexer.token() - return ret - - def pop(self): - """Pop a PLY lexer off the stack.""" - self.lexer = self.stack.pop() - self.filename = self.lexer.qasm_file - self.lineno = self.lexer.qasm_line - - def push(self, filename): - """Push a PLY lexer on the stack to parse filename.""" - self.lexer.qasm_file = self.filename - self.lexer.qasm_line = self.lineno - self.stack.append(self.lexer) - self.__mklexer__(filename) - - # ---- Beginning of the PLY lexer ---- - literals = r'=()[]{};<>,.+-/*^"' - reserved = { - "barrier": "BARRIER", - "creg": "CREG", - "gate": "GATE", - "if": "IF", - "measure": "MEASURE", - "opaque": "OPAQUE", - "qreg": "QREG", - "pi": "PI", - "reset": "RESET", - } - tokens = [ - "NNINTEGER", - "REAL", - "CX", - "U", - "FORMAT", - "ASSIGN", - "MATCHES", - "ID", - "STRING", - ] + list(reserved.values()) - - def t_REAL(self, t): - r"(([0-9]+|([0-9]+)?\.[0-9]+|[0-9]+\.)[eE][+-]?[0-9]+)|(([0-9]+)?\.[0-9]+|[0-9]+\.)" - if np.iscomplex(t): - return t.real - else: - return t - - def t_NNINTEGER(self, t): - r"[1-9]+[0-9]*|0" - t.value = int(t.value) - return t - - def t_ASSIGN(self, t): - "->" - return t - - def t_MATCHES(self, t): - "==" - return t - - def t_STRING(self, t): - r"\"([^\\\"]|\\.)*\"" # fmt: skip - return t - - def t_INCLUDE(self, _): - "include" - # Now eat up the next two tokens which must be - # 1 - the name of the include file, and - # 2 - a terminating semicolon - # - # Then push the current lexer onto the stack, create a new one from - # the include file, and push it onto the stack. - # - # When we hit eof (the t_eof) rule, we pop. - next_token = self.lexer.token() - lineno = next_token.lineno - if isinstance(next_token.value, str): - incfile = next_token.value.strip('"') - else: - raise QasmError("Invalid include: must be a quoted string.") - - if incfile in CORE_LIBS: - incfile = os.path.join(CORE_LIBS_PATH, incfile) - - next_token = self.lexer.token() - if next_token is None or next_token.value != ";": - raise QasmError('Invalid syntax, missing ";" at line', str(lineno)) - - if not os.path.exists(incfile): - raise QasmError( - "Include file %s cannot be found, line %s, file %s" - % (incfile, str(next_token.lineno), self.filename) - ) - self.push(incfile) - return self.lexer.token() - - def t_FORMAT(self, t): - r"OPENQASM\s+[0-9]+(\.[0-9]+)?" - return t - - def t_COMMENT(self, _): - r"//.*" - pass - - def t_CX(self, t): - "CX" - return t - - def t_U(self, t): - "U" - return t - - def t_ID(self, t): - r"[a-z][a-zA-Z0-9_]*" - - t.type = self.reserved.get(t.value, "ID") - if t.type == "ID": - t.value = node.Id(t.value, self.lineno, self.filename) - return t - - def t_newline(self, t): - r"\n+" - self.lineno += len(t.value) - t.lexer.lineno = self.lineno - - def t_eof(self, _): - if self.stack: - self.pop() - return self.lexer.token() - return None - - t_ignore = " \t\r" - - def t_error(self, t): - raise QasmError( - "Unable to match any token rule, got -->%s<-- " - "Check your OPENQASM source and any include statements." % t.value[0] - ) diff --git a/qiskit/qasm/qasmparser.py b/qiskit/qasm/qasmparser.py deleted file mode 100644 index f5c2dc2c1fa0..000000000000 --- a/qiskit/qasm/qasmparser.py +++ /dev/null @@ -1,1156 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""OpenQASM parser.""" - -import os -import shutil -import tempfile - -import numpy as np -from ply import yacc - -from . import node -from .exceptions import QasmError -from .qasmlexer import QasmLexer - - -class QasmParser: - """OPENQASM Parser.""" - - # pylint: disable=missing-function-docstring,invalid-name - - def __init__(self, filename): - """Create the parser.""" - if filename is None: - filename = "" - self.lexer = QasmLexer(filename) - self.tokens = self.lexer.tokens - self.parse_dir = tempfile.mkdtemp(prefix="qiskit") - self.precedence = ( - ("left", "+", "-"), - ("left", "*", "/"), - ("left", "negative", "positive"), - ("right", "^"), - ) - # For yacc, also, write_tables = Bool and optimize = Bool - self.parser = yacc.yacc(module=self, debug=False, outputdir=self.parse_dir) - self.qasm = None - self.parse_deb = False - self.global_symtab = {} # global symtab - self.current_symtab = self.global_symtab # top of symbol stack - self.symbols = [] # symbol stack - self.external_functions = ["sin", "cos", "tan", "exp", "ln", "sqrt", "acos", "atan", "asin"] - - def __enter__(self): - return self - - def __exit__(self, *args): - if os.path.exists(self.parse_dir): - shutil.rmtree(self.parse_dir) - - def update_symtab(self, obj): - """Update a node in the symbol table. - - Everything in the symtab must be a node with these attributes: - name - the string name of the object - type - the string type of the object - line - the source line where the type was first found - file - the source file where the type was first found - """ - if obj.name in self.current_symtab: - prev = self.current_symtab[obj.name] - raise QasmError( - "Duplicate declaration for", - obj.type + " '" + obj.name + "' at line", - str(obj.line) + ", file", - obj.file + ".\nPrevious occurrence at line", - str(prev.line) + ", file", - prev.file, - ) - self.current_symtab[obj.name] = obj - - def verify_declared_bit(self, obj): - """Verify a qubit id against the gate prototype.""" - # We are verifying gate args against the formal parameters of a - # gate prototype. - if obj.name not in self.current_symtab: - raise QasmError( - "Cannot find symbol '" + obj.name + "' in argument list for gate, line", - str(obj.line), - "file", - obj.file, - ) - - # This insures the thing is from the bitlist and not from the - # argument list. - sym = self.current_symtab[obj.name] - if not (sym.type == "id" and sym.is_bit): - raise QasmError("Bit", obj.name, "is not declared as a bit in the gate.") - - def verify_bit_list(self, obj): - """Verify each qubit in a list of ids.""" - # We expect the object to be a bitlist or an idlist, we don't care. - # We will iterate it and ensure everything in it is declared as a bit, - # and throw if not. - for children in obj.children: - self.verify_declared_bit(children) - - def verify_exp_list(self, obj): - """Verify each expression in a list.""" - # A tad harder. This is a list of expressions each of which could be - # the head of a tree. We need to recursively walk each of these and - # ensure that any Id elements resolve to the current stack. - # - # I believe we only have to look at the current symtab. - if obj.children is not None: - for children in obj.children: - if isinstance(children, node.Id): - if children.name in self.external_functions: - continue - - if children.name not in self.current_symtab: - raise QasmError( - "Argument '" - + children.name - + "' in expression cannot be " - + "found, line", - str(children.line), - "file", - children.file, - ) - else: - if hasattr(children, "children"): - self.verify_exp_list(children) - - def verify_as_gate(self, obj, bitlist, arglist=None): - """Verify a user defined gate call.""" - if obj.name not in self.global_symtab: - raise QasmError( - "Cannot find gate definition for '" + obj.name + "', line", - str(obj.line), - "file", - obj.file, - ) - g_sym = self.global_symtab[obj.name] - if g_sym.type not in ("gate", "opaque"): - raise QasmError( - "'" - + obj.name - + "' is used as a gate " - + "or opaque call but the symbol is neither;" - + " it is a '" - + g_sym.type - + "' line", - str(obj.line), - "file", - obj.file, - ) - - if g_sym.n_bits() != bitlist.size(): - raise QasmError( - "Gate or opaque call to '" + obj.name + "' uses", - str(bitlist.size()), - "qubits but is declared for", - str(g_sym.n_bits()), - "qubits", - "line", - str(obj.line), - "file", - obj.file, - ) - - if arglist: - if g_sym.n_args() != arglist.size(): - raise QasmError( - "Gate or opaque call to '" + obj.name + "' uses", - str(arglist.size()), - "qubits but is declared for", - str(g_sym.n_args()), - "qubits", - "line", - str(obj.line), - "file", - obj.file, - ) - else: - if g_sym.n_args() > 0: - raise QasmError( - "Gate or opaque call to '" - + obj.name - + "' has no arguments but is declared for", - str(g_sym.n_args()), - "qubits", - "line", - str(obj.line), - "file", - obj.file, - ) - - def verify_reg(self, obj, object_type): - """Verify a register.""" - # How to verify: - # types must match - # indexes must be checked - if obj.name not in self.global_symtab: - raise QasmError( - "Cannot find definition for", - object_type, - "'" + obj.name + "'", - "at line", - str(obj.line), - "file", - obj.file, - ) - - g_sym = self.global_symtab[obj.name] - - if g_sym.type != object_type: - raise QasmError( - "Type for '" - + g_sym.name - + "' should be '" - + object_type - + "' but was found to be '" - + g_sym.type - + "'", - "line", - str(obj.line), - "file", - obj.file, - ) - - if obj.type == "indexed_id": - bound = g_sym.index - ndx = obj.index - if ndx < 0 or ndx >= bound: - raise QasmError( - "Register index for '" + g_sym.name + "' out of bounds. Index is", - str(ndx), - "bound is 0 <= index <", - str(bound), - "at line", - str(obj.line), - "file", - obj.file, - ) - - def verify_reg_list(self, obj, object_type): - """Verify a list of registers.""" - # We expect the object to be a bitlist or an idlist, we don't care. - # We will iterate it and ensure everything in it is declared as a bit, - # and throw if not. - for children in obj.children: - self.verify_reg(children, object_type) - - def id_tuple_list(self, id_node): - """Return a list of (name, index) tuples for this id node.""" - if id_node.type != "id": - raise QasmError("internal error, id_tuple_list") - bit_list = [] - try: - g_sym = self.current_symtab[id_node.name] - except KeyError: - g_sym = self.global_symtab[id_node.name] - if g_sym.type in ("qreg", "creg"): - # Return list of (name, idx) for reg ids - for idx in range(g_sym.index): - bit_list.append((id_node.name, idx)) - else: - # Return (name, -1) for other ids - bit_list.append((id_node.name, -1)) - return bit_list - - def verify_distinct(self, list_of_nodes): - """Check that objects in list_of_nodes represent distinct (qu)bits. - - list_of_nodes is a list containing nodes of type id, indexed_id, - primary_list, or id_list. We assume these are all the same type - 'qreg' or 'creg'. - This method raises an exception if list_of_nodes refers to the - same object more than once. - """ - bit_list = [] - line_number = -1 - filename = "" - for node_ in list_of_nodes: - # id node: add all bits in register or (name, -1) for id - if node_.type == "id": - bit_list.extend(self.id_tuple_list(node_)) - line_number = node_.line - filename = node_.file - # indexed_id: add the bit - elif node_.type == "indexed_id": - bit_list.append((node_.name, node_.index)) - line_number = node_.line - filename = node_.file - # primary_list: for each id or indexed_id child, add - elif node_.type == "primary_list": - for child in node_.children: - if child.type == "id": - bit_list.extend(self.id_tuple_list(child)) - else: - bit_list.append((child.name, child.index)) - line_number = child.line - filename = child.file - # id_list: for each id, add - elif node_.type == "id_list": - for child in node_.children: - bit_list.extend(self.id_tuple_list(child)) - line_number = child.line - filename = child.file - else: - raise QasmError("internal error, verify_distinct") - if len(bit_list) != len(set(bit_list)): - raise QasmError("duplicate identifiers at line %d file %s" % (line_number, filename)) - - def pop_scope(self): - """Return to the previous scope.""" - self.current_symtab = self.symbols.pop() - - def push_scope(self): - """Enter a new scope.""" - self.symbols.append(self.current_symtab) - self.current_symtab = {} - - # ---- Begin the PLY parser ---- - start = "main" - - def p_main(self, program): - """ - main : program - """ - self.qasm = program[1] - - # ---------------------------------------- - # program : statement - # | program statement - # ---------------------------------------- - def p_program_0(self, program): - """ - program : statement - """ - program[0] = node.Program([program[1]]) - - def p_program_1(self, program): - """ - program : program statement - """ - program[0] = program[1] - program[0].add_child(program[2]) - - # ---------------------------------------- - # statement : decl - # | quantum_op ';' - # | format ';' - # ---------------------------------------- - def p_statement(self, program): - """ - statement : decl - | quantum_op ';' - | format ';' - | ignore - | quantum_op error - | format error - """ - if len(program) > 2: - if program[2] != ";": - raise QasmError( - "Missing ';' at end of statement; " + "received", str(program[2].value) - ) - program[0] = program[1] - - def p_format(self, program): - """ - format : FORMAT - """ - version = node.Format(program[1]) - if (version.majorversion != "2") or (version.minorversion != "0"): - provided_version = f"{version.majorversion}.{version.minorversion}" - raise QasmError( - f"Invalid version: '{provided_version}'. This module supports OpenQASM 2.0 only." - ) - program[0] = version - - # ---------------------------------------- - # id : ID - # ---------------------------------------- - def p_id(self, program): - """ - id : ID - """ - program[0] = program[1] - - def p_id_e(self, program): - """ - id : error - """ - raise QasmError("Expected an ID, received '" + str(program[1].value) + "'") - - # ---------------------------------------- - # indexed_id : ID [ int ] - # ---------------------------------------- - def p_indexed_id(self, program): - """ - indexed_id : id '[' NNINTEGER ']' - | id '[' NNINTEGER error - | id '[' error - """ - if len(program) == 4: - raise QasmError("Expecting an integer index; received", str(program[3].value)) - if program[4] != "]": - raise QasmError("Missing ']' in indexed ID; received", str(program[4].value)) - program[0] = node.IndexedId([program[1], node.Int(program[3])]) - - # ---------------------------------------- - # primary : id - # | indexed_id - # ---------------------------------------- - def p_primary(self, program): - """ - primary : id - | indexed_id - """ - program[0] = program[1] - - # ---------------------------------------- - # id_list : id - # | id_list ',' id - # ---------------------------------------- - def p_id_list_0(self, program): - """ - id_list : id - """ - program[0] = node.IdList([program[1]]) - - def p_id_list_1(self, program): - """ - id_list : id_list ',' id - """ - program[0] = program[1] - program[0].add_child(program[3]) - - # ---------------------------------------- - # gate_id_list : id - # | gate_id_list ',' id - # ---------------------------------------- - def p_gate_id_list_0(self, program): - """ - gate_id_list : id - """ - program[0] = node.IdList([program[1]]) - self.update_symtab(program[1]) - - def p_gate_id_list_1(self, program): - """ - gate_id_list : gate_id_list ',' id - """ - program[0] = program[1] - program[0].add_child(program[3]) - self.update_symtab(program[3]) - - # ---------------------------------------- - # bit_list : bit - # | bit_list ',' bit - # ---------------------------------------- - def p_bit_list_0(self, program): - """ - bit_list : id - """ - program[0] = node.IdList([program[1]]) - program[1].is_bit = True - self.update_symtab(program[1]) - - def p_bit_list_1(self, program): - """ - bit_list : bit_list ',' id - """ - program[0] = program[1] - program[0].add_child(program[3]) - program[3].is_bit = True - self.update_symtab(program[3]) - - # ---------------------------------------- - # primary_list : primary - # | primary_list ',' primary - # ---------------------------------------- - def p_primary_list_0(self, program): - """ - primary_list : primary - """ - program[0] = node.PrimaryList([program[1]]) - - def p_primary_list_1(self, program): - """ - primary_list : primary_list ',' primary - """ - program[0] = program[1] - program[1].add_child(program[3]) - - # ---------------------------------------- - # decl : qreg_decl - # | creg_decl - # | gate_decl - # ---------------------------------------- - def p_decl(self, program): - """ - decl : qreg_decl ';' - | creg_decl ';' - | qreg_decl error - | creg_decl error - | gate_decl - """ - if len(program) > 2: - if program[2] != ";": - raise QasmError( - "Missing ';' in qreg or creg declaration." - " Instead received '" + program[2].value + "'" - ) - program[0] = program[1] - - # ---------------------------------------- - # qreg_decl : QREG indexed_id - # ---------------------------------------- - def p_qreg_decl(self, program): - """ - qreg_decl : QREG indexed_id - """ - program[0] = node.Qreg([program[2]]) - if program[2].name in self.external_functions: - raise QasmError( - "QREG names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - if program[2].index == 0: - raise QasmError("QREG size must be positive") - self.update_symtab(program[0]) - - def p_qreg_decl_e(self, program): - """ - qreg_decl : QREG error - """ - raise QasmError( - "Expecting indexed id (ID[int]) in QREG" + " declaration; received", program[2].value - ) - - # ---------------------------------------- - # creg_decl : QREG indexed_id - # ---------------------------------------- - def p_creg_decl(self, program): - """ - creg_decl : CREG indexed_id - """ - program[0] = node.Creg([program[2]]) - if program[2].name in self.external_functions: - raise QasmError( - "CREG names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - if program[2].index == 0: - raise QasmError("CREG size must be positive") - self.update_symtab(program[0]) - - def p_creg_decl_e(self, program): - """ - creg_decl : CREG error - """ - raise QasmError( - "Expecting indexed id (ID[int]) in CREG" + " declaration; received", program[2].value - ) - - # Gate_body will throw if there are errors, so we don't need to cover - # that here. Same with the id_lists - if they are not legal, we die - # before we get here - # - # ---------------------------------------- - # gate_decl : GATE id gate_scope bit_list gate_body - # | GATE id gate_scope '(' ')' bit_list gate_body - # | GATE id gate_scope '(' gate_id_list ')' bit_list gate_body - # - # ---------------------------------------- - def p_gate_decl_0(self, program): - """ - gate_decl : GATE id gate_scope bit_list gate_body - """ - program[0] = node.Gate([program[2], program[4], program[5]]) - if program[2].name in self.external_functions: - raise QasmError( - "GATE names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - self.pop_scope() - self.update_symtab(program[0]) - - def p_gate_decl_1(self, program): - """ - gate_decl : GATE id gate_scope '(' ')' bit_list gate_body - """ - program[0] = node.Gate([program[2], program[6], program[7]]) - if program[2].name in self.external_functions: - raise QasmError( - "GATE names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - self.pop_scope() - self.update_symtab(program[0]) - - def p_gate_decl_2(self, program): - """ - gate_decl : GATE id gate_scope '(' gate_id_list ')' bit_list gate_body - """ - program[0] = node.Gate([program[2], program[5], program[7], program[8]]) - if program[2].name in self.external_functions: - raise QasmError( - "GATE names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - self.pop_scope() - self.update_symtab(program[0]) - - def p_gate_scope(self, _): - """ - gate_scope : - """ - self.push_scope() - - # ---------------------------------------- - # gate_body : '{' gate_op_list '}' - # | '{' '}' - # - # | '{' gate_op_list error - # | '{' error - # - # Error handling: gete_op will throw if there's a problem so we won't - # get here with in the gate_op_list - # ---------------------------------------- - def p_gate_body_0(self, program): - """ - gate_body : '{' '}' - """ - if program[2] != "}": - raise QasmError( - "Missing '}' in gate definition; received'" + str(program[2].value) + "'" - ) - program[0] = node.GateBody(None) - - def p_gate_body_1(self, program): - """ - gate_body : '{' gate_op_list '}' - """ - program[0] = node.GateBody(program[2]) - - # ---------------------------------------- - # gate_op_list : gate_op - # | gate_op_ist gate_op - # - # Error handling: gete_op will throw if there's a problem so we won't - # get here with errors - # ---------------------------------------- - def p_gate_op_list_0(self, program): - """ - gate_op_list : gate_op - """ - program[0] = [program[1]] - - def p_gate_op_list_1(self, program): - """ - gate_op_list : gate_op_list gate_op - """ - program[0] = program[1] - program[0].append(program[2]) - - # ---------------------------------------- - # These are for use outside of gate_bodies and allow - # indexed ids everywhere. - # - # unitary_op : U '(' exp_list ')' primary - # | CX primary ',' primary - # | id primary_list - # | id '(' ')' primary_list - # | id '(' exp_list ')' primary_list - # - # Note that it might not be unitary - this is the mechanism that - # is also used to invoke calls to 'opaque' - # ---------------------------------------- - def p_unitary_op_0(self, program): - """ - unitary_op : U '(' exp_list ')' primary - """ - program[0] = node.UniversalUnitary([program[3], program[5]]) - self.verify_reg(program[5], "qreg") - self.verify_exp_list(program[3]) - - def p_unitary_op_1(self, program): - """ - unitary_op : CX primary ',' primary - """ - program[0] = node.Cnot([program[2], program[4]]) - self.verify_reg(program[2], "qreg") - self.verify_reg(program[4], "qreg") - self.verify_distinct([program[2], program[4]]) - # TODO: check that if both primary are id, same size - # TODO: this needs to be checked in other cases too - - def p_unitary_op_2(self, program): - """ - unitary_op : id primary_list - """ - program[0] = node.CustomUnitary([program[1], program[2]]) - self.verify_as_gate(program[1], program[2]) - self.verify_reg_list(program[2], "qreg") - self.verify_distinct([program[2]]) - - def p_unitary_op_3(self, program): - """ - unitary_op : id '(' ')' primary_list - """ - program[0] = node.CustomUnitary([program[1], program[4]]) - self.verify_as_gate(program[1], program[4]) - self.verify_reg_list(program[4], "qreg") - self.verify_distinct([program[4]]) - - def p_unitary_op_4(self, program): - """ - unitary_op : id '(' exp_list ')' primary_list - """ - program[0] = node.CustomUnitary([program[1], program[3], program[5]]) - self.verify_as_gate(program[1], program[5], arglist=program[3]) - self.verify_reg_list(program[5], "qreg") - self.verify_exp_list(program[3]) - self.verify_distinct([program[5]]) - - # ---------------------------------------- - # This is a restricted set of "quantum_op" which also - # prohibits indexed ids, for use in a gate_body - # - # gate_op : U '(' exp_list ')' id ';' - # | CX id ',' id ';' - # | id id_list ';' - # | id '(' ')' id_list ';' - # | id '(' exp_list ')' id_list ';' - # | BARRIER id_list ';' - # ---------------------------------------- - def p_gate_op_0(self, program): - """ - gate_op : U '(' exp_list ')' id ';' - """ - program[0] = node.UniversalUnitary([program[3], program[5]]) - self.verify_declared_bit(program[5]) - self.verify_exp_list(program[3]) - - def p_gate_op_0e1(self, p): - """ - gate_op : U '(' exp_list ')' error - """ - raise QasmError("Invalid U inside gate definition. " + "Missing bit id or ';'") - - def p_gate_op_0e2(self, _): - """ - gate_op : U '(' exp_list error - """ - raise QasmError("Missing ')' in U invocation in gate definition.") - - def p_gate_op_1(self, program): - """ - gate_op : CX id ',' id ';' - """ - program[0] = node.Cnot([program[2], program[4]]) - self.verify_declared_bit(program[2]) - self.verify_declared_bit(program[4]) - self.verify_distinct([program[2], program[4]]) - - def p_gate_op_1e1(self, program): - """ - gate_op : CX error - """ - raise QasmError( - "Invalid CX inside gate definition. " - + "Expected an ID or ',', received '" - + str(program[2].value) - + "'" - ) - - def p_gate_op_1e2(self, program): - """ - gate_op : CX id ',' error - """ - raise QasmError( - "Invalid CX inside gate definition. " - + "Expected an ID or ';', received '" - + str(program[4].value) - + "'" - ) - - def p_gate_op_2(self, program): - """ - gate_op : id id_list ';' - """ - program[0] = node.CustomUnitary([program[1], program[2]]) - # To verify: - # 1. id is declared as a gate in global scope - # 2. everything in the id_list is declared as a bit in local scope - self.verify_as_gate(program[1], program[2]) - self.verify_bit_list(program[2]) - self.verify_distinct([program[2]]) - - def p_gate_op_2e(self, _): - """ - gate_op : id id_list error - """ - raise QasmError("Invalid gate invocation inside gate definition.") - - def p_gate_op_3(self, program): - """ - gate_op : id '(' ')' id_list ';' - """ - program[0] = node.CustomUnitary([program[1], program[4]]) - self.verify_as_gate(program[1], program[4]) - self.verify_bit_list(program[4]) - self.verify_distinct([program[4]]) - - def p_gate_op_4(self, program): - """ - gate_op : id '(' exp_list ')' id_list ';' - """ - program[0] = node.CustomUnitary([program[1], program[3], program[5]]) - self.verify_as_gate(program[1], program[5], arglist=program[3]) - self.verify_bit_list(program[5]) - self.verify_exp_list(program[3]) - self.verify_distinct([program[5]]) - - def p_gate_op_4e0(self, _): - """ - gate_op : id '(' ')' error - """ - raise QasmError("Invalid bit list inside gate definition or" + " missing ';'") - - def p_gate_op_4e1(self, _): - """ - gate_op : id '(' error - """ - raise QasmError("Unmatched () for gate invocation inside gate" + " invocation.") - - def p_gate_op_5(self, program): - """ - gate_op : BARRIER id_list ';' - """ - program[0] = node.Barrier([program[2]]) - self.verify_bit_list(program[2]) - self.verify_distinct([program[2]]) - - def p_gate_op_5e(self, _): - """ - gate_op : BARRIER error - """ - raise QasmError("Invalid barrier inside gate definition.") - - # ---------------------------------------- - # opaque : OPAQUE id gate_scope bit_list - # | OPAQUE id gate_scope '(' ')' bit_list - # | OPAQUE id gate_scope '(' gate_id_list ')' bit_list - # - # These are like gate declarations only without a body. - # ---------------------------------------- - def p_opaque_0(self, program): - """ - opaque : OPAQUE id gate_scope bit_list - """ - # TODO: Review Opaque function - program[0] = node.Opaque([program[2], program[4]]) - if program[2].name in self.external_functions: - raise QasmError( - "OPAQUE names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - self.pop_scope() - self.update_symtab(program[0]) - - def p_opaque_1(self, program): - """ - opaque : OPAQUE id gate_scope '(' ')' bit_list - """ - program[0] = node.Opaque([program[2], program[6]]) - self.pop_scope() - self.update_symtab(program[0]) - - def p_opaque_2(self, program): - """ - opaque : OPAQUE id gate_scope '(' gate_id_list ')' bit_list - """ - program[0] = node.Opaque([program[2], program[5], program[7]]) - if program[2].name in self.external_functions: - raise QasmError( - "OPAQUE names cannot be reserved words. " + "Received '" + program[2].name + "'" - ) - self.pop_scope() - self.update_symtab(program[0]) - - def p_opaque_1e(self, _): - """ - opaque : OPAQUE id gate_scope '(' error - """ - raise QasmError("Poorly formed OPAQUE statement.") - - # ---------------------------------------- - # measure : MEASURE primary ASSIGN primary - # ---------------------------------------- - def p_measure(self, program): - """ - measure : MEASURE primary ASSIGN primary - """ - program[0] = node.Measure([program[2], program[4]]) - self.verify_reg(program[2], "qreg") - self.verify_reg(program[4], "creg") - - def p_measure_e(self, program): - """ - measure : MEASURE primary error - """ - raise QasmError("Illegal measure statement." + str(program[3].value)) - - # ---------------------------------------- - # barrier : BARRIER primary_list - # - # Errors are covered by handling errors in primary_list - # ---------------------------------------- - def p_barrier(self, program): - """ - barrier : BARRIER primary_list - """ - program[0] = node.Barrier([program[2]]) - self.verify_reg_list(program[2], "qreg") - self.verify_distinct([program[2]]) - - # ---------------------------------------- - # reset : RESET primary - # ---------------------------------------- - def p_reset(self, program): - """ - reset : RESET primary - """ - program[0] = node.Reset([program[2]]) - self.verify_reg(program[2], "qreg") - - # ---------------------------------------- - # IF '(' ID MATCHES NNINTEGER ')' quantum_op - # ---------------------------------------- - def p_if(self, program): - """ - if : IF '(' id MATCHES NNINTEGER ')' quantum_op - if : IF '(' id error - if : IF '(' id MATCHES error - if : IF '(' id MATCHES NNINTEGER error - if : IF error - """ - if len(program) == 3: - raise QasmError("Ill-formed IF statement. Perhaps a" + " missing '('?") - if len(program) == 5: - raise QasmError( - "Ill-formed IF statement. Expected '==', " + "received '" + str(program[4].value) - ) - if len(program) == 6: - raise QasmError( - "Ill-formed IF statement. Expected a number, " - + "received '" - + str(program[5].value) - ) - if len(program) == 7: - raise QasmError("Ill-formed IF statement, unmatched '('") - - if program[7].type == "if": - raise QasmError("Nested IF statements not allowed") - if program[7].type == "barrier": - raise QasmError("barrier not permitted in IF statement") - - program[0] = node.If([program[3], node.Int(program[5]), program[7]]) - - # ---------------------------------------- - # These are all the things you can have outside of a gate declaration - # quantum_op : unitary_op - # | opaque - # | measure - # | reset - # | barrier - # | if - # - # ---------------------------------------- - def p_quantum_op(self, program): - """ - quantum_op : unitary_op - | opaque - | measure - | barrier - | reset - | if - """ - program[0] = program[1] - - # ---------------------------------------- - # unary : NNINTEGER - # | REAL - # | PI - # | ID - # | '(' expression ')' - # | id '(' expression ')' - # - # We will trust 'expression' to throw before we have to handle it here - # ---------------------------------------- - def p_unary_0(self, program): - """ - unary : NNINTEGER - """ - program[0] = node.Int(program[1]) - - def p_unary_1(self, program): - """ - unary : REAL - """ - program[0] = node.Real(program[1]) - - def p_unary_2(self, program): - """ - unary : PI - """ - program[0] = node.Real(np.pi) - - def p_unary_3(self, program): - """ - unary : id - """ - program[0] = program[1] - - def p_unary_4(self, program): - """ - unary : '(' expression ')' - """ - program[0] = program[2] - - def p_unary_6(self, program): - """ - unary : id '(' expression ')' - """ - # note this is a semantic check, not syntactic - if program[1].name not in self.external_functions: - raise QasmError("Illegal external function call: ", str(program[1].name)) - program[0] = node.External([program[1], program[3]]) - - # ---------------------------------------- - # Prefix - # ---------------------------------------- - - def p_expression_1(self, program): - """ - expression : '-' expression %prec negative - | '+' expression %prec positive - """ - program[0] = node.Prefix([node.UnaryOperator(program[1]), program[2]]) - - def p_expression_0(self, program): - """ - expression : expression '*' expression - | expression '/' expression - | expression '+' expression - | expression '-' expression - | expression '^' expression - """ - program[0] = node.BinaryOp([node.BinaryOperator(program[2]), program[1], program[3]]) - - def p_expression_2(self, program): - """ - expression : unary - """ - program[0] = program[1] - - # ---------------------------------------- - # exp_list : exp - # | exp_list ',' exp - # ---------------------------------------- - def p_exp_list_0(self, program): - """ - exp_list : expression - """ - program[0] = node.ExpressionList([program[1]]) - - def p_exp_list_1(self, program): - """ - exp_list : exp_list ',' expression - """ - program[0] = program[1] - program[0].add_child(program[3]) - - def p_ignore(self, _): - """ - ignore : STRING - """ - # this should never hit but it keeps the insuppressible warnings at bay - pass - - def p_error(self, program): - # EOF is a special case because the stupid error token isn't placed - # on the stack - if not program: - raise QasmError("Error at end of file. " + "Perhaps there is a missing ';'") - - col = self.find_column(self.lexer.data, program) - print("Error near line", str(self.lexer.lineno), "Column", col) - - def find_column(self, input_, token): - """Compute the column. - - Input is the input text string. - token is a token instance. - """ - if token is None: - return 0 - last_cr = input_.rfind("\n", 0, token.lexpos) - last_cr = max(last_cr, 0) - column = (token.lexpos - last_cr) + 1 - return column - - def read_tokens(self): - """finds and reads the tokens.""" - try: - while True: - token = self.lexer.token() - - if not token: - break - - yield token - except QasmError as e: - print("Exception tokenizing qasm file:", e.msg) - - def parse_debug(self, val): - """Set the parse_deb field.""" - if val is True: - self.parse_deb = True - elif val is False: - self.parse_deb = False - else: - raise QasmError("Illegal debug value '" + str(val) + "' must be True or False.") - - def parse(self, data): - """Parse some data.""" - self.parser.parse(data, lexer=self.lexer, debug=self.parse_deb) - if self.qasm is None: - raise QasmError("Uncaught exception in parser; " + "see previous messages for details.") - return self.qasm - - def print_tree(self): - """Print parsed OPENQASM.""" - if self.qasm is not None: - self.qasm.to_string(0) - else: - print("No parsed qasm to print") - - def run(self, data): - """Parser runner. - - To use this module stand-alone. - """ - ast = self.parser.parse(data, debug=True) - self.parser.parse(data, debug=True) - ast.to_string(0) diff --git a/qiskit/qasm2/__init__.py b/qiskit/qasm2/__init__.py index e14ab420f380..485c210c0632 100644 --- a/qiskit/qasm2/__init__.py +++ b/qiskit/qasm2/__init__.py @@ -397,19 +397,6 @@ def add_one(x): serialisation format, and expanded its behaviour as Qiskit expanded. The new parser under all its defaults implements the specification more strictly. -The complete legacy code-paths are - -.. code-block:: python - - from qiskit.converters import ast_to_dag, dag_to_circuit - from qiskit.qasm import Qasm - - def from_qasm_file(path: str): - dag_to_circuit(ast_to_dag(Qasm(filename=path).parse())) - - def from_qasm_str(qasm_str: str): - dag_to_circuit(ast_to_dag(Qasm(data=qasm_str).parse())) - In particular, in the legacy importers: * the `include_path` is effectively: diff --git a/qiskit/tools/jupyter/library.py b/qiskit/tools/jupyter/library.py index 773416ad3a04..57a27ece8cdc 100644 --- a/qiskit/tools/jupyter/library.py +++ b/qiskit/tools/jupyter/library.py @@ -131,67 +131,6 @@ def properties_widget(circuit: QuantumCircuit) -> wid.VBox: return properties -@_optionals.HAS_PYGMENTS.require_in_call -@deprecate_func( - since="0.25.0", - additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", - package_name="qiskit-terra", -) -def qasm_widget(circuit: QuantumCircuit) -> wid.VBox: - """Generate an OpenQASM widget with header for a quantum circuit. - - Args: - circuit: Input quantum circuit. - - Returns: - Output widget. - """ - import pygments - from pygments.formatters import HtmlFormatter - from qiskit.qasm.pygments import QasmHTMLStyle, OpenQASMLexer - - qasm_code = circuit.qasm() - code = pygments.highlight(qasm_code, OpenQASMLexer(), HtmlFormatter()) - - html_style = HtmlFormatter(style=QasmHTMLStyle).get_style_defs(".highlight") - - code_style = ( - """ - - """ - % html_style - ) - - out = wid.HTML( - code_style + code, - layout=wid.Layout(max_height="500px", height="auto", overflow="scroll scroll"), - ) - - out_label = wid.HTML( - f"

OpenQASM

", - layout=wid.Layout(margin="0px 0px 10px 0px"), - ) - - qasm = wid.VBox( - children=[out_label, out], - layout=wid.Layout( - height="auto", max_height="500px", width="60%", margin="0px 0px 0px 20px" - ), - ) - - qasm._code_length = len(qasm_code.split("\n")) - return qasm - - @deprecate_func( since="0.25.0", additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", @@ -230,8 +169,7 @@ def circuit_library_widget(circuit: QuantumCircuit) -> None: Args: circuit: Input quantum circuit. """ - qasm_wid = qasm_widget(circuit) - sep_length = str(min(20 * qasm_wid._code_length, 495)) + sep_length = str(min(20, 495)) # The separator widget sep = wid.HTML( @@ -239,7 +177,7 @@ def circuit_library_widget(circuit: QuantumCircuit) -> None: layout=wid.Layout(height="auto", max_height="495px", margin="40px 0px 0px 20px"), ) bottom = wid.HBox( - children=[properties_widget(circuit), sep, qasm_widget(circuit)], + children=[properties_widget(circuit), sep], layout=wid.Layout(max_height="550px", height="auto"), ) diff --git a/releasenotes/notes/remove-legacy-qasm2-parser-53ad3f1817fd68cc.yaml b/releasenotes/notes/remove-legacy-qasm2-parser-53ad3f1817fd68cc.yaml new file mode 100644 index 000000000000..d3a581761060 --- /dev/null +++ b/releasenotes/notes/remove-legacy-qasm2-parser-53ad3f1817fd68cc.yaml @@ -0,0 +1,34 @@ +--- +upgrade: + - | + The legacy OpenQASM 2 parser module previously present in ``qiskit.qasm`` has + been removed. It was marked as deprecated in Qiskit 0.46.0 release. The + OpenQASM 2 parser has been superseded by the :mod:`qiskit.qasm2` module which + provides a faster more correct parser for QASM2. + - | + The ``qiskit.converters.ast_to_dag`` function has been removed. It + previously was used to convert the abstract syntax tree generated by the + legacy OpenQASM 2 parser (in the ``qiskit.qasm`` module which no longer exists) + and convert that directly to a :class:`.DAGCircuit`. This function was + marked as deprecated in the Qiskit 0.46.0 release. As the legacy + OpenQASM 2 parser has been removed this function no longer serves a purpose. + If you were previously using this, you can instead parse your OpenQASM 2 files + into a :class:`.QuantumCircuit` using the + :meth:`.QuantumCircuit.from_qasm_file` or + :meth:`.QuantumCircuit.from_qasm_str` constructor methods and then + converting that :class:`.QuantumCircuit` into a :class:`.DAGCircuit` with + :func:`.circuit_to_dag`. + - | + Removed the ``QuantumCircuit.qasm()`` method to generate a OpenQASM 2 + representation of the :class:`.QuantumCircuit` object. Instead the + :func:`.qasm2.dump` or :func:`.qasm2.dumps` functions should be used. This + function was marked as deprecated in the 0.46.0 release. If you were using + the ``QuantumCircuit.qasm()`` method to generate pygments formatted output + you should instead look at the standalone ``openqasm-pygments`` package + to provide this functionality. + - | + Removed the ``qiskit.tools.jupyter.library.qasm_widget`` function which + was used to visualize qasm strings in a jupyter notebook. This function + was marked as deprecated as part of the Qiskit 0.44.0 release. The function + was originally used for building documentation but hasn't been used in some + time and has been removed from Qiskit. diff --git a/requirements-optional.txt b/requirements-optional.txt index 6afa25271ab9..6c523a4937f3 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -17,7 +17,6 @@ ipywidgets>=7.3.0 matplotlib>=3.3 pillow>=4.2.1 pydot -pygments>=2.4 pylatexenc>=1.4 seaborn>=0.9.0 diff --git a/requirements.txt b/requirements.txt index 31206e951128..6402e9a6549f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ rustworkx>=0.13.0 numpy>=1.17,<2 -ply>=3.10 psutil>=5 scipy>=1.5 sympy>=1.3 diff --git a/setup.py b/setup.py index 55950efef410..62e6f43bcbc6 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,6 @@ "pillow>=4.2.1", "pylatexenc>=1.4", "seaborn>=0.9.0", - "pygments>=2.4", ] z3_requirements = [ "z3-solver>=4.7", diff --git a/test/benchmarks/converters.py b/test/benchmarks/converters.py index 041e40dd6de1..d99822fa8016 100644 --- a/test/benchmarks/converters.py +++ b/test/benchmarks/converters.py @@ -14,7 +14,6 @@ # pylint: disable=attribute-defined-outside-init,unsubscriptable-object from qiskit import converters -from qiskit import qasm from .utils import random_circuit @@ -38,7 +37,6 @@ def setup(self, n_qubits, depth): raise NotImplementedError self.qc = random_circuit(n_qubits, depth, measure=True, conditional=True, seed=seed) self.dag = converters.circuit_to_dag(self.qc) - self.qasm = qasm.Qasm(data=self.qc.qasm()).parse() def time_circuit_to_dag(self, *_): converters.circuit_to_dag(self.qc) @@ -48,6 +46,3 @@ def time_circuit_to_instruction(self, *_): def time_dag_to_circuit(self, *_): converters.dag_to_circuit(self.dag) - - def time_ast_to_circuit(self, *_): - converters.ast_to_dag(self.qasm) diff --git a/test/python/basicaer/test_qasm_simulator.py b/test/python/basicaer/test_qasm_simulator.py index 90bb10149943..6deb874d9c82 100644 --- a/test/python/basicaer/test_qasm_simulator.py +++ b/test/python/basicaer/test_qasm_simulator.py @@ -25,6 +25,7 @@ from qiskit.compiler import transpile, assemble from qiskit.providers.basicaer import QasmSimulatorPy from qiskit.test import providers +from qiskit.qasm2 import dumps class StreamHandlerRaiseException(StreamHandler): @@ -306,7 +307,7 @@ def test_teleport(self): "1": data["1 0 0"] + data["1 1 0"] + data["1 0 1"] + data["1 1 1"], } self.log.info("test_teleport: circuit:") - self.log.info(circuit.qasm()) + self.log.info(dumps(circuit)) self.log.info("test_teleport: data %s", data) self.log.info("test_teleport: alice %s", alice) self.log.info("test_teleport: bob %s", bob) diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py index 25bfa0bfae49..aacc5425d74b 100644 --- a/test/python/circuit/library/test_permutation.py +++ b/test/python/circuit/library/test_permutation.py @@ -25,6 +25,7 @@ from qiskit.circuit.library import Permutation, PermutationGate from qiskit.quantum_info import Operator from qiskit.qpy import dump, load +from qiskit.qasm2 import dumps class TestPermutationLibrary(QiskitTestCase): @@ -160,9 +161,9 @@ def test_qasm(self): "gate permutation__2_4_3_0_1_ q0,q1,q2,q3,q4 { swap q2,q3; swap q1,q4; swap q0,q3; }\n" "qreg q0[5];\n" "permutation__2_4_3_0_1_ q0[0],q0[1],q0[2],q0[3],q0[4];\n" - "h q0[0];\n" + "h q0[0];" ) - self.assertEqual(expected_qasm, circuit.qasm()) + self.assertEqual(expected_qasm, dumps(circuit)) def test_qpy(self): """Test qpy for circuits with permutations.""" diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 5b27f968d021..7102df18bf68 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -20,7 +20,8 @@ from qiskit.test import QiskitTestCase from qiskit.circuit import Parameter, Qubit, Clbit, Gate from qiskit.circuit.library import C3SXGate, CCZGate, CSGate, CSdgGate, PermutationGate -from qiskit.qasm.exceptions import QasmError +from qiskit.qasm2.exceptions import QASM2Error as QasmError +from qiskit.qasm2 import dumps # Regex pattern to match valid OpenQASM identifiers VALID_QASM2_IDENTIFIER = re.compile("[a-z][a-zA-Z_0-9]*") @@ -69,8 +70,8 @@ def test_circuit_qasm(self): barrier qr1[0],qr2[0],qr2[1]; measure qr1[0] -> cr[0]; measure qr2[0] -> cr[1]; -measure qr2[1] -> cr[2];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +measure qr2[1] -> cr[2];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_composite_circuit(self): """Test circuit qasm() method when a composite circuit instruction @@ -103,8 +104,8 @@ def test_circuit_qasm_with_composite_circuit(self): barrier qr[0],qr[1]; composite_circ qr[0],qr[1]; measure qr[0] -> cr[0]; -measure qr[1] -> cr[1];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +measure qr[1] -> cr[1];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_multiple_same_composite_circuits(self): """Test circuit qasm() method when a composite circuit is added @@ -139,8 +140,8 @@ def test_circuit_qasm_with_multiple_same_composite_circuits(self): composite_circ qr[0],qr[1]; composite_circ qr[0],qr[1]; measure qr[0] -> cr[0]; -measure qr[1] -> cr[1];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +measure qr[1] -> cr[1];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_multiple_composite_circuits_with_same_name(self): """Test circuit qasm() method when multiple composite circuit instructions @@ -175,10 +176,10 @@ def test_circuit_qasm_with_multiple_composite_circuits_with_same_name(self): qreg qr[1]; my_gate qr[0]; my_gate_{1} qr[0]; -my_gate_{0} qr[0];\n""".format( +my_gate_{0} qr[0];""".format( my_gate_inst3_id, my_gate_inst2_id ) - self.assertEqual(circuit.qasm(), expected_qasm) + self.assertEqual(dumps(circuit), expected_qasm) def test_circuit_qasm_with_composite_circuit_with_children_composite_circuit(self): """Test circuit qasm() method when composite circuits with children @@ -205,16 +206,16 @@ def test_circuit_qasm_with_composite_circuit_with_children_composite_circuit(sel gate parent_circ q0,q1,q2 { child_circ q0,q1; h q2; } gate grandparent_circ q0,q1,q2,q3 { parent_circ q0,q1,q2; x q3; } qreg q[4]; -grandparent_circ q[0],q[1],q[2],q[3];\n""" +grandparent_circ q[0],q[1],q[2],q[3];""" - self.assertEqual(qc.qasm(), expected_qasm) + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_pi(self): """Test circuit qasm() method with pi params.""" circuit = QuantumCircuit(2) circuit.cz(0, 1) circuit.u(2 * pi, 3 * pi, -5 * pi, 0) - qasm_str = circuit.qasm() + qasm_str = dumps(circuit) circuit2 = QuantumCircuit.from_qasm_str(qasm_str) self.assertEqual(circuit, circuit2) @@ -227,10 +228,10 @@ def test_circuit_qasm_with_composite_circuit_with_one_param(self): gate nG0(param0) q0 { h q0; } qreg q[3]; creg c[3]; -nG0(pi) q[0];\n""" +nG0(pi) q[0];""" qc = QuantumCircuit.from_qasm_str(original_str) - self.assertEqual(original_str, qc.qasm()) + self.assertEqual(original_str, dumps(qc)) def test_circuit_qasm_with_composite_circuit_with_many_params_and_qubits(self): """Test circuit qasm() method when a composite circuit instruction @@ -243,10 +244,10 @@ def test_circuit_qasm_with_composite_circuit_with_many_params_and_qubits(self): qreg r[3]; creg c[3]; creg d[3]; -nG0(pi,pi/2) q[0],r[0];\n""" +nG0(pi,pi/2) q[0],r[0];""" qc = QuantumCircuit.from_qasm_str(original_str) - self.assertEqual(original_str, qc.qasm()) + self.assertEqual(original_str, dumps(qc)) def test_c3sxgate_roundtrips(self): """Test that C3SXGate correctly round trips. @@ -256,12 +257,11 @@ def test_c3sxgate_roundtrips(self): resolution issues.""" qc = QuantumCircuit(4) qc.append(C3SXGate(), qc.qubits, []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; qreg q[4]; -c3sqrtx q[0],q[1],q[2],q[3]; -""" +c3sqrtx q[0],q[1],q[2],q[3];""" self.assertEqual(qasm, expected) parsed = QuantumCircuit.from_qasm_str(qasm) self.assertIsInstance(parsed.data[0].operation, C3SXGate) @@ -275,39 +275,36 @@ def test_cczgate_qasm(self): """Test that CCZ dumps definition as a non-qelib1 gate.""" qc = QuantumCircuit(3) qc.append(CCZGate(), qc.qubits, []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate ccz q0,q1,q2 { h q2; ccx q0,q1,q2; h q2; } qreg q[3]; -ccz q[0],q[1],q[2]; -""" +ccz q[0],q[1],q[2];""" self.assertEqual(qasm, expected) def test_csgate_qasm(self): """Test that CS dumps definition as a non-qelib1 gate.""" qc = QuantumCircuit(2) qc.append(CSGate(), qc.qubits, []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate cs q0,q1 { p(pi/4) q0; cx q0,q1; p(-pi/4) q1; cx q0,q1; p(pi/4) q1; } qreg q[2]; -cs q[0],q[1]; -""" +cs q[0],q[1];""" self.assertEqual(qasm, expected) def test_csdggate_qasm(self): """Test that CSdg dumps definition as a non-qelib1 gate.""" qc = QuantumCircuit(2) qc.append(CSdgGate(), qc.qubits, []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate csdg q0,q1 { p(-pi/4) q0; cx q0,q1; p(pi/4) q1; cx q0,q1; p(-pi/4) q1; } qreg q[2]; -csdg q[0],q[1]; -""" +csdg q[0],q[1];""" self.assertEqual(qasm, expected) def test_rzxgate_qasm(self): @@ -315,14 +312,13 @@ def test_rzxgate_qasm(self): qc = QuantumCircuit(2) qc.rzx(0, 0, 1) qc.rzx(pi / 2, 1, 0) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; } qreg q[2]; rzx(0) q[0],q[1]; -rzx(pi/2) q[1],q[0]; -""" +rzx(pi/2) q[1],q[0];""" self.assertEqual(qasm, expected) def test_ecrgate_qasm(self): @@ -330,28 +326,26 @@ def test_ecrgate_qasm(self): qc = QuantumCircuit(2) qc.ecr(0, 1) qc.ecr(1, 0) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; } gate ecr q0,q1 { rzx(pi/4) q0,q1; x q0; rzx(-pi/4) q0,q1; } qreg q[2]; ecr q[0],q[1]; -ecr q[1],q[0]; -""" +ecr q[1],q[0];""" self.assertEqual(qasm, expected) def test_unitary_qasm(self): """Test that UnitaryGate can be dumped to OQ2 correctly.""" qc = QuantumCircuit(1) qc.unitary([[1, 0], [0, 1]], 0) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate unitary q0 { u(0,0,0) q0; } qreg q[1]; -unitary q[0]; -""" +unitary q[0];""" self.assertEqual(qasm, expected) def test_multiple_unitary_qasm(self): @@ -363,7 +357,7 @@ def test_multiple_unitary_qasm(self): qc.unitary([[1, 0], [0, 1]], 0) qc.unitary([[0, 1], [1, 0]], 1) qc.append(custom.to_gate(), [0], []) - qasm = qc.qasm() + qasm = dumps(qc) expected = re.compile( r"""OPENQASM 2.0; include "qelib1.inc"; @@ -374,8 +368,7 @@ def test_multiple_unitary_qasm(self): qreg q\[2\]; unitary q\[0\]; (?P=u1) q\[1\]; -custom q\[0\]; -""", +custom q\[0\];""", re.MULTILINE, ) self.assertRegex(qasm, expected) @@ -386,7 +379,7 @@ def test_unbound_circuit_raises(self): theta = Parameter("θ") qc.rz(theta, 0) with self.assertRaises(QasmError): - qc.qasm() + dumps(qc) def test_gate_qasm_with_ctrl_state(self): """Test gate qasm() with controlled gate that has ctrl_state setting.""" @@ -394,7 +387,7 @@ def test_gate_qasm_with_ctrl_state(self): qc = QuantumCircuit(2) qc.ch(0, 1, ctrl_state=0) - qasm_str = qc.qasm() + qasm_str = dumps(qc) self.assertEqual(Operator(qc), Operator(QuantumCircuit.from_qasm_str(qasm_str))) def test_circuit_qasm_with_mcx_gate(self): @@ -410,8 +403,8 @@ def test_circuit_qasm_with_mcx_gate(self): include "qelib1.inc"; gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; } qreg q[4]; -mcx q[0],q[1],q[2],q[3];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +mcx q[0],q[1],q[2],q[3];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_mcx_gate_variants(self): """Test circuit qasm() method with MCXGrayCode, MCXRecursive, MCXVChain""" @@ -435,9 +428,9 @@ def test_circuit_qasm_with_mcx_gate_variants(self): qreg q[9]; mcx_gray q[0],q[1],q[2],q[3],q[4],q[5]; mcx_recursive q[0],q[1],q[2],q[3],q[4],q[5],q[6]; -mcx_vchain q[0],q[1],q[2],q[3],q[4],q[5],q[6],q[7],q[8];\n""" +mcx_vchain q[0],q[1],q[2],q[3],q[4],q[5],q[6],q[7],q[8];""" - self.assertEqual(qc.qasm(), expected_qasm) + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_registerless_bits(self): """Test that registerless bits do not have naming collisions in their registers.""" @@ -446,7 +439,7 @@ def test_circuit_qasm_with_registerless_bits(self): # Match a 'qreg identifier[3];'-like QASM register declaration. register_regex = re.compile(r"\s*[cq]reg\s+(\w+)\s*\[\d+\]\s*", re.M) qasm_register_names = set() - for statement in qc.qasm().split(";"): + for statement in dumps(qc).split(";"): match = register_regex.match(statement) if match: qasm_register_names.add(match.group(1)) @@ -462,7 +455,7 @@ def test_circuit_qasm_with_registerless_bits(self): for generated_name in generated_names: qc.add_register(QuantumRegister(1, name=generated_name)) qasm_register_names = set() - for statement in qc.qasm().split(";"): + for statement in dumps(qc).split(";"): match = register_regex.match(statement) if match: qasm_register_names.add(match.group(1)) @@ -498,9 +491,9 @@ def test_circuit_qasm_with_repeated_instruction_names(self): h q[0]; x q[1]; custom q[0]; -custom_{id(gate2)} q[1],q[0];\n""" +custom_{id(gate2)} q[1],q[0];""" # Check qasm() produced the correct string - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) # Check instruction names were not changed by qasm() names = ["h", "x", "custom", "custom"] for idx, instruction in enumerate(qc._data): @@ -539,12 +532,11 @@ def test_circuit_qasm_with_invalid_identifiers(self): "qreg q[2];", "gate_A___ q[0];", "invalid_name_ q[1],q[0];", - "", ] ) # Check qasm() produces the correct string - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) # Check instruction names were not changed by qasm() names = ["A[$]", "invalid[name]"] @@ -568,7 +560,7 @@ def test_circuit_qasm_with_duplicate_invalid_identifiers(self): # Check qasm is correctly produced names = set() - for match in re.findall(r"gate (\S+)", base.qasm()): + for match in re.findall(r"gate (\S+)", dumps(base)): self.assertTrue(VALID_QASM2_IDENTIFIER.fullmatch(match)) names.add(match) self.assertEqual(len(names), 2) @@ -584,15 +576,14 @@ def test_circuit_qasm_escapes_register_names(self): qc = QuantumCircuit(QuantumRegister(2, "?invalid"), QuantumRegister(2, "!invalid")) qc.cx(0, 1) qc.cx(2, 3) - qasm = qc.qasm() + qasm = dumps(qc) match = re.fullmatch( rf"""OPENQASM 2.0; include "qelib1.inc"; qreg ({VALID_QASM2_IDENTIFIER.pattern})\[2\]; qreg ({VALID_QASM2_IDENTIFIER.pattern})\[2\]; cx \1\[0\],\1\[1\]; -cx \2\[0\],\2\[1\]; -""", +cx \2\[0\],\2\[1\];""", qasm, ) self.assertTrue(match) @@ -604,14 +595,13 @@ def test_circuit_qasm_escapes_reserved(self): gate = Gate("gate", 1, []) gate.definition = QuantumCircuit(1) qc.append(gate, [qc.qubits[0]]) - qasm = qc.qasm() + qasm = dumps(qc) match = re.fullmatch( rf"""OPENQASM 2.0; include "qelib1.inc"; gate ({VALID_QASM2_IDENTIFIER.pattern}) q0 {{ }} qreg ({VALID_QASM2_IDENTIFIER.pattern})\[1\]; -\1 \2\[0\]; -""", +\1 \2\[0\];""", qasm, ) self.assertTrue(match) @@ -632,8 +622,8 @@ def test_circuit_qasm_with_double_precision_rotation_angle(self): qreg q[1]; p(0.123456789) q[0]; p(9.869604401089358) q[0]; -p(51.26548245743669) q[0];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +p(51.26548245743669) q[0];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_qasm_with_rotation_angles_close_to_pi(self): """Test that qasm() properly rounds values closer than 1e-12 to pi.""" @@ -645,8 +635,8 @@ def test_circuit_qasm_with_rotation_angles_close_to_pi(self): include "qelib1.inc"; qreg q[1]; p(3.141592653599793) q[0]; -p(pi) q[0];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +p(pi) q[0];""" + self.assertEqual(dumps(qc), expected_qasm) def test_circuit_raises_on_single_bit_condition(self): """OpenQASM 2 can't represent single-bit conditions, so test that a suitable error is @@ -655,7 +645,7 @@ def test_circuit_raises_on_single_bit_condition(self): qc.x(0).c_if(0, True) with self.assertRaisesRegex(QasmError, "OpenQASM 2 can only condition on registers"): - qc.qasm() + dumps(qc) def test_circuit_raises_invalid_custom_gate_no_qubits(self): """OpenQASM 2 exporter of custom gates with no qubits. @@ -665,7 +655,7 @@ def test_circuit_raises_invalid_custom_gate_no_qubits(self): legit_circuit.append(empty_circuit) with self.assertRaisesRegex(QasmError, "acts on zero qubits"): - legit_circuit.qasm() + dumps(legit_circuit) def test_circuit_raises_invalid_custom_gate_clbits(self): """OpenQASM 2 exporter of custom instruction. @@ -679,7 +669,7 @@ def test_circuit_raises_invalid_custom_gate_clbits(self): qc.append(custom_instruction, [0, 1], [0, 1]) with self.assertRaisesRegex(QasmError, "acts on 2 classical bits"): - qc.qasm() + dumps(qc) def test_circuit_qasm_with_permutations(self): """Test circuit qasm() method with Permutation gates.""" @@ -691,8 +681,8 @@ def test_circuit_qasm_with_permutations(self): include "qelib1.inc"; gate permutation__2_1_0_ q0,q1,q2 { swap q0,q2; } qreg q[4]; -permutation__2_1_0_ q[0],q[1],q[2];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +permutation__2_1_0_ q[0],q[1],q[2];""" + self.assertEqual(dumps(qc), expected_qasm) def test_multiple_permutation(self): """Test that multiple PermutationGates can be added to a circuit.""" @@ -704,7 +694,7 @@ def test_multiple_permutation(self): qc.append(PermutationGate([2, 1, 0]), [0, 1, 2], []) qc.append(PermutationGate([1, 2, 0]), [0, 1, 2], []) qc.append(custom.to_gate(), [1, 3, 2], []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; gate permutation__2_1_0_ q0,q1,q2 { swap q0,q2; } @@ -714,8 +704,7 @@ def test_multiple_permutation(self): qreg q[4]; permutation__2_1_0_ q[0],q[1],q[2]; permutation__1_2_0_ q[0],q[1],q[2]; -custom q[1],q[3],q[2]; -""" +custom q[1],q[3],q[2];""" self.assertEqual(qasm, expected) def test_circuit_qasm_with_reset(self): @@ -727,8 +716,8 @@ def test_circuit_qasm_with_reset(self): include "qelib1.inc"; qreg q[2]; reset q[0]; -reset q[1];\n""" - self.assertEqual(qc.qasm(), expected_qasm) +reset q[1];""" + self.assertEqual(dumps(qc), expected_qasm) def test_nested_gate_naming_clashes(self): """Test that gates that have naming clashes but only appear in the body of another gate @@ -755,7 +744,7 @@ def _define(self): qc = QuantumCircuit(1) qc.append(Outer(1.0), [0], []) qc.append(Outer(2.0), [0], []) - qasm = qc.qasm() + qasm = dumps(qc) expected = re.compile( r"""OPENQASM 2\.0; @@ -766,8 +755,7 @@ def _define(self): gate (?Pouter_[0-9]*)\(param0\) q0 { (?P=inner1)\(2\.0\) q0; } qreg q\[1\]; outer\(1\.0\) q\[0\]; -(?P=outer1)\(2\.0\) q\[0\]; -""", +(?P=outer1)\(2\.0\) q\[0\];""", re.MULTILINE, ) self.assertRegex(qasm, expected) @@ -782,7 +770,7 @@ def test_opaque_output(self): qc.append(Gate("my_a", 1, []), [1]) qc.append(Gate("my_b", 2, [1.0]), [1, 0]) qc.append(custom.to_gate(), [0], []) - qasm = qc.qasm() + qasm = dumps(qc) expected = """OPENQASM 2.0; include "qelib1.inc"; opaque my_a q0; @@ -793,8 +781,7 @@ def test_opaque_output(self): my_a q[0]; my_a q[1]; my_b(1.0) q[1],q[0]; -custom q[0]; -""" +custom q[0];""" self.assertEqual(qasm, expected) def test_sequencial_inner_gates_with_same_name(self): @@ -824,10 +811,9 @@ def test_sequencial_inner_gates_with_same_name(self): a_{gate_a_id} q[0],q[1],q[2]; z q[0]; z q[1]; -z q[2]; -""" +z q[2];""" - self.assertEqual(qc.qasm(), expected_output) + self.assertEqual(dumps(qc), expected_output) def test_empty_barrier(self): """Test that a blank barrier statement in _Qiskit_ acts over all qubits, while an explicitly @@ -842,9 +828,8 @@ def test_empty_barrier(self): include "qelib1.inc"; qreg qr1[2]; qreg qr2[3]; -barrier qr1[0],qr1[1],qr2[0],qr2[1],qr2[2]; -""" - self.assertEqual(qc.qasm(), expected) +barrier qr1[0],qr1[1],qr2[0],qr2[1],qr2[2];""" + self.assertEqual(dumps(qc), expected) def test_small_angle_valid(self): """Test that small angles do not get converted to invalid OQ2 floating-point values.""" @@ -856,9 +841,8 @@ def test_small_angle_valid(self): OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; -rx(1.e-06) q[0]; -""" - self.assertEqual(qc.qasm(), expected) +rx(1.e-06) q[0];""" + self.assertEqual(dumps(qc), expected) if __name__ == "__main__": diff --git a/test/python/circuit/test_circuit_registers.py b/test/python/circuit/test_circuit_registers.py index d6a1a63cb469..ca0ce6d0bed3 100644 --- a/test/python/circuit/test_circuit_registers.py +++ b/test/python/circuit/test_circuit_registers.py @@ -26,6 +26,7 @@ ) from qiskit.circuit.exceptions import CircuitError from qiskit.test import QiskitTestCase +from qiskit.qasm2 import dumps class TestCircuitRegisters(QiskitTestCase): @@ -284,7 +285,7 @@ def test_apply_barrier_to_slice(self): num_qubits = 2 qc = QuantumCircuit(qr, cr) qc.barrier(qr[0:num_qubits]) - self.log.info(qc.qasm()) + self.log.info(dumps(qc)) self.assertEqual(len(qc.data), 1) self.assertEqual(qc.data[0].operation.name, "barrier") self.assertEqual(len(qc.data[0].qubits), num_qubits) diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 27367372f329..26f3eb3fea2e 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -25,6 +25,7 @@ from qiskit.quantum_info.random import random_unitary from qiskit.quantum_info.operators import Operator from qiskit.transpiler.passes import CXCancellation +from qiskit.qasm2 import dumps class TestUnitaryGate(QiskitTestCase): @@ -79,7 +80,7 @@ def test_1q_unitary(self): qc.x(qr[0]) qc.append(UnitaryGate(matrix), [qr[0]]) # test of qasm output - self.log.info(qc.qasm()) + self.log.info(dumps(qc)) # test of text drawer self.log.info(qc) dag = circuit_to_dag(qc) @@ -105,7 +106,7 @@ def test_2q_unitary(self): passman.append(CXCancellation()) qc2 = passman.run(qc) # test of qasm output - self.log.info(qc2.qasm()) + self.log.info(dumps(qc2)) # test of text drawer self.log.info(qc2) dag = circuit_to_dag(qc) @@ -221,9 +222,9 @@ def test_qasm_unitary_only_one_def(self): "qreg q0[2];\ncreg c0[1];\n" "x q0[0];\n" "unitary q0[0];\n" - "unitary q0[1];\n" + "unitary q0[1];" ) - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) def test_qasm_unitary_twice(self): """test that a custom unitary can be converted to qasm and that if @@ -245,10 +246,10 @@ def test_qasm_unitary_twice(self): "qreg q0[2];\ncreg c0[1];\n" "x q0[0];\n" "unitary q0[0];\n" - "unitary q0[1];\n" + "unitary q0[1];" ) - self.assertEqual(expected_qasm, qc.qasm()) - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) + self.assertEqual(expected_qasm, dumps(qc)) def test_qasm_2q_unitary(self): """test that a 2 qubit custom unitary can be converted to qasm""" @@ -270,9 +271,9 @@ def test_qasm_2q_unitary(self): "creg c0[1];\n" "x q0[0];\n" "unitary q0[0],q0[1];\n" - "unitary q0[1],q0[0];\n" + "unitary q0[1],q0[0];" ) - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) def test_qasm_unitary_noop(self): """Test that an identity unitary can be converted to OpenQASM 2""" @@ -283,9 +284,9 @@ def test_qasm_unitary_noop(self): 'include "qelib1.inc";\n' "gate unitary q0,q1,q2 { }\n" "qreg q0[3];\n" - "unitary q0[0],q0[1],q0[2];\n" + "unitary q0[0],q0[1],q0[2];" ) - self.assertEqual(expected_qasm, qc.qasm()) + self.assertEqual(expected_qasm, dumps(qc)) def test_unitary_decomposition(self): """Test decomposition for unitary gates over 2 qubits.""" diff --git a/test/python/compiler/test_compiler.py b/test/python/compiler/test_compiler.py index 910d1e5866fe..627d5310531f 100644 --- a/test/python/compiler/test_compiler.py +++ b/test/python/compiler/test_compiler.py @@ -24,6 +24,7 @@ from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeRueschlikon, FakeTenerife from qiskit.qobj import QasmQobj +from qiskit.qasm2 import dumps class TestCompiler(QiskitTestCase): @@ -100,8 +101,8 @@ def test_compile_coupling_map(self): ) job = backend.run(qc_b, shots=shots, seed_simulator=88) result = job.result() - qasm_to_check = qc.qasm() - self.assertEqual(len(qasm_to_check), 173) + qasm_to_check = dumps(qc) + self.assertEqual(len(qasm_to_check), 172) counts = result.get_counts(qc) target = {"000": shots / 2, "111": shots / 2} diff --git a/test/python/converters/test_ast_to_dag.py b/test/python/converters/test_ast_to_dag.py deleted file mode 100644 index 490465b5967b..000000000000 --- a/test/python/converters/test_ast_to_dag.py +++ /dev/null @@ -1,67 +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. - -"""Tests for the converters.""" - -import os -import unittest - -from qiskit.converters import ast_to_dag, circuit_to_dag -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit import qasm -from qiskit.test import QiskitTestCase - - -class TestAstToDag(QiskitTestCase): - """Test AST to DAG.""" - - def setUp(self): - super().setUp() - qr = QuantumRegister(3) - cr = ClassicalRegister(3) - self.circuit = QuantumCircuit(qr, cr) - self.circuit.ccx(qr[0], qr[1], qr[2]) - self.circuit.measure(qr, cr) - self.dag = circuit_to_dag(self.circuit) - - def test_from_ast_to_dag(self): - """Test Unroller.execute()""" - qasm_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm") - ast = qasm.Qasm(os.path.join(qasm_dir, "example.qasm")).parse() - dag_circuit = ast_to_dag(ast) - expected_result = """\ -OPENQASM 2.0; -include "qelib1.inc"; -qreg q[3]; -qreg r[3]; -creg c[3]; -creg d[3]; -h q[0]; -h q[1]; -h q[2]; -cx q[0],r[0]; -cx q[1],r[1]; -cx q[2],r[2]; -barrier q[0],q[1],q[2]; -measure q[0] -> c[0]; -measure q[1] -> c[1]; -measure q[2] -> c[2]; -measure r[0] -> d[0]; -measure r[1] -> d[1]; -measure r[2] -> d[2]; -""" - expected_dag = circuit_to_dag(QuantumCircuit.from_qasm_str(expected_result)) - self.assertEqual(dag_circuit, expected_dag) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/test/python/qasm2/test_circuit_methods.py b/test/python/qasm2/test_circuit_methods.py index 9027fb4d54b9..e6b35e582ccb 100644 --- a/test/python/qasm2/test_circuit_methods.py +++ b/test/python/qasm2/test_circuit_methods.py @@ -21,6 +21,7 @@ from qiskit.test import QiskitTestCase from qiskit.transpiler.passes import Unroller from qiskit.converters.circuit_to_dag import circuit_to_dag +from qiskit.qasm2 import dumps class LoadFromQasmTest(QiskitTestCase): @@ -254,18 +255,15 @@ def test_qasm_example_file(self): def test_qasm_qas_string_order(self): """Test that gates are returned in qasm in ascending order.""" - expected_qasm = ( - "\n".join( - [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - "qreg q[3];", - "h q[0];", - "h q[1];", - "h q[2];", - ] - ) - + "\n" + expected_qasm = "\n".join( + [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + "qreg q[3];", + "h q[0];", + "h q[1];", + "h q[2];", + ] ) qasm_string = """OPENQASM 2.0; include "qelib1.inc"; @@ -273,7 +271,7 @@ def test_qasm_qas_string_order(self): h q;""" q_circuit = QuantumCircuit.from_qasm_str(qasm_string) - self.assertEqual(q_circuit.qasm(), expected_qasm) + self.assertEqual(dumps(q_circuit), expected_qasm) def test_from_qasm_str_custom_gate1(self): """Test load custom gates (simple case)""" diff --git a/test/python/qasm2/test_legacy_importer.py b/test/python/qasm2/test_legacy_importer.py deleted file mode 100644 index dea4d53f8fef..000000000000 --- a/test/python/qasm2/test_legacy_importer.py +++ /dev/null @@ -1,508 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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 cases for the legacy OpenQASM 2 parser.""" - -# pylint: disable=missing-function-docstring - - -import os - -from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit import Gate, Parameter -from qiskit.converters import ast_to_dag, dag_to_circuit -from qiskit.exceptions import QiskitError -from qiskit.qasm import Qasm -from qiskit.test import QiskitTestCase -from qiskit.transpiler.passes import Unroller -from qiskit.converters.circuit_to_dag import circuit_to_dag - - -def from_qasm_str(qasm_str): - return dag_to_circuit(ast_to_dag(Qasm(data=qasm_str).parse())) - - -def from_qasm_file(path): - return dag_to_circuit(ast_to_dag(Qasm(filename=path).parse())) - - -class LoadFromQasmTest(QiskitTestCase): - """Test circuit.from_qasm_* set of methods.""" - - def setUp(self): - super().setUp() - self.qasm_file_name = "entangled_registers.qasm" - self.qasm_dir = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm" - ) - self.qasm_file_path = os.path.join(self.qasm_dir, self.qasm_file_name) - - def test_qasm_file(self): - """ - Test qasm_file and get_circuit. - - If all is correct we should get the qasm file loaded in _qasm_file_path - """ - q_circuit = from_qasm_file(self.qasm_file_path) - qr_a = QuantumRegister(4, "a") - qr_b = QuantumRegister(4, "b") - cr_c = ClassicalRegister(4, "c") - cr_d = ClassicalRegister(4, "d") - q_circuit_2 = QuantumCircuit(qr_a, qr_b, cr_c, cr_d) - q_circuit_2.h(qr_a) - q_circuit_2.cx(qr_a, qr_b) - q_circuit_2.barrier(qr_a) - q_circuit_2.barrier(qr_b) - q_circuit_2.measure(qr_a, cr_c) - q_circuit_2.measure(qr_b, cr_d) - self.assertEqual(q_circuit, q_circuit_2) - - def test_loading_all_qelib1_gates(self): - """Test setting up a circuit with all gates defined in qiskit/qasm/libs/qelib1.inc.""" - from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, CU1Gate, CU3Gate, UGate - - all_gates_qasm = os.path.join(self.qasm_dir, "all_gates.qasm") - qasm_circuit = from_qasm_file(all_gates_qasm) - - ref_circuit = QuantumCircuit(3, 3) - - # abstract gates (legacy) - ref_circuit.append(UGate(0.2, 0.1, 0.6), [0]) - ref_circuit.cx(0, 1) - # the hardware primitives - ref_circuit.append(U3Gate(0.2, 0.1, 0.6), [0]) - ref_circuit.append(U2Gate(0.1, 0.6), [0]) - ref_circuit.append(U1Gate(0.6), [0]) - ref_circuit.id(0) - ref_circuit.cx(0, 1) - # the standard single qubit gates - ref_circuit.u(0.2, 0.1, 0.6, 0) - ref_circuit.p(0.6, 0) - ref_circuit.x(0) - ref_circuit.y(0) - ref_circuit.z(0) - ref_circuit.h(0) - ref_circuit.s(0) - ref_circuit.t(0) - ref_circuit.sdg(0) - ref_circuit.tdg(0) - ref_circuit.sx(0) - ref_circuit.sxdg(0) - # the standard rotations - ref_circuit.rx(0.1, 0) - ref_circuit.ry(0.1, 0) - ref_circuit.rz(0.1, 0) - # the barrier - ref_circuit.barrier() - # the standard user-defined gates - ref_circuit.swap(0, 1) - ref_circuit.cswap(0, 1, 2) - ref_circuit.cy(0, 1) - ref_circuit.cz(0, 1) - ref_circuit.ch(0, 1) - ref_circuit.csx(0, 1) - ref_circuit.append(CU1Gate(0.6), [0, 1]) - ref_circuit.append(CU3Gate(0.2, 0.1, 0.6), [0, 1]) - ref_circuit.cp(0.6, 0, 1) - ref_circuit.cu(0.2, 0.1, 0.6, 0, 0, 1) - ref_circuit.ccx(0, 1, 2) - ref_circuit.crx(0.6, 0, 1) - ref_circuit.cry(0.6, 0, 1) - ref_circuit.crz(0.6, 0, 1) - ref_circuit.rxx(0.2, 0, 1) - ref_circuit.rzz(0.2, 0, 1) - ref_circuit.measure([0, 1, 2], [0, 1, 2]) - - self.assertEqual(qasm_circuit, ref_circuit) - - def test_fail_qasm_file(self): - """ - Test fail_qasm_file. - - If all is correct we should get a QiskitError - """ - self.assertRaises(QiskitError, from_qasm_file, "") - - def test_qasm_text(self): - """ - Test qasm_text and get_circuit. - - If all is correct we should get the qasm file loaded from the string - """ - qasm_string = "// A simple 8 qubit example\nOPENQASM 2.0;\n" - qasm_string += 'include "qelib1.inc";\nqreg a[4];\n' - qasm_string += "qreg b[4];\ncreg c[4];\ncreg d[4];\nh a;\ncx a, b;\n" - qasm_string += "barrier a;\nbarrier b;\nmeasure a[0]->c[0];\n" - qasm_string += "measure a[1]->c[1];\nmeasure a[2]->c[2];\n" - qasm_string += "measure a[3]->c[3];\nmeasure b[0]->d[0];\n" - qasm_string += "measure b[1]->d[1];\nmeasure b[2]->d[2];\n" - qasm_string += "measure b[3]->d[3];" - q_circuit = from_qasm_str(qasm_string) - - qr_a = QuantumRegister(4, "a") - qr_b = QuantumRegister(4, "b") - cr_c = ClassicalRegister(4, "c") - cr_d = ClassicalRegister(4, "d") - ref = QuantumCircuit(qr_a, qr_b, cr_c, cr_d) - ref.h(qr_a[3]) - ref.cx(qr_a[3], qr_b[3]) - ref.h(qr_a[2]) - ref.cx(qr_a[2], qr_b[2]) - ref.h(qr_a[1]) - ref.cx(qr_a[1], qr_b[1]) - ref.h(qr_a[0]) - ref.cx(qr_a[0], qr_b[0]) - ref.barrier(qr_b) - ref.measure(qr_b, cr_d) - ref.barrier(qr_a) - ref.measure(qr_a, cr_c) - - self.assertEqual(len(q_circuit.cregs), 2) - self.assertEqual(len(q_circuit.qregs), 2) - self.assertEqual(q_circuit, ref) - - def test_qasm_text_conditional(self): - """ - Test qasm_text and get_circuit when conditionals are present. - """ - qasm_string = ( - "\n".join( - [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - "qreg q[1];", - "creg c0[4];", - "creg c1[4];", - "x q[0];", - "if(c1==4) x q[0];", - ] - ) - + "\n" - ) - q_circuit = from_qasm_str(qasm_string) - - qr = QuantumRegister(1, "q") - cr0 = ClassicalRegister(4, "c0") - cr1 = ClassicalRegister(4, "c1") - ref = QuantumCircuit(qr, cr0, cr1) - ref.x(qr[0]) - ref.x(qr[0]).c_if(cr1, 4) - - self.assertEqual(len(q_circuit.cregs), 2) - self.assertEqual(len(q_circuit.qregs), 1) - self.assertEqual(q_circuit, ref) - - def test_opaque_gate(self): - """ - Test parse an opaque gate - - See https://github.com/Qiskit/qiskit-terra/issues/1566. - """ - - qasm_string = ( - "\n".join( - [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - "opaque my_gate(theta,phi,lambda) a,b;", - "qreg q[3];", - "my_gate(1,2,3) q[1],q[2];", - ] - ) - + "\n" - ) - circuit = from_qasm_str(qasm_string) - - qr = QuantumRegister(3, "q") - expected = QuantumCircuit(qr) - expected.append(Gate(name="my_gate", num_qubits=2, params=[1, 2, 3]), [qr[1], qr[2]]) - - self.assertEqual(circuit, expected) - - def test_qasm_example_file(self): - """Loads qasm/example.qasm.""" - qasm_filename = os.path.join(self.qasm_dir, "example.qasm") - expected_circuit = from_qasm_str( - "\n".join( - [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - "qreg q[3];", - "qreg r[3];", - "creg c[3];", - "creg d[3];", - "h q[2];", - "cx q[2],r[2];", - "measure r[2] -> d[2];", - "h q[1];", - "cx q[1],r[1];", - "measure r[1] -> d[1];", - "h q[0];", - "cx q[0],r[0];", - "measure r[0] -> d[0];", - "barrier q[0],q[1],q[2];", - "measure q[2] -> c[2];", - "measure q[1] -> c[1];", - "measure q[0] -> c[0];", - ] - ) - + "\n" - ) - - q_circuit = from_qasm_file(qasm_filename) - - self.assertEqual(q_circuit, expected_circuit) - self.assertEqual(len(q_circuit.cregs), 2) - self.assertEqual(len(q_circuit.qregs), 2) - - def test_qasm_qas_string_order(self): - """Test that gates are returned in qasm in ascending order.""" - expected_qasm = ( - "\n".join( - [ - "OPENQASM 2.0;", - 'include "qelib1.inc";', - "qreg q[3];", - "h q[0];", - "h q[1];", - "h q[2];", - ] - ) - + "\n" - ) - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - qreg q[3]; - h q;""" - q_circuit = from_qasm_str(qasm_string) - - self.assertEqual(q_circuit.qasm(), expected_qasm) - - def test_from_qasm_str_custom_gate1(self): - """Test load custom gates (simple case)""" - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate rinv q {sdg q; h q; sdg q; h q; } - qreg qr[1]; - rinv qr[0];""" - circuit = from_qasm_str(qasm_string) - - rinv_q = QuantumRegister(1, name="q") - rinv_gate = QuantumCircuit(rinv_q, name="rinv") - rinv_gate.sdg(rinv_q) - rinv_gate.h(rinv_q) - rinv_gate.sdg(rinv_q) - rinv_gate.h(rinv_q) - rinv = rinv_gate.to_instruction() - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(rinv, [qr[0]]) - - self.assertEqualUnroll(["sdg", "h"], circuit, expected) - - def test_from_qasm_str_custom_gate2(self): - """Test load custom gates (no so simple case, different bit order) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate swap2 a,b { - cx a,b; - cx b,a; // different bit order - cx a,b; - } - qreg qr[3]; - swap2 qr[0], qr[1]; - swap2 qr[1], qr[2];""" - circuit = from_qasm_str(qasm_string) - - ab_args = QuantumRegister(2, name="ab") - swap_gate = QuantumCircuit(ab_args, name="swap2") - swap_gate.cx(ab_args[0], ab_args[1]) - swap_gate.cx(ab_args[1], ab_args[0]) - swap_gate.cx(ab_args[0], ab_args[1]) - swap = swap_gate.to_instruction() - - qr = QuantumRegister(3, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(swap, [qr[0], qr[1]]) - expected.append(swap, [qr[1], qr[2]]) - - self.assertEqualUnroll(["cx"], expected, circuit) - - def test_from_qasm_str_custom_gate3(self): - """Test load custom gates (no so simple case, different bit count) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate cswap2 a,b,c - { - cx c,b; // different bit count - ccx a,b,c; //previously defined gate - cx c,b; - } - qreg qr[3]; - cswap2 qr[1], qr[0], qr[2];""" - circuit = from_qasm_str(qasm_string) - - abc_args = QuantumRegister(3, name="abc") - cswap_gate = QuantumCircuit(abc_args, name="cswap2") - cswap_gate.cx(abc_args[2], abc_args[1]) - cswap_gate.ccx(abc_args[0], abc_args[1], abc_args[2]) - cswap_gate.cx(abc_args[2], abc_args[1]) - cswap = cswap_gate.to_instruction() - - qr = QuantumRegister(3, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(cswap, [qr[1], qr[0], qr[2]]) - - self.assertEqualUnroll(["cx", "h", "tdg", "t"], circuit, expected) - - def test_from_qasm_str_custom_gate4(self): - """Test load custom gates (parameterized) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate my_gate(phi,lambda) q {u(1.5707963267948966,phi,lambda) q;} - qreg qr[1]; - my_gate(pi, pi) qr[0];""" - circuit = from_qasm_str(qasm_string) - - my_gate_circuit = QuantumCircuit(1, name="my_gate") - phi = Parameter("phi") - lam = Parameter("lambda") - my_gate_circuit.u(1.5707963267948966, phi, lam, 0) - my_gate = my_gate_circuit.to_gate() - - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(my_gate, [qr[0]]) - expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) - - self.assertEqualUnroll("u", circuit, expected) - - def test_from_qasm_str_custom_gate5(self): - """Test load custom gates (parameterized, with biop and constant) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate my_gate(phi,lambda) q {u(pi/2,phi,lambda) q;} // biop with pi - qreg qr[1]; - my_gate(pi, pi) qr[0];""" - circuit = from_qasm_str(qasm_string) - - my_gate_circuit = QuantumCircuit(1, name="my_gate") - phi = Parameter("phi") - lam = Parameter("lambda") - my_gate_circuit.u(1.5707963267948966, phi, lam, 0) - my_gate = my_gate_circuit.to_gate() - - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(my_gate, [qr[0]]) - expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) - - self.assertEqualUnroll("u", circuit, expected) - - def test_from_qasm_str_custom_gate6(self): - """Test load custom gates (parameters used in expressions) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-591668924 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate my_gate(phi,lambda) q - {rx(phi+pi) q; ry(lambda/2) q;} // parameters used in expressions - qreg qr[1]; - my_gate(pi, pi) qr[0];""" - circuit = from_qasm_str(qasm_string) - - my_gate_circuit = QuantumCircuit(1, name="my_gate") - phi = Parameter("phi") - lam = Parameter("lambda") - my_gate_circuit.rx(phi + 3.141592653589793, 0) - my_gate_circuit.ry(lam / 2, 0) - my_gate = my_gate_circuit.to_gate() - - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.append(my_gate, [qr[0]]) - expected = expected.assign_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) - - self.assertEqualUnroll(["rx", "ry"], circuit, expected) - - def test_from_qasm_str_custom_gate7(self): - """Test load custom gates (build in functions) - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-592208951 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate my_gate(phi,lambda) q - {u(asin(cos(phi)/2), phi+pi, lambda/2) q;} // build func - qreg qr[1]; - my_gate(pi, pi) qr[0];""" - circuit = from_qasm_str(qasm_string) - - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.u(-0.5235987755982988, 6.283185307179586, 1.5707963267948966, qr[0]) - self.assertEqualUnroll("u", circuit, expected) - - def test_from_qasm_str_nested_custom_gate(self): - """Test chain of custom gates - See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-592261942 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - gate my_other_gate(phi,lambda) q - {u(asin(cos(phi)/2), phi+pi, lambda/2) q;} - gate my_gate(phi) r - {my_other_gate(phi, phi+pi) r;} - qreg qr[1]; - my_gate(pi) qr[0];""" - circuit = from_qasm_str(qasm_string) - - qr = QuantumRegister(1, name="qr") - expected = QuantumCircuit(qr, name="circuit") - expected.u(-0.5235987755982988, 6.283185307179586, 3.141592653589793, qr[0]) - self.assertEqualUnroll("u", circuit, expected) - - def test_from_qasm_str_delay(self): - """Test delay instruction/opaque-gate - See: https://github.com/Qiskit/qiskit-terra/issues/6510 - """ - qasm_string = """OPENQASM 2.0; - include "qelib1.inc"; - - opaque delay(time) q; - - qreg q[1]; - delay(172) q[0];""" - circuit = from_qasm_str(qasm_string) - - qr = QuantumRegister(1, name="q") - expected = QuantumCircuit(qr, name="circuit") - expected.delay(172, qr[0]) - self.assertEqualUnroll("u", circuit, expected) - - def assertEqualUnroll(self, basis, circuit, expected): - """Compares the dags after unrolling to basis""" - circuit_dag = circuit_to_dag(circuit) - expected_dag = circuit_to_dag(expected) - with self.assertWarns(DeprecationWarning): - circuit_result = Unroller(basis).run(circuit_dag) - expected_result = Unroller(basis).run(expected_dag) - - self.assertEqual(circuit_result, expected_result) diff --git a/test/python/test_qasm_parser.py b/test/python/test_qasm_parser.py deleted file mode 100644 index 49cdc2f12673..000000000000 --- a/test/python/test_qasm_parser.py +++ /dev/null @@ -1,126 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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 for the QASM parser""" - -import os -import unittest -import ply -import ddt - -from qiskit.qasm import Qasm, QasmError -from qiskit.qasm.node.node import Node -from qiskit.test import QiskitTestCase - - -def parse(file_path): - """ - Simple helper - - file_path: Path to the OpenQASM file - - prec: Precision for the returned string - """ - qasm = Qasm(file_path) - return qasm.parse().qasm() - - -@ddt.ddt -class TestParser(QiskitTestCase): - """QasmParser""" - - def setUp(self): - super().setUp() - self.qasm_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qasm") - self.qasm_file_path = os.path.join(self.qasm_dir, "example.qasm") - self.qasm_file_path_fail = os.path.join(self.qasm_dir, "example_fail.qasm") - self.qasm_file_path_if = os.path.join(self.qasm_dir, "example_if.qasm") - self.qasm_file_path_version_fail = os.path.join(self.qasm_dir, "example_version_fail.qasm") - self.qasm_file_path_version_2 = os.path.join(self.qasm_dir, "example_version_2.qasm") - self.qasm_file_path_minor_ver_fail = os.path.join( - self.qasm_dir, "example_minor_version_fail.qasm" - ) - - def test_parser(self): - """should return a correct response for a valid circuit.""" - - res = parse(self.qasm_file_path) - self.log.info(res) - # TODO: For now only some basic checks. - starts_expected = "OPENQASM 2.0;\ngate " - ends_expected = "\n".join( - [ - "}", - "qreg q[3];", - "qreg r[3];", - "h q;", - "cx q,r;", - "creg c[3];", - "creg d[3];", - "barrier q;", - "measure q -> c;", - "measure r -> d;", - "", - ] - ) - - self.assertEqual(res[: len(starts_expected)], starts_expected) - self.assertEqual(res[-len(ends_expected) :], ends_expected) - - def test_parser_fail(self): - """should fail a for a not valid circuit.""" - - self.assertRaisesRegex( - QasmError, "Perhaps there is a missing", parse, file_path=self.qasm_file_path_fail - ) - - @ddt.data("example_version_fail.qasm", "example_minor_version_fail.qasm") - def test_parser_version_fail(self, filename): - """Ensure versions other than 2.0 or 2 fail.""" - filename = os.path.join(self.qasm_dir, filename) - with self.assertRaisesRegex( - QasmError, r"Invalid version: '.+'\. This module supports OpenQASM 2\.0 only\." - ): - parse(filename) - - def test_parser_version_2(self): - """should succeed for OPENQASM version 2. Parser should automatically add minor verison.""" - res = parse(self.qasm_file_path_version_2) - version_start = "OPENQASM 2.0;" - self.assertEqual(res[: len(version_start)], version_start) - - def test_all_valid_nodes(self): - """Test that the tree contains only Node subclasses.""" - - def inspect(node): - """Inspect node children.""" - for child in node.children: - self.assertTrue(isinstance(child, Node)) - inspect(child) - - # Test the canonical example file. - qasm = Qasm(self.qasm_file_path) - res = qasm.parse() - inspect(res) - - # Test a file containing if instructions. - qasm_if = Qasm(self.qasm_file_path_if) - res_if = qasm_if.parse() - inspect(res_if) - - def test_generate_tokens(self): - """Test whether we get only valid tokens.""" - qasm = Qasm(self.qasm_file_path) - for token in qasm.generate_tokens(): - self.assertTrue(isinstance(token, ply.lex.LexToken)) - - -if __name__ == "__main__": - unittest.main() From 35567401c029385a5f02575f3f5a3d6ab14b6ad5 Mon Sep 17 00:00:00 2001 From: sven-oxionics Date: Wed, 29 Nov 2023 14:19:16 +0000 Subject: [PATCH 011/124] Decomposition of single R gate in RR basis should be one R gate, not two (#11304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Decomposition of single R gate in RR basis should be one R gate, not two Currently, a single R gate may come out of RR-decomposition as two R gates, which is obviously not ideal. For example, `RGate(0.1, 0.2)` is currently decomposed (in RR basis) as ``` ┌────────────────┐┌──────────┐ 0: ┤ R(-3.0416,0.2) ├┤ R(π,0.2) ├ └────────────────┘└──────────┘ ``` Two `R(𝜗, 𝜑)` gates with the same 𝜑 parameter can be combined into one by simply adding up the 𝜗 values (giving us `R(0.1, 0.2)`, unsurprisingly). In terms of `U(𝜗, 𝜑, 𝜆)`, it is the case when `𝜑 = -𝜆` that the two R-gates we construct have the same second parameter and therefore should be expressed as a single R gate. For example, `U3Gate(0.1, 0.2, -0.2)` currently produces this RR-decomposition: ``` ┌───────────────────┐┌─────────────┐ 0: ┤ R(-3.0416,1.7708) ├┤ R(π,1.7708) ├ └───────────────────┘└─────────────┘ ``` which also unnecessarily consists of two R gates instead of just one. This commit adds the two examples above as unit tests, ensuring they RR-decompose two just one R gate, as well as the code changes to make these two new tests pass, along with all existing tests, of course. The condition for this special case is that the 𝜑 parameters of the two R-gates we would emit are the same (thus `mod_2pi(PI / 2. - lam)=mod_2pi(0.5 * (phi - lam + PI)`, simplified as `mod_2pi((phi + lam) / 2)=0`). * Add release note --- .../src/euler_one_qubit_decomposer.rs | 28 +++++++++++-------- ...omposition-synthesis-70eb88ada9305916.yaml | 8 ++++++ test/python/quantum_info/test_synthesis.py | 3 ++ 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/rr-decomposition-synthesis-70eb88ada9305916.yaml diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 1a37528dbc8e..8fe0eaf87b45 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -380,22 +380,26 @@ fn circuit_rr( if !simplify { atol = -1.0; } - if theta.abs() < atol && phi.abs() < atol && lam.abs() < atol { - return OneQubitGateSequence { - gates: circuit, - global_phase: phase, - }; - } - if (theta - PI).abs() > atol { + + if mod_2pi((phi + lam) / 2., atol).abs() < atol { + // This can be expressed as a single R gate + if theta.abs() > atol { + circuit.push((String::from("r"), vec![theta, mod_2pi(PI / 2. + phi, atol)])); + } + } else { + // General case: use two R gates + if (theta - PI).abs() > atol { + circuit.push(( + String::from("r"), + vec![theta - PI, mod_2pi(PI / 2. - lam, atol)], + )); + } circuit.push(( String::from("r"), - vec![theta - PI, mod_2pi(PI / 2. - lam, atol)], + vec![PI, mod_2pi(0.5 * (phi - lam + PI), atol)], )); } - circuit.push(( - String::from("r"), - vec![PI, mod_2pi(0.5 * (phi - lam + PI), atol)], - )); + OneQubitGateSequence { gates: circuit, global_phase: phase, diff --git a/releasenotes/notes/rr-decomposition-synthesis-70eb88ada9305916.yaml b/releasenotes/notes/rr-decomposition-synthesis-70eb88ada9305916.yaml new file mode 100644 index 000000000000..e4ec6a970a3c --- /dev/null +++ b/releasenotes/notes/rr-decomposition-synthesis-70eb88ada9305916.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue in :func:`.unitary_to_gate_sequence` which caused unitary + decomposition in RR basis to emit two R gates in some cases where the + matrix can be expressed as a single R gate. Previously, in those cases you + would get two R gates with the same phi parameter. Now, they are combined + into one. diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 2d134967e34f..c04337d3ffd3 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -30,6 +30,7 @@ from qiskit.circuit.library import ( HGate, IGate, + RGate, SdgGate, SGate, U3Gate, @@ -446,6 +447,8 @@ def test_special_RR(self): self.check_oneq_special_cases(U3Gate(-np.pi, 0.2, 0.0).to_matrix(), "RR", {"r": 1}) self.check_oneq_special_cases(U3Gate(np.pi, 0.0, 0.2).to_matrix(), "RR", {"r": 1}) self.check_oneq_special_cases(U3Gate(0.1, 0.2, 0.3).to_matrix(), "RR", {"r": 2}) + self.check_oneq_special_cases(U3Gate(0.1, 0.2, -0.2).to_matrix(), "RR", {"r": 1}) + self.check_oneq_special_cases(RGate(0.1, 0.2).to_matrix(), "RR", {"r": 1}) def test_special_U1X(self): """Special cases of U1X""" From c100c7ba2449b903d13dd7719238b5038d70aae9 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 29 Nov 2023 14:36:45 -0500 Subject: [PATCH 012/124] Add Python 3.12 to ASV conf. (#11342) --- asv.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asv.conf.json b/asv.conf.json index 64dfa188fae4..9725442b6e9a 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -17,7 +17,7 @@ "dvcs": "git", "environment_type": "virtualenv", "show_commit_url": "http://github.com/Qiskit/qiskit/commit/", - "pythons": ["3.8", "3.9", "3.10", "3.11"], + "pythons": ["3.8", "3.9", "3.10", "3.11", "3.12"], "benchmark_dir": "test/benchmarks", "env_dir": ".asv/env", "results_dir": ".asv/results" From 4c9cdee6b56436f85aba03d58c7721a45a04edc5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 29 Nov 2023 22:37:31 +0000 Subject: [PATCH 013/124] Move build configuration to static configuration files (#10958) * Move build configuration to static configuration files This removes the vast majority of the dynamic code from our `setup.py` files, and instead uses static configuration to specify them. As part of this, we also use a single `VERSION.txt` as a single source of truth for both the `qiskit-terra` main package and the `qiskit` "metapackage". This should avoid a situation where we let the two get out of sync during a release, which can cause us severe problems for fixing the problem with respect to git tags. * Update components changed in rebase Co-authored-by: Matthew Treinish * Update contact email * Remove reference to 'qiskit_pkg' --------- Co-authored-by: Matthew Treinish --- pyproject.toml | 128 ++++++++++++++++ .../passes/synthesis/high_level_synthesis.py | 4 +- qiskit/transpiler/passes/synthesis/plugin.py | 43 +++--- .../transpiler/preset_passmanagers/plugin.py | 30 ++-- qiskit/utils/optionals.py | 2 +- requirements-optional.txt | 2 +- setup.py | 137 ++---------------- tox.ini | 2 +- 8 files changed, 173 insertions(+), 175 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 172c0625d25b..154754c224b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,134 @@ requires = ["setuptools", "wheel", "setuptools-rust"] build-backend = "setuptools.build_meta" +[project] +name = "qiskit" +description = "An open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives." +requires-python = ">=3.8" +license = { file = "LICENSE.txt" } +authors = [ + { name = "Qiskit Development Team", email = "qiskit@us.ibm.com" }, +] +keywords = [ + "qiskit", + "quantum circuit", + "quantum computing", + "quantum programming language", + "quantum", + "sdk", +] +classifiers = [ + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", +] +# These are configured in the `tool.setuptools.dynamic` table. +dynamic = ["version", "readme", "dependencies"] + +# If modifying this table, be sure to sync with `requirements-optional.txt` and +# `qiskit.utils.optionals`. +[project.optional-dependencies] +qasm3-import = [ + "qiskit-qasm3-import >= 0.1.0", +] +visualization = [ + "matplotlib >= 3.3", + "ipywidgets >= 7.3.0", + "pydot", + "Pillow >= 4.2.1", + "pylatexenc >= 1.4", + "seaborn >= 0.9.0", +] +crosstalk-pass = [ + "z3-solver >= 4.7", +] +csp-layout-pass = [ + "python-constraint >= 1.4", +] +# This will make the resolution work for installers from PyPI, but `pip install .[all]` will be +# unreliable because `qiskit` will resolve to the PyPI version, so local changes in the +# optionals won't be reflected. +all = ["qiskit[qasm3-import,visualization,crosstalk-pass,csp-layout-pass]"] + +[project.urls] +Homepage = "https://qiskit.org" +Documentation = "https://qiskit.org/documentation" +Repository = "https://github.com/Qiskit/qiskit" +Issues = "https://github.com/Qiskit/qiskit/issues" +Changelog = "https://qiskit.org/documentation/release_notes.html" + +[project.entry-points."qiskit.unitary_synthesis"] +default = "qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis" +aqc = "qiskit.transpiler.synthesis.aqc.aqc_plugin:AQCSynthesisPlugin" +sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis" + +[project.entry-points."qiskit.synthesis"] +"clifford.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisClifford" +"clifford.ag" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:AGSynthesisClifford" +"clifford.bm" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BMSynthesisClifford" +"clifford.greedy" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:GreedySynthesisClifford" +"clifford.layers" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerSynthesisClifford" +"clifford.lnn" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerLnnSynthesisClifford" +"linear_function.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisLinearFunction" +"linear_function.kms" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisLinearFunction" +"linear_function.pmh" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:PMHSynthesisLinearFunction" +"permutation.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation" +"permutation.kms" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisPermutation" +"permutation.basic" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation" +"permutation.acg" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:ACGSynthesisPermutation" + +[project.entry-points."qiskit.transpiler.init"] +default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" + +[project.entry-points."qiskit.transpiler.translation"] +synthesis = "qiskit.transpiler.preset_passmanagers.builtin_plugins:UnitarySynthesisPassManager" +translator = "qiskit.transpiler.preset_passmanagers.builtin_plugins:BasisTranslatorPassManager" +unroller = "qiskit.transpiler.preset_passmanagers.builtin_plugins:UnrollerPassManager" + +[project.entry-points."qiskit.transpiler.routing"] +basic = "qiskit.transpiler.preset_passmanagers.builtin_plugins:BasicSwapPassManager" +lookahead = "qiskit.transpiler.preset_passmanagers.builtin_plugins:LookaheadSwapPassManager" +none = "qiskit.transpiler.preset_passmanagers.builtin_plugins:NoneRoutingPassManager" +sabre = "qiskit.transpiler.preset_passmanagers.builtin_plugins:SabreSwapPassManager" +stochastic = "qiskit.transpiler.preset_passmanagers.builtin_plugins:StochasticSwapPassManager" + +[project.entry-points."qiskit.transpiler.optimization"] +default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:OptimizationPassManager" + +[project.entry-points."qiskit.transpiler.layout"] +default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultLayoutPassManager" +dense = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DenseLayoutPassManager" +noise_adaptive = "qiskit.transpiler.preset_passmanagers.builtin_plugins:NoiseAdaptiveLayoutPassManager" +sabre = "qiskit.transpiler.preset_passmanagers.builtin_plugins:SabreLayoutPassManager" +trivial = "qiskit.transpiler.preset_passmanagers.builtin_plugins:TrivialLayoutPassManager" + +[project.entry-points."qiskit.transpiler.scheduling"] +alap = "qiskit.transpiler.preset_passmanagers.builtin_plugins:AlapSchedulingPassManager" +asap = "qiskit.transpiler.preset_passmanagers.builtin_plugins:AsapSchedulingPassManager" +default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultSchedulingPassManager" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.dynamic] +version = { file = "qiskit/VERSION.txt" } +readme = { file = "README.md", content-type = "text/markdown" } +dependencies = {file = "requirements.txt" } + +[tool.setuptools.packages.find] +include = ["qiskit", "qiskit.*"] + [tool.black] line-length = 100 target-version = ['py38', 'py39', 'py310', 'py311'] diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 22bcf5abb127..2843ef077378 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -74,8 +74,8 @@ class HLSConfig: hls_config = HLSConfig(permutation=[(ACGSynthesisPermutation(), {})]) hls_config = HLSConfig(permutation=[ACGSynthesisPermutation()]) - The names of the synthesis algorithms should be declared in ``entry_points`` for - ``qiskit.synthesis`` in ``setup.py``, in the form + The names of the synthesis algorithms should be declared in ``entry-points`` table for + ``qiskit.synthesis`` in ``pyproject.toml``, in the form .. The standard higher-level-objects are recommended to have a synthesis method diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 2aaf96c59eab..4154c0186430 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -130,18 +130,15 @@ def run(self, unitary, **options): The second step is to expose the :class:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin` as a setuptools entry point in the package metadata. This is done by simply adding -an ``entry_points`` entry to the ``setuptools.setup`` call in the ``setup.py`` -for the plugin package with the necessary entry points under the -``qiskit.unitary_synthesis`` namespace. For example:: - - entry_points = { - 'qiskit.unitary_synthesis': [ - 'special = qiskit_plugin_pkg.module.plugin:SpecialUnitarySynthesis', - ] - }, - -(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 +an ``entry-points`` table in ``pyproject.toml`` for the plugin package with the necessary entry +points under the ``qiskit.unitary_synthesis`` namespace. For example: + +.. code-block:: toml + + [project.entry-points."qiskit.unitary-synthesis"] + "special" = "qiskit_plugin_pkg.module.plugin:SpecialUnitarySynthesis" + +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. The name ``default`` is used by Qiskit itself and can't be used in a plugin. @@ -218,19 +215,15 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** The second step is to expose the :class:`~qiskit.transpiler.passes.synthesis.plugin.HighLevelSynthesisPlugin` as a setuptools entry point in the package metadata. This is done by adding -an ``entry_points`` entry to the ``setuptools.setup`` call in the ``setup.py`` -for the plugin package with the necessary entry points under the -``qiskit.synthesis`` namespace. For example:: - - entry_points = { - 'qiskit.synthesis': [ - 'clifford.special = qiskit_plugin_pkg.module.plugin:SpecialSynthesisClifford', - ] - }, - -(note that the entry point ``name = path`` is a single string not a Python -expression). The ``name`` consists of two parts separated by dot ".": the -name of the +an ``entry-points`` table in ``pyproject.toml`` for the plugin package with the necessary entry +points under the ``qiskit.unitary_synthesis`` namespace. For example: + +.. code-block:: toml + + [project.entry-points."qiskit.synthesis"] + "clifford.special" = "qiskit_plugin_pkg.module.plugin:SpecialSynthesisClifford" + +The ``name`` consists of two parts separated by dot ".": the name of the type of :class:`~qiskit.circuit.Operation` to which the synthesis plugin applies (``clifford``), and the name of the plugin (``special``). There isn't a limit to the number of plugins a single package can diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index e0fabffc9d85..e80bfd92078b 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -134,23 +134,19 @@ def pass_manager(self, pass_manager_config, optimization_level): The second step is to expose the :class:`~.PassManagerStagePlugin` 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, entry points, 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. +an ``entry-points`` table in ``pyproject.toml`` for the plugin package with the necessary entry +points under the appropriate namespace for the stage your plugin is for. You can see the list of +stages, entry points, and expectations from the stage in :ref:`stage_table`. For example, +continuing from the example plugin above:: + +.. code-block:: toml + + [project.entry-points."qiskit.transpiler.layout"] + "vf2" = "qiskit_plugin_pkg.module.plugin:VF2LayoutPlugin" + +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 ========== diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index 07dc6e4376e5..cb69c6b5136c 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -212,7 +212,7 @@ """ # NOTE: If you're changing this file, sync it with `requirements-optional.txt` and potentially -# `setup.py` as well. +# `pyproject.toml` as well. import logging as _logging diff --git a/requirements-optional.txt b/requirements-optional.txt index 6c523a4937f3..aacce00a9c92 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -3,7 +3,7 @@ # standard pip conventions, even though none of these are required. # # If updating this, you probably want to update `qiskit.utils.optionals` and -# maybe `setup.py` too. +# maybe `pyproject.toml` too. # Test-runner enhancements. fixtures diff --git a/setup.py b/setup.py index 62e6f43bcbc6..91fb20c83cf2 100644 --- a/setup.py +++ b/setup.py @@ -13,90 +13,24 @@ "The Qiskit Terra setup file." import os -import re -from setuptools import setup, find_packages +from setuptools import setup from setuptools_rust import Binding, RustExtension +# Most of this configuration is managed by `pyproject.toml`. This only includes the extra bits to +# configure `setuptools-rust`, because we do a little dynamic trick with the debug setting, and we +# also want an explicit `setup.py` file to exist so we can manually call +# +# python setup.py build_rust --inplace --release +# +# to make optimised Rust components even for editable releases, which would otherwise be quite +# unergonomic to do otherwise. -with open("requirements.txt") as f: - REQUIREMENTS = f.read().splitlines() - -# Read long description from README. -README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md") -with open(README_PATH) as readme_file: - README = re.sub( - ".*", - "", - readme_file.read(), - flags=re.S | re.M, - ) # If RUST_DEBUG is set, force compiling in debug mode. Else, use the default behavior of whether # it's an editable installation. rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None -# If modifying these optional extras, make sure to sync with `requirements-optional.txt` and -# `qiskit.utils.optionals` as well. -qasm3_import_extras = [ - "qiskit-qasm3-import>=0.1.0", -] -visualization_extras = [ - "matplotlib>=3.3", - "ipywidgets>=7.3.0", - "pydot", - "pillow>=4.2.1", - "pylatexenc>=1.4", - "seaborn>=0.9.0", -] -z3_requirements = [ - "z3-solver>=4.7", -] -csp_requirements = ["python-constraint>=1.4"] - - setup( - name="qiskit", - version="1.0.0", - description="Software for developing quantum computing programs", - long_description=README, - long_description_content_type="text/markdown", - url="https://github.com/Qiskit/qiskit", - author="Qiskit Development Team", - author_email="hello@qiskit.org", - license="Apache 2.0", - classifiers=[ - "Environment :: Console", - "License :: OSI Approved :: Apache Software License", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering", - ], - keywords="qiskit sdk quantum", - packages=find_packages(exclude=["test*"]), - install_requires=REQUIREMENTS, - include_package_data=True, - python_requires=">=3.8", - extras_require={ - "qasm3-import": qasm3_import_extras, - "visualization": visualization_extras, - "crosstalk-pass": z3_requirements, - "csp-layout-pass": csp_requirements, - "all": visualization_extras + z3_requirements + csp_requirements + qasm3_import_extras, - }, - project_urls={ - "Bug Tracker": "https://github.com/Qiskit/qiskit-terra/issues", - "Documentation": "https://qiskit.org/documentation/", - "Source Code": "https://github.com/Qiskit/qiskit-terra", - }, rust_extensions=[ RustExtension( "qiskit._accelerate", @@ -112,57 +46,4 @@ ), ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, - zip_safe=False, - entry_points={ - "qiskit.unitary_synthesis": [ - "default = qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis", - "aqc = qiskit.transpiler.synthesis.aqc.aqc_plugin:AQCSynthesisPlugin", - "sk = qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevSynthesis", - ], - "qiskit.synthesis": [ - "clifford.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisClifford", - "clifford.ag = qiskit.transpiler.passes.synthesis.high_level_synthesis:AGSynthesisClifford", - "clifford.bm = qiskit.transpiler.passes.synthesis.high_level_synthesis:BMSynthesisClifford", - "clifford.greedy = qiskit.transpiler.passes.synthesis.high_level_synthesis:GreedySynthesisClifford", - "clifford.layers = qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerSynthesisClifford", - "clifford.lnn = qiskit.transpiler.passes.synthesis.high_level_synthesis:LayerLnnSynthesisClifford", - "linear_function.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisLinearFunction", - "linear_function.kms = qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisLinearFunction", - "linear_function.pmh = qiskit.transpiler.passes.synthesis.high_level_synthesis:PMHSynthesisLinearFunction", - "permutation.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation", - "permutation.kms = qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisPermutation", - "permutation.basic = qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation", - "permutation.acg = qiskit.transpiler.passes.synthesis.high_level_synthesis:ACGSynthesisPermutation", - ], - "qiskit.transpiler.init": [ - "default = qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager", - ], - "qiskit.transpiler.translation": [ - "translator = qiskit.transpiler.preset_passmanagers.builtin_plugins:BasisTranslatorPassManager", - "unroller = qiskit.transpiler.preset_passmanagers.builtin_plugins:UnrollerPassManager", - "synthesis = qiskit.transpiler.preset_passmanagers.builtin_plugins:UnitarySynthesisPassManager", - ], - "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", - ], - "qiskit.transpiler.optimization": [ - "default = qiskit.transpiler.preset_passmanagers.builtin_plugins:OptimizationPassManager", - ], - "qiskit.transpiler.layout": [ - "default = qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultLayoutPassManager", - "trivial = qiskit.transpiler.preset_passmanagers.builtin_plugins:TrivialLayoutPassManager", - "dense = qiskit.transpiler.preset_passmanagers.builtin_plugins:DenseLayoutPassManager", - "noise_adaptive = qiskit.transpiler.preset_passmanagers.builtin_plugins:NoiseAdaptiveLayoutPassManager", - "sabre = qiskit.transpiler.preset_passmanagers.builtin_plugins:SabreLayoutPassManager", - ], - "qiskit.transpiler.scheduling": [ - "alap = qiskit.transpiler.preset_passmanagers.builtin_plugins:AlapSchedulingPassManager", - "asap = qiskit.transpiler.preset_passmanagers.builtin_plugins:AsapSchedulingPassManager", - "default = qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultSchedulingPassManager", - ], - }, ) diff --git a/tox.ini b/tox.ini index df46820fc026..b790f698e034 100644 --- a/tox.ini +++ b/tox.ini @@ -41,7 +41,7 @@ allowlist_externals = git commands = ruff check qiskit test tools examples setup.py black --check {posargs} qiskit test tools examples setup.py - -git fetch -q https://github.com/Qiskit/qiskit-terra.git :lint_incr_latest + -git fetch -q https://github.com/Qiskit/qiskit.git :lint_incr_latest python {toxinidir}/tools/pylint_incr.py -rn -j4 -sn --paths :/qiskit/*.py :/test/*.py :/tools/*.py python {toxinidir}/tools/pylint_incr.py -rn -j4 -sn --disable='invalid-name,missing-module-docstring,redefined-outer-name' --paths :(glob,top)examples/python/*.py python {toxinidir}/tools/verify_headers.py qiskit test tools examples From ba161e92940ebed8fdf3f086a42616851c3c57ce Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Nov 2023 12:31:46 +0000 Subject: [PATCH 014/124] Ensure `QuantumCircuit.append` validates captures in control-flow (#10974) * Add definition of `Store` instruction This does not yet add the implementation of `QuantumCircuit.store`, which will come later as part of expanding the full API of `QuantumCircuit` to be able to support these runtime variables. The `is_lvalue` helper is added generally to the `classical.expr` module because it's generally useful, while `types.cast_kind` is moved from being a private method in `expr` to a public-API function so `Store` can use it. These now come with associated unit tests. * Add variable-handling methods to `QuantumCircuit` This adds all the new `QuantumCircuit` methods discussed in the variable-declaration RFC[^1], and threads the support for them through the methods that are called in turn, such as `QuantumCircuit.append`. It does yet not add support to methods such as `copy` or `compose`, which will be done in a follow-up. The APIs discussed in the RFC necessitated making `Var` nodes hashable. This is done in this commit, as it is logically connected. These nodes now have enforced immutability, which is technically a minor breaking change, but in practice required for the properties of such expressions to be tracked correctly through circuits. A helper attribute `Var.standalone` is added to unify the handling of whether a variable is an old-style existing-memory wrapper, or a new "proper" variable with its own memory. [^1]: https://github.com/Qiskit/RFCs/pull/50 * Support manual variables `QuantumCircuit` copy methods This commit adds support to the `QuantumCircuit` methods `copy` and `copy_empty_like` for manual variables. This involves the non-trivial extension to the original RFC[^1] that variables can now be uninitialised; this is somewhat required for the logic of how the `Store` instruction works and the existence of `QuantumCircuit.copy_empty_like`; a variable could be initialised with the result of a `measure` that no longer exists, therefore it must be possible for variables to be uninitialised. This was not originally intended to be possible in the design document, but is somewhat required for logical consistency. A method `add_uninitialized_var` is added, so that the behaviour of `copy_empty_like` is not an awkward special case only possible through that method, but instead a complete part of the data model that must be reasoned about. The method however is deliberately a bit less ergononmic to type and to use, because really users _should_ use `add_var` in almost all circumstances. [^1]: https://github.com/Qiskit/RFCs/pull/50 * Ensure `QuantumCircuit.append` validates captures in control-flow This adds an inner check to the control-flow operations that their blocks do not contain input variables, and to `QuantumCircuit.append` that any captures within blocks are validate (in the sense of the variables existing in the outer circuit). In order to avoid an `import` on every call to `QuantumCircuit.append` (especially since we're already eating the cost of an extra `isinstance` check), this reorganises the import structure of `qiskit.circuit.controlflow` to sit strictly _before_ `qiskit.circuit.quantumcircuit` in the import tree. Since those are key parts of the circuit data structure, that does make sense, although by their nature the structures are of course recursive at runtime. * Update documentation Co-authored-by: Matthew Treinish * Catch simple error case in '_prepare_new_var' Co-authored-by: Matthew Treinish * Add partial release note --------- Co-authored-by: Matthew Treinish --- qiskit/circuit/__init__.py | 2 + qiskit/circuit/classical/expr/__init__.py | 8 +- qiskit/circuit/classical/expr/constructors.py | 52 +- qiskit/circuit/classical/expr/expr.py | 62 ++- qiskit/circuit/classical/expr/visitors.py | 63 +++ qiskit/circuit/classical/types/__init__.py | 27 +- qiskit/circuit/classical/types/ordering.py | 59 +++ qiskit/circuit/classical/types/types.py | 6 + qiskit/circuit/controlflow/_builder_utils.py | 9 +- qiskit/circuit/controlflow/builder.py | 3 +- qiskit/circuit/controlflow/control_flow.py | 19 +- qiskit/circuit/controlflow/for_loop.py | 13 +- qiskit/circuit/controlflow/if_else.py | 13 +- qiskit/circuit/controlflow/switch_case.py | 10 +- qiskit/circuit/controlflow/while_loop.py | 10 +- qiskit/circuit/quantumcircuit.py | 474 ++++++++++++++++-- qiskit/circuit/store.py | 87 ++++ .../classical-store-e64ee1286219a862.yaml | 56 +++ ...r-hashable-var-types-7cf2aaa00b201ae6.yaml | 5 + .../expr-var-standalone-2c1116583a2be9fd.yaml | 6 + .../circuit/classical/test_expr_helpers.py | 27 + .../circuit/classical/test_expr_properties.py | 36 +- .../circuit/classical/test_types_ordering.py | 10 + .../python/circuit/test_circuit_operations.py | 72 +++ test/python/circuit/test_circuit_vars.py | 393 +++++++++++++++ test/python/circuit/test_control_flow.py | 188 +++++++ test/python/circuit/test_store.py | 199 ++++++++ 27 files changed, 1802 insertions(+), 107 deletions(-) create mode 100644 qiskit/circuit/store.py create mode 100644 releasenotes/notes/classical-store-e64ee1286219a862.yaml create mode 100644 releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml create mode 100644 releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml create mode 100644 test/python/circuit/test_circuit_vars.py create mode 100644 test/python/circuit/test_store.py diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 1a41c299d53e..f214eb5c72dd 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + Store Control Flow Operations ----------------------- @@ -375,6 +376,7 @@ from .delay import Delay from .measure import Measure from .reset import Reset +from .store import Store from .parameter import Parameter from .parametervector import ParameterVector from .parameterexpression import ParameterExpression diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index b2b4d138fca7..4502aa52779a 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -160,6 +160,11 @@ suitable "key" functions to do the comparison. .. autofunction:: structurally_equivalent + +Some expressions have associated memory locations, and others may be purely temporary. +You can use :func:`is_lvalue` to determine whether an expression has an associated memory location. + +.. autofunction:: is_lvalue """ __all__ = [ @@ -172,6 +177,7 @@ "ExprVisitor", "iter_vars", "structurally_equivalent", + "is_lvalue", "lift", "cast", "bit_not", @@ -191,7 +197,7 @@ ] from .expr import Expr, Var, Value, Cast, Unary, Binary -from .visitors import ExprVisitor, iter_vars, structurally_equivalent +from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue from .constructors import ( lift, cast, diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 1406a86237c5..64a19a2aee2a 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -35,65 +35,27 @@ "lift_legacy_condition", ] -import enum import typing from .expr import Expr, Var, Value, Unary, Binary, Cast +from ..types import CastKind, cast_kind from .. import types if typing.TYPE_CHECKING: import qiskit -class _CastKind(enum.Enum): - EQUAL = enum.auto() - """The two types are equal; no cast node is required at all.""" - IMPLICIT = enum.auto() - """The 'from' type can be cast to the 'to' type implicitly. A ``Cast(implicit=True)`` node is - the minimum required to specify this.""" - LOSSLESS = enum.auto() - """The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This - requires a ``Cast(implicit=False)`` node, but there's no danger from inserting one.""" - DANGEROUS = enum.auto() - """The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose - data. A user would need to manually specify casts.""" - NONE = enum.auto() - """There is no casting permitted from the 'from' type to the 'to' type.""" - - -def _uint_cast(from_: types.Uint, to_: types.Uint, /) -> _CastKind: - if from_.width == to_.width: - return _CastKind.EQUAL - if from_.width < to_.width: - return _CastKind.LOSSLESS - return _CastKind.DANGEROUS - - -_ALLOWED_CASTS = { - (types.Bool, types.Bool): lambda _a, _b, /: _CastKind.EQUAL, - (types.Bool, types.Uint): lambda _a, _b, /: _CastKind.LOSSLESS, - (types.Uint, types.Bool): lambda _a, _b, /: _CastKind.IMPLICIT, - (types.Uint, types.Uint): _uint_cast, -} - - -def _cast_kind(from_: types.Type, to_: types.Type, /) -> _CastKind: - if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: - return _CastKind.NONE - return coercer(from_, to_) - - def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: """Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is lossless. Otherwise, raise a ``TypeError``.""" - kind = _cast_kind(expr.type, type) - if kind is _CastKind.EQUAL: + kind = cast_kind(expr.type, type) + if kind is CastKind.EQUAL: return expr - if kind is _CastKind.IMPLICIT: + if kind is CastKind.IMPLICIT: return Cast(expr, type, implicit=True) - if kind is _CastKind.LOSSLESS: + if kind is CastKind.LOSSLESS: return Cast(expr, type, implicit=False) - if kind is _CastKind.DANGEROUS: + if kind is CastKind.DANGEROUS: raise TypeError(f"cannot cast '{expr}' to '{type}' without loss of precision") raise TypeError(f"no cast is defined to take '{expr}' to '{type}'") @@ -198,7 +160,7 @@ def cast(operand: typing.Any, type: types.Type, /) -> Expr: Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) """ operand = lift(operand) - if _cast_kind(operand.type, type) is _CastKind.NONE: + if cast_kind(operand.type, type) is CastKind.NONE: raise TypeError(f"cannot cast '{operand}' to '{type}'") return Cast(operand, type) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 3adbacfd6926..c22870e51fee 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -115,10 +115,24 @@ class Var(Expr): associated name; and an old-style variable that wraps a :class:`.Clbit` or :class:`.ClassicalRegister` instance that is owned by some containing circuit. In general, construction of variables for use in programs should use :meth:`Var.new` or - :meth:`.QuantumCircuit.add_var`.""" + :meth:`.QuantumCircuit.add_var`. + + Variables are immutable after construction, so they can be used as dictionary keys.""" __slots__ = ("var", "name") + var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID + """A reference to the backing data storage of the :class:`Var` instance. When lifting + old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`, + this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a + new-style classical variable (one that owns its own storage separate to the old + :class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID` + to uniquely identify it.""" + name: str | None + """The name of the variable. This is required to exist if the backing :attr:`var` attribute + is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is + an old-style variable.""" + def __init__( self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, @@ -126,27 +140,32 @@ def __init__( *, name: str | None = None, ): - self.type = type - self.var = var - """A reference to the backing data storage of the :class:`Var` instance. When lifting - old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`, - this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a - new-style classical variable (one that owns its own storage separate to the old - :class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID` - to uniquely identify it.""" - self.name = name - """The name of the variable. This is required to exist if the backing :attr:`var` attribute - is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is - an old-style variable.""" + super().__setattr__("type", type) + super().__setattr__("var", var) + super().__setattr__("name", name) @classmethod def new(cls, name: str, type: types.Type) -> typing.Self: """Generate a new named variable that owns its own backing storage.""" return cls(uuid.uuid4(), type, name=name) + @property + def standalone(self) -> bool: + """Whether this :class:`Var` is a standalone variable that owns its storage location. If + false, this is a wrapper :class:`Var` around a pre-existing circuit object.""" + return isinstance(self.var, uuid.UUID) + def accept(self, visitor, /): return visitor.visit_var(self) + def __setattr__(self, key, value): + if hasattr(self, key): + raise AttributeError(f"'Var' object attribute '{key}' is read-only") + raise AttributeError(f"'Var' object has no attribute '{key}'") + + def __hash__(self): + return hash((self.type, self.var, self.name)) + def __eq__(self, other): return ( isinstance(other, Var) @@ -160,6 +179,23 @@ def __repr__(self): return f"Var({self.var}, {self.type})" return f"Var({self.var}, {self.type}, name='{self.name}')" + def __getstate__(self): + return (self.var, self.type, self.name) + + def __setstate__(self, state): + var, type, name = state + super().__setattr__("type", type) + super().__setattr__("var", var) + super().__setattr__("name", name) + + def __copy__(self): + # I am immutable... + return self + + def __deepcopy__(self, memo): + # ... as are all my consituent parts. + return self + @typing.final class Value(Expr): diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 07ad36a8e0e4..c0c1a5894af6 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -215,3 +215,66 @@ def structurally_equivalent( True """ return left.accept(_StructuralEquivalenceImpl(right, left_var_key, right_var_key)) + + +class _IsLValueImpl(ExprVisitor[bool]): + __slots__ = () + + def visit_var(self, node, /): + return True + + def visit_value(self, node, /): + return False + + def visit_unary(self, node, /): + return False + + def visit_binary(self, node, /): + return False + + def visit_cast(self, node, /): + return False + + +_IS_LVALUE = _IsLValueImpl() + + +def is_lvalue(node: expr.Expr, /) -> bool: + """Return whether this expression can be used in l-value positions, that is, whether it has a + well-defined location in memory, such as one that might be writeable. + + Being an l-value is a necessary but not sufficient for this location to be writeable; it is + permissible that a larger object containing this memory location may not allow writing from + the scope that attempts to write to it. This would be an access property of the containing + program, however, and not an inherent property of the expression system. + + Examples: + Literal values are never l-values; there's no memory location associated with (for example) + the constant ``1``:: + + >>> from qiskit.circuit.classical import expr + >>> expr.is_lvalue(expr.lift(2)) + False + + :class:`~.expr.Var` nodes are always l-values, because they always have some associated + memory location:: + + >>> from qiskit.circuit.classical import types + >>> from qiskit.circuit import Clbit + >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) + True + >>> expr.is_lvalue(expr.lift(Clbit())) + True + + Currently there are no unary or binary operations on variables that can produce an l-value + expression, but it is likely in the future that some sort of "indexing" operation will be + added, which could produce l-values:: + + >>> a = expr.Var.new("a", types.Uint(8)) + >>> b = expr.Var.new("b", types.Uint(8)) + >>> expr.is_lvalue(a) and expr.is_lvalue(b) + True + >>> expr.is_lvalue(expr.bit_and(a, b)) + False + """ + return node.accept(_IS_LVALUE) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index c55c724315cc..93ab90e32166 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -15,6 +15,8 @@ Typing (:mod:`qiskit.circuit.classical.types`) ============================================== +Representation +============== The type system of the expression tree is exposed through this module. This is inherently linked to the expression system in the :mod:`~.classical.expr` module, as most expressions can only be @@ -41,11 +43,18 @@ Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. + +Working with types +================== + There are some functions on these types exposed here as well. These are mostly expected to be used only in manipulations of the expression tree; users who are building expressions using the :ref:`user-facing construction interface ` should not need to use these. +Partial ordering of types +------------------------- + The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as ":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the directed graph that describes the allowed explicit casting operations between types. The partial @@ -66,6 +75,20 @@ .. autofunction:: is_subtype .. autofunction:: is_supertype .. autofunction:: greater + + +Casting between types +--------------------- + +It is common to need to cast values of one type to another type. The casting rules for this are +embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: + +.. autofunction:: cast_kind + +The return values from this function are an enumeration explaining the types of cast that are +allowed from the left type to the right type. + +.. autoclass:: CastKind """ __all__ = [ @@ -77,7 +100,9 @@ "is_subtype", "is_supertype", "greater", + "CastKind", + "cast_kind", ] from .types import Type, Bool, Uint -from .ordering import Ordering, order, is_subtype, is_supertype, greater +from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index aceb9aeefbcf..b000e91cf5ed 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -20,6 +20,8 @@ "is_supertype", "order", "greater", + "CastKind", + "cast_kind", ] import enum @@ -161,3 +163,60 @@ def greater(left: Type, right: Type, /) -> Type: if order_ is Ordering.NONE: raise TypeError(f"no ordering exists between '{left}' and '{right}'") return left if order_ is Ordering.GREATER else right + + +class CastKind(enum.Enum): + """A return value indicating the type of cast that can occur from one type to another.""" + + EQUAL = enum.auto() + """The two types are equal; no cast node is required at all.""" + IMPLICIT = enum.auto() + """The 'from' type can be cast to the 'to' type implicitly. A :class:`~.expr.Cast` node with + ``implicit==True`` is the minimum required to specify this.""" + LOSSLESS = enum.auto() + """The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This + requires a :class:`~.expr.Cast`` node with ``implicit=False``, but there's no danger from + inserting one.""" + DANGEROUS = enum.auto() + """The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose + data. A user would need to manually specify casts.""" + NONE = enum.auto() + """There is no casting permitted from the 'from' type to the 'to' type.""" + + +def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind: + if from_.width == to_.width: + return CastKind.EQUAL + if from_.width < to_.width: + return CastKind.LOSSLESS + return CastKind.DANGEROUS + + +_ALLOWED_CASTS = { + (Bool, Bool): lambda _a, _b, /: CastKind.EQUAL, + (Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS, + (Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT, + (Uint, Uint): _uint_cast, +} + + +def cast_kind(from_: Type, to_: Type, /) -> CastKind: + """Determine the sort of cast that is required to move from the left type to the right type. + + Examples: + + .. code-block:: python + + >>> from qiskit.circuit.classical import types + >>> types.cast_kind(types.Bool(), types.Bool()) + + >>> types.cast_kind(types.Uint(8), types.Bool()) + + >>> types.cast_kind(types.Bool(), types.Uint(8)) + + >>> types.cast_kind(types.Uint(16), types.Uint(8)) + + """ + if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: + return CastKind.NONE + return coercer(from_, to_) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 711f82db5fc0..04266aefd410 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -89,6 +89,9 @@ class Bool(Type, metaclass=_Singleton): def __repr__(self): return "Bool()" + def __hash__(self): + return hash(self.__class__) + def __eq__(self, other): return isinstance(other, Bool) @@ -107,5 +110,8 @@ def __init__(self, width: int): def __repr__(self): return f"Uint({self.width})" + def __hash__(self): + return hash((self.__class__, self.width)) + def __eq__(self, other): return isinstance(other, Uint) and self.width == other.width diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index 5ba5c9612c9d..aa1e331eb41a 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -15,15 +15,17 @@ from __future__ import annotations import dataclasses -from typing import Iterable, Tuple, Set, Union, TypeVar +from typing import Iterable, Tuple, Set, Union, TypeVar, TYPE_CHECKING from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.register import Register from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.quantumregister import QuantumRegister +if TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + _ConditionT = TypeVar( "_ConditionT", bound=Union[Tuple[ClassicalRegister, int], Tuple[Clbit, int], expr.Expr] ) @@ -159,6 +161,9 @@ def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's t This function will always rebuild the objects into new :class:`.QuantumCircuit` instances. """ + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + qubits, clbits = set(), set() for circuit in circuits: qubits.update(circuit.qubits) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c8ada706e5fe..d997a080f991 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -33,7 +33,7 @@ from ._builder_utils import condition_resources, node_resources if typing.TYPE_CHECKING: - import qiskit # pylint: disable=cyclic-import + import qiskit class InstructionResources(typing.NamedTuple): @@ -403,6 +403,7 @@ def build( and using the minimal set of resources necessary to support them, within the enclosing scope. """ + # pylint: disable=cyclic-import from qiskit.circuit import QuantumCircuit, SwitchCaseOp # There's actually no real problem with building a scope more than once. This flag is more diff --git a/qiskit/circuit/controlflow/control_flow.py b/qiskit/circuit/controlflow/control_flow.py index 11ac283132f4..fefa27efa27f 100644 --- a/qiskit/circuit/controlflow/control_flow.py +++ b/qiskit/circuit/controlflow/control_flow.py @@ -13,15 +13,26 @@ "Container to encapsulate all control flow operations." from __future__ import annotations + +import typing from abc import ABC, abstractmethod -from typing import Iterable -from qiskit.circuit import QuantumCircuit, Instruction +from qiskit.circuit.instruction import Instruction +from qiskit.circuit.exceptions import CircuitError + +if typing.TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit class ControlFlowOp(Instruction, ABC): """Abstract class to encapsulate all control flow operations.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for block in self.blocks: + if block.num_input_vars: + raise CircuitError("control-flow blocks cannot contain input variables") + @property @abstractmethod def blocks(self) -> tuple[QuantumCircuit, ...]: @@ -29,10 +40,9 @@ def blocks(self) -> tuple[QuantumCircuit, ...]: execution of this ControlFlowOp. May be parameterized by a loop parameter to be resolved at run time. """ - pass @abstractmethod - def replace_blocks(self, blocks: Iterable[QuantumCircuit]) -> "ControlFlowOp": + def replace_blocks(self, blocks: typing.Iterable[QuantumCircuit]) -> ControlFlowOp: """Replace blocks and return new instruction. Args: blocks: Tuple of QuantumCircuits to replace in instruction. @@ -40,4 +50,3 @@ def replace_blocks(self, blocks: Iterable[QuantumCircuit]) -> "ControlFlowOp": Returns: New ControlFlowOp with replaced blocks. """ - pass diff --git a/qiskit/circuit/controlflow/for_loop.py b/qiskit/circuit/controlflow/for_loop.py index f0e79e47ca81..f62348898cec 100644 --- a/qiskit/circuit/controlflow/for_loop.py +++ b/qiskit/circuit/controlflow/for_loop.py @@ -10,16 +10,20 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"Circuit operation representing a ``for`` loop." +"""Circuit operation representing a ``for`` loop.""" + +from __future__ import annotations import warnings -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Union, TYPE_CHECKING from qiskit.circuit.parameter import Parameter from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.quantumcircuit import QuantumCircuit from .control_flow import ControlFlowOp +if TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + class ForLoopOp(ControlFlowOp): """A circuit operation which repeatedly executes a subcircuit @@ -69,6 +73,9 @@ def params(self): @params.setter def params(self, parameters): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + indexset, loop_parameter, body = parameters if not isinstance(loop_parameter, (Parameter, type(None))): diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 775432fc671e..c06206b15871 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -14,10 +14,10 @@ from __future__ import annotations -from typing import Optional, Union, Iterable +from typing import Optional, Union, Iterable, TYPE_CHECKING import itertools -from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.classical import expr from qiskit.circuit.instructionset import InstructionSet from qiskit.circuit.exceptions import CircuitError @@ -31,6 +31,9 @@ condition_resources, ) +if TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + # This is just an indication of what's actually meant to be the public API. __all__ = ("IfElseOp",) @@ -82,6 +85,9 @@ def __init__( false_body: QuantumCircuit | None = None, label: str | None = None, ): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + # Type checking generally left to @params.setter, but required here for # finding num_qubits and num_clbits. if not isinstance(true_body, QuantumCircuit): @@ -103,6 +109,9 @@ def params(self): @params.setter def params(self, parameters): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + true_body, false_body = parameters if not isinstance(true_body, QuantumCircuit): diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 0f215a9bcbb8..027f2cfbfff3 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -17,9 +17,9 @@ __all__ = ("SwitchCaseOp", "CASE_DEFAULT") import contextlib -from typing import Union, Iterable, Any, Tuple, Optional, List, Literal +from typing import Union, Iterable, Any, Tuple, Optional, List, Literal, TYPE_CHECKING -from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError @@ -27,6 +27,9 @@ from .control_flow import ControlFlowOp from ._builder_utils import unify_circuit_resources, partition_registers, node_resources +if TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + class _DefaultCaseType: """The type of the default-case singleton. This is used instead of just having @@ -71,6 +74,9 @@ def __init__( *, label: Optional[str] = None, ): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + if isinstance(target, expr.Expr): if target.type.kind not in (types.Uint, types.Bool): raise CircuitError( diff --git a/qiskit/circuit/controlflow/while_loop.py b/qiskit/circuit/controlflow/while_loop.py index 98fefa3ce8c2..8bd8e8d2d067 100644 --- a/qiskit/circuit/controlflow/while_loop.py +++ b/qiskit/circuit/controlflow/while_loop.py @@ -14,12 +14,17 @@ from __future__ import annotations -from qiskit.circuit import Clbit, ClassicalRegister, QuantumCircuit +from typing import TYPE_CHECKING + +from qiskit.circuit.classicalregister import Clbit, ClassicalRegister from qiskit.circuit.classical import expr from qiskit.circuit.exceptions import CircuitError from ._builder_utils import validate_condition, condition_resources from .control_flow import ControlFlowOp +if TYPE_CHECKING: + from qiskit.circuit import QuantumCircuit + class WhileLoopOp(ControlFlowOp): """A circuit operation which repeatedly executes a subcircuit (``body``) until @@ -70,6 +75,9 @@ def params(self): @params.setter def params(self, parameters): + # pylint: disable=cyclic-import + from qiskit.circuit import QuantumCircuit + (body,) = parameters if not isinstance(body, QuantumCircuit): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index cb164404ac1e..0bca0ef5d914 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -16,6 +16,7 @@ from __future__ import annotations import copy +import itertools import multiprocessing as mp import warnings import typing @@ -47,7 +48,14 @@ from qiskit.utils.deprecation import deprecate_func from . import _classical_resource_map from ._utils import sort_parameters -from .classical import expr +from .controlflow import ControlFlowOp +from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder +from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder +from .controlflow.for_loop import ForLoopOp, ForLoopContext +from .controlflow.if_else import IfElseOp, IfContext +from .controlflow.switch_case import SwitchCaseOp, SwitchContext +from .controlflow.while_loop import WhileLoopOp, WhileLoopContext +from .classical import expr, types from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit @@ -59,6 +67,7 @@ from .bit import Bit from .quantumcircuitdata import QuantumCircuitData, CircuitInstruction from .delay import Delay +from .store import Store if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -136,9 +145,27 @@ class QuantumCircuit: circuit. This gets stored as free-form data in a dict in the :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will not be directly used in the circuit. + inputs: any variables to declare as ``input`` runtime variables for this circuit. These + should already be existing :class:`.expr.Var` nodes that you build from somewhere else; + if you need to create the inputs as well, use :meth:`QuantumCircuit.add_input`. The + variables given in this argument will be passed directly to :meth:`add_input`. A + circuit cannot have both ``inputs`` and ``captures``. + captures: any variables that that this circuit scope should capture from a containing scope. + The variables given here will be passed directly to :meth:`add_capture`. A circuit + cannot have both ``inputs`` and ``captures``. + declarations: any variables that this circuit should declare and initialize immediately. + You can order this input so that later declarations depend on earlier ones (including + inputs or captures). If you need to depend on values that will be computed later at + runtime, use :meth:`add_var` at an appropriate point in the circuit execution. + + This argument is intended for convenient circuit initialization when you already have a + set of created variables. The variables used here will be directly passed to + :meth:`add_var`, which you can use directly if this is the first time you are creating + the variable. Raises: CircuitError: if the circuit name, if given, is not valid. + CircuitError: if both ``inputs`` and ``captures`` are given. Examples: @@ -198,6 +225,9 @@ def __init__( name: str | None = None, global_phase: ParameterValueType = 0, metadata: dict | None = None, + inputs: Iterable[expr.Var] = (), + captures: Iterable[expr.Var] = (), + declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): if any(not isinstance(reg, (list, QuantumRegister, ClassicalRegister)) for reg in regs): # check if inputs are integers, but also allow e.g. 2.0 @@ -267,6 +297,20 @@ def __init__( self._global_phase: ParameterValueType = 0 self.global_phase = global_phase + # Add classical variables. Resolve inputs and captures first because they can't depend on + # anything, but declarations might depend on them. + self._vars_input: dict[str, expr.Var] = {} + self._vars_capture: dict[str, expr.Var] = {} + self._vars_local: dict[str, expr.Var] = {} + for input_ in inputs: + self.add_input(input_) + for capture in captures: + self.add_capture(capture) + if isinstance(declarations, Mapping): + declarations = declarations.items() + for var, initial in declarations: + self.add_var(var, initial) + self.duration = None self.unit = "dt" self.metadata = {} if metadata is None else metadata @@ -886,8 +930,6 @@ def compose( lcr_1: 0 ═══════════ lcr_1: 0 ═══════════════════════ """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.switch_case import SwitchCaseOp if inplace and front and self._control_flow_scopes: # If we're composing onto ourselves while in a stateful control-flow builder context, @@ -1110,6 +1152,65 @@ def ancillas(self) -> list[AncillaQubit]: """ return self._ancillas + @property + def num_vars(self) -> int: + """The number of runtime classical variables in the circuit. + + This is the length of the :meth:`iter_vars` iterable.""" + return self.num_input_vars + self.num_captured_vars + self.num_declared_vars + + @property + def num_input_vars(self) -> int: + """The number of runtime classical variables in the circuit marked as circuit inputs. + + This is the length of the :meth:`iter_input_vars` iterable. If this is non-zero, + :attr:`num_captured_vars` must be zero.""" + return len(self._vars_input) + + @property + def num_captured_vars(self) -> int: + """The number of runtime classical variables in the circuit marked as captured from an + enclosing scope. + + This is the length of the :meth:`iter_captured_vars` iterable. If this is non-zero, + :attr:`num_input_vars` must be zero.""" + return len(self._vars_capture) + + @property + def num_declared_vars(self) -> int: + """The number of runtime classical variables in the circuit that are declared by this + circuit scope, excluding inputs or captures. + + This is the length of the :meth:`iter_declared_vars` iterable.""" + return len(self._vars_local) + + def iter_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables in scope within this circuit. + + This method will iterate over all variables in scope. For more fine-grained iterators, see + :meth:`iter_declared_vars`, :meth:`iter_input_vars` and :meth:`iter_captured_vars`.""" + return itertools.chain( + self._vars_input.values(), self._vars_capture.values(), self._vars_local.values() + ) + + def iter_declared_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are declared with automatic + storage duration in this scope. This excludes input variables (see :meth:`iter_input_vars`) + and captured variables (see :meth:`iter_captured_vars`).""" + return self._vars_local.values() + + def iter_input_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are declared as inputs to this + circuit scope. This excludes locally declared variables (see :meth:`iter_declared_vars`) + and captured variables (see :meth:`iter_captured_vars`).""" + return self._vars_input.values() + + def iter_captured_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are captured by this circuit + scope from a containing scope. This excludes input variables (see :meth:`iter_input_vars`) + and locally declared variables (see :meth:`iter_declared_vars`).""" + return self._vars_capture.values() + def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": """Overload & to implement self.compose.""" return self.compose(rhs) @@ -1224,12 +1325,17 @@ def _resolve_classical_resource(self, specifier): def _validate_expr(self, node: expr.Expr) -> expr.Expr: for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): + if var.standalone: + if not self.has_var(var): + raise CircuitError(f"Variable '{var}' is not present in this circuit.") + elif isinstance(var.var, Clbit): if var.var not in self._clbit_indices: raise CircuitError(f"Clbit {var.var} is not present in this circuit.") elif isinstance(var.var, ClassicalRegister): if var.var not in self.cregs: raise CircuitError(f"Register {var.var} is not present in this circuit.") + else: + raise RuntimeError(f"unhandled Var inner type in '{var}'") return node def append( @@ -1287,10 +1393,28 @@ def append( ) # Make copy of parameterized gate instances - if hasattr(operation, "params"): - is_parameter = any(isinstance(param, Parameter) for param in operation.params) + if params := getattr(operation, "params", ()): + is_parameter = False + for param in params: + is_parameter = is_parameter or isinstance(param, Parameter) + if isinstance(param, expr.Expr): + self._validate_expr(param) if is_parameter: operation = copy.deepcopy(operation) + if isinstance(operation, ControlFlowOp): + # Verify that any variable bindings are valid. Control-flow ops are already enforced + # by the class not to contain 'input' variables. + if bad_captures := { + var + for var in itertools.chain.from_iterable( + block.iter_captured_vars() for block in operation.blocks + ) + if not self.has_var(var) + }: + raise CircuitError( + f"Control-flow op attempts to capture '{bad_captures}'" + " which are not in this circuit" + ) expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] @@ -1406,6 +1530,276 @@ def _update_parameter_table(self, instruction: CircuitInstruction): # clear cache if new parameter is added self._parameters = None + @typing.overload + def get_var(self, name: str, default: T) -> Union[expr.Var, T]: + ... + + # The builtin `types` module has `EllipsisType`, but only from 3.10+! + @typing.overload + def get_var(self, name: str, default: type(...) = ...) -> expr.Var: + ... + + # We use a _literal_ `Ellipsis` as the marker value to leave `None` available as a default. + def get_var(self, name: str, default: typing.Any = ...): + """Retrieve a variable that is accessible in this circuit scope by name. + + Args: + name: the name of the variable to retrieve. + default: if given, this value will be returned if the variable is not present. If it + is not given, a :exc:`KeyError` is raised instead. + + Returns: + The corresponding variable. + + Raises: + KeyError: if no default is given, but the variable does not exist. + + Examples: + Retrieve a variable by name from a circuit:: + + from qiskit.circuit import QuantumCircuit + + # Create a circuit and create a variable in it. + qc = QuantumCircuit() + my_var = qc.add_var("my_var", False) + + # We can use 'my_var' as a variable, but let's say we've lost the Python object and + # need to retrieve it. + my_var_again = qc.get_var("my_var") + + assert my_var is my_var_again + + Get a variable from a circuit by name, returning some default if it is not present:: + + assert qc.get_var("my_var", None) is my_var + assert qc.get_var("unknown_variable", None) is None + """ + + if (out := self._vars_local.get(name)) is not None: + return out + if (out := self._vars_capture.get(name)) is not None: + return out + if (out := self._vars_input.get(name)) is not None: + return out + if default is Ellipsis: + raise KeyError(f"no variable named '{name}' is present") + return default + + def has_var(self, name_or_var: str | expr.Var, /) -> bool: + """Check whether a variable is defined in this scope. + + Args: + name_or_var: the variable, or name of a variable to check. If this is a + :class:`.expr.Var` node, the variable must be exactly the given one for this + function to return ``True``. + + Returns: + whether a matching variable is present. + + See also: + :meth:`QuantumCircuit.get_var`: retrieve a named variable from a circuit. + """ + if isinstance(name_or_var, str): + return self.get_var(name_or_var, None) is not None + return self.get_var(name_or_var.name, None) == name_or_var + + def _prepare_new_var( + self, name_or_var: str | expr.Var, type_: types.Type | None, / + ) -> expr.Var: + """The common logic for preparing and validating a new :class:`~.expr.Var` for the circuit. + + The given ``type_`` can be ``None`` if the variable specifier is already a :class:`.Var`, + and must be a :class:`~.types.Type` if it is a string. The argument is ignored if the given + first argument is a :class:`.Var` already. + + Returns the validated variable, which is guaranteed to be safe to add to the circuit.""" + if isinstance(name_or_var, str): + if type_ is None: + raise CircuitError("the type must be known when creating a 'Var' from a string") + var = expr.Var.new(name_or_var, type_) + else: + var = name_or_var + if not var.standalone: + raise CircuitError( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances." + " Use `add_bits` or `add_register` as appropriate." + ) + + # The `var` is guaranteed to have a name because we already excluded the cases where it's + # wrapping a bit/register. + if (previous := self.get_var(var.name, default=None)) is not None: + if previous == var: + raise CircuitError(f"'{var}' is already present in the circuit") + raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") + return var + + def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.Var: + """Add a classical variable with automatic storage and scope to this circuit. + + The variable is considered to have been "declared" at the beginning of the circuit, but it + only becomes initialized at the point of the circuit that you call this method, so it can + depend on variables defined before it. + + Args: + name_or_var: either a string of the variable name, or an existing instance of + :class:`~.expr.Var` to re-use. Variables cannot shadow names that are already in + use within the circuit. + initial: the value to initialize this variable with. If the first argument was given + as a string name, the type of the resulting variable is inferred from the initial + expression; to control this more manually, either use :meth:`.Var.new` to manually + construct a new variable with the desired type, or use :func:`.expr.cast` to cast + the initializer to the desired type. + + This must be either a :class:`~.expr.Expr` node, or a value that can be lifted to + one using :class:`.expr.lift`. + + Returns: + The created variable. If a :class:`~.expr.Var` instance was given, the exact same + object will be returned. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + + Examples: + Define a new variable given just a name and an initializer expression:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2) + my_var = qc.add_var("my_var", False) + + Reuse a variable that may have been taken from a related circuit, or otherwise + constructed manually, and initialize it to some more complicated expression:: + + from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister + from qiskit.circuit.classical import expr, types + + my_var = expr.Var.new("my_var", types.Uint(8)) + + cr1 = ClassicalRegister(8, "cr1") + cr2 = ClassicalRegister(8, "cr2") + qc = QuantumCircuit(QuantumRegister(8), cr1, cr2) + + # Get some measurement results into each register. + qc.h(0) + for i in range(1, 8): + qc.cx(0, i) + qc.measure(range(8), cr1) + + qc.reset(range(8)) + qc.h(0) + for i in range(1, 8): + qc.cx(0, i) + qc.measure(range(8), cr2) + + # Now when we add the variable, it is initialized using the runtime state of the two + # classical registers we measured into above. + qc.add_var(my_var, expr.bit_and(cr1, cr2)) + """ + # Validate the initialiser first to catch cases where the variable to be declared is being + # used in the initialiser. + initial = self._validate_expr(expr.lift(initial)) + var = self._prepare_new_var(name_or_var, initial.type) + # Store is responsible for ensuring the type safety of the initialisation. We build this + # before actually modifying any of our own state, so we don't get into an inconsistent state + # if an exception is raised later. + store = Store(var, initial) + + self._vars_local[var.name] = var + self._append(CircuitInstruction(store, (), ())) + return var + + def add_uninitialized_var(self, var: expr.Var, /): + """Add a variable with no initializer. + + In most cases, you should use :meth:`add_var` to initialize the variable. To use this + function, you must already hold a :class:`~.expr.Var` instance, as the use of the function + typically only makes sense in copying contexts. + + .. warning:: + + Qiskit makes no assertions about what an uninitialized variable will evaluate to at + runtime, and some hardware may reject this as an error. + + You should treat this function with caution, and as a low-level primitive that is useful + only in special cases of programmatically rebuilding two like circuits. + + Args: + var: the variable to add. + """ + # This function is deliberately meant to be a bit harder to find, to have a long descriptive + # name, and to be a bit less ergonomic than `add_var` (i.e. not allowing the (name, type) + # overload) to discourage people from using it when they should use `add_var`. + # + # This function exists so that there is a method to emulate `copy_empty_like`'s behaviour of + # adding uninitialised variables, which there's no obvious way around. We need to be sure + # that _some_ sort of handling of uninitialised variables is taken into account in our + # structures, so that doesn't become a huge edge case, even though we make no assertions + # about the _meaning_ if such an expression was run on hardware. + var = self._prepare_new_var(var, None) + self._vars_local[var.name] = var + + def add_capture(self, var: expr.Var): + """Add a variable to the circuit that it should capture from a scope it will be contained + within. + + This method requires a :class:`~.expr.Var` node to enforce that you've got a handle to one, + because you will need to declare the same variable using the same object into the outer + circuit. + + This is a low-level method, which is only really useful if you are manually constructing + control-flow operations. You typically will not need to call this method, assuming you + are using the builder interface for control-flow scopes (``with`` context-manager statements + for :meth:`if_test` and the other scoping constructs). The builder interface will + automatically make the inner scopes closures on your behalf by capturing any variables that + are used within them. + + Args: + var: the variable to capture from an enclosing scope. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + """ + if self._vars_input: + raise CircuitError( + "circuits with input variables cannot be enclosed, so cannot be closures" + ) + self._vars_capture[var.name] = self._prepare_new_var(var, None) + + @typing.overload + def add_input(self, name_or_var: str, type_: types.Type, /) -> expr.Var: + ... + + @typing.overload + def add_input(self, name_or_var: expr.Var, type_: None = None, /) -> expr.Var: + ... + + def add_input( # pylint: disable=missing-raises-doc + self, name_or_var: str | expr.Var, type_: types.Type | None = None, / + ) -> expr.Var: + """Register a variable as an input to the circuit. + + Args: + name_or_var: either a string name, or an existing :class:`~.expr.Var` node to use as the + input variable. + type_: if the name is given as a string, then this must be a :class:`~.types.Type` to + use for the variable. If the variable is given as an existing :class:`~.expr.Var`, + then this must not be given, and will instead be read from the object itself. + + Returns: + the variable created, or the same variable as was passed in. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + """ + if self._vars_capture: + raise CircuitError("circuits to be enclosed with captures cannot have input variables") + if isinstance(name_or_var, expr.Var) and type_ is not None: + raise ValueError("cannot give an explicit type with an existing Var") + var = self._prepare_new_var(name_or_var, type_) + self._vars_input[var.name] = var + return var + def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: """Add registers.""" if not regs: @@ -2077,6 +2471,14 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": * global phase * all the qubits and clbits, including the registers + .. warning:: + + If the circuit contains any local variable declarations (those added by the + ``declarations`` argument to the circuit constructor, or using :meth:`add_var`), they + will be **uninitialized** in the output circuit. You will need to manually add store + instructions for them (see :class:`.Store` and :meth:`.QuantumCircuit.store`) to + initialize them. + Args: name (str): Name for the copied circuit. If None, then the name stays the same. @@ -2095,6 +2497,13 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() + # Note that this causes the local variables to be uninitialised, because the stores are not + # copied. This can leave the circuit in a potentially dangerous state for users if they + # don't re-add initialiser stores. + cpy._vars_local = self._vars_local.copy() + cpy._vars_input = self._vars_input.copy() + cpy._vars_capture = self._vars_capture.copy() + cpy._parameter_table = ParameterTable() cpy._data = CircuitData(self._data.qubits, self._data.clbits) @@ -2148,6 +2557,31 @@ def reset(self, qubit: QubitSpecifier) -> InstructionSet: return self.append(Reset(), [qubit], []) + def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: + """Store the result of the given runtime classical expression ``rvalue`` in the memory + location defined by ``lvalue``. + + Typically ``lvalue`` will be a :class:`~.expr.Var` node and ``rvalue`` will be some + :class:`~.expr.Expr` to write into it, but anything that :func:`.expr.lift` can raise to an + :class:`~.expr.Expr` is permissible in both places, and it will be called on them. + + Args: + lvalue: a valid specifier for a memory location in the circuit. This will typically be + a :class:`~.expr.Var` node, but you can also write to :class:`.Clbit` or + :class:`.ClassicalRegister` memory locations if your hardware supports it. The + memory location must already be present in the circuit. + rvalue: a runtime classical expression whose result should be written into the given + memory location. + + .. seealso:: + :class:`~.circuit.Store` + The backing :class:`~.circuit.Instruction` class that represents this operation. + + :meth:`add_var` + Create a new variable in the circuit that can be written to with this method. + """ + return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), ()) + def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). @@ -4864,7 +5298,7 @@ def while_loop( clbits: None, *, label: str | None, - ) -> "qiskit.circuit.controlflow.while_loop.WhileLoopContext": + ) -> WhileLoopContext: ... @typing.overload @@ -4922,9 +5356,6 @@ def while_loop(self, condition, body=None, qubits=None, clbits=None, *, label=No Raises: CircuitError: if an incorrect calling convention is used. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.while_loop import WhileLoopOp, WhileLoopContext - if isinstance(condition, expr.Expr): condition = self._validate_expr(condition) else: @@ -4954,7 +5385,7 @@ def for_loop( clbits: None, *, label: str | None, - ) -> "qiskit.circuit.controlflow.for_loop.ForLoopContext": + ) -> ForLoopContext: ... @typing.overload @@ -5022,9 +5453,6 @@ def for_loop( Raises: CircuitError: if an incorrect calling convention is used. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.for_loop import ForLoopOp, ForLoopContext - if body is None: if qubits is not None or clbits is not None: raise CircuitError( @@ -5047,7 +5475,7 @@ def if_test( clbits: None, *, label: str | None, - ) -> "qiskit.circuit.controlflow.if_else.IfContext": + ) -> IfContext: ... @typing.overload @@ -5130,9 +5558,6 @@ def if_test( Returns: A handle to the instruction created. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.if_else import IfElseOp, IfContext - if isinstance(condition, expr.Expr): condition = self._validate_expr(condition) else: @@ -5199,9 +5624,6 @@ def if_else( Returns: A handle to the instruction created. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.if_else import IfElseOp - if isinstance(condition, expr.Expr): condition = self._validate_expr(condition) else: @@ -5218,7 +5640,7 @@ def switch( clbits: None, *, label: Optional[str], - ) -> "qiskit.circuit.controlflow.switch_case.SwitchContext": + ) -> SwitchContext: ... @typing.overload @@ -5284,8 +5706,6 @@ def switch(self, target, cases=None, qubits=None, clbits=None, *, label=None): Raises: CircuitError: if an incorrect calling convention is used. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.switch_case import SwitchCaseOp, SwitchContext if isinstance(target, expr.Expr): target = self._validate_expr(target) @@ -5324,9 +5744,6 @@ def break_loop(self) -> InstructionSet: CircuitError: if this method was called within a builder context, but not contained within a loop. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder - if self._control_flow_scopes: operation = BreakLoopPlaceholder() resources = operation.placeholder_resources() @@ -5354,9 +5771,6 @@ def continue_loop(self) -> InstructionSet: CircuitError: if this method was called within a builder context, but not contained within a loop. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder - if self._control_flow_scopes: operation = ContinueLoopPlaceholder() resources = operation.placeholder_resources() diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py new file mode 100644 index 000000000000..100fe0e629b9 --- /dev/null +++ b/qiskit/circuit/store.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The 'Store' operation.""" + +from __future__ import annotations + +import typing + +from .exceptions import CircuitError +from .classical import expr, types +from .instruction import Instruction + + +def _handle_equal_types(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: + return lvalue, rvalue + + +def _handle_implicit_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: + return lvalue, expr.Cast(rvalue, lvalue.type, implicit=True) + + +def _requires_lossless_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: + raise CircuitError(f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}'") + + +def _requires_dangerous_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: + raise CircuitError( + f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}', which may be lossy" + ) + + +def _no_cast_possible(lvalue: expr.Expr, rvalue: expr.Expr) -> typing.NoReturn: + raise CircuitError(f"no cast is possible from '{rvalue.type}' to '{lvalue.type}'") + + +_HANDLE_CAST = { + types.CastKind.EQUAL: _handle_equal_types, + types.CastKind.IMPLICIT: _handle_implicit_cast, + types.CastKind.LOSSLESS: _requires_lossless_cast, + types.CastKind.DANGEROUS: _requires_dangerous_cast, + types.CastKind.NONE: _no_cast_possible, +} + + +class Store(Instruction): + """A manual storage of some classical value to a classical memory location. + + This is a low-level primitive of the classical-expression handling (similar to how + :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for + subclassing. It is likely to become a special-case instruction in later versions of Qiskit + circuit and compiler internal representations.""" + + def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): + if not expr.is_lvalue(lvalue): + raise CircuitError(f"'{lvalue}' is not an l-value") + + cast_kind = types.cast_kind(rvalue.type, lvalue.type) + if (handler := _HANDLE_CAST.get(cast_kind)) is None: + raise RuntimeError(f"unhandled cast kind required: {cast_kind}") + lvalue, rvalue = handler(lvalue, rvalue) + + super().__init__("store", 0, 0, [lvalue, rvalue]) + + @property + def lvalue(self): + """Get the l-value :class:`~.expr.Expr` node that is being stored to.""" + return self.params[0] + + @property + def rvalue(self): + """Get the r-value :class:`~.expr.Expr` node that is being written into the l-value.""" + return self.params[1] + + def c_if(self, classical, val): + raise NotImplementedError( + "stores cannot be conditioned with `c_if`; use a full `if_test` context instead" + ) diff --git a/releasenotes/notes/classical-store-e64ee1286219a862.yaml b/releasenotes/notes/classical-store-e64ee1286219a862.yaml new file mode 100644 index 000000000000..729c70a6989c --- /dev/null +++ b/releasenotes/notes/classical-store-e64ee1286219a862.yaml @@ -0,0 +1,56 @@ +--- +features: + - | + A :class:`.QuantumCircuit` can now contain typed classical variables:: + + from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister + from qiskit.circuit.classical import expr, types + + qr = QuantumRegister(2, "q") + cr = ClassicalRegister(2, "c") + qc = QuantumCircuit(qr, cr) + # Add two input variables to the circuit with different types. + a = qc.add_input("a", types.Bool()) + mask = qc.add_input("mask", types.Uint(2)) + + # Test whether the input variable was true at runtime. + with qc.if_test(a) as else_: + qc.x(0) + with else_: + qc.h(0) + + qc.cx(0, 1) + qc.measure(qr, cr) + + # Add a typed variable manually, initialized to the same value as the classical register. + b = qc.add_var("b", expr.lift(cr)) + + qc.reset([0, 1]) + qc.h(0) + qc.cx(0, 1) + qc.measure(qr, cr) + + # Store some calculated value into the `b` variable. + qc.store(b, expr.bit_and(b, cr)) + # Test whether we had equality, up to a mask. + with qc.if_test(expr.equal(expr.bit_and(b, mask), mask)): + qc.x(0) + + These variables can be specified either as *inputs* to the circuit, or as scoped variables. + The circuit object does not yet have support for representing typed classical-variable *outputs*, + but this will be added later when hardware and the result interfaces are in more of a position + to support it. Circuits that represent a block of an inner scope may also capture variables + from outer scopes. + + A variable is a :class:`.Var` node, which can now contain an arbitrary type, and represents a + unique memory location within its live range when added to a circuit. These can be constructed + in a circuit using :meth:`.QuantumCircuit.add_var` and :meth:`~.QuantumCircuit.add_input`, or + at a lower level using :meth:`.Var.new`. + + Variables can be manually stored to, using the :class:`.Store` instruction and its corresponding + circuit method :meth:`.QuantumCircuit.store`. This includes writing to :class:`.Clbit` and + :class:`.ClassicalRegister` instances wrapped in :class:`.Var` nodes. + + Variables can be used wherever classical expressions (see :mod:`qiskit.circuit.classical.expr`) + are valid. Currently this is the target expressions of control-flow operations, though we plan + to expand this to gate parameters in the future, as the type and expression system are expanded. diff --git a/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml new file mode 100644 index 000000000000..70a1cf81d061 --- /dev/null +++ b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Classical types (subclasses of :class:`~classical.types.Type`) and variables (:class:`~.expr.Var`) + are now hashable. diff --git a/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml new file mode 100644 index 000000000000..71ec0320032e --- /dev/null +++ b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + :class:`~.expr.Var` nodes now have a :attr:`.Var.standalone` property to quickly query whether + they are new-style memory-owning variables, or whether they wrap old-style classical memory in + the form of a :class:`.Clbit` or :class:`.ClassicalRegister`. diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index f7b420c07144..31b4d7028a8b 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -115,3 +115,30 @@ def always_equal(_): # ``True`` instead. self.assertFalse(expr.structurally_equivalent(left, right, not_handled, not_handled)) self.assertTrue(expr.structurally_equivalent(left, right, always_equal, always_equal)) + + +@ddt.ddt +class TestIsLValue(QiskitTestCase): + @ddt.data( + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(8)), + expr.Var(Clbit(), types.Bool()), + expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)), + ) + def test_happy_cases(self, lvalue): + self.assertTrue(expr.is_lvalue(lvalue)) + + @ddt.data( + expr.Value(3, types.Uint(2)), + expr.Value(False, types.Bool()), + expr.Cast(expr.Var.new("a", types.Uint(2)), types.Uint(8)), + expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var.new("a", types.Bool()), types.Bool()), + expr.Binary( + expr.Binary.Op.LOGIC_AND, + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Bool()), + types.Bool(), + ), + ) + def test_bad_cases(self, not_an_lvalue): + self.assertFalse(expr.is_lvalue(not_an_lvalue)) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index efda6ba37758..f8c1277cd0f4 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -14,11 +14,12 @@ import copy import pickle +import uuid import ddt from qiskit.test import QiskitTestCase -from qiskit.circuit import ClassicalRegister +from qiskit.circuit import ClassicalRegister, Clbit from qiskit.circuit.classical import expr, types @@ -98,3 +99,36 @@ def test_var_uuid_clone(self): self.assertEqual(var_a_u8, pickle.loads(pickle.dumps(var_a_u8))) self.assertEqual(var_a_u8, copy.copy(var_a_u8)) self.assertEqual(var_a_u8, copy.deepcopy(var_a_u8)) + + def test_var_standalone(self): + """Test that the ``Var.standalone`` property is set correctly.""" + self.assertTrue(expr.Var.new("a", types.Bool()).standalone) + self.assertTrue(expr.Var.new("a", types.Uint(8)).standalone) + self.assertFalse(expr.Var(Clbit(), types.Bool()).standalone) + self.assertFalse(expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)).standalone) + + def test_var_hashable(self): + clbits = [Clbit(), Clbit()] + cregs = [ClassicalRegister(2, "cr1"), ClassicalRegister(2, "cr2")] + + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + expr.Var(clbits[0], types.Bool()), + expr.Var(clbits[1], types.Bool()), + expr.Var(cregs[0], types.Uint(2)), + expr.Var(cregs[1], types.Uint(2)), + ] + duplicates = [ + expr.Var(uuid.UUID(bytes=vars_[0].var.bytes), types.Bool(), name=vars_[0].name), + expr.Var(uuid.UUID(bytes=vars_[1].var.bytes), types.Uint(16), name=vars_[1].name), + expr.Var(clbits[0], types.Bool()), + expr.Var(clbits[1], types.Bool()), + expr.Var(cregs[0], types.Uint(2)), + expr.Var(cregs[1], types.Uint(2)), + ] + + # Smoke test. + self.assertEqual(vars_, duplicates) + # Actual test of hashability properties. + self.assertEqual(set(vars_ + duplicates), set(vars_)) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 374e1ecff1b1..58417fb17c03 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -58,3 +58,13 @@ def test_greater(self): self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) + + +class TestTypesCastKind(QiskitTestCase): + def test_basic_examples(self): + """This is used extensively throughout the expression construction functions, but since it + is public API, it should have some direct unit tests as well.""" + self.assertIs(types.cast_kind(types.Bool(), types.Bool()), types.CastKind.EQUAL) + self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) + self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 245d05e29111..af94a1a290e1 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -19,6 +19,7 @@ from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, QuantumRegister, execute from qiskit.circuit import Gate, Instruction, Measure, Parameter, Barrier from qiskit.circuit.bit import Bit +from qiskit.circuit.classical import expr, types from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.controlflow import IfElseOp @@ -391,6 +392,77 @@ def test_copy_empty_like_circuit(self): copied = qc.copy_empty_like("copy") self.assertEqual(copied.name, "copy") + def test_copy_variables(self): + """Test that a full copy of circuits including variables copies them across.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + copied = qc.copy() + self.assertEqual({a}, set(copied.iter_input_vars())) + self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual( + [instruction.operation for instruction in qc], + [instruction.operation for instruction in copied.data], + ) + + # Check that the original circuit is not mutated. + copied.add_input(b) + copied.add_var(d, 0xFF) + self.assertEqual({a, b}, set(copied.iter_input_vars())) + self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({c}, set(qc.iter_declared_vars())) + + qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) + copied = qc.copy() + self.assertEqual({b}, set(copied.iter_captured_vars())) + self.assertEqual({a, c}, set(copied.iter_declared_vars())) + self.assertEqual( + [instruction.operation for instruction in qc], + [instruction.operation for instruction in copied.data], + ) + + # Check that the original circuit is not mutated. + copied.add_capture(d) + self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_captured_vars())) + + def test_copy_empty_variables(self): + """Test that an empty copy of circuits including variables copies them across, but does not + initialise them.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + copied = qc.copy_empty_like() + self.assertEqual({a}, set(copied.iter_input_vars())) + self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_input(b) + copied.add_var(d, 0xFF) + self.assertEqual({a, b}, set(copied.iter_input_vars())) + self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({c}, set(qc.iter_declared_vars())) + + qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) + copied = qc.copy_empty_like() + self.assertEqual({b}, set(copied.iter_captured_vars())) + self.assertEqual({a, c}, set(copied.iter_declared_vars())) + self.assertEqual([], list(copied.data)) + + # Check that the original circuit is not mutated. + copied.add_capture(d) + self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_captured_vars())) + def test_circuit_copy_rejects_invalid_types(self): """Test copy method rejects argument with type other than 'string' and 'None' type.""" qc = QuantumCircuit(1, 1) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py new file mode 100644 index 000000000000..c09e4717af3f --- /dev/null +++ b/test/python/circuit/test_circuit_vars.py @@ -0,0 +1,393 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +from qiskit.test import QiskitTestCase +from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister +from qiskit.circuit.classical import expr, types + + +class TestCircuitVars(QiskitTestCase): + """Tests for variable-manipulation routines on circuits. More specific functionality is likely + tested in the suites of the specific methods.""" + + def test_initialise_inputs(self): + vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + qc = QuantumCircuit(inputs=vars_) + self.assertEqual(set(vars_), set(qc.iter_vars())) + self.assertEqual(qc.num_vars, len(vars_)) + self.assertEqual(qc.num_input_vars, len(vars_)) + self.assertEqual(qc.num_captured_vars, 0) + self.assertEqual(qc.num_declared_vars, 0) + + def test_initialise_captures(self): + vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + qc = QuantumCircuit(captures=vars_) + self.assertEqual(set(vars_), set(qc.iter_vars())) + self.assertEqual(qc.num_vars, len(vars_)) + self.assertEqual(qc.num_input_vars, 0) + self.assertEqual(qc.num_captured_vars, len(vars_)) + self.assertEqual(qc.num_declared_vars, 0) + + def test_initialise_declarations_iterable(self): + vars_ = [ + (expr.Var.new("a", types.Bool()), expr.lift(True)), + (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF)), + ] + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) + self.assertEqual(qc.num_vars, len(vars_)) + self.assertEqual(qc.num_input_vars, 0) + self.assertEqual(qc.num_captured_vars, 0) + self.assertEqual(qc.num_declared_vars, len(vars_)) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + + def test_initialise_declarations_mapping(self): + # Dictionary iteration order is guaranteed to be insertion order. + vars_ = { + expr.Var.new("a", types.Bool()): expr.lift(True), + expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF), + } + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual(set(vars_), set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual( + operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_.items()] + ) + + def test_initialise_declarations_dependencies(self): + """Test that the cirucit initialiser can take in declarations with dependencies between + them, provided they're specified in a suitable order.""" + a = expr.Var.new("a", types.Bool()) + vars_ = [ + (a, expr.lift(True)), + (expr.Var.new("b", types.Bool()), a), + ] + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + + def test_initialise_inputs_declarations(self): + a = expr.Var.new("a", types.Uint(16)) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.bit_and(a, 0xFFFF) + qc = QuantumCircuit(inputs=[a], declarations={b: b_init}) + + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({b}, set(qc.iter_declared_vars())) + self.assertEqual({a, b}, set(qc.iter_vars())) + self.assertEqual(qc.num_vars, 2) + self.assertEqual(qc.num_input_vars, 1) + self.assertEqual(qc.num_captured_vars, 0) + self.assertEqual(qc.num_declared_vars, 1) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", b, b_init)]) + + def test_initialise_captures_declarations(self): + a = expr.Var.new("a", types.Uint(16)) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.bit_and(a, 0xFFFF) + qc = QuantumCircuit(captures=[a], declarations={b: b_init}) + + self.assertEqual({a}, set(qc.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_declared_vars())) + self.assertEqual({a, b}, set(qc.iter_vars())) + self.assertEqual(qc.num_vars, 2) + self.assertEqual(qc.num_input_vars, 0) + self.assertEqual(qc.num_captured_vars, 1) + self.assertEqual(qc.num_declared_vars, 1) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", b, b_init)]) + + def test_add_uninitialized_var(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + qc.add_uninitialized_var(a) + self.assertEqual({a}, set(qc.iter_vars())) + self.assertEqual([], list(qc.data)) + + def test_add_var_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_var("a", expr.lift(True)) + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Bool()) + + b = qc.add_var("b", expr.Value(0xFF, types.Uint(8))) + self.assertEqual(b.name, "b") + self.assertEqual(b.type, types.Uint(8)) + + def test_add_var_returns_input(self): + """Test that the `Var` returned by `add_var` is the same as the input if `Var`.""" + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + a_other = qc.add_var(a, expr.lift(True)) + self.assertIs(a, a_other) + + def test_add_input_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_input("a", types.Bool()) + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Bool()) + + b = qc.add_input("b", types.Uint(8)) + self.assertEqual(b.name, "b") + self.assertEqual(b.type, types.Uint(8)) + + def test_add_input_returns_input(self): + """Test that the `Var` returned by `add_input` is the same as the input if `Var`.""" + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + a_other = qc.add_input(a) + self.assertIs(a, a_other) + + def test_cannot_have_both_inputs_and_captures(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + QuantumCircuit(inputs=[a], captures=[b]) + + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + qc.add_capture(b) + + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "circuits to be enclosed.*cannot have input"): + qc.add_input(b) + + def test_cannot_add_cyclic_declaration(self): + a = expr.Var.new("a", types.Bool()) + with self.assertRaisesRegex(CircuitError, "not present in this circuit"): + QuantumCircuit(declarations=[(a, a)]) + + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "not present in this circuit"): + qc.add_var(a, a) + + def test_initialise_inputs_equal_to_add_input(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(16)) + + qc_init = QuantumCircuit(inputs=[a, b]) + qc_manual = QuantumCircuit() + qc_manual.add_input(a) + qc_manual.add_input(b) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + qc_manual = QuantumCircuit() + a = qc_manual.add_input("a", types.Bool()) + b = qc_manual.add_input("b", types.Uint(16)) + qc_init = QuantumCircuit(inputs=[a, b]) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + def test_initialise_captures_equal_to_add_capture(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(16)) + + qc_init = QuantumCircuit(captures=[a, b]) + qc_manual = QuantumCircuit() + qc_manual.add_capture(a) + qc_manual.add_capture(b) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + def test_initialise_declarations_equal_to_add_var(self): + a = expr.Var.new("a", types.Bool()) + a_init = expr.lift(False) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.lift(0xFFFF) + + qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) + qc_manual = QuantumCircuit() + qc_manual.add_var(a, a_init) + qc_manual.add_var(b, b_init) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + self.assertEqual(qc_init.data, qc_manual.data) + + qc_manual = QuantumCircuit() + a = qc_manual.add_var("a", a_init) + b = qc_manual.add_var("b", b_init) + qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + self.assertEqual(qc_init.data, qc_manual.data) + + def test_cannot_shadow_vars(self): + """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are + detected and rejected.""" + a = expr.Var.new("a", types.Bool()) + a_init = expr.lift(True) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(inputs=[a, a]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(captures=[a, a]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(declarations=[(a, a_init), (a, a_init)]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(inputs=[a], declarations=[(a, a_init)]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(captures=[a], declarations=[(a, a_init)]) + + def test_cannot_shadow_names(self): + """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are + detected and rejected.""" + a_bool1 = expr.Var.new("a", types.Bool()) + a_bool2 = expr.Var.new("a", types.Bool()) + a_uint = expr.Var.new("a", types.Uint(16)) + a_bool_init = expr.lift(True) + a_uint_init = expr.lift(0xFFFF) + + tests = [ + ((a_bool1, a_bool_init), (a_bool2, a_bool_init)), + ((a_bool1, a_bool_init), (a_uint, a_uint_init)), + ] + for (left, left_init), (right, right_init) in tests: + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(inputs=(left, right)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=(left, right)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(declarations=[(left, left_init), (right, right_init)]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(inputs=[left], declarations=[(right, right_init)]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[left], declarations=[(right, right_init)]) + + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_input(right) + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit(captures=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_capture(right) + qc = QuantumCircuit(captures=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit() + qc.add_var("a", expr.lift(True)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var("a", expr.lift(True)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var("a", expr.lift(0xFF)) + + def test_cannot_add_vars_wrapping_clbits(self): + a = expr.Var(Clbit(), types.Bool()) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(inputs=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(captures=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_capture(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(declarations=[(a, expr.lift(True))]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_var(a, expr.lift(True)) + + def test_cannot_add_vars_wrapping_cregs(self): + a = expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(inputs=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(captures=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_capture(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(declarations=[(a, expr.lift(0xFF))]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_var(a, expr.lift(0xFF)) + + def test_get_var_success(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations={b: expr.Value(0xFF, types.Uint(8))}) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + qc = QuantumCircuit(captures=[a, b]) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + qc = QuantumCircuit(declarations={a: expr.lift(True), b: expr.Value(0xFF, types.Uint(8))}) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + def test_get_var_missing(self): + qc = QuantumCircuit() + with self.assertRaises(KeyError): + qc.get_var("a") + + a = expr.Var.new("a", types.Bool()) + qc.add_input(a) + with self.assertRaises(KeyError): + qc.get_var("b") + + def test_get_var_default(self): + qc = QuantumCircuit() + self.assertIs(qc.get_var("a", None), None) + + missing = "default" + a = expr.Var.new("a", types.Bool()) + qc.add_input(a) + self.assertIs(qc.get_var("b", missing), missing) + self.assertIs(qc.get_var("b", a), a) + + def test_has_var(self): + a = expr.Var.new("a", types.Bool()) + self.assertFalse(QuantumCircuit().has_var("a")) + self.assertTrue(QuantumCircuit(inputs=[a]).has_var("a")) + self.assertTrue(QuantumCircuit(captures=[a]).has_var("a")) + self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var("a")) + self.assertTrue(QuantumCircuit(inputs=[a]).has_var(a)) + self.assertTrue(QuantumCircuit(captures=[a]).has_var(a)) + self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var(a)) + + # When giving an `Var`, the match must be exact, not just the name. + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Uint(8)))) + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Bool()))) diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index e827b679a4a2..ecb5de96f793 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -510,6 +510,49 @@ def test_switch_rejects_cases_after_default(self): with self.assertRaisesRegex(CircuitError, "cases after the default are unreachable"): SwitchCaseOp(creg, [(CASE_DEFAULT, case1), (1, case2)]) + def test_if_else_rejects_input_vars(self): + """Bodies must not contain input variables.""" + cond = (Clbit(), False) + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + bad_body = QuantumCircuit(inputs=[a]) + good_body = QuantumCircuit(captures=[a], declarations=[(b, expr.lift(False))]) + + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + IfElseOp(cond, bad_body, None) + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + IfElseOp(cond, bad_body, good_body) + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + IfElseOp(cond, good_body, bad_body) + + def test_while_rejects_input_vars(self): + """Bodies must not contain input variables.""" + cond = (Clbit(), False) + a = expr.Var.new("a", types.Bool()) + bad_body = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + WhileLoopOp(cond, bad_body) + + def test_for_rejects_input_vars(self): + """Bodies must not contain input variables.""" + a = expr.Var.new("a", types.Bool()) + bad_body = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + ForLoopOp(range(3), None, bad_body) + + def test_switch_rejects_input_vars(self): + """Bodies must not contain input variables.""" + target = ClassicalRegister(3, "cr") + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + bad_body = QuantumCircuit(inputs=[a]) + good_body = QuantumCircuit(captures=[a], declarations=[(b, expr.lift(False))]) + + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + SwitchCaseOp(target, [(0, bad_body)]) + with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): + SwitchCaseOp(target, [(0, good_body), (1, bad_body)]) + @ddt class TestAddingControlFlowOperations(QiskitTestCase): @@ -874,3 +917,148 @@ def test_nested_parameters_can_be_assigned(self): ) self.assertEqual(assigned, expected) + + def test_can_add_op_with_captures_of_inputs(self): + """Test circuit methods can capture input variables.""" + outer = QuantumCircuit(1, 1) + a = outer.add_input("a", types.Bool()) + + inner = QuantumCircuit(1, 1, captures=[a]) + + outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "while_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.for_loop(range(3), None, inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "for_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "switch_case") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + def test_can_add_op_with_captures_of_captures(self): + """Test circuit methods can capture captured variables.""" + outer = QuantumCircuit(1, 1) + a = expr.Var.new("a", types.Bool()) + outer.add_capture(a) + + inner = QuantumCircuit(1, 1, captures=[a]) + + outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "while_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.for_loop(range(3), None, inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "for_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "switch_case") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + def test_can_add_op_with_captures_of_locals(self): + """Test circuit methods can capture declared variables.""" + outer = QuantumCircuit(1, 1) + a = outer.add_var("a", expr.lift(True)) + + inner = QuantumCircuit(1, 1, captures=[a]) + + outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "if_else") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "while_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.for_loop(range(3), None, inner.copy(), [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "for_loop") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + + outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) + added = outer.data[-1].operation + self.assertEqual(added.name, "switch_case") + self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + + def test_cannot_capture_unknown_variables_methods(self): + """Control-flow operations should not be able to capture variables that don't exist in the + outer circuit.""" + outer = QuantumCircuit(1, 1) + + a = expr.Var.new("a", types.Bool()) + inner = QuantumCircuit(1, 1, captures=[a]) + + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.for_loop(range(3), None, inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) + + def test_cannot_capture_unknown_variables_append(self): + """Control-flow operations should not be able to capture variables that don't exist in the + outer circuit.""" + outer = QuantumCircuit(1, 1) + + a = expr.Var.new("a", types.Bool()) + inner = QuantumCircuit(1, 1, captures=[a]) + + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), None), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(WhileLoopOp((outer.clbits[0], False), inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(ForLoopOp(range(3), None, inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append( + SwitchCaseOp(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())]), + [0], + [0], + ) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py new file mode 100644 index 000000000000..7977765d8e45 --- /dev/null +++ b/test/python/circuit/test_store.py @@ -0,0 +1,199 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +from qiskit.test import QiskitTestCase +from qiskit.circuit import Store, Clbit, CircuitError, QuantumCircuit, ClassicalRegister +from qiskit.circuit.classical import expr, types + + +class TestStoreInstruction(QiskitTestCase): + """Tests of the properties of the ``Store`` instruction itself.""" + + def test_happy_path_construction(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.lift(Clbit()) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, rvalue) + + def test_implicit_cast(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.Var.new("b", types.Uint(8)) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) + + def test_rejects_non_lvalue(self): + not_an_lvalue = expr.logic_and( + expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool()) + ) + rvalue = expr.lift(False) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + Store(not_an_lvalue, rvalue) + + def test_rejects_explicit_cast(self): + lvalue = expr.Var.new("a", types.Uint(16)) + rvalue = expr.Var.new("b", types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): + Store(lvalue, rvalue) + + def test_rejects_dangerous_cast(self): + lvalue = expr.Var.new("a", types.Uint(8)) + rvalue = expr.Var.new("b", types.Uint(16)) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): + Store(lvalue, rvalue) + + def test_rejects_c_if(self): + instruction = Store(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool())) + with self.assertRaises(NotImplementedError): + instruction.c_if(Clbit(), False) + + +class TestStoreCircuit(QiskitTestCase): + """Tests of the `QuantumCircuit.store` method and appends of `Store`.""" + + def test_produces_expected_operation(self): + a = expr.Var.new("a", types.Bool()) + value = expr.Value(True, types.Bool()) + + qc = QuantumCircuit(inputs=[a]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + qc = QuantumCircuit(captures=[a]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + qc = QuantumCircuit(declarations=[(a, expr.lift(False))]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + def test_allows_stores_with_clbits(self): + clbits = [Clbit(), Clbit()] + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(clbits, inputs=[a]) + qc.store(clbits[0], True) + qc.store(expr.Var(clbits[1], types.Bool()), a) + qc.store(clbits[0], clbits[1]) + qc.store(expr.lift(clbits[0]), expr.lift(clbits[1])) + qc.store(a, expr.lift(clbits[1])) + + expected = [ + Store(expr.lift(clbits[0]), expr.lift(True)), + Store(expr.lift(clbits[1]), a), + Store(expr.lift(clbits[0]), expr.lift(clbits[1])), + Store(expr.lift(clbits[0]), expr.lift(clbits[1])), + Store(a, expr.lift(clbits[1])), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + + def test_allows_stores_with_cregs(self): + cregs = [ClassicalRegister(8, "cr1"), ClassicalRegister(8, "cr2")] + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(*cregs, captures=[a]) + qc.store(cregs[0], 0xFF) + qc.store(expr.Var(cregs[1], types.Uint(8)), a) + qc.store(cregs[0], cregs[1]) + qc.store(expr.lift(cregs[0]), expr.lift(cregs[1])) + qc.store(a, cregs[1]) + + expected = [ + Store(expr.lift(cregs[0]), expr.lift(0xFF)), + Store(expr.lift(cregs[1]), a), + Store(expr.lift(cregs[0]), expr.lift(cregs[1])), + Store(expr.lift(cregs[0]), expr.lift(cregs[1])), + Store(a, expr.lift(cregs[1])), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + + def test_lifts_values(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(captures=[a]) + qc.store(a, True) + self.assertEqual(qc.data[-1].operation, Store(a, expr.lift(True))) + + b = expr.Var.new("b", types.Uint(16)) + qc.add_capture(b) + qc.store(b, 0xFFFF) + self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) + + def test_rejects_vars_not_in_circuit(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "'a'.*not present"): + qc.store(expr.Var.new("a", types.Bool()), True) + + # Not the same 'a' + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "'a'.*not present"): + qc.store(expr.Var.new("a", types.Bool()), True) + with self.assertRaisesRegex(CircuitError, "'b'.*not present"): + qc.store(a, b) + + def test_rejects_bits_not_in_circuit(self): + a = expr.Var.new("a", types.Bool()) + clbit = Clbit() + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(clbit, False) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(clbit, a) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(a, clbit) + + def test_rejects_cregs_not_in_circuit(self): + a = expr.Var.new("a", types.Uint(8)) + creg = ClassicalRegister(8, "cr1") + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(creg, 0xFF) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(creg, a) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(a, creg) + + def test_rejects_non_lvalue(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(inputs=[a, b]) + not_an_lvalue = expr.logic_and(a, b) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + qc.store(not_an_lvalue, expr.lift(False)) + + def test_rejects_explicit_cast(self): + lvalue = expr.Var.new("a", types.Uint(16)) + rvalue = expr.Var.new("b", types.Uint(8)) + qc = QuantumCircuit(inputs=[lvalue, rvalue]) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): + qc.store(lvalue, rvalue) + + def test_rejects_dangerous_cast(self): + lvalue = expr.Var.new("a", types.Uint(8)) + rvalue = expr.Var.new("b", types.Uint(16)) + qc = QuantumCircuit(inputs=[lvalue, rvalue]) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): + qc.store(lvalue, rvalue) + + def test_rejects_c_if(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit([Clbit()], inputs=[a]) + instruction_set = qc.store(a, True) + with self.assertRaises(NotImplementedError): + instruction_set.c_if(qc.clbits[0], False) From c0702bf66d2e51678feceebbc3c09b674560d674 Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:36:12 +0200 Subject: [PATCH 015/124] Revert pulse model (#11348) --- qiskit/pulse/__init__.py | 15 -- qiskit/pulse/model/__init__.py | 102 ------------ qiskit/pulse/model/frames.py | 155 ------------------ qiskit/pulse/model/mixed_frames.py | 77 --------- qiskit/pulse/model/pulse_target.py | 203 ------------------------ qiskit/transpiler/__init__.py | 2 +- test/python/pulse/test_frames.py | 77 --------- test/python/pulse/test_mixed_frames.py | 65 -------- test/python/pulse/test_pulse_targets.py | 89 ----------- 9 files changed, 1 insertion(+), 784 deletions(-) delete mode 100644 qiskit/pulse/model/__init__.py delete mode 100644 qiskit/pulse/model/frames.py delete mode 100644 qiskit/pulse/model/mixed_frames.py delete mode 100644 qiskit/pulse/model/pulse_target.py delete mode 100644 test/python/pulse/test_frames.py delete mode 100644 test/python/pulse/test_mixed_frames.py delete mode 100644 test/python/pulse/test_pulse_targets.py diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index c946fafa80da..34ef5c072020 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -40,8 +40,6 @@ .. automodule:: qiskit.pulse.schedule .. automodule:: qiskit.pulse.transforms .. automodule:: qiskit.pulse.builder -.. automodule:: qiskit.pulse.model - .. currentmodule:: qiskit.pulse @@ -170,16 +168,3 @@ ) from qiskit.pulse.library.samplers.decorators import functional_pulse from qiskit.pulse.schedule import Schedule, ScheduleBlock - -from qiskit.pulse.model import ( - PulseTarget, - Port, - LogicalElement, - Qubit, - Coupler, - Frame, - GenericFrame, - QubitFrame, - MeasurementFrame, - MixedFrame, -) diff --git a/qiskit/pulse/model/__init__.py b/qiskit/pulse/model/__init__.py deleted file mode 100644 index df12fa082e6a..000000000000 --- a/qiskit/pulse/model/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -r""" -========================================================== -Pulse Targets & Frames (:mod:`qiskit.pulse.model`) -========================================================== - -Pulse is meant to be agnostic to the underlying hardware implementation, while still allowing -low-level control. Qiskit Pulse's pulse targets and frames create a flexible framework -to define where pulse instructions are applied, and what would be their carrier frequency and phase -(because typically AC pulses are used). Each :class:`.PulseTarget` represents a separate component -in the quantum computing system on which instructions could be applied. On the other hand, -each :class:`.Frame` represents a frequency and phase duo for the carrier of the pulse. - -While :class:`.PulseTarget` includes a :class`.Port` variant allowing for direct control over -hardware ports, an abstraction layer is provided by :class:`.LogicalElement`. -The abstraction allows to write pulse level programs with less knowledge of the hardware, and in -a level which is more similar to the circuit level programing. i.e., instead of specifying -ports, one can use Qubits, Couplers, etc. - -This logical and virtual representation allows the user to write template pulse -programs without worrying about the exact details of the hardware implementation -(are the pulses to be played via the same port? Which NCO is used?), while still -allowing for effective utilization of the quantum hardware. The burden of mapping -the different combinations of :class:`.LogicalElement` and :class:`.Frame` -to hardware aware objects is left to the Pulse Compiler. - -.. _pulse_targets: - -PulseTarget -================ -:class:`.PulseTarget` includes :class:`.Port` who's objects are identified by a unique string identifier -defined by the control system, and :class:`.LogicalElement` who's objects are identified by their type -and index. Currently, the most prominent example of a :class:`.LogicalElement` is the -:class:`~.pulse.Qubit`. - -.. autosummary:: - :toctree: ../stubs/ - - Port - Qubit - Coupler - - -.. _frames: - -Frame -============= -:class:`.Frame` s are identified by their type and unique identifier. A :class:`.GenericFrame` is used to -specify custom frequency -and phase duos, while :class:`.QubitFrame` and :class:`.MeasurementFrame` are used to indicate that -backend defaults are to be used (for the qubit's driving frequency and measurement frequency -respectively). - -.. autosummary:: - :toctree: ../stubs/ - - GenericFrame - QubitFrame - MeasurementFrame - - -.. _mixed_frames: - -MixedFrame -============= -The combination of a :class:`.LogicalElement` and :class:`.Frame` is dubbed a :class:`.MixedFrame`. - -.. autosummary:: - :toctree: ../stubs/ - - MixedFrame -""" - -from .pulse_target import ( - PulseTarget, - Port, - LogicalElement, - Qubit, - Coupler, -) - -from .frames import ( - Frame, - GenericFrame, - QubitFrame, - MeasurementFrame, -) - -from .mixed_frames import ( - MixedFrame, -) diff --git a/qiskit/pulse/model/frames.py b/qiskit/pulse/model/frames.py deleted file mode 100644 index 803573301320..000000000000 --- a/qiskit/pulse/model/frames.py +++ /dev/null @@ -1,155 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Frames -""" - -from abc import ABC - -import numpy as np - -from qiskit.pulse.exceptions import PulseError - - -class Frame(ABC): - """Base class for pulse module frame. - - Because pulses used in Quantum hardware are typically AC pulses, the carrier frequency and phase - must be defined. The :class:`Frame` is the object which identifies the frequency and phase for - the carrier. - and each pulse and most other instructions are associated with a frame. The different types of frames - dictate how the frequency and phase duo are defined. - - The default initial phase for every frame is 0. - """ - - -class GenericFrame(Frame): - """Pulse module GenericFrame. - - The :class:`GenericFrame` is used for custom user defined frames, which are not associated with any - backend defaults. It is especially useful when the frame doesn't correspond to any frame of - the typical qubit model, like qudit control for example. Because no backend defaults exist for - these frames, during compilation an initial frequency and phase will need to be provided. - - :class:`GenericFrame` objects are identified by their unique name. - """ - - def __init__(self, name: str): - """Create ``GenericFrame``. - - Args: - name: A unique identifier used to identify the frame. - """ - self._name = name - - @property - def name(self) -> str: - """Return the name of the frame.""" - return self._name - - def __repr__(self) -> str: - return f"GenericFrame({self._name})" - - def __eq__(self, other): - return type(self) is type(other) and self._name == other._name - - def __hash__(self): - return hash((type(self), self._name)) - - -class QubitFrame(Frame): - """A frame associated with the driving of a qubit. - - :class:`QubitFrame` is a frame associated with the driving of a specific qubit. - The initial frequency of - the frame will be taken as the default driving frequency provided by the backend - during compilation. - """ - - def __init__(self, index: int): - """Create ``QubitFrame``. - - Args: - index: The index of the qubit represented by the frame. - """ - self._validate_index(index) - self._index = index - - @property - def index(self) -> int: - """Return the qubit index of the qubit frame.""" - return self._index - - def _validate_index(self, index) -> None: - """Raise a ``PulseError`` if the qubit index is invalid. Namely, check if the index is a - non-negative integer. - - Raises: - PulseError: If ``identifier`` (index) is a negative integer. - """ - if not isinstance(index, (int, np.integer)) or index < 0: - raise PulseError("Qubit index must be a non-negative integer") - - def __repr__(self) -> str: - return f"QubitFrame({self._index})" - - def __eq__(self, other): - return type(self) is type(other) and self._index == other._index - - def __hash__(self): - return hash((type(self), self._index)) - - -class MeasurementFrame(Frame): - """A frame associated with the measurement of a qubit. - - ``MeasurementFrame`` is a frame associated with the readout of a specific qubit, - which requires a stimulus tone driven at frequency off resonant to qubit drive. - - If not set otherwise, the initial frequency of the frame will be taken as the default - measurement frequency provided by the backend during compilation. - """ - - def __init__(self, index: int): - """Create ``MeasurementFrame``. - - Args: - index: The index of the qubit represented by the frame. - """ - self._validate_index(index) - self._index = index - - @property - def index(self) -> int: - """Return the qubit index of the measurement frame.""" - return self._index - - def _validate_index(self, index) -> None: - """Raise a ``PulseError`` if the qubit index is invalid. Namely, check if the index is a - non-negative integer. - - Raises: - PulseError: If ``index`` is a negative integer. - """ - if not isinstance(index, (int, np.integer)) or index < 0: - raise PulseError("Qubit index must be a non-negative integer") - - def __repr__(self) -> str: - return f"MeasurementFrame({self._index})" - - def __eq__(self, other): - return type(self) is type(other) and self._index == other._index - - def __hash__(self): - return hash((type(self), self._index)) diff --git a/qiskit/pulse/model/mixed_frames.py b/qiskit/pulse/model/mixed_frames.py deleted file mode 100644 index 454cdbf0d2c5..000000000000 --- a/qiskit/pulse/model/mixed_frames.py +++ /dev/null @@ -1,77 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Mixed Frames -""" - -from .frames import Frame -from .pulse_target import PulseTarget - - -class MixedFrame: - """Representation of a :class:`PulseTarget` and :class:`Frame` combination. - - Most instructions need to be associated with both a :class:`PulseTarget` and a - :class:`Frame`. The combination - of the two is called a mixed frame and is represented by a :class:`MixedFrame` object. - - In most cases the :class:`MixedFrame` is used more by the compiler, and a pulse program - can be written without :class:`MixedFrame` s, by setting :class:`PulseTarget` and - :class:`Frame` independently. However, in some cases using :class:`MixedFrame` s can - better convey the meaning of the code, and change the compilation process. One example - is the use of the shift/set frequency/phase instructions which are not broadcasted to other - :class:`MixedFrame` s if applied on a specific :class:`MixedFrame` (unlike the behavior - of :class:`Frame`). User can also use a subclass of :class:`MixedFrame` for a particular - combination of logical elements and frames as if a syntactic sugar. This might - increase the readability of a user pulse program. As an example consider the cross - resonance architecture, in which a pulse is played on a target qubit frame and applied - to a control qubit logical element. - """ - - def __init__(self, pulse_target: PulseTarget, frame: Frame): - """Create ``MixedFrame``. - - Args: - pulse_target: The ``PulseTarget`` associated with the mixed frame. - frame: The frame associated with the mixed frame. - """ - self._pulse_target = pulse_target - self._frame = frame - - @property - def pulse_target(self) -> PulseTarget: - """Return the target of this mixed frame.""" - return self._pulse_target - - @property - def frame(self) -> Frame: - """Return the ``Frame`` of this mixed frame.""" - return self._frame - - def __repr__(self) -> str: - return f"MixedFrame({self.pulse_target},{self.frame})" - - def __eq__(self, other: "MixedFrame") -> bool: - """Return True iff self and other are equal, specifically, iff they have the same target - and frame. - - Args: - other: The mixed frame to compare to this one. - - Returns: - True iff equal. - """ - return self._pulse_target == other._pulse_target and self._frame == other._frame - - def __hash__(self) -> int: - return hash((self._pulse_target, self._frame, type(self))) diff --git a/qiskit/pulse/model/pulse_target.py b/qiskit/pulse/model/pulse_target.py deleted file mode 100644 index bb9702ccfad4..000000000000 --- a/qiskit/pulse/model/pulse_target.py +++ /dev/null @@ -1,203 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -PulseTarget -""" -from abc import ABC, abstractmethod -from typing import Tuple -import numpy as np - -from qiskit.pulse.exceptions import PulseError - - -class PulseTarget(ABC): - """Base class of pulse target. - - A :class:`PulseTarget` object identifies a hardware component the user can control, the typical - example being playing pulses on. Other examples include measurement related instruments. - - When playing a pulse on a quantum hardware, one typically has to define on what hardware component - the pulse will be played, and the frame (frequency and phase) of the carrier wave. - :class:`PulseTarget` addresses only the first of the two, and identifies the component which is the - target of the pulse. Every played pulse and most other instructions are associated with a - :class:`PulseTarget` on which they are performed. - - A subclass of :class:`PulseTarget` has to be hashable. - """ - - @abstractmethod - def __hash__(self) -> int: - pass - - -class Port(PulseTarget): - """A ``Port`` type ``PulseTarget``. - - A :class:`Port` is the most basic ``PulseTarget`` - simply a hardware port the user can control, - (typically for playing pulses, but not only, for example data acquisition). - - A :class:`Port` is identified by a string, which is set, and must be recognized, by the - backend. Therefore, using pulse level control with :class:`Port` requires an extensive - knowledge of the hardware. Programs with string identifiers which are not recognized by the - backend will fail to execute. - """ - - def __init__(self, name: str): - """Create ``Port``. - - Args: - name: A string identifying the port. - """ - self._name = name - - @property - def name(self) -> str: - """Return the ``name`` of this port.""" - return self._name - - def __eq__(self, other: "Port") -> bool: - """Return True iff self and other are equal, specifically, iff they have the same type - and the same ``name``. - - Args: - other: The Port to compare to this one. - - Returns: - True iff equal. - """ - return type(self) is type(other) and self._name == other._name - - def __hash__(self) -> int: - return hash((self._name, type(self))) - - def __repr__(self) -> str: - return f"Port({self._name})" - - -class LogicalElement(PulseTarget, ABC): - """Base class of logical elements. - - Class :class:`LogicalElement` provides an abstraction layer to ``PulseTarget``. The abstraction - allows to write pulse level programs with less knowledge of the hardware, and in a level which - is more similar to the circuit level programing. i.e., instead of specifying specific ports, one - can use Qubits, Couplers, etc. - - A logical element is identified by its type and index. - """ - - def __init__(self, index: Tuple[int, ...]): - """Create ``LogicalElement``. - - Args: - index: Tuple of indices of the logical element. - """ - self._validate_index(index) - self._index = index - - @property - def index(self) -> Tuple[int, ...]: - """Return the ``index`` of this logical element.""" - return self._index - - @abstractmethod - def _validate_index(self, index) -> None: - """Raise a PulseError if the logical element ``index`` is invalid. - - Raises: - PulseError: If ``index`` is not valid. - """ - pass - - def __eq__(self, other: "LogicalElement") -> bool: - """Return True iff self and other are equal, specifically, iff they have the same type - and the same ``index``. - - Args: - other: The logical element to compare to this one. - - Returns: - True iff equal. - """ - return type(self) is type(other) and self._index == other._index - - def __hash__(self) -> int: - return hash((self._index, type(self))) - - def __repr__(self) -> str: - ind_str = str(self._index) if len(self._index) > 1 else f"({self._index[0]})" - return type(self).__name__ + ind_str - - -class Qubit(LogicalElement): - """Qubit logical element. - - ``Qubit`` represents the different qubits in the system, as identified by - their (positive integer) index values. - """ - - def __init__(self, index: int): - """Qubit logical element. - - Args: - index: Qubit index (positive integer). - """ - super().__init__((index,)) - - @property - def qubit_index(self): - """Index of the Qubit""" - return self.index[0] - - def _validate_index(self, index) -> None: - """Raise a ``PulseError`` if the qubit index is invalid. Namely, check if the index is a - non-negative integer. - - Raises: - PulseError: If ``index`` is a negative integer. - """ - if not isinstance(index[0], (int, np.integer)) or index[0] < 0: - raise PulseError("Qubit index must be a non-negative integer") - - -class Coupler(LogicalElement): - """Coupler logical element. - - :class:`Coupler` represents an element which couples qubits, and can be controlled on its own. - It is identified by the tuple of indices of the coupled qubits. - """ - - def __init__(self, *qubits): - """Coupler logical element. - - The coupler ``index`` is defined as the ``tuple`` (\\*qubits). - - Args: - *qubits: any number of qubit indices coupled by the coupler. - """ - super().__init__(tuple(qubits)) - - def _validate_index(self, index) -> None: - """Raise a ``PulseError`` if the coupler ``index`` is invalid. Namely, - check if coupled qubit indices are non-negative integers, at least two indices were provided, - and that the indices don't repeat. - - Raises: - PulseError: If ``index`` is invalid. - """ - if len(index) < 2: - raise PulseError("At least two qubit indices are needed for a Coupler") - for qubit_index in index: - if not isinstance(qubit_index, (int, np.integer)) or qubit_index < 0: - raise PulseError("Both indices of coupled qubits must be non-negative integers") - if len(set(index)) != len(index): - raise PulseError("Indices of a coupler can not repeat") diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index d6ce7b31ca4c..71f596c95dad 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -1059,7 +1059,7 @@ C ░░░░░░░░░░░░░░░░▒▒░ However, the :class:`.QuantumCircuit` representation is not accurate enough to represent -this model. In the circuit representation, the corresponding :class:`.pulse.Qubit` is occupied +this model. In the circuit representation, the corresponding :class:`.circuit.Qubit` is occupied by the stimulus microwave signal during the first half of the interval, and the :class:`.Clbit` is only occupied at the very end of the interval. diff --git a/test/python/pulse/test_frames.py b/test/python/pulse/test_frames.py deleted file mode 100644 index 1a9b99e1139f..000000000000 --- a/test/python/pulse/test_frames.py +++ /dev/null @@ -1,77 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test pulse logical elements and frames""" - -from qiskit.pulse import ( - PulseError, - GenericFrame, - QubitFrame, - MeasurementFrame, -) -from qiskit.test import QiskitTestCase - - -class TestFrames(QiskitTestCase): - """Test frames.""" - - def test_generic_frame_initialization(self): - """Test that Frame objects are created correctly""" - frame = GenericFrame(name="frame1") - self.assertEqual(frame.name, "frame1") - self.assertEqual(str(frame), "GenericFrame(frame1)") - - def test_generic_frame_comparison(self): - """Test that GenericFrame objects are compared correctly""" - frame1 = GenericFrame(name="frame1") - - self.assertEqual(frame1, GenericFrame(name="frame1")) - self.assertNotEqual(frame1, GenericFrame(name="frame2")) - self.assertNotEqual(frame1, QubitFrame(3)) - - def test_qubit_frame_initialization(self): - """Test that QubitFrame type frames are created and validated correctly""" - frame = QubitFrame(2) - self.assertEqual(frame.index, 2) - self.assertEqual(str(frame), "QubitFrame(2)") - - with self.assertRaises(PulseError): - QubitFrame(0.5) - with self.assertRaises(PulseError): - QubitFrame(-0.5) - with self.assertRaises(PulseError): - QubitFrame(-1) - - def test_qubit_frame_comparison(self): - """Test the comparison of QubitFrame""" - self.assertEqual(QubitFrame(0), QubitFrame(0)) - self.assertNotEqual(QubitFrame(0), QubitFrame(1)) - self.assertNotEqual(MeasurementFrame(0), QubitFrame(0)) - - def test_measurement_frame_initialization(self): - """Test that MeasurementFrame type frames are created and validated correctly""" - frame = MeasurementFrame(2) - self.assertEqual(frame.index, 2) - self.assertEqual(str(frame), "MeasurementFrame(2)") - - with self.assertRaises(PulseError): - MeasurementFrame(0.5) - with self.assertRaises(PulseError): - MeasurementFrame(-0.5) - with self.assertRaises(PulseError): - MeasurementFrame(-1) - - def test_measurement_frame_comparison(self): - """Test the comparison of measurement frames""" - self.assertEqual(MeasurementFrame(0), MeasurementFrame(0)) - self.assertNotEqual(MeasurementFrame(0), MeasurementFrame(1)) - self.assertNotEqual(MeasurementFrame(0), QubitFrame(0)) diff --git a/test/python/pulse/test_mixed_frames.py b/test/python/pulse/test_mixed_frames.py deleted file mode 100644 index 95ebd556d1c0..000000000000 --- a/test/python/pulse/test_mixed_frames.py +++ /dev/null @@ -1,65 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test pulse logical elements and frames""" - -from qiskit.pulse import ( - Port, - Qubit, - GenericFrame, - MixedFrame, -) -from qiskit.test import QiskitTestCase - - -class TestMixedFrames(QiskitTestCase): - """Test mixed frames.""" - - def test_mixed_frame_initialization(self): - """Test that MixedFrame objects are created correctly""" - frame = GenericFrame("frame1") - qubit = Qubit(1) - mixed_frame = MixedFrame(qubit, frame) - self.assertEqual(mixed_frame.pulse_target, qubit) - self.assertEqual(mixed_frame.frame, frame) - - port = Port("d0") - mixed_frame = MixedFrame(port, frame) - self.assertEqual(mixed_frame.pulse_target, port) - - def test_mixed_frames_comparison(self): - """Test the comparison of various mixed frames""" - self.assertEqual( - MixedFrame(Qubit(1), GenericFrame("a")), - MixedFrame(Qubit(1), GenericFrame("a")), - ) - - self.assertEqual( - MixedFrame(Port("s"), GenericFrame("a")), - MixedFrame(Port("s"), GenericFrame("a")), - ) - - self.assertNotEqual( - MixedFrame(Qubit(1), GenericFrame("a")), - MixedFrame(Qubit(2), GenericFrame("a")), - ) - self.assertNotEqual( - MixedFrame(Qubit(1), GenericFrame("a")), - MixedFrame(Qubit(1), GenericFrame("b")), - ) - - def test_mixed_frame_repr(self): - """Test MixedFrame __repr__""" - frame = GenericFrame("frame1") - qubit = Qubit(1) - mixed_frame = MixedFrame(qubit, frame) - self.assertEqual(str(mixed_frame), f"MixedFrame({qubit},{frame})") diff --git a/test/python/pulse/test_pulse_targets.py b/test/python/pulse/test_pulse_targets.py deleted file mode 100644 index d3a8c92c3216..000000000000 --- a/test/python/pulse/test_pulse_targets.py +++ /dev/null @@ -1,89 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test pulse logical elements and frames""" - -from qiskit.pulse import ( - PulseError, - Qubit, - Coupler, - Port, -) -from qiskit.test import QiskitTestCase - - -class TestLogicalElements(QiskitTestCase): - """Test logical elements.""" - - def test_qubit_initialization(self): - """Test that Qubit type logical elements are created and validated correctly""" - qubit = Qubit(0) - self.assertEqual(qubit.index, (0,)) - self.assertEqual(qubit.qubit_index, 0) - self.assertEqual(str(qubit), "Qubit(0)") - - with self.assertRaises(PulseError): - Qubit(0.5) - with self.assertRaises(PulseError): - Qubit(-0.5) - with self.assertRaises(PulseError): - Qubit(-1) - - def test_coupler_initialization(self): - """Test that Coupler type logical elements are created and validated correctly""" - coupler = Coupler(0, 3) - self.assertEqual(coupler.index, (0, 3)) - self.assertEqual(str(coupler), "Coupler(0, 3)") - - coupler = Coupler(0, 3, 2) - self.assertEqual(coupler.index, (0, 3, 2)) - - with self.assertRaises(PulseError): - Coupler(-1, 0) - with self.assertRaises(PulseError): - Coupler(2, -0.5) - with self.assertRaises(PulseError): - Coupler(3, -1) - with self.assertRaises(PulseError): - Coupler(0, 0, 1) - with self.assertRaises(PulseError): - Coupler(0) - - def test_logical_elements_comparison(self): - """Test the comparison of various logical elements""" - self.assertEqual(Qubit(0), Qubit(0)) - self.assertNotEqual(Qubit(0), Qubit(1)) - - self.assertEqual(Coupler(0, 1), Coupler(0, 1)) - self.assertNotEqual(Coupler(0, 1), Coupler(0, 2)) - - -class TestPorts(QiskitTestCase): - """Test ports.""" - - def test_ports_initialization(self): - """Test that Ports are created correctly""" - port = Port("d0") - self.assertEqual(port.name, "d0") - - def test_ports_comparison(self): - """Test that Ports are compared correctly""" - port1 = Port("d0") - port2 = Port("d0") - port3 = Port("d1") - self.assertEqual(port1, port2) - self.assertNotEqual(port1, port3) - - def test_ports_representation(self): - """Test Ports repr""" - port1 = Port("d0") - self.assertEqual(str(port1), "Port(d0)") From a79e87966450a9ee52e90aa4d9b8f87d2377a79b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Nov 2023 20:35:09 +0000 Subject: [PATCH 016/124] Track runtime variables in control-flow builders (#10977) * Track runtime variables in control-flow builders This adds support for all the new classical runtime variables through the control-flow builder interface. In particular, most usefully it automatically manages the scoping rules for new declarations and inner variable accesses, and ensures that its built scopes automatically close over any variables used within them (including making sure nested scopes do the same thing). The builder API is factored out a little into an explicit interface object, with `QuantumCircuit` getting an explicit implementation of that. This is done because the number of separate API methods we would have needed to pass around / infer was getting overly large, and this allows us to just use standard virtual dispatch to automatically do the right thing. Python doesn't have a way to have an object implement an interface other than by structural (duck) typing, so to avoid name leakage and collisions, we instead make `QuantumCircuit`'s implementation a friend class that handles the inner state on its behalf. Not everything control-flow-builder related is factored out into the API because it didn't seem overly useful to do this, especially when the overridden behaviour would just have been to throw exceptions. * :us: Co-authored-by: Matthew Treinish * More descriptive variable name * Remove superfluous private statement --------- Co-authored-by: Matthew Treinish --- qiskit/circuit/controlflow/builder.py | 242 +++++++++-- qiskit/circuit/quantumcircuit.py | 285 +++++++------ .../circuit/test_control_flow_builders.py | 387 +++++++++++++++++- 3 files changed, 732 insertions(+), 182 deletions(-) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index d997a080f991..edd5394449db 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -17,12 +17,14 @@ # having a far more complete builder of all circuits, with more classical control and creation, in # the future. +from __future__ import annotations import abc import itertools import typing -from typing import Callable, Collection, Iterable, List, FrozenSet, Tuple, Union, Optional +from typing import Collection, Iterable, List, FrozenSet, Tuple, Union, Optional, Sequence +from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import Clbit, ClassicalRegister from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.instruction import Instruction @@ -36,6 +38,124 @@ import qiskit +class CircuitScopeInterface(abc.ABC): + """An interface that circuits and builder blocks explicitly fulfill, which contains the primitive + methods of circuit construction and object validation. + + This allows core circuit methods to be applied to the currently open builder scope, and allows + the builders to hook into all places where circuit resources might be used. This allows the + builders to track the resources being used, without getting in the way of + :class:`.QuantumCircuit` doing its own thing. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def instructions(self) -> Sequence[CircuitInstruction]: + """Indexable view onto the :class:`.CircuitInstruction`s backing this scope.""" + + @abc.abstractmethod + def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + """Low-level 'append' primitive; this may assume that the qubits, clbits and operation are + all valid for the circuit. + + Abstraction of :meth:`.QuantumCircuit._append` (the low-level one, not the high-level). + + Args: + instruction: the resource-validated instruction context object. + + Returns: + the instruction context object actually appended. This is not required to be the same + as the object given (but typically will be). + """ + + @abc.abstractmethod + def resolve_classical_resource( + self, specifier: Clbit | ClassicalRegister | int + ) -> Clbit | ClassicalRegister: + """Resolve a single bit-like classical-resource specifier. + + A resource refers to either a classical bit or a register, where integers index into the + classical bits of the greater circuit. + + This is called whenever a classical bit or register is being used outside the standard + :class:`.Clbit` usage of instructions in :meth:`append`, such as in a legacy two-tuple + condition. + + Args: + specifier: the classical resource specifier. + + Returns: + the resolved resource. This cannot be an integer any more; an integer input is resolved + into a classical bit. + + Raises: + CircuitError: if the resource cannot be used by the scope, such as an out-of-range index + or a :class:`.Clbit` that isn't actually in the circuit. + """ + + @abc.abstractmethod + def add_uninitialized_var(self, var: expr.Var): + """Add an uninitialized variable to the circuit scope. + + The general circuit context is responsible for ensuring the variable is initialized. These + uninitialized variables are guaranteed to be standalone. + + Args: + var: the variable to add, if valid. + + Raises: + CircuitError: if the variable cannot be added, such as because it invalidly shadows or + redefines an existing name. + """ + + @abc.abstractmethod + def remove_var(self, var: expr.Var): + """Remove a variable from the locals of this scope. + + This is only called in the case that an exception occurred while initializing the variable, + and is not exposed to users. + + Args: + var: the variable to remove. It can be assumed that this was already the subject of an + :meth:`add_uninitialized_var` call. + """ + + @abc.abstractmethod + def use_var(self, var: expr.Var): + """Called for every standalone classical runtime variable being used by some circuit + instruction. + + The given variable is guaranteed to be a stand-alone variable; bit-like resource-wrapping + variables will have been filtered out and their resources given to + :meth:`resolve_classical_resource`. + + Args: + var: the variable to validate. + + Returns: + the same variable. + + Raises: + CircuitError: if the variable is not valid for this scope. + """ + + @abc.abstractmethod + def get_var(self, name: str) -> Optional[expr.Var]: + """Get the variable (if any) in scope with the given name. + + This should call up to the parent scope if in a control-flow builder scope, in case the + variable exists in an outer scope. + + Args: + name: the name of the symbol to lookup. + + Returns: + the variable if it is found, otherwise ``None``. + """ + + class InstructionResources(typing.NamedTuple): """The quantum and classical resources used within a particular instruction. @@ -169,7 +289,7 @@ def repeat(self, n): raise CircuitError("Cannot repeat a placeholder instruction.") -class ControlFlowBuilderBlock: +class ControlFlowBuilderBlock(CircuitScopeInterface): """A lightweight scoped block for holding instructions within a control-flow builder context. This class is designed only to be used by :obj:`.QuantumCircuit` as an internal context for @@ -199,15 +319,17 @@ class ControlFlowBuilderBlock: """ __slots__ = ( - "instructions", + "_instructions", "qubits", "clbits", "registers", "global_phase", "_allow_jumps", - "_resource_requester", + "_parent", "_built", "_forbidden_message", + "_vars_local", + "_vars_capture", ) def __init__( @@ -215,8 +337,8 @@ def __init__( qubits: Iterable[Qubit], clbits: Iterable[Clbit], *, + parent: CircuitScopeInterface, registers: Iterable[Register] = (), - resource_requester: Callable, allow_jumps: bool = True, forbidden_message: Optional[str] = None, ): @@ -238,26 +360,22 @@ def __init__( uses *exactly* the same set of resources. We cannot verify this from within the builder interface (and it is too expensive to do when the ``for`` op is made), so we fail safe, and require the user to use the more verbose, internal form. - resource_requester: A callback function that takes in some classical resource specifier, - and returns a concrete classical resource, if this scope is allowed to access that - resource. In almost all cases, this should be a resolver from the - :obj:`.QuantumCircuit` that this scope is contained in. See - :meth:`.QuantumCircuit._resolve_classical_resource` for the normal expected input - here, and the documentation of :obj:`.InstructionSet`, which uses this same - callback. + parent: The scope interface of the containing scope. forbidden_message: If a string is given here, a :exc:`.CircuitError` will be raised on any attempts to append instructions to the scope with this message. This is used by pseudo scopes where the state machine of the builder scopes has changed into a position where no instructions should be accepted, such as when inside a ``switch`` but outside any cases. """ - self.instructions: List[CircuitInstruction] = [] + self._instructions: List[CircuitInstruction] = [] self.qubits = set(qubits) self.clbits = set(clbits) self.registers = set(registers) self.global_phase = 0.0 + self._vars_local = {} + self._vars_capture = {} self._allow_jumps = allow_jumps - self._resource_requester = resource_requester + self._parent = parent self._built = False self._forbidden_message = forbidden_message @@ -275,9 +393,11 @@ def allow_jumps(self): """ return self._allow_jumps + @property + def instructions(self): + return self._instructions + def append(self, instruction: CircuitInstruction) -> CircuitInstruction: - """Add an instruction into the scope, keeping track of the qubits and clbits that have been - used in total.""" if self._forbidden_message is not None: raise CircuitError(self._forbidden_message) @@ -293,50 +413,77 @@ def append(self, instruction: CircuitInstruction) -> CircuitInstruction: " because it is not in a loop." ) - self.instructions.append(instruction) + self._instructions.append(instruction) self.qubits.update(instruction.qubits) self.clbits.update(instruction.clbits) return instruction - def request_classical_resource(self, specifier): - """Resolve a single classical resource specifier into a concrete resource, raising an error - if the specifier is invalid, and track it as now being used in scope. - - Args: - specifier (Union[Clbit, ClassicalRegister, int]): a specifier of a classical resource - present in this circuit. An ``int`` will be resolved into a :obj:`.Clbit` using the - same conventions that measurement operations on this circuit use. - - Returns: - Union[Clbit, ClassicalRegister]: the requested resource, resolved into a concrete - instance of :obj:`.Clbit` or :obj:`.ClassicalRegister`. - - Raises: - CircuitError: if the resource is not present in this circuit, or if the integer index - passed is out-of-bounds. - """ + def resolve_classical_resource(self, specifier): if self._built: raise CircuitError("Cannot add resources after the scope has been built.") # Allow the inner resolve to propagate exceptions. - resource = self._resource_requester(specifier) + resource = self._parent.resolve_classical_resource(specifier) if isinstance(resource, Clbit): self.add_bits((resource,)) else: self.add_register(resource) return resource + def add_uninitialized_var(self, var: expr.Var): + if self._built: + raise CircuitError("Cannot add resources after the scope has been built.") + # We can shadow a name if it was declared in an outer scope, but only if we haven't already + # captured it ourselves yet. + if (previous := self._vars_local.get(var.name)) is not None: + if previous == var: + raise CircuitError(f"'{var}' is already present in the scope") + raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") + if var.name in self._vars_capture: + raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") + self._vars_local[var.name] = var + + def remove_var(self, var: expr.Var): + if self._built: + raise RuntimeError("exception handler 'remove_var' called after scope built") + self._vars_local.pop(var.name) + + def get_var(self, name: str): + if (out := self._vars_local.get(name)) is not None: + return out + return self._parent.get_var(name) + + def use_var(self, var: expr.Var): + if (local := self._vars_local.get(var.name)) is not None: + if local == var: + return + raise CircuitError(f"cannot use '{var}' which is shadowed by the local '{local}'") + if self._vars_capture.get(var.name) == var: + return + if self._parent.get_var(var.name) != var: + raise CircuitError(f"cannot close over '{var}', which is not in scope") + self._parent.use_var(var) + self._vars_capture[var.name] = var + + def iter_local_vars(self): + """Iterator over the variables currently declared in this scope.""" + return self._vars_local.values() + + def iter_captured_vars(self): + """Iterator over the variables currently captured in this scope.""" + return self._vars_capture.values() + def peek(self) -> CircuitInstruction: """Get the value of the most recent instruction tuple in this scope.""" - if not self.instructions: + if not self._instructions: raise CircuitError("This scope contains no instructions.") - return self.instructions[-1] + return self._instructions[-1] def pop(self) -> CircuitInstruction: """Get the value of the most recent instruction in this scope, and remove it from this object.""" - if not self.instructions: + if not self._instructions: raise CircuitError("This scope contains no instructions.") - return self.instructions.pop() + return self._instructions.pop() def add_bits(self, bits: Iterable[Union[Qubit, Clbit]]): """Add extra bits to this scope that are not associated with any concrete instruction yet. @@ -421,10 +568,18 @@ def build( # We start off by only giving the QuantumCircuit the qubits we _know_ it will need, and add # more later as needed. out = QuantumCircuit( - list(self.qubits), list(self.clbits), *self.registers, global_phase=self.global_phase + list(self.qubits), + list(self.clbits), + *self.registers, + global_phase=self.global_phase, + captures=self._vars_capture.values(), ) + for var in self._vars_local.values(): + # The requisite `Store` instruction to initialise the variable will have been appended + # into the instructions. + out.add_uninitialized_var(var) - for instruction in self.instructions: + for instruction in self._instructions: if isinstance(instruction.operation, InstructionPlaceholder): operation, resources = instruction.operation.concrete_instruction( all_qubits, all_clbits @@ -483,11 +638,14 @@ def copy(self) -> "ControlFlowBuilderBlock": a semi-shallow copy of this object. """ out = type(self).__new__(type(self)) - out.instructions = self.instructions.copy() + out._instructions = self._instructions.copy() out.qubits = self.qubits.copy() out.clbits = self.clbits.copy() out.registers = self.registers.copy() out.global_phase = self.global_phase + out._vars_local = self._vars_local.copy() + out._vars_capture = self._vars_capture.copy() + out._parent = self._parent out._allow_jumps = self._allow_jumps out._forbidden_message = self._forbidden_message return out diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 0bca0ef5d914..ae8d171607c8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -49,6 +49,7 @@ from . import _classical_resource_map from ._utils import sort_parameters from .controlflow import ControlFlowOp +from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder from .controlflow.for_loop import ForLoopOp, ForLoopContext @@ -257,6 +258,10 @@ def __init__( self.name = name self._increment_instances() + # An explicit implementation of the circuit scope builder interface used to dispatch appends + # and the like to the relevant control-flow scope. + self._builder_api = _OuterCircuitScopeInterface(self) + self._op_start_times = None # A stack to hold the instruction sets that are being built up during for-, if- and @@ -951,6 +956,10 @@ def compose( # has to be strictly larger. This allows composing final measurements onto unitary circuits. if isinstance(other, QuantumCircuit): if not self.clbits and other.clbits: + if dest._control_flow_scopes: + raise CircuitError( + "cannot implicitly add clbits while within a control-flow scope" + ) dest.add_bits(other.clbits) for reg in other.cregs: dest.add_register(reg) @@ -1027,9 +1036,9 @@ def compose( # adjust new instrs before original ones and update all parameters mapped_instrs.extend(dest._data) dest.clear() - append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append + circuit_scope = dest._current_scope() for instr in mapped_instrs: - append(instr) + circuit_scope.append(instr) for gate, cals in other.calibrations.items(): dest._calibrations[gate].update(cals) @@ -1189,6 +1198,9 @@ def iter_vars(self) -> typing.Iterable[expr.Var]: This method will iterate over all variables in scope. For more fine-grained iterators, see :meth:`iter_declared_vars`, :meth:`iter_input_vars` and :meth:`iter_captured_vars`.""" + if self._control_flow_scopes: + builder = self._control_flow_scopes[-1] + return itertools.chain(builder.iter_captured_vars(), builder.iter_local_vars()) return itertools.chain( self._vars_input.values(), self._vars_capture.values(), self._vars_local.values() ) @@ -1197,18 +1209,24 @@ def iter_declared_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all runtime classical variables that are declared with automatic storage duration in this scope. This excludes input variables (see :meth:`iter_input_vars`) and captured variables (see :meth:`iter_captured_vars`).""" + if self._control_flow_scopes: + return self._control_flow_scopes[-1].iter_local_vars() return self._vars_local.values() def iter_input_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all runtime classical variables that are declared as inputs to this circuit scope. This excludes locally declared variables (see :meth:`iter_declared_vars`) and captured variables (see :meth:`iter_captured_vars`).""" + if self._control_flow_scopes: + return () return self._vars_input.values() def iter_captured_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all runtime classical variables that are captured by this circuit scope from a containing scope. This excludes input variables (see :meth:`iter_input_vars`) and locally declared variables (see :meth:`iter_declared_vars`).""" + if self._control_flow_scopes: + return self._control_flow_scopes[-1].iter_captured_vars() return self._vars_capture.values() def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": @@ -1283,61 +1301,6 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list clbit_representation, self.clbits, self._clbit_indices, Clbit ) - def _resolve_classical_resource(self, specifier): - """Resolve a single classical resource specifier into a concrete resource, raising an error - if the specifier is invalid. - - This is slightly different to :meth:`.cbit_argument_conversion`, because it should not - unwrap :obj:`.ClassicalRegister` instances into lists, and in general it should not allow - iterables or broadcasting. It is expected to be used as a callback for things like - :meth:`.InstructionSet.c_if` to check the validity of their arguments. - - Args: - specifier (Union[Clbit, ClassicalRegister, int]): a specifier of a classical resource - present in this circuit. An ``int`` will be resolved into a :obj:`.Clbit` using the - same conventions as measurement operations on this circuit use. - - Returns: - Union[Clbit, ClassicalRegister]: the resolved resource. - - Raises: - CircuitError: if the resource is not present in this circuit, or if the integer index - passed is out-of-bounds. - """ - if isinstance(specifier, Clbit): - if specifier not in self._clbit_indices: - raise CircuitError(f"Clbit {specifier} is not present in this circuit.") - return specifier - if isinstance(specifier, ClassicalRegister): - # This is linear complexity for something that should be constant, but QuantumCircuit - # does not currently keep a hashmap of registers, and requires non-trivial changes to - # how it exposes its registers publicly before such a map can be safely stored so it - # doesn't miss updates. (Jake, 2021-11-10). - if specifier not in self.cregs: - raise CircuitError(f"Register {specifier} is not present in this circuit.") - return specifier - if isinstance(specifier, int): - try: - return self._data.clbits[specifier] - except IndexError: - raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None - raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.") - - def _validate_expr(self, node: expr.Expr) -> expr.Expr: - for var in expr.iter_vars(node): - if var.standalone: - if not self.has_var(var): - raise CircuitError(f"Variable '{var}' is not present in this circuit.") - elif isinstance(var.var, Clbit): - if var.var not in self._clbit_indices: - raise CircuitError(f"Clbit {var.var} is not present in this circuit.") - elif isinstance(var.var, ClassicalRegister): - if var.var not in self.cregs: - raise CircuitError(f"Register {var.var} is not present in this circuit.") - else: - raise RuntimeError(f"unhandled Var inner type in '{var}'") - return node - def append( self, instruction: Operation | CircuitInstruction, @@ -1392,13 +1355,15 @@ def append( "Object to append must be an Operation or have a to_instruction() method." ) + circuit_scope = self._current_scope() + # Make copy of parameterized gate instances if params := getattr(operation, "params", ()): is_parameter = False for param in params: is_parameter = is_parameter or isinstance(param, Parameter) if isinstance(param, expr.Expr): - self._validate_expr(param) + param = _validate_expr(circuit_scope, param) if is_parameter: operation = copy.deepcopy(operation) if isinstance(operation, ControlFlowOp): @@ -1419,30 +1384,18 @@ def append( expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] - if self._control_flow_scopes: - circuit_data = self._control_flow_scopes[-1].instructions - appender = self._control_flow_scopes[-1].append - requester = self._control_flow_scopes[-1].request_classical_resource - else: - circuit_data = self._data - appender = self._append - requester = self._resolve_classical_resource - instructions = InstructionSet(resource_requester=requester) - if isinstance(operation, Instruction): - for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): - self._check_dups(qarg) - data_idx = len(circuit_data) - appender(CircuitInstruction(operation, qarg, carg)) - instructions._add_ref(circuit_data, data_idx) - else: - # For Operations that are non-Instructions, we use the Instruction's default method - for qarg, carg in Instruction.broadcast_arguments( - operation, expanded_qargs, expanded_cargs - ): - self._check_dups(qarg) - data_idx = len(circuit_data) - appender(CircuitInstruction(operation, qarg, carg)) - instructions._add_ref(circuit_data, data_idx) + instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) + # For Operations that are non-Instructions, we use the Instruction's default method + broadcast_iter = ( + operation.broadcast_arguments(expanded_qargs, expanded_cargs) + if isinstance(operation, Instruction) + else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) + ) + for qarg, carg in broadcast_iter: + self._check_dups(qarg) + instruction = CircuitInstruction(operation, qarg, carg) + circuit_scope.append(instruction) + instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions # Preferred new style. @@ -1574,19 +1527,14 @@ def get_var(self, name: str, default: typing.Any = ...): assert qc.get_var("my_var", None) is my_var assert qc.get_var("unknown_variable", None) is None """ - - if (out := self._vars_local.get(name)) is not None: - return out - if (out := self._vars_capture.get(name)) is not None: - return out - if (out := self._vars_input.get(name)) is not None: + if (out := self._current_scope().get_var(name)) is not None: return out if default is Ellipsis: raise KeyError(f"no variable named '{name}' is present") return default def has_var(self, name_or_var: str | expr.Var, /) -> bool: - """Check whether a variable is defined in this scope. + """Check whether a variable is accessible in this scope. Args: name_or_var: the variable, or name of a variable to check. If this is a @@ -1594,7 +1542,7 @@ def has_var(self, name_or_var: str | expr.Var, /) -> bool: function to return ``True``. Returns: - whether a matching variable is present. + whether a matching variable is accessible. See also: :meth:`QuantumCircuit.get_var`: retrieve a named variable from a circuit. @@ -1698,15 +1646,24 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V """ # Validate the initialiser first to catch cases where the variable to be declared is being # used in the initialiser. - initial = self._validate_expr(expr.lift(initial)) - var = self._prepare_new_var(name_or_var, initial.type) - # Store is responsible for ensuring the type safety of the initialisation. We build this - # before actually modifying any of our own state, so we don't get into an inconsistent state - # if an exception is raised later. - store = Store(var, initial) - - self._vars_local[var.name] = var - self._append(CircuitInstruction(store, (), ())) + circuit_scope = self._current_scope() + initial = _validate_expr(circuit_scope, expr.lift(initial)) + if isinstance(name_or_var, str): + var = expr.Var.new(name_or_var, initial.type) + elif not name_or_var.standalone: + raise CircuitError( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances." + ) + else: + var = name_or_var + circuit_scope.add_uninitialized_var(var) + try: + # Store is responsible for ensuring the type safety of the initialisation. + store = Store(var, initial) + except CircuitError: + circuit_scope.remove_var(var) + raise + circuit_scope.append(CircuitInstruction(store, (), ())) return var def add_uninitialized_var(self, var: expr.Var, /): @@ -1736,8 +1693,11 @@ def add_uninitialized_var(self, var: expr.Var, /): # that _some_ sort of handling of uninitialised variables is taken into account in our # structures, so that doesn't become a huge edge case, even though we make no assertions # about the _meaning_ if such an expression was run on hardware. - var = self._prepare_new_var(var, None) - self._vars_local[var.name] = var + if self._control_flow_scopes: + raise CircuitError("cannot add an uninitialized variable in a control-flow scope") + if not var.standalone: + raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") + self._builder_api.add_uninitialized_var(var) def add_capture(self, var: expr.Var): """Add a variable to the circuit that it should capture from a scope it will be contained @@ -1760,6 +1720,11 @@ def add_capture(self, var: expr.Var): Raises: CircuitError: if the variable cannot be created due to shadowing an existing variable. """ + if self._control_flow_scopes: + # Allow manual capturing. Not sure why it'd be useful, but there's a clear expected + # behaviour here. + self._control_flow_scopes[-1].use_var(var) + return if self._vars_input: raise CircuitError( "circuits with input variables cannot be enclosed, so cannot be closures" @@ -1792,6 +1757,8 @@ def add_input( # pylint: disable=missing-raises-doc Raises: CircuitError: if the variable cannot be created due to shadowing an existing variable. """ + if self._control_flow_scopes: + raise CircuitError("cannot add an input variable in a control-flow scope") if self._vars_capture: raise CircuitError("circuits to be enclosed with captures cannot have input variables") if isinstance(name_or_var, expr.Var) and type_ is not None: @@ -2493,6 +2460,7 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": # copy registers correctly, in copy.copy they are only copied via reference cpy.qregs = self.qregs.copy() cpy.cregs = self.cregs.copy() + cpy._builder_api = _OuterCircuitScopeInterface(cpy) cpy._ancillas = self._ancillas.copy() cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() @@ -3297,7 +3265,9 @@ def delay( else: qubits.append(qarg) - instructions = InstructionSet(resource_requester=self._resolve_classical_resource) + instructions = InstructionSet( + resource_requester=self._current_scope().resolve_classical_resource + ) for q in qubits: inst: tuple[ Instruction, Sequence[QubitSpecifier] | None, Sequence[ClbitSpecifier] | None @@ -5187,6 +5157,11 @@ def snapshot(self, label, snapshot_type="statevector", qubits=None, params=None) return self.append(snap, qubits) + def _current_scope(self) -> CircuitScopeInterface: + if self._control_flow_scopes: + return self._control_flow_scopes[-1] + return self._builder_api + def _push_scope( self, qubits: Iterable[Qubit] = (), @@ -5207,28 +5182,18 @@ def _push_scope( forbidden_message: If given, all attempts to add instructions to this scope will raise a :exc:`.CircuitError` with this message. """ - # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock - - # Chain resource requests so things like registers added to inner scopes via conditions are - # requested in the outer scope as well. - if self._control_flow_scopes: - resource_requester = self._control_flow_scopes[-1].request_classical_resource - else: - resource_requester = self._resolve_classical_resource - self._control_flow_scopes.append( ControlFlowBuilderBlock( qubits, clbits, - resource_requester=resource_requester, + parent=self._current_scope(), registers=registers, allow_jumps=allow_jumps, forbidden_message=forbidden_message, ) ) - def _pop_scope(self) -> "qiskit.circuit.controlflow.builder.ControlFlowBuilderBlock": + def _pop_scope(self) -> ControlFlowBuilderBlock: """Finish a scope used in the control-flow builder interface, and return it to the caller. This should only be done by the control-flow context managers, since they naturally @@ -5356,10 +5321,11 @@ def while_loop(self, condition, body=None, qubits=None, clbits=None, *, label=No Raises: CircuitError: if an incorrect calling convention is used. """ + circuit_scope = self._current_scope() if isinstance(condition, expr.Expr): - condition = self._validate_expr(condition) + condition = _validate_expr(circuit_scope, condition) else: - condition = (self._resolve_classical_resource(condition[0]), condition[1]) + condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) if body is None: if qubits is not None or clbits is not None: @@ -5558,10 +5524,11 @@ def if_test( Returns: A handle to the instruction created. """ + circuit_scope = self._current_scope() if isinstance(condition, expr.Expr): - condition = self._validate_expr(condition) + condition = _validate_expr(circuit_scope, condition) else: - condition = (self._resolve_classical_resource(condition[0]), condition[1]) + condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) if true_body is None: if qubits is not None or clbits is not None: @@ -5624,10 +5591,11 @@ def if_else( Returns: A handle to the instruction created. """ + circuit_scope = self._current_scope() if isinstance(condition, expr.Expr): - condition = self._validate_expr(condition) + condition = _validate_expr(circuit_scope, condition) else: - condition = (self._resolve_classical_resource(condition[0]), condition[1]) + condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) return self.append(IfElseOp(condition, true_body, false_body, label), qubits, clbits) @@ -5707,10 +5675,11 @@ def switch(self, target, cases=None, qubits=None, clbits=None, *, label=None): CircuitError: if an incorrect calling convention is used. """ + circuit_scope = self._current_scope() if isinstance(target, expr.Expr): - target = self._validate_expr(target) + target = _validate_expr(circuit_scope, target) else: - target = self._resolve_classical_resource(target) + target = circuit_scope.resolve_classical_resource(target) if cases is None: if qubits is not None or clbits is not None: raise CircuitError( @@ -5931,6 +5900,78 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float: QuantumCircuit.isometry = QuantumCircuit.iso +class _OuterCircuitScopeInterface(CircuitScopeInterface): + # This is an explicit interface-fulfilling object friend of QuantumCircuit that acts as its + # implementation of the control-flow builder scope methods. + + __slots__ = ("circuit",) + + def __init__(self, circuit: QuantumCircuit): + self.circuit = circuit + + @property + def instructions(self): + return self.circuit._data + + def append(self, instruction): + # QuantumCircuit._append is semi-public, so we just call back to it. + return self.circuit._append(instruction) + + def resolve_classical_resource(self, specifier): + # This is slightly different to cbit_argument_conversion, because it should not + # unwrap :obj:`.ClassicalRegister` instances into lists, and in general it should not allow + # iterables or broadcasting. It is expected to be used as a callback for things like + # :meth:`.InstructionSet.c_if` to check the validity of their arguments. + if isinstance(specifier, Clbit): + if specifier not in self.circuit._clbit_indices: + raise CircuitError(f"Clbit {specifier} is not present in this circuit.") + return specifier + if isinstance(specifier, ClassicalRegister): + # This is linear complexity for something that should be constant, but QuantumCircuit + # does not currently keep a hashmap of registers, and requires non-trivial changes to + # how it exposes its registers publically before such a map can be safely stored so it + # doesn't miss updates. (Jake, 2021-11-10). + if specifier not in self.circuit.cregs: + raise CircuitError(f"Register {specifier} is not present in this circuit.") + return specifier + if isinstance(specifier, int): + try: + return self.circuit._data.clbits[specifier] + except IndexError: + raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None + raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.") + + def add_uninitialized_var(self, var): + var = self.circuit._prepare_new_var(var, None) + self.circuit._vars_local[var.name] = var + + def remove_var(self, var): + self.circuit._vars_local.pop(var.name) + + def get_var(self, name): + if (out := self.circuit._vars_local.get(name)) is not None: + return out + if (out := self.circuit._vars_capture.get(name)) is not None: + return out + return self.circuit._vars_input.get(name) + + def use_var(self, var): + if self.get_var(var.name) != var: + raise CircuitError(f"'{var}' is not present in this circuit") + + +def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> expr.Expr: + # This takes the `circuit_scope` object as an argument rather than being a circuit method and + # inferring it because we may want to call this several times, and we almost invariably already + # need the interface implementation for something else anyway. + for var in set(expr.iter_vars(node)): + if var.standalone: + circuit_scope.use_var(var) + else: + circuit_scope.resolve_classical_resource(var.var) + return node + + class _ParameterBindsDict: __slots__ = ("mapping", "allowed_keys") diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index aa4974f4cdec..ce8088fcd26c 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=missing-function-docstring + """Test operations on the builder interfaces for control flow in dynamic QuantumCircuits.""" import copy @@ -25,10 +27,10 @@ QuantumCircuit, QuantumRegister, Qubit, + Store, ) from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp, CASE_DEFAULT -from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock from qiskit.circuit.controlflow.if_else import IfElsePlaceholder from qiskit.circuit.exceptions import CircuitError from qiskit.test import QiskitTestCase @@ -2998,6 +3000,251 @@ def test_global_phase_of_blocks(self): [i * math.pi / 7 for i in range(1, 7)], ) + def test_can_capture_input(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + base = QuantumCircuit(inputs=[a, b]) + with base.for_loop(range(3)): + base.store(a, expr.lift(True)) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + + def test_can_capture_declared(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + base = QuantumCircuit(declarations=[(a, expr.lift(False)), (b, expr.lift(True))]) + with base.if_test(expr.lift(False)): + base.store(a, expr.lift(True)) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + + def test_can_capture_capture(self): + # It's a bit wild to be manually building an outer circuit that's intended to be a subblock, + # but be using the control-flow builder interface internally, but eh, it should work. + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + base = QuantumCircuit(captures=[a, b]) + with base.while_loop(expr.lift(False)): + base.store(a, expr.lift(True)) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + + def test_can_capture_from_nested(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + base = QuantumCircuit(inputs=[a, b]) + with base.switch(expr.lift(False)) as case, case(case.DEFAULT): + base.add_var(c, expr.lift(False)) + with base.if_test(expr.lift(False)): + base.store(a, c) + outer_block = base.data[-1].operation.blocks[0] + inner_block = outer_block.data[-1].operation.blocks[0] + self.assertEqual(set(inner_block.iter_captured_vars()), {a, c}) + + # The containing block should have captured it as well, despite not using it explicitly. + self.assertEqual(set(outer_block.iter_captured_vars()), {a}) + self.assertEqual(set(outer_block.iter_declared_vars()), {c}) + + def test_can_manually_capture(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + base = QuantumCircuit(inputs=[a, b]) + with base.while_loop(expr.lift(False)): + # Why do this? Who knows, but it clearly has a well-defined meaning. + base.add_capture(a) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + + def test_later_blocks_do_not_inherit_captures(self): + """Neither 'if' nor 'switch' should have later blocks inherit the captures from the earlier + blocks, and the earlier blocks shouldn't be affected by later ones.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + + base = QuantumCircuit(inputs=[a, b, c]) + with base.if_test(expr.lift(False)) as else_: + base.store(a, expr.lift(False)) + with else_: + base.store(b, expr.lift(False)) + blocks = base.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) + + base = QuantumCircuit(inputs=[a, b, c]) + with base.switch(expr.lift(False)) as case: + with case(0): + base.store(a, expr.lift(False)) + with case(case.DEFAULT): + base.store(b, expr.lift(False)) + blocks = base.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) + + def test_blocks_have_independent_declarations(self): + """The blocks of if and switch should be separate scopes for declarations.""" + b1 = expr.Var.new("b", types.Bool()) + b2 = expr.Var.new("b", types.Bool()) + self.assertNotEqual(b1, b2) + + base = QuantumCircuit() + with base.if_test(expr.lift(False)) as else_: + base.add_var(b1, expr.lift(False)) + with else_: + base.add_var(b2, expr.lift(False)) + blocks = base.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) + self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) + + base = QuantumCircuit() + with base.switch(expr.lift(False)) as case: + with case(0): + base.add_var(b1, expr.lift(False)) + with case(case.DEFAULT): + base.add_var(b2, expr.lift(False)) + blocks = base.data[-1].operation.blocks + self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) + self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) + + def test_can_shadow_outer_name(self): + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + base = QuantumCircuit(inputs=[outer]) + with base.if_test(expr.lift(False)): + base.add_var(inner, expr.lift(True)) + block = base.data[-1].operation.blocks[0] + self.assertEqual(set(block.iter_declared_vars()), {inner}) + self.assertEqual(set(block.iter_captured_vars()), set()) + + def test_iterators_run_over_scope(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + c = expr.Var.new("c", types.Bool()) + d = expr.Var.new("d", types.Bool()) + + base = QuantumCircuit(inputs=[a, b, c]) + self.assertEqual(set(base.iter_input_vars()), {a, b, c}) + self.assertEqual(set(base.iter_declared_vars()), set()) + self.assertEqual(set(base.iter_captured_vars()), set()) + + with base.switch(expr.lift(3)) as case: + with case(0): + # Nothing here. + self.assertEqual(set(base.iter_vars()), set()) + self.assertEqual(set(base.iter_input_vars()), set()) + self.assertEqual(set(base.iter_declared_vars()), set()) + self.assertEqual(set(base.iter_captured_vars()), set()) + + # Capture a variable. + base.store(a, expr.lift(False)) + self.assertEqual(set(base.iter_captured_vars()), {a}) + + # Declare a variable. + base.add_var(d, expr.lift(False)) + self.assertEqual(set(base.iter_declared_vars()), {d}) + self.assertEqual(set(base.iter_vars()), {a, d}) + + with case(1): + # We should have reset. + self.assertEqual(set(base.iter_vars()), set()) + self.assertEqual(set(base.iter_input_vars()), set()) + self.assertEqual(set(base.iter_declared_vars()), set()) + self.assertEqual(set(base.iter_captured_vars()), set()) + + # Capture a variable. + base.store(b, expr.lift(False)) + self.assertEqual(set(base.iter_captured_vars()), {b}) + + # Capture some more in another scope. + with base.while_loop(expr.lift(False)): + self.assertEqual(set(base.iter_vars()), set()) + base.store(c, expr.lift(False)) + self.assertEqual(set(base.iter_captured_vars()), {c}) + + self.assertEqual(set(base.iter_captured_vars()), {b, c}) + self.assertEqual(set(base.iter_vars()), {b, c}) + # And back to the outer scope. + self.assertEqual(set(base.iter_input_vars()), {a, b, c}) + self.assertEqual(set(base.iter_declared_vars()), set()) + self.assertEqual(set(base.iter_captured_vars()), set()) + + def test_get_var_respects_scope(self): + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + base = QuantumCircuit(inputs=[outer]) + self.assertEqual(base.get_var("a"), outer) + with base.if_test(expr.lift(False)) as else_: + # Before we've done anything, getting the variable should get the outer one. + self.assertEqual(base.get_var("a"), outer) + + # If we shadow it, we should get the shadowed one after. + base.add_var(inner, expr.lift(False)) + self.assertEqual(base.get_var("a"), inner) + with else_: + # In a new scope, we should see the outer one again. + self.assertEqual(base.get_var("a"), outer) + # ... until we shadow it. + base.add_var(inner, expr.lift(False)) + self.assertEqual(base.get_var("a"), inner) + self.assertEqual(base.get_var("a"), outer) + + def test_has_var_respects_scope(self): + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + base = QuantumCircuit(inputs=[outer]) + self.assertEqual(base.get_var("a"), outer) + with base.if_test(expr.lift(False)) as else_: + self.assertFalse(base.has_var("b")) + + # Before we've done anything, we should see the outer one. + self.assertTrue(base.has_var("a")) + self.assertTrue(base.has_var(outer)) + self.assertFalse(base.has_var(inner)) + + # If we shadow it, we should see the shadowed one after. + base.add_var(inner, expr.lift(False)) + self.assertTrue(base.has_var("a")) + self.assertFalse(base.has_var(outer)) + self.assertTrue(base.has_var(inner)) + with else_: + # In a new scope, we should see the outer one again. + self.assertTrue(base.has_var("a")) + self.assertTrue(base.has_var(outer)) + self.assertFalse(base.has_var(inner)) + + # ... until we shadow it. + base.add_var(inner, expr.lift(False)) + self.assertTrue(base.has_var("a")) + self.assertFalse(base.has_var(outer)) + self.assertTrue(base.has_var(inner)) + + self.assertTrue(base.has_var("a")) + self.assertTrue(base.has_var(outer)) + self.assertFalse(base.has_var(inner)) + + def test_store_to_clbit_captures_bit(self): + base = QuantumCircuit(1, 2) + with base.if_test(expr.lift(False)): + base.store(expr.lift(base.clbits[0]), expr.lift(True)) + + expected = QuantumCircuit(1, 2) + body = QuantumCircuit([expected.clbits[0]]) + body.store(expr.lift(expected.clbits[0]), expr.lift(True)) + expected.if_test(expr.lift(False), body, [], [0]) + + self.assertEqual(base, expected) + + def test_store_to_register_captures_register(self): + cr1 = ClassicalRegister(2, "cr1") + cr2 = ClassicalRegister(2, "cr2") + base = QuantumCircuit(cr1, cr2) + with base.if_test(expr.lift(False)): + base.store(expr.lift(cr1), expr.lift(3)) + + body = QuantumCircuit(cr1) + body.store(expr.lift(cr1), expr.lift(3)) + expected = QuantumCircuit(cr1, cr2) + expected.if_test(expr.lift(False), body, [], cr1[:]) + + self.assertEqual(base, expected) + @ddt.ddt class TestControlFlowBuildersFailurePaths(QiskitTestCase): @@ -3505,23 +3752,6 @@ def test_non_context_manager_calling_states_reject_missing_resources(self, resou ): test.switch(test.clbits[0], [(False, body)], qubits=qubits, clbits=clbits) - @ddt.data(None, [Clbit()], 0) - def test_builder_block_add_bits_reject_bad_bits(self, bit): - """Test that :obj:`.ControlFlowBuilderBlock` raises if something is given that is an - incorrect type. - - This isn't intended to be something users do at all; the builder block is an internal - construct only, but this keeps coverage checking happy.""" - - def dummy_requester(resource): - raise CircuitError - - builder_block = ControlFlowBuilderBlock( - qubits=(), clbits=(), resource_requester=dummy_requester - ) - 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.""" @@ -3546,3 +3776,124 @@ def test_compose_new_invalid_within_builder(self): with outer.if_test((outer.clbits[0], 1)): with self.assertRaisesRegex(CircuitError, r"Cannot emit a new composed circuit.*"): outer.compose(inner, inplace=False) + + def test_cannot_capture_variable_not_in_scope(self): + a = expr.Var.new("a", types.Bool()) + + base = QuantumCircuit(1, 1) + with base.if_test((0, True)) as else_, self.assertRaisesRegex(CircuitError, "not in scope"): + base.store(a, expr.lift(False)) + with else_, self.assertRaisesRegex(CircuitError, "not in scope"): + base.store(a, expr.lift(False)) + + base.add_input(a) + with base.while_loop((0, True)), self.assertRaisesRegex(CircuitError, "not in scope"): + base.store(expr.Var.new("a", types.Bool()), expr.lift(False)) + + with base.for_loop(range(3)): + with base.switch(base.clbits[0]) as case, case(0): + with self.assertRaisesRegex(CircuitError, "not in scope"): + base.store(expr.Var.new("a", types.Bool()), expr.lift(False)) + + def test_cannot_add_existing_variable(self): + a = expr.Var.new("a", types.Bool()) + base = QuantumCircuit() + with base.if_test(expr.lift(False)) as else_: + base.add_var(a, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "already present"): + base.add_var(a, expr.lift(False)) + with else_: + base.add_var(a, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "already present"): + base.add_var(a, expr.lift(False)) + + def test_cannot_shadow_in_same_scope(self): + a = expr.Var.new("a", types.Bool()) + base = QuantumCircuit() + with base.switch(expr.lift(3)) as case: + with case(0): + base.add_var(a, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + base.add_var(a.name, expr.lift(False)) + with case(case.DEFAULT): + base.add_var(a, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + base.add_var(a.name, expr.lift(False)) + + def test_cannot_shadow_captured_variable(self): + """It shouldn't be possible to shadow a variable that has already been captured into the + block.""" + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + + base = QuantumCircuit(inputs=[outer]) + with base.while_loop(expr.lift(True)): + # Capture the outer. + base.store(outer, expr.lift(True)) + # Attempt to shadow it. + with self.assertRaisesRegex(CircuitError, "its name shadows"): + base.add_var(inner, expr.lift(False)) + + def test_cannot_use_outer_variable_after_shadow(self): + """If we've shadowed a variable, the outer one shouldn't be visible to us for use.""" + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + + base = QuantumCircuit(inputs=[outer]) + with base.for_loop(range(3)): + # Shadow the outer. + base.add_var(inner, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "cannot use.*shadowed"): + base.store(outer, expr.lift(True)) + + def test_cannot_use_beyond_outer_shadow(self): + outer = expr.Var.new("a", types.Bool()) + inner = expr.Var.new("a", types.Bool()) + base = QuantumCircuit(inputs=[outer]) + with base.while_loop(expr.lift(True)): + # Shadow 'outer' + base.add_var(inner, expr.lift(True)) + with base.switch(expr.lift(3)) as case, case(0): + with self.assertRaisesRegex(CircuitError, "not in scope"): + # Attempt to access the shadowed variable. + base.store(outer, expr.lift(False)) + + def test_exception_during_initialisation_does_not_add_variable(self): + uint_var = expr.Var.new("a", types.Uint(16)) + bool_expr = expr.Value(False, types.Bool()) + with self.assertRaises(CircuitError): + Store(uint_var, bool_expr) + base = QuantumCircuit() + with base.while_loop(expr.lift(False)): + # Should succeed. + b = base.add_var("b", expr.lift(False)) + try: + base.add_var(uint_var, bool_expr) + except CircuitError: + pass + # Should succeed. + c = base.add_var("c", expr.lift(False)) + local_vars = set(base.iter_vars()) + self.assertEqual(local_vars, {b, c}) + + def test_cannot_use_old_var_not_in_circuit(self): + base = QuantumCircuit() + with base.if_test(expr.lift(False)) as else_: + with self.assertRaisesRegex(CircuitError, "not present"): + base.store(expr.lift(Clbit()), expr.lift(False)) + with else_: + with self.assertRaisesRegex(CircuitError, "not present"): + with base.if_test(expr.equal(ClassicalRegister(2, "c"), 3)): + pass + + def test_cannot_add_input_in_scope(self): + base = QuantumCircuit() + with base.for_loop(range(3)): + with self.assertRaisesRegex(CircuitError, "cannot add an input variable"): + base.add_input("a", types.Bool()) + + def test_cannot_add_uninitialized_in_scope(self): + base = QuantumCircuit() + with base.for_loop(range(3)): + with self.assertRaisesRegex(CircuitError, "cannot add an uninitialized variable"): + base.add_uninitialized_var(expr.Var.new("a", types.Bool())) From f12db3b5350a75b52c9c61b0c1477d30141ef6e2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 30 Nov 2023 17:42:28 -0500 Subject: [PATCH 017/124] Don't substitute ideal gates in target with Optimize1qGatesDecomposition (#11351) * Don't substitute ideal gates in target with Optimize1qGatesDecomposition This commit fixes an issue in Optimize1qGatesDecomposition where the pass would defer to synthesized gates if the output from synthesis was ideal even if the input gate was also ideal. This typically comes up in simulators where there are no error rates for any gates and everything is listed as ideal. This would cause the transpiler to translate gates unnecessarily which was unexpected. This commit fixes this by adding an additional check to the subsitution condition to ensure we're only substituting a gate if it's not in the target (when they're all ideal). Fixes #10568 * Update releasenotes/notes/fix-optimize-1q-sim-407b88e45e6062b6.yaml Co-authored-by: Jake Lishman * Update qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py * Fix formatting --------- Co-authored-by: Jake Lishman --- .../optimization/optimize_1q_decomposition.py | 10 ++++++++-- .../fix-optimize-1q-sim-407b88e45e6062b6.yaml | 10 ++++++++++ .../transpiler/test_optimize_1q_decomposition.py | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-optimize-1q-sim-407b88e45e6062b6.yaml diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index f96ed999061d..9eb31b8f8134 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -173,10 +173,16 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. + new_error = 0.0 + old_error = 0.0 + if not uncalibrated_and_not_basis_p: + new_error = self._error(new_circ, qubit) + old_error = self._error(old_run, qubit) + return ( uncalibrated_and_not_basis_p - or (uncalibrated_p and self._error(new_circ, qubit) < self._error(old_run, qubit)) - or math.isclose(self._error(new_circ, qubit)[0], 0) + or (uncalibrated_p and new_error < old_error) + or (math.isclose(new_error[0], 0) and not math.isclose(old_error[0], 0)) ) @control_flow.trivial_recurse diff --git a/releasenotes/notes/fix-optimize-1q-sim-407b88e45e6062b6.yaml b/releasenotes/notes/fix-optimize-1q-sim-407b88e45e6062b6.yaml new file mode 100644 index 000000000000..f531561789e0 --- /dev/null +++ b/releasenotes/notes/fix-optimize-1q-sim-407b88e45e6062b6.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fixed an issue with the :class:`.Optimize1qGatesDecomposition` transpiler + pass where it would potentially resynthesize a single ideal (meaning the + error rate is ``0.0``) gate which was present in the :class:`.Target`. This + is now fixed so the pass :class:`.Optimize1qGatesDecomposition` will defer + to the circuit's gate if the error rate (which includes number of gates) + are the same. + Fixed `#10568 `__ diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 5d3ebb3d7245..72dbae0d3115 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -745,6 +745,20 @@ def test_nested_control_flow(self): result = passmanager.run(test) self.assertEqual(result, expected) + def test_prefer_no_substitution_if_all_ideal(self): + """Test that gates are not substituted if all our ideal gates in basis.""" + target = Target(num_qubits=1) + target.add_instruction(HGate(), {(0,): InstructionProperties(error=0)}) + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), + {(0,): InstructionProperties(error=0)}, + ) + qc = QuantumCircuit(1) + qc.h(0) + opt_pass = Optimize1qGatesDecomposition(target) + res = opt_pass(qc) + self.assertEqual(res, qc) + if __name__ == "__main__": unittest.main() From c75af150fb29d2b6cdb8ef6710cad1f79503e049 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:50:07 -0500 Subject: [PATCH 018/124] Remove non-API docs and tutorials (#11352) * Remove non-API docs and tutorials * Remove sphinx-design * Fix bad reference * Fix bad deploy step * Remove translatable strings script --- .azure/tutorials-linux.yml | 42 - .github/workflows/docs_deploy.yml | 61 +- azure-pipelines.yml | 14 +- docs/_static/images/1xp.png | Bin 82310 -> 0 bytes docs/_static/images/ibm_qlab.png | Bin 82644 -> 0 bytes docs/_static/images/strangeworks.png | Bin 95526 -> 0 bytes docs/api_redirects.txt | 277 - docs/conf.py | 141 +- docs/configuration.rst | 98 - docs/explanation/endianness.rst | 47 - docs/explanation/index.rst | 12 - docs/faq.rst | 78 - docs/getting_started.rst | 263 - docs/how_to/index.rst | 15 - docs/how_to/use_estimator.rst | 269 - docs/how_to/use_sampler.rst | 255 - docs/images/favicon.ico | Bin 1150 -> 0 bytes docs/images/logo.png | Bin 38644 -> 0 bytes docs/images/noise_cancel.png | Bin 35619 -> 0 bytes docs/images/qiskit_nutshell.png | Bin 92754 -> 0 bytes docs/images/quantum_interference.png | Bin 369710 -> 0 bytes docs/images/system_error.png | Bin 103584 -> 0 bytes docs/images/system_one.jpeg | Bin 197993 -> 0 bytes docs/images/teleportation_detailed.png | Bin 289144 -> 0 bytes docs/index.rst | 77 +- docs/intro_tutorial1.rst | 262 - docs/legacy_release_notes.rst | 33335 ---------------- .../migration_guides/algorithms_migration.rst | 948 - docs/migration_guides/index.rst | 10 - docs/migration_guides/opflow_migration.rst | 1674 - docs/migration_guides/qi_migration.rst | 670 - docs/qc_intro.rst | 234 - docs/release_notes.rst | 3 +- docs/tutorials.rst | 36 - .../circuits/01_circuit_basics.ipynb | 824 - .../1_getting_started_with_qiskit.ipynb | 872 - .../circuits/2_plotting_data_in_qiskit.ipynb | 985 - .../3_summary_of_quantum_operations.ipynb | 3679 -- .../01_advanced_circuits.ipynb | 1012 - .../02_operators_overview.ipynb | 1383 - .../03_advanced_circuit_visualization.ipynb | 872 - ...04_transpiler_passes_and_passmanager.ipynb | 6957 ---- .../circuits_advanced/05_pulse_gates.ipynb | 529 - .../06_building_pulse_schedules.ipynb | 924 - .../07_pulse_scheduler.ipynb | 460 - .../08_gathering_system_information.ipynb | 911 - .../circuits_advanced/pulse_modulation.png | Bin 750127 -> 0 bytes qiskit/circuit/__init__.py | 249 +- .../platform-support-f7f693aaf5dec044.yaml | 2 +- requirements-dev.txt | 18 +- requirements-optional.txt | 2 +- requirements-tutorials.txt | 10 - tools/deploy_translatable_strings.sh | 110 - tools/execute_tutorials.py | 100 - tools/github_poBranch_update_key.enc | Bin 416 -> 0 bytes tox.ini | 16 - 56 files changed, 148 insertions(+), 58588 deletions(-) delete mode 100644 .azure/tutorials-linux.yml delete mode 100644 docs/_static/images/1xp.png delete mode 100644 docs/_static/images/ibm_qlab.png delete mode 100644 docs/_static/images/strangeworks.png delete mode 100644 docs/api_redirects.txt delete mode 100644 docs/configuration.rst delete mode 100644 docs/explanation/endianness.rst delete mode 100644 docs/explanation/index.rst delete mode 100644 docs/faq.rst delete mode 100644 docs/getting_started.rst delete mode 100644 docs/how_to/index.rst delete mode 100644 docs/how_to/use_estimator.rst delete mode 100644 docs/how_to/use_sampler.rst delete mode 100644 docs/images/favicon.ico delete mode 100644 docs/images/logo.png delete mode 100644 docs/images/noise_cancel.png delete mode 100644 docs/images/qiskit_nutshell.png delete mode 100644 docs/images/quantum_interference.png delete mode 100644 docs/images/system_error.png delete mode 100644 docs/images/system_one.jpeg delete mode 100644 docs/images/teleportation_detailed.png delete mode 100644 docs/intro_tutorial1.rst delete mode 100644 docs/legacy_release_notes.rst delete mode 100644 docs/migration_guides/algorithms_migration.rst delete mode 100644 docs/migration_guides/index.rst delete mode 100644 docs/migration_guides/opflow_migration.rst delete mode 100644 docs/migration_guides/qi_migration.rst delete mode 100644 docs/qc_intro.rst delete mode 100644 docs/tutorials.rst delete mode 100644 docs/tutorials/circuits/01_circuit_basics.ipynb delete mode 100644 docs/tutorials/circuits/1_getting_started_with_qiskit.ipynb delete mode 100644 docs/tutorials/circuits/2_plotting_data_in_qiskit.ipynb delete mode 100644 docs/tutorials/circuits/3_summary_of_quantum_operations.ipynb delete mode 100644 docs/tutorials/circuits_advanced/01_advanced_circuits.ipynb delete mode 100644 docs/tutorials/circuits_advanced/02_operators_overview.ipynb delete mode 100644 docs/tutorials/circuits_advanced/03_advanced_circuit_visualization.ipynb delete mode 100644 docs/tutorials/circuits_advanced/04_transpiler_passes_and_passmanager.ipynb delete mode 100644 docs/tutorials/circuits_advanced/05_pulse_gates.ipynb delete mode 100644 docs/tutorials/circuits_advanced/06_building_pulse_schedules.ipynb delete mode 100644 docs/tutorials/circuits_advanced/07_pulse_scheduler.ipynb delete mode 100644 docs/tutorials/circuits_advanced/08_gathering_system_information.ipynb delete mode 100644 docs/tutorials/circuits_advanced/pulse_modulation.png delete mode 100644 requirements-tutorials.txt delete mode 100755 tools/deploy_translatable_strings.sh delete mode 100644 tools/execute_tutorials.py delete mode 100644 tools/github_poBranch_update_key.enc diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml deleted file mode 100644 index c7cc9ba57ad5..000000000000 --- a/.azure/tutorials-linux.yml +++ /dev/null @@ -1,42 +0,0 @@ -parameters: - - name: "pythonVersion" - type: string - displayName: "Version of Python to test" - -jobs: - - job: "Tutorials" - pool: {vmImage: 'ubuntu-latest'} - - variables: - PIP_CACHE_DIR: $(Pipeline.Workspace)/.pip - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '${{ parameters.pythonVersion }}' - displayName: 'Use Python ${{ parameters.pythonVersion }}' - - - bash: tools/install_ubuntu_docs_dependencies.sh - displayName: 'Install dependencies' - - - bash: tox -e tutorials - displayName: "Execute tutorials" - env: - QISKIT_CELL_TIMEOUT: 300 - - - task: ArchiveFiles@2 - inputs: - rootFolderOrFile: 'executed_tutorials' - archiveType: tar - archiveFile: '$(Build.ArtifactStagingDirectory)/executed_tutorials.tar.gz' - verbose: true - condition: succeededOrFailed() - - - task: PublishBuildArtifacts@1 - displayName: 'Publish updated tutorials' - inputs: - pathtoPublish: '$(Build.ArtifactStagingDirectory)' - artifactName: 'executed_tutorials' - Parallel: true - ParallelCount: 8 - condition: succeededOrFailed() diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 8f922121f938..a375b8750d4e 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -16,10 +16,6 @@ on: description: "Push to qiskit.org?" required: false type: boolean - do_translatables: - description: "Push translatable strings?" - required: false - type: boolean jobs: build: @@ -53,33 +49,8 @@ jobs: - name: Install dependencies run: tools/install_ubuntu_docs_dependencies.sh - # This is just to have tox create the environment, so we can use it to execute the tutorials. - # We want to re-use it later for the build, hence 'tox run --notest' instead of 'tox devenv'. - - name: Prepare Python environment - run: tox run -e docs --notest - - # The reason to use the custom script rather than letting 'nbsphinx' do its thing normally - # within the Sphinx build is so that the execution process is the same as in the test CI. - - name: Execute tutorials in place - run: .tox/docs/bin/python tools/execute_tutorials.py docs/tutorials - env: - QISKIT_CELL_TIMEOUT: "300" - - name: Build documentation - # We can skip re-installing the package, since we just did it a couple of steps ago. - run: tox run -e docs --skip-pkg-install - env: - QISKIT_ENABLE_ANALYTICS: "true" - # We've already built them. - QISKIT_DOCS_BUILD_TUTORIALS: "never" - DOCS_PROD_BUILD: "true" - - - name: Build translatable strings - run: tox run -e gettext - env: - # We've already built them. - QISKIT_DOCS_BUILD_TUTORIALS: "never" - DOCS_PROD_BUILD: "true" + run: tox run -e docs - name: Store built documentation artifact uses: actions/upload-artifact@v3 @@ -91,13 +62,6 @@ jobs: !**/.buildinfo if-no-files-found: error - - name: Store translatable strings artifact - uses: actions/upload-artifact@v3 - with: - name: qiskit-translatables - path: ./docs/locale/en/* - if-no-files-found: error - deploy: if: github.event_name != 'workflow_dispatch' || inputs.do_deployment name: Deploy to qiskit.org @@ -198,26 +162,3 @@ jobs: JOINED_PREFIXES: ${{ steps.choose.outputs.joined_prefixes }} RCLONE_KEY: ${{ secrets.ENCRYPTED_RCLONE_KEY}} RCLONE_IV: ${{ secrets.ENCRYPTED_RCLONE_IV }} - - deploy_translatables: - if: (github.event_name == 'workflow_dispatch' && inputs.do_translatables) || (github.event_name == 'push' && github.ref_type == 'tag' && github.ref_name == needs.build.outputs.latest_tag) - name: Push translatable strings - needs: [build] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - path: 'qiskit' - - - uses: actions/download-artifact@v3 - with: - name: qiskit-translatables - path: 'deploy' - - - name: Deploy translations - id: ssh_key - run: qiskit/tools/deploy_translatable_strings.sh "${{ github.workspace }}/deploy" - env: - encrypted_deploy_po_branch_key: ${{ secrets.ENCRYPTED_DEPLOY_PO_BRANCH_KEY }} - encrypted_deploy_po_branch_iv: ${{ secrets.ENCRYPTED_DEPLOY_PO_BRANCH_IV }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8fc21b7d8db1..58370138160a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,11 +52,6 @@ parameters: type: string default: "3.9" - - name: "tutorialsPythonVersion" - displayName: "Version of Python to use to run the tutorials job" - type: string - default: "3.8" - # Sync with 'python-version' in '.github/workflows/docs_deploy.yml'. - name: "documentationPythonVersion" displayName: "Version of Python to use to build Sphinx documentation" @@ -162,18 +157,13 @@ stages: testImages: true # The rest of the PR pipeline is to test the oldest and newest supported - # versions of Python, along with the integration tests (via the tutorials). - # It's very rare for a failure to be specific to an intermediate version of + # versions of Python. It's very rare for a failure to be specific to an intermediate version of # Python, so we just catch those in the cron-job pipeline to reduce the # amount of resources used. - - stage: "Tutorials_and_Tests" + - stage: "Tests" displayName: "Main tests" dependsOn: "Lint_Docs_Prelim_Tests" jobs: - - template: ".azure/tutorials-linux.yml" - parameters: - pythonVersion: ${{ parameters.tutorialsPythonVersion }} - - template: ".azure/test-linux.yml" parameters: pythonVersion: ${{ parameters.maximumPythonVersion }} diff --git a/docs/_static/images/1xp.png b/docs/_static/images/1xp.png deleted file mode 100644 index c7ee75a81bd1b6b1473da3d3af4859c8b07b5ed6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82310 zcmbSyWmH_VZ86?RwPELG}K>$u2&!*ExQN;ZDGSLi` zk{**lIX3u*6@Y};1e=|t`e!-AH)?`+zYGjs`8I>#V_Y(t&0>!Gctxd@< zYKpEgT*xZQ_ICK3)|3CKI7_l{aLW$Qvd?5wo=Gv2XLL*bPHO6;Q^gGAIUf18;*()= zwa8qK}j?(wikc8Omc2+>dkj;6h&FzX8| z#wc027Of;@NeNl9?qIS*0?)GQpcJGU$zE*Kn?EPY*>TBAZp!8#Wh25yv`_6@UbUEK zRozYI$favP0n>vDrU#~DYF{PO7r%EhWL5nZVnA(Ga-VaJQ8e=V7w4DhA8~RNEHNvu z)Ti3naD*xWy|7T6g?%znNFbE+(=G9dg!tB&xLG z)hHY91PrqcX~JtF24n~=1u5$hagYooklKn^ACO3Y#Fd%=LDrxfMIodkT~^sDkds|b z?y`npJgwzobKv)9W0zMvt(=AKe~oyVrO3oceGd77YxF8KTDq6l3S$CKxc9-8R{WJ2 ziUsgbRx@l>ykmhVk3b9)9}|XQ8fHus-j|&JiwYO~BOr>O6fU*>qABA0p%>jD_RLRy zPz>b@z#{Ig<=W$T-8-8(XtS-M<$d8p6Zw5Lh4Q;v0dgip5(=bID&a$u{#RPdq*t+)e2o*@z!bv@4kG9i2MSxzB!~g zCt*ooLEn3}$i>yxHf#7R$m7uM@z*W~fofWN0kUth*LS(hEZr`PKRh8pe+W-bB!!=*7?$hUe z(Eh#ZIv0+#@=EZ!kQ|A~0@-QJ&m;)_7#W$4EG+b6B9e9&Dw#jY;?67Hm%~UN(rm2g zDu0x#Nt}?f|9q*&-baaB#Sp<94Z>c1)r@x7wKRom79@F$=chmr7N$ukW)+mlpcjR* z9>Pl~^@c23Zawk!8{9!Txv1A!!Nao9BrF@QTFg@UONPkA6o_acQA@b^d!IxYgypsD zjHdH6S%U)LhiYoL!Vk<{FF~T*SdyPiyH=iWjY1FQD92NFU!GIUhj+Zc;|nUa0%wwM zNAd9qk;D~Qk!PZ2ho@Pl8Q|(AyzK#Yga9qu{{nKNrrgjDF}}Kz2zGBBT#ze;~S*x2?^8&ni1&kJw)41j= zCa8;^ijIjwM72hP0nKi9;`{P%cRoVKl zSHF_W7_`~+Nc=H8_7_ znf_eb{!_bjb{gpudqWU7(?~@^zFp(thr0e(?VaCeGxp{BrNY0>^#YX`rz$>j&Z8O; zRwg&*e@U7xm!7SjwQzKOYv=I6foih(=R}nfjH;BpGOMIT(IawObj0514VPo31y_e& z^sHT_j#0uc*;2@8@!9U#uJLXOd}&_JnbJA_OXHW8OYQ@_14(KwYT?{Bxus&dVgMl6 zlh4%AH2yQ_@YRw(7zUJrlOt-*QGKv4_Aa;)?J(M6+8;dQJX0%`foe}F$USs_=>{ya zqznE?$S#gA^Uk_{Tl49eifHetkG#Rzz~!K9bouMdI1O259sLNNuq*gd5MWKTp=n)n zLb0LK-_8tXMrNL4=1`5#I?tR`dr%E8=1@(@mbBmHHHbCo|D?cCFg6U~99I3*I9SoM zU)K`n{mZ-N=JS~^$!qLl`G`KO7?K!C6`i4pbOBBTr;Bk?Wu~5?UU|iQ#j^2&0gLfi z)93mCy8@S<8n)Wfx)w*3hKr_LCu3{hnfQ+b^+Pqr_Fo5S8ooA-)mGJZ?goi@iC|FA zihAO);9OB~QsmcPXCyoC z7#kEK5;$q*#7`Sy+Ofxqpj%}eGjDr=jgBb@4q_gA1iNmuc=4$Dx4~Kcy^0;9m|kZIe)o6gaky~ z0mU0yPJEDL5wkZQegE10@K8Lht-ei`flDj?H!yjyurx)i4{u%D4Q6vR$iZ@@#PG)W zEb0E_M(ySNF`&cr6UT%6?T_)Q>aydW)Izqql27WyYR>s~QqF7jCGzz0ooWLmXT?p! zyxDwT`$MxTva;>y>n80Rh8r_Y`CEO}cXJLr*gQP8AIC=ynC?vzts}E=&PG}gC;B8d)T4u@G{U$QhD~Q z0mu-%Jv3j@04sY@{zacsm!~42Grsfi^fu_?f-+V()#bWgtNCQRcw7zl?d;pM?C5N4 zVX3*~`l18jZI=dP+@^Gw2e4XwrK9iS<*d=z{5-Y7yY%Uwz)5gk?#36hdE)x=(&d_f zsn1JGTfSX1@Vp)0sY|mJ(wZt;DUZMhnnBj*IjO3(3<cVu8klrIH$w`0kLf&u3 zthbW~KAIjKET{8fGZKq1JkxD)RE}V*z;_AM+;@icr4q6=WOy-z(&z z0*1W1-mixdiSS~x{-(pguuSJ09#`eMdbo7S_t~2jJ!)CEr(2(blnr{ANNjLG94*XLQ<40gZ;g)Bc9`znJe4FA&B2@iBi>h-$ct#)o zYBs=3wTd*uL|;Ew!)yr;YLIN6unYLYo?lDtO>}i;e5)$yU`R3s+-`;y3C_q;!MGfK zZYQ(lh?<58{aRv2EA=(F-%aYle>gP-?4<^DGb6lINCQl{ zhkld*+be$MkuV#6HSKJwGNuKx#rc#qHn=h`*|Z)2PsFHA<8}+pNNZ_lbox~86o+g9 zt{?LDYP_0q21+Jf;LAKjd+Xo!N-NA^He3QsqD+itQ8k}#hGlpJ!&pVH{ z4ch5McyfFIF%1&S+vvfp(@W^(wJK$6b#8VKAmyen^-qgvB}8Ict7?@Q&AkhOL+n*4 z+|`jEDx#*a zIM=W^$M@7t*~MzaAhjo+`MeRbXle!4cMl^LQ=G6ZXDKy|6v>e1_@xbYAb#Xk3?O*j{>S8Tv~Jy8rd&Vx={LROP^x$^Mggf*NlR5T zU_a8w?K<^L@lh*B99rs>7VlRcdn*Ip)B=#7R!D7$v*wG3C&}Z{0?cw<){^6?wmt~m zxjot6ci~otC7{?#<)Bm#iM;DDv5yMjLAO=ZF|g$@C2^Y8!FPA>B?fJpzCS5@Y}kGr zd+^Mu2#n9Zy(zpxq{vu%EWZ26OB&H_nlDZb6?kiw=h=Njx9{T)&M1h5st6(2@BvV^ zDr3E^Ek+jq$&{f~NhQE2kF#!$U-tz)wZHdtbcN09qrIUUF3!5xtEzN;iEIDJJfP#sdXAD-?!lSbGHmm;3Ry7~!?cX5OT7cF zY?b^EfwKqjv^JQung4J9?~5MA(|T*>5{a*<~MYAgZ(84AAG8o~fsMjEF0 zmNVfwdx_j|kWE{BnMB#tf?iRE2OITt5x7*vnV~{_#(YNqk^J|Kpssr|v%}4%3ib=b+a+AA){9Bt>z#C?5d36v;^j$4CXM&bZ7bKPH?i3Getk{VUrQ#cp;PB;LUlA&do~ zez%Tpx5ijOxNe?w<>K&R2k!S|IMY1Un#P>?x$;e(rQ+Y-fp(KBEXBNL>+-gCeskMf zx>U_8nn_OMU%hjHu(m^N$z8B+dW-b^nNvJ(?WG4O40&v%&S|M2b^IX40Tx#K80Syew+TvO|nx8fv6Ylb}nGXXc%7@Oz@(5|R=YH;V(lBiMnh{^Q zu}CF{mE>v3`YwZ6MHU2xlqe;EfBy&!)!w`5<(W4e!ozk{0bVa_N2wg%j8edH2ha(q zBWZkgS7QZ6U4Is-WZd?oi453fJ8y5POFf+1xP#E2*fhMMt51(dPvZIF>ltP~4(rv@ z0zc7dBgN5Cw%i7&cPuB_szk(GbKdO9(L`$a5?=;A{8n$!xGB`(>F^{_KzA;b-20I` zsnW4In8A(zH-T5n8~%%?K?A6%;LniYYKkA4JI~}_k8avsJzG_~TZFgSm2^oopryVe z17>4s2U5U$MOhPK2??t5Mc^X$M(H9KSjNx^Vap$lI_a%Q!{Z#{gOQn&f$*)xwciIO!M8r?=JsZKhR=&?jdD^B_K*P5|BYcJ%0lBi%3tXqMjGgPKPG>AVJgJ z;24(!6+|mx-8A=8!CoRK*mK$&IskhcgqQ0($)u6H`$GyyVYyX`x9*-JJX$%b7T39X zjPRK#=m;VL_CKtcptwB=kLiFCnxC?LHLeN{$@^N+Tbex0*J025W7x@x(dX^;7B|pX-VtWL~~~sKtnna;E(hL)W_wIAmW!YV2OekcARRZ1j2@ z^EOfr=oU&n%hiS+^+Ie$b(`e;9MCjLT=A{Z2pYpjyk#BVp<44$Gv!#^D<(2~?# zl=OCzyFu$h3GlhIDL4ej=SwJoMhn-=axnhxPnw2c_b(f`y7*u*;A@<{0L2oUBK(JR znAkw!Pgu@X!h2Glb48XL_el*J%Nf}Ys7;n$J2ZuAYo9eUkvkXIZb&-ElwaEm#7nBG zHn405QWr1O07yZjjNGc6yJ9mW7g-{f@9yul6fEZzO!$F0o*v7AMdc4_=^1B!>$@Ko z+yh$dQcF&kBn>;v7jz#LrF2>N!)BPfck94_Lyq&Z&$m$paU$zjzA`(2o3g4|os9CW zL}|5L^YXWI%U>=BVFo{XJO>_Dyv?xgD6G2+H@ecQs~Od7YlpFpWTA7xd8btAqqjMw z9iMZ1Thw*!f$I7OE~b%bad)(IbyE)_p@)GWjU#G*?Wa}VH{UYJSKPA1KCdxPcoaWP zQMi`qm~57F%l1?-s)JKU;|i+%#G-Q6ulwK2vx-wIVEqaDn5aP}MMUx$qBqd;XFM2( zJB4U{@uL|?`KuwkzV@@5rvLpw&?ETsj7lpQ&%3DHEbXZOxtz-$Xy)x{)6NJ-eV^G7 zDRpK~L7bq|c$?-zN@Hy%ioUR#Z+$2^-(ZX>s0!OEui7*e+bII#DFN~a*JNMih|Ib} zJ^)*B96q7@uq6y#r#5@~+C7%?X-WdiN1_P(OU=3)4k@ZW8`u`6h+Jfjy5s5@q@EQw zP9F>beSF6Y#BD}OM~XU=o7&u+jZL@mb{kQcCRNBvn*E9l=MLL>CohcxP4k2do`^a` zJMp5vQrrG!DY^4^a(^!8?>z%rG!$PgM>vX-LTucw_=}$xH9heOUrI^%mLUY-^Qf&R zoQF?_0Z#}YzJT>7C&DVF)}oYYcZlBRaK10ICa5(1SecRn~$<89B~Ve8n?<0gyLT0i+|A`Cax z_Jnl-nu5nM(8HnYBd$(Q1y4>oR5xh+y9MK3VhMNS79IZdzK z_W(`p%(A`_^(x-%p(_(x&SN)b5!Le=nLgtV9Zm8F$uxaQOe2d?oYBcC&o#WsMrjD> zD^^=Lqn??l5U>P*>xiDJ4v&$ardA^KZ(@lVwjzG6h)lsd9#VyX3qe7XpB@TfZVmXk z4FTbfH()BbNSaQo(v&xIER7BLA?XoJ`J)RtabsQ0&>NUB>TbF7Sl?bd*2PUJTy{Jp9Okew|b{ zW(|$K)oDsNe!ZoVJ&l%D*O;gek~$sKk@!U9Ms=dTAU?A9a;5nRH_R`t?;-yP#gymb zLdVqF?MZ(F^NQ!`elsxaKHp@6+2X^aA!%twgKD|}qjd#`ozJ*vBE+yS3F<%EVvyx1 zA*avmbYteGkH;S&s!^!Q`ARB9fX9mZ{1yob5b1Waeo_te;thPfpTwjw6QP-Iel90b zebcgoN#>DB(&^_Q9#I>x`e`9Y1z1t%VRNler8e*Hna&Q(QjRNL^mS}oXPNA$MB&(G z&HlvFJDni{Jws^b8TvEd_&zkwa-3Er{lc(CEKcbAXBS!@z3~W{)A7)N`&*M&8F<~z zV(Nc$noKW(>K@SFC85)v`Yx=J=jK;b=`WvRDNN3N?MTyWvM-awoB}U@_C8kxO_>bH zz=*vYXa;9*MKo4J)Tz8J`X1!`%1$@oJ`5)Bpucf<88u)mmNW7OpoRR;?Hn=X8oN46 zlS^D2&%OIWr7b2RXHcIU4)PyNDX!jQP<)aa<63k&9DRB_Ovv9Qv%8MLcz=wPWT?%h<46X&w3hO&O_W$6cCLj zXdYUQbtV5MD$`6|&gXh95*<-dhj-B&3uRp)Ismv||EpQT{;8ukAs+%A489byHDz z%5xPu<(LFyQMjZHiVa;bi2CJt3e^lgdP030&xuMGX4}&X%hOATSZVWpT*aQc<=~r2 z_?DANOpEPb70Yd-RcI|Ql>GIm*$ez^+>(0|?m8bToCrPzgcnS9=@>xu&0zY5xq+v; z3LTTjKoAB@-A54t-KBWXeE1|J3GNRs;?2+iSI>Aesgb1!8y`LGiS`2f&VIiw(9>|V z*GBUUcanyF`YiVsN85(6jf=ef{Ecc|ys zQDa*e(SN5{l0fpcy)5G#%+rf_BpTQXz^lcpGT)JQxHg4VM)D_@bmrj42n3b-JDjZd zg*sNXr+*>w8;?=t))}RB5V4uTU&k6^QB$`Cd!@KIR3aSN+f7X&gQYw=eM(OkRe=&@ zw6wHk{@2vvLpK>T&vYbzsDU@tq=}l{9xmTxvsunrt1L@n;`yMIV8c|>4E=I^VRe&} ze#=(iDC4u00%!&+D9qE7&+A39m3$#gFW@2JhT+2-sKoXVwBC*$i%&D&FL*|scYH2< zHARMwtjax1;U7&~3kM#gZgNQCufamC4l@rEK6tZ#VYZy@CR52t=CGfFn;VZ+ggN4* zt22GHO4U7Za#ozAmnu}fD~eeV@`ys2vg+@HRuOnSiGi^we@6Un`8y%Mw@kTOz^c?V zRg5~}l&AW$aW}F$X+}L>bYM81jDl=UlqF`*AjLM}U2yDHbq1peE^}VLnoFpamF62K z23Zs}8%qlZobO8RhWWkjWEMzim#+tSrz7>`0TZ_e#MUU(bN&~lU5Z0g?>(VQj?l;VczMxb->&xuAP>oYt~ z272_9hwq)gu1Ep3vn z%l};rYNP<2_bmh^n4RT5^8~ypK3?yP0=oD{7&v-EQC34Z&vxj-?$<77?Q|)B;Rfd+ z|D<6Fy^6%AD${mr9hVN?Cu_pHmJxb3@-fH_r>!u zHcDQNTzPuopvE=3Q8pqedyA!zzt0GMiXxRBj!o44q{n5TFK!$CJy8k5X^lcbXKxwU z*U=b}#3F-6SRR#OPRCwOnH0%`?`Qg9`<++e)1H3k`dS5pgO!yPuaJ;X#T2nn;PG^c zWCMf&5XLXOp~D7 zls)%$8sBfG;IUNQ?|E(7ITd82!L4HxKJuX!_%NA*7%KbTLGaV`ZO87Lky_7q;|HBw z?Khnu76jeU7!PuTp*tjz+a&dKM@hCHn+lDypy9XGd%tq77Rih%1fQsseOZr+p46^< z1o(p#rl`c6Ebtn_m=HU&#Z?8_5lZtZLLt}DnOAHrDGR+UlkDCjI{4l5ZHlD3*<^NM zs!-6m#B}Cj_r@RmMm7%$>)1=~C=2AC)$iO(;tavcro9S$$?HWH5pa-XlK+Gykj7dI zSpysBpk<7y46&HrkZ|qj-lDz1vtO&uRwS8_VCFs4sL`@X+jNa`RL9ZNSkm+Ylh6$aP~IrIL3pZ zpHQF7aI(63ZR6U0DyM40ZiJjLr?Ss;KP9CzbZW zCbl{oLT{a=q+P=!L_#>U-4hH18wmb>nRpCb_Y6+4d!CUc-}Pd;blhx*8oRWoWoN(9 z=&9PkO6g#@+AT=H+aAu^pkrICOgNnu{d%~F>6qQrFk9sgl-TW}x3~N}yG-;G{K