diff --git a/.codespell.allow b/.codespell.allow new file mode 100644 index 000000000..1edf5ded2 --- /dev/null +++ b/.codespell.allow @@ -0,0 +1,5 @@ +Braket +braket +te +Ket +ket diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..5e98ae4a7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = false +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..7fddd081d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + ignore: + # Optional: Official actions have moving tags like v1; + # if you use those, you don't need updates. + - dependency-name: "actions/*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..bb242ba80 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,350 @@ +--- + +name: CI + +on: + workflow_dispatch: + merge_group: + pull_request: + push: + branches: + - master + - develop + - v* + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + standard: + strategy: + fail-fast: false + matrix: + runs-on: [ubuntu-latest, windows-latest, macos-latest] + python: + - 3.8 + - 3.9 + - '3.10' + - '3.11' + - '3.12' + + name: "Python ${{ matrix.python }} • ${{ matrix.runs-on }} • x64 ${{ matrix.args }}" + runs-on: ${{ matrix.runs-on }} + + steps: + - uses: actions/checkout@v3 + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Setup Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + architecture: 'x64' + cache: 'pip' + cache-dependency-path: | + setup.cfg + pyproject.toml + + - name: Prepare env + run: | + python -m pip install -U "setuptools>=61.0.0" + + - name: Generate requirement file (Unix) + if: runner.os != 'Windows' + run: | + python setup.py gen_reqfile --include-extras=test,azure-quantum,braket,revkit + + - name: Generate requirement file (Windows) + if: runner.os == 'Windows' + run: | + python setup.py gen_reqfile --include-extras=test,azure-quantum,braket + + - name: Prepare env + run: | + python -m pip install -U pip setuptools wheel pybind11 + cat requirements.txt + python -m pip install -r requirements.txt --prefer-binary + python -m pip install coveralls + + - name: Setup annotations on Linux + if: runner.os == 'Linux' + run: python -m pip install pytest-github-actions-annotate-failures + + - name: Build and install package (Unix) + if: runner.os != 'Windows' + run: python -m pip install -ve .[azure-quantum,braket,revkit,test] + + - name: Build and install package (Windows) + if: runner.os == 'Windows' + run: python -m pip install -ve .[azure-quantum,braket,test] + + - name: Pytest + run: | + echo 'backend: Agg' > matplotlibrc + python -m pytest -p no:warnings --cov=projectq + + - name: Coveralls.io + run: coveralls --service=github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: python-${{ matrix.python }}-${{ matrix.runs-on }}-x64 + COVERALLS_PARALLEL: true + + finish: + needs: standard + runs-on: ubuntu-latest + container: python:3-slim + steps: + - name: Coveralls Finished + run: | + pip3 install --upgrade coveralls + coveralls --finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + clang: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + clang: + - 11 + - latest + env: + CC: clang + CXX: clang++ + PROJECTQ_CLEANUP_COMPILER_FLAGS: ${{ (matrix.clang <= 10) && 1 || 0 }} + + name: "Python 3 • Clang ${{ matrix.clang }} • x64" + container: "silkeh/clang:${{ matrix.clang }}" + + steps: + - name: Install Git + run: | + apt-get update && apt-get install -y git --no-install-recommends + + # Work-around for https://github.com/actions/runner-images/issues/6775 + - name: Change Owner of Container Working Directory + run: chown root:root . + + - uses: actions/checkout@v3 + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Prepare env + run: > + apt-get update && apt-get install -y python3-dev python3-pip python3-setuptools python3-wheel + python3-numpy python3-scipy python3-matplotlib python3-requests python3-networkx + python3-pytest python3-pytest-cov python3-flaky python3-venv + --no-install-recommends + + - name: Prepare Python env + run: | + python3 -m venv venv + ./venv/bin/python3 -m pip install -U pip setuptools wheel + ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + cat requirements.txt + ./venv/bin/python3 -m pip install -r requirements.txt --prefer-binary + + - name: Upgrade pybind11 and flaky + run: ./venv/bin/python3 -m pip install --upgrade pybind11 flaky --prefer-binary + + - name: Build and install package + run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,braket,test] + + - name: Pytest + run: | + echo 'backend: Agg' > matplotlibrc + ./venv/bin/python3 -m pytest -p no:warnings + + + gcc: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + gcc: + - 9 + - latest + + name: "Python 3 • GCC ${{ matrix.gcc }} • x64" + container: "gcc:${{ matrix.gcc }}" + + steps: + - uses: actions/checkout@v3 + + # Work-around for https://github.com/actions/runner-images/issues/6775 + - name: Change Owner of Container Working Directory + run: chown root:root . + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Prepare env + run: > + apt-get update && apt-get install -y python3-dev python3-pip python3-setuptools python3-wheel + python3-numpy python3-scipy python3-matplotlib python3-requests python3-networkx + python3-pytest python3-pytest-cov python3-flaky python3-venv + --no-install-recommends + + - name: Prepare Python env + run: | + python3 -m venv venv + ./venv/bin/python3 -m pip install -U pip setuptools wheel + ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + cat requirements.txt + ./venv/bin/python3 -m pip install -r requirements.txt --prefer-binary + + - name: Upgrade pybind11 and flaky + run: ./venv/bin/python3 -m pip install --upgrade pybind11 flaky --prefer-binary + + - name: Build and install package + run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,braket,test] + + - name: Pytest + run: | + echo 'backend: Agg' > matplotlibrc + ./venv/bin/python3 -m pytest -p no:warnings + + + # Testing on CentOS (manylinux uses a centos base, and this is an easy way + # to get GCC 4.8, which is the manylinux1 compiler). + centos: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + centos: + - 7 # GCC 4.8 + - 8 + include: + - centos: 7 + python_pkg: rh-python38-python-pip rh-python38-python-devel + enable_repo: --enablerepo=centos-sclo-rh + - centos: 8 + python_pkg: python38-devel + + + name: "Python 3 • CentOS ${{ matrix.centos }} • x64" + container: "centos:${{ matrix.centos }}" + + steps: + - name: Enable cache for yum + run: echo 'keepcache=1' >> /etc/yum.conf + + - name: Setup yum cache + uses: actions/cache@v3 + with: + path: | + /var/cache/yum/ + /var/cache/dnf/ + key: ${{ runner.os }}-centos${{ matrix.centos }}-yum-${{ secrets.yum_cache }} + + - name: Fix repository URLs (CentOS 8 only) + if: matrix.centos == 8 + run: | + sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + + - name: Update YUM/DNF + run: yum update -y + + - name: Enable extra repositories && modify PATH (CentOS 7) + if: matrix.centos == 7 + run: | + yum install -y centos-release-scl-rh + yum -y install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm + echo '/opt/rh/rh-python38/root/usr/bin' >> $GITHUB_PATH + + - name: Add Python 3 and other dependencies + run: yum install -y ${{ matrix.enable_repo }} ${{ matrix.python_pkg }} gcc-c++ make + + - name: Install Git > 2.18 + run: | + yum install -y git + git config --global --add safe.directory /__w/ProjectQ/ProjectQ + + # Work-around for https://github.com/actions/runner-images/issues/6775 + - name: Change Owner of Container Working Directory + run: chown root:root . + + - uses: actions/checkout@v3 + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Create pip cache dir + run: mkdir -p ~/.cache/pip + + - name: Cache wheels + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-centos${{ matrix.centos }}-pip-${{ hashFiles('**/setup.cfg', '**/pyproject.toml') }} + restore-keys: ${{ runner.os }}-centos-pip- + + - name: Install dependencies + run: | + python3 -m pip install -U pip setuptools wheel + python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + cat requirements.txt + python3 -m pip install -r requirements.txt --prefer-binary + + - name: Build and install package + run: python3 -m pip install -ve .[azure-quantum,braket,test] + + - name: Pytest + run: | + echo 'backend: Agg' > matplotlibrc + python3 -m pytest -p no:warnings + + + documentation: + name: "Documentation build test" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + architecture: 'x64' + cache: 'pip' + cache-dependency-path: | + setup.cfg + pyproject.toml + + - name: Install docs & setup requirements + run: | + python3 -m pip install .[docs] + + - name: Build docs + run: python3 -m sphinx -b html docs docs/.build + + # NB: disabling until setup.py is updated to remove any mention of distutils + # - name: Make SDist + # run: python3 setup.py sdist diff --git a/.github/workflows/draft_release.yml b/.github/workflows/draft_release.yml new file mode 100644 index 000000000..63092b6dc --- /dev/null +++ b/.github/workflows/draft_release.yml @@ -0,0 +1,77 @@ +--- + +name: "Draft new release" + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + tag: + description: 'Tag to prepare (format: vXX.YY.ZZ)' + required: true +jobs: + new-release: + name: "Draft a new release" + runs-on: ubuntu-latest + steps: + - name: Install git-flow + run: sudo apt update && sudo apt install -y git-flow + + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: '**/setup.cfg' + + - name: Configure git-flow + run: | + git fetch --tags --depth=1 origin master develop + git flow init --default --tag v + + - name: Create release branch + run: git flow release start ${{ github.event.inputs.tag }} + + - name: Initialize mandatory git config + run: | + git config user.name "GitHub actions" + git config user.email noreply@github.com + + - name: Update CHANGELOG + run: | + python3 -m pip install mdformat-gfm 'git+https://github.com/Takishima/keepachangelog@v1.0.1' + python3 -m keepachangelog release "${{ github.event.inputs.tag }}" + python3 -m mdformat CHANGELOG.md + + - name: Commit changelog and manifest files + id: make-commit + run: | + git add CHANGELOG.md + git commit --message "Preparing release ${{ github.event.inputs.tag }}" + + echo "::set-output name=commit::$(git rev-parse HEAD)" + + - name: Push new branch + run: git flow release publish ${{ github.event.inputs.tag }} + + # yamllint disable rule:line-length + - name: Create pull request + uses: thomaseizinger/create-pull-request@1.3.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + head: release/${{ github.event.inputs.tag }} + base: master + title: Release version ${{ github.event.inputs.tag }} + reviewers: ${{ github.actor }} + # Write a nice message to the user. + # We are claiming things here based on the `publish-new-release.yml` workflow. + # You should obviously adopt it to say the truth depending on your release workflow :) + body: | + Hi @${{ github.actor }}! + + This PR was created in response to a manual trigger of the release workflow here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}. + I've updated the changelog and bumped the versions in the manifest files in this commit: ${{ steps.make-commit.outputs.commit }}. + + Merging this PR will create a GitHub release and upload any assets that are created as part of the release build. diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..18a5a10c3 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,41 @@ +--- + +name: Format + +on: + workflow_dispatch: + merge_group: + pull_request: + push: + branches: + - master + - stable + - "v*" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + clang-tidy: + name: Clang-Tidy + runs-on: ubuntu-latest + container: silkeh/clang:14 + env: + CC: clang + CXX: clang++ + + steps: + - uses: actions/checkout@v3 + + - name: Prepare env + run: > + apt-get update && apt-get install -y python3-dev python3-pip python3-setuptools python3-wheel + --no-install-recommends + + - name: Upgrade pybind11 and setuptools + run: python3 -m pip install --upgrade pybind11 "setuptools>=61.0.0" --prefer-binary + + - name: Run clang-tidy + run: python3 setup.py clang_tidy --warning-as-errors diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 000000000..1695a745d --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,275 @@ +--- + +name: "Publish new release" + +on: + workflow_dispatch: + push: + tags: + - v[0-9]+.* + pull_request: + branches: + - master + types: + - closed + +jobs: + packaging: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + if: > + startsWith(github.ref, 'refs/tags/') + || (github.event_name == 'pull_request' + && github.event.pull_request.merged == true) + || github.event_name == 'workflow_dispatch' + + strategy: + matrix: + cibw_archs: ["auto64"] + os: [ubuntu-latest, windows-latest, macos-latest] + # include: + # - os: ubuntu-18.04 + # cibw_archs: "aarch64" + steps: + - name: Set up QEMU + if: matrix.cibw_archs == 'aarch64' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - uses: actions/checkout@v3 + if: github.event_name != 'workflow_dispatch' + + - uses: actions/checkout@v3 + if: github.event_name == 'workflow_dispatch' + with: + ref: 'master' + + - name: Get history and tags for SCM versioning to work + if: ${{ !env.ACT }} + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + # ======================================================================== + + - name: Extract version from branch name (for release branches) (Unix) + if: > + github.event_name == 'pull_request' + && startsWith(github.event.pull_request.head.ref, 'release/') + && runner.os != 'Windows' + run: | + TAG_NAME="${GITHUB_REF/refs\/tags\//}" + VERSION=${TAG_NAME#v} + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Extract version from branch name (for hotfix branches) (Unix) + if: > + github.event_name == 'pull_request' + && startsWith(github.event.pull_request.head.ref, 'hotfix/') + && runner.os != 'Windows' + run: | + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + VERSION=${BRANCH_NAME#release/v} + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + # ------------------------------------------------------------------------ + + - name: Extract version from branch name (for release branches) (Windows) + if: > + github.event_name == 'pull_request' + && startsWith(github.event.pull_request.head.ref, 'release/') + && runner.os == 'Windows' + run: | + $BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + $VERSION = $BRANCH_NAME -replace "release/v","" + Write-Output "VERSION = ${VERSION}" >> $Env:GITHUB_ENV + + - name: Extract version from branch name (for hotfix branches) (Windows) + if: > + github.event_name == 'pull_request' + && startsWith(github.event.pull_request.head.ref, 'hotfix/') + && runner.os == 'Windows' + run: | + $BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + $VERSION = $BRANCH_NAME -replace "hotfix/","" + Write-Output "VERSION = ${VERSION}" >> $Env:GITHUB_ENV + + # ======================================================================== + + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install Python packages + run: python -m pip install -U --prefer-binary pip setuptools build wheel twine 'cibuildwheel<3,>=2' + + - name: Build source distribution (Linux) + if: runner.os == 'Linux' + id: src-dist + run: | + python -m build --sdist + python -m twine check dist/* + + - name: Build binary wheels + continue-on-error: true + id: binary-dist + run: | + python -m cibuildwheel --output-dir binary_dist + python -m twine check binary_dist/* + env: + CIBW_ARCHS: ${{ matrix.cibw_archs }} + + - name: Build binary wheels without (failing) testing + if: steps.binary-dist.outcome == 'failure' + id: failed-dist + run: | + python -m cibuildwheel --output-dir failed_dist + env: + CIBW_ARCHS: ${{ matrix.cibw_archs }} + CIBW_TEST_SKIP: '*' + + - name: Files for Pypi upload + uses: actions/upload-artifact@v3 + if: steps.src-dist.outcome == 'success' + with: + name: pypy_wheels + path: ./dist + + - name: Binary wheels + uses: actions/upload-artifact@v3 + if: steps.binary-dist.outcome == 'success' + with: + name: wheels + path: ./binary_dist + + - name: Binary wheels that failed tests + uses: actions/upload-artifact@v3 + if: steps.failed-dist.outcome == 'success' + with: + name: failed_wheels + path: ./failed_dist + + + release: + name: Publish new release + runs-on: ubuntu-latest + needs: + - packaging + steps: + - name: Extract version from tag name (workflow_dispatch) + if: github.event_name == 'workflow_dispatch' + run: | + TAG_NAME=$(git describe --tags `git rev-list --tags --max-count=1`) + VERSION=${TAG_NAME#v} + + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Extract version from tag name + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + run: | + TAG_NAME="${GITHUB_REF/refs\/tags\//}" + VERSION=${TAG_NAME#v} + + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Extract version from branch name (for release branches) + if: github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'release/') + run: | + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + VERSION=${BRANCH_NAME#release/v} + + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Extract version from branch name (for hotfix branches) + if: github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'hotfix/') + run: | + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + VERSION=${BRANCH_NAME#hotfix/v} + + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + # ------------------------------------------------------------------------ + # Checkout repository to get CHANGELOG + + - uses: actions/checkout@v3 + if: github.event_name != 'workflow_dispatch' + + - uses: actions/checkout@v3 + if: github.event_name == 'workflow_dispatch' + with: + ref: 'master' + + # ------------------------------------------------------------------------ + + - uses: actions/download-artifact@v3 + + # Code below inspired from this action: + # - uses: taiki-e/create-gh-release-action@v1 + # with: + # title: ProjectQ $tag + # changelog: CHANGELOG.md + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create release + env: + target: x86_64-unknown-linux-musl + source_url: https://github.com/taiki-e/parse-changelog/releases/download + parse_changelog_tag: v0.5.1 + changelog: CHANGELOG.md + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # https://github.com/taiki-e/parse-changelog + curl -LsSf "${source_url}/${parse_changelog_tag}/parse-changelog-${target}.tar.gz" | tar xzf - + notes=$(./parse-changelog "${changelog}" "${RELEASE_VERSION}") + rm -f ./parse-changelog + + if [[ "${tag}" =~ ^v?[0-9\.]+-[a-zA-Z_0-9\.-]+(\+[a-zA-Z_0-9\.-]+)?$ ]]; then + prerelease="--prerelease" + fi + + mkdir -p wheels pypy_wheels + gh release create "v${RELEASE_VERSION}" ${prerelease:-} \ + --title "ProjectQ v${RELEASE_VERSION}" \ + --notes "${notes:-}" \ + pypy_wheels/* wheels/* + + + upload_to_pypi: + name: Upload to PyPI + runs-on: ubuntu-latest + needs: release + steps: + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - uses: actions/download-artifact@v3 + + - name: Publish standard package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.pypi_password }} + packages_dir: pypy_wheels/ + + master_to_develop_pr: + name: Merge master back into develop + runs-on: ubuntu-latest + needs: + - release + - upload_to_pypi + steps: + - name: Merge master into develop branch + uses: thomaseizinger/create-pull-request@1.3.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + head: master + base: develop + title: Merge master into develop branch + # yamllint disable rule:line-length + body: | + This PR merges the master branch back into develop. + This happens to ensure that the updates that happend on the release branch, i.e. CHANGELOG and manifest updates are also present on the develop branch. diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 000000000..b7a90cdde --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,21 @@ +--- + +name: PR +on: + merge_group: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + # Enforces the update of a changelog file on every pull request + changelog: + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/master' + steps: + - uses: actions/checkout@v3 + + - id: changelog-enforcer + uses: dangoslen/changelog-enforcer@v3 + with: + changeLogPath: 'CHANGELOG.md' + skipLabels: 'Skip-Changelog' diff --git a/.gitignore b/.gitignore index c3957d2e0..680678f56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,187 @@ -# python artifacts -*.pyc +# Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/_doc_gen/ +docs/doxygen + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# ============================================================================== +# Prerequisites +*.d + +# C++ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Others +err.txt + +# ============================================================================== + +VERSION.txt + +# Windows artifacts +thumbs.db + +# Mac OSX artifacts +*.DS_Store + +# ============================================================================== diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..cf59ca9c0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,126 @@ +# To use: +# +# pre-commit run -a +# +# Or: +# +# pre-commit install # (runs every time you commit in git) +# +# To update this file: +# +# pre-commit autoupdate +# +# See https://github.com/pre-commit/pre-commit + +--- + +ci: + skip: [check-manifest] + +repos: + - repo: meta + hooks: + - id: check-useless-excludes + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + # Changes tabs to spaces + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: remove-tabs + + - repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + exclude: (_test.*\.py)$ + additional_dependencies: [toml] + + - repo: https://github.com/PyCQA/doc8/ + rev: v1.1.1 + hooks: + - id: doc8 + require_serial: false + additional_dependencies: [tomli] + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + require_serial: false + files: .*\.(py|txt|cmake|md|rst|sh|ps1|hpp|tpp|cpp|cc)$ + args: [-S, '.git,third_party', -I, .codespell.allow] + + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint + require_serial: false + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: [--py37-plus, --keep-mock] + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/asottile/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + args: [-S, -l, '120'] + additional_dependencies: [black==22.10.0] + + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + name: flake8-strict + exclude: ^(.*_test\.py)$ + additional_dependencies: [flake8-comprehensions, flake8-breakpoint, + flake8-eradicate, flake8-mutable, + flake8-docstrings] + - id: flake8 + name: flake8-test-files + additional_dependencies: [flake8-comprehensions, flake8-breakpoint, + flake8-eradicate, flake8-mutable] + files: ^(.*_test\.py)$ + + - repo: https://github.com/pre-commit/mirrors-pylint + rev: 'v3.0.0a5' + hooks: + - id: pylint + args: ['--score=n', '--disable=no-member'] + additional_dependencies: [pybind11>=2.6, numpy, requests, matplotlib, networkx] + + - repo: https://github.com/mgedmin/check-manifest + rev: '0.49' + hooks: + - id: check-manifest + additional_dependencies: ['setuptools-scm', 'pybind11>=2.6'] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..e43492a1b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,21 @@ +--- + +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +sphinx: + configuration: docs/conf.py + +formats: all + +python: + version: 3.8 + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index 89700c84f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -sudo: false -language: python -matrix: - include: - - os: linux - python: "2.7" - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['gcc-4.9', 'g++-4.9', 'gcc-7', 'g++-7'] - env: CC=gcc-4.9 CXX=g++-4.9 PYTHON=2.7 - - os: linux - python: "3.4" - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['gcc-4.9', 'g++-4.9', 'gcc-7', 'g++-7'] - env: CC=gcc-4.9 CXX=g++-4.9 PYTHON=3.4 - - os: linux - python: "3.5" - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['gcc-4.9', 'g++-4.9', 'gcc-7', 'g++-7'] - env: CC=gcc-4.9 CXX=g++-4.9 PYTHON=3.5 - - os: linux - python: "3.6" - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['gcc-4.9', 'g++-4.9', 'gcc-7', 'g++-7'] - env: CC=gcc-4.9 CXX=g++-4.9 PYTHON=3.6 - -install: - - if [ "${PYTHON:0:1}" = "3" ]; then export PY=3; fi - - pip$PY install --upgrade pip setuptools wheel - - pip$PY install --only-binary=numpy,scipy numpy scipy - - pip$PY install -r requirements.txt - - pip$PY install pytest-cov - - pip$PY install coveralls - - CC=g++-7 pip$PY install revkit - - if [ "${PYTHON:0:1}" = "3" ]; then pip$PY install dormouse; fi - - pip$PY install -e . - -# command to run tests -script: export OMP_NUM_THREADS=1 && pytest -W error projectq --cov projectq - -after_success: - - coveralls diff --git a/.yamllint b/.yamllint new file mode 100644 index 000000000..9770e81ca --- /dev/null +++ b/.yamllint @@ -0,0 +1,8 @@ +--- + +extends: default + +rules: + line-length: + max: 120 + level: error diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e58e12027 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,256 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Python context manager `with flushing(MainEngine()) as eng:` + +### Fixed + +- Fixed some typos (thanks to @eltociear, @Darkdragon84) +- Fixed support for Python 3.12 + +### Removed + +- Support for Python 3.7 + +### Repository + +- Update GitHub workflows to work with merge queues +- Update GitHub workflow action versions: `actions/cache@v3`, `actions/checkout@v3`, `actions/setup-python@v4`, `thomaseizinger/create-pull-request` +- Update pre-commit hook versions +- Fix failing GitHub workflows (Clang-10) +- Introduce pre-commit CI +- Update to clang-tidy 14 in GitHub workflow +- Added new pre-commit hooks: `codespell`, `doc8`, `pydocstyle` and `yamllint` +- Keep `flake8` hook to version 5.0.4 until plugins support Flake8 6.X +- Disable `no-member` warning for Pylint on pre-commit CI + + +## [v0.8.0] - 2022-10-18 + +### Added + +- New backend for the Azure Quantum platform + +### Changed + +- Support for Python 3.6 and earlier is now deprecated +- Moved package metadata into pyproject.toml + +### Fixed + +- Fixed installation on Apple Silicon with older Python versions (< 3.9) + +### Repository + +- Update `docker/setup-qemu-action` GitHub action to v2 +- Fixed CentOS 7 configuration issue +- Added two new pre-commit hooks: `blacken-docs` and `pyupgrade` + +## [v0.7.3] - 2022-04-27 + +### Fixed + +- Fixed IonQ dynamic backends fetch, which relied on an incorrect path. + +## [v0.7.2] - 2022-04-11 + +### Changed + +- Added IonQ dynamic backends fetch. + +### Repository + +- Fix issues with building on CentOS 7 & 8 +- Update `pre-commit/pre-commit-hooks` to v4.2.0 +- Update `Lucas-C/pre-commit-hooks` hook to v1.1.13 +- Update `flake8` hook to v4.0.1 +- Update `pylint` hook to v3.0.0a4 +- Update `black` hook to v22.3.0 +- Update `check-manifest` to v0.48 + +## [0.7.1] - 2022-01-10 + +### Added + +- Added environment variable to avoid -march=native when building ProjectQ +- Added environment variable to force build failure if extensions do not compile on CI + +### Changed + +### Deprecated + +### Fixed + +- Fix compiler flags cleanup function for use on CI +- Fix workflow YAML to allow execution of GitHub Actions locally using `act` +- GitHub action using deprecated and vulnerable `pre-commit` version +- Fixed issue with `gen_reqfile` command if `--include-extras` is not provided + +### Removed + +### Repository + +- Add configuration for CIBuildWheel in `pyproject.toml` +- Remove use of deprecated images `windows-2016` in GitHub workflows +- Re-add build of Python binary wheels in release publishing GitHub workflow +- Update `dangoslen/changelog-enforcer` GitHub action to v3 +- Update `thomaseizinger/keep-a-changelog-new-release` GiHub action to v1.3.0 +- Update `thomaseizinger/create-pull-request` GiHub action to v1.2.2 +- Update pre-commit hook `pre-commit/pre-commit-hooks` to v4.1.0 +- Update pre-commit hook `PyCQA/isort` to v5.10.1 +- Update pre-commit hook `psf/black` to v21.12b0 +- Update pre-commit hook `PyCQA/flake8` to v4.0.1 +- Update pre-commit hook `mgedmin/check-manifest` to v0.47 + +## [0.7.0] - 2021-07-14 + +### Added + +- UnitarySimulator backend for computing the unitary transformation corresponding to a quantum circuit + +### Changed + +- Moved some exceptions classes into their own files to avoid code duplication + +### Deprecated + +### Fixed + +- Prevent infinite recursion errors when too many compiler engines are added to the MainEngine +- Error in testing the decomposition for the phase estimation gate +- Fixed small issue with matplotlib drawing backend +- Make all docstrings PEP257 compliant + +### Removed + +- Some compatibility code for Python 2.x + +### Repository + +- Added `isort` to the list of pre-commit hooks +- Added some more flake8 plugins to the list used by `pre-commit`: + - flake8-breakpoint + - flake8-comprehensions + - flake8-docstrings + - flake8-eradicate + - flake8-mutable + +## [0.6.1] - 2021-06-23 + +### Repository + +- Fix GitHub workflow for publishing a new release + +## [0.6.0] - 2021-06-23 + +### Added + +- New backend for the IonQ platform +- New backend for the AWS Braket platform +- New gates for quantum math operations on quantum registers +- Support for state-dependent control qubits (ie. negatively or positively controlled gates) + +### Changed + +- Name of the single parameter of the `LocalOptimizer` has been changed from `m` to `cache_size` in order to better represent its actual use. + +### Deprecated + +- Compatibility with Python <= 3.5 +- `LocalOptimizer(m=10)` should be changed into `LocalOptimizer(cache_size=10)`. Using of the old name is still possible, but is deprecated and will be removed in a future version of ProjectQ. + +### Fixed + +- Installation on Mac OS Big Sur +- IBM Backend issues with new API + +### Removed + +- Compatibility with Python 2.7 +- Support for multi-qubit measurement gates has been dropped; use `All(Measure) | qureg` instead + +### Repository + +- Use `setuptools-scm` for versioning + +- Added `.editorconfig` file + +- Added `pyproject.toml` and `setup.cfg` + +- Added CHANGELOG.md + +- Added support for GitHub Actions + - Build and testing on various platforms and compilers + - Automatic draft of new release + - Automatic publication of new release once ready + - Automatic upload of releases artifacts to PyPi and GitHub + +- Added pre-commit configuration file + +- Updated cibuildwheels action to v1.11.1 + +- Updated thomaseizinger/create-pull-request action to v1.1.0 + +## [0.5.1] - 2019-02-15 + +### Added + +- Add histogram plot function for measurement results (thanks @AriJordan ) +- New backend for AQT (thanks @dbretaud ) + +### Fixed + +- Fix Qiskit backend (thanks @dbretaud ) +- Fix bug with matplotlib drawer (thanks @AriJordan ) + +## [0.5.0] - 2020 + +### Added + +- New [PhaseEstimation](https://projectq.readthedocs.io/en/latest/projectq.ops.html#projectq.ops.QPE) and [AmplitudeAmplification](https://projectq.readthedocs.io/en/latest/projectq.ops.html#projectq.ops.QAA) gates (thanks @fernandodelaiglesia) +- New [Rxx](https://projectq.readthedocs.io/en/latest/projectq.ops.html#projectq.ops.Rxx), [Ryy](https://projectq.readthedocs.io/en/latest/projectq.ops.html#projectq.ops.Ryy) and [Rzz](https://projectq.readthedocs.io/en/latest/projectq.ops.html#projectq.ops.Rzz) gates (thanks @dwierichs) +- New decomposition rules and compiler setup for trapped ion quantum based computers (thanks @dbretaud) +- Added basic circuit drawer compiler engine based on Matplotlib [CircuitDrawerMatplotlib](https://projectq.readthedocs.io/en/latest/projectq.backends.html#projectq.backends.CircuitDrawerMatplotlib) (thanks @Bombenchris) + +### Changed + +- Significantly improved C++ simulator performances (thanks @melven) +- Allow user modification of the qubit drawing order for the `CircuitDrawer` compiler engine (thanks @alexandrupaler) +- Update to the installation script. The installation now automatically defaults to the pure Python implementation if compilation of the C++ simulator (or other C++ modules) should fail (#337) +- Automatic generation of the documentation (#339) + +### Fixes + +- Fixed connection issues between IBM backend and the IBM Quantum Experience API (thanks @alexandrupaler) + +### Deprecated + +The ProjectQ v0.5.x release branch is the last one that is guaranteed to work with Python 2.7.x. + +Future releases might introduce changes that will require Python 3.5 (Python 3.4 and earlier have already been declared deprecated at the time of this writing) + +[Unreleased]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.8.0...HEAD + +[v0.8.0]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.7.3...v0.8.0 + +[v0.7.3]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.7.2...v0.7.3 + +[v0.7.2]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.7.1...v0.7.2 + +[0.7.1]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.7.0...v0.7.1 + +[0.7.0]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.6.1...v0.7.0 + +[0.6.0]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.5.1...v0.6.0 + +[0.5.1]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.5.0...v0.5.1 + +[0.5.0]: https://github.com/ProjectQ-Framework/ProjectQ/compare/v0.4.2...v0.5.0 diff --git a/MANIFEST.in b/MANIFEST.in index b087d38fd..30dbe21d7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,10 @@ include LICENSE +include CHANGELOG.md include MANIFEST.in include NOTICE -include pytest.ini include README.rst -include requirements.txt include setup.py +include setup.cfg +include pyproject.toml recursive-include projectq *.py *.hpp *.cpp diff --git a/README.rst b/README.rst index 0a31fb186..3397c4070 100755 --- a/README.rst +++ b/README.rst @@ -1,20 +1,23 @@ ProjectQ - An open source software framework for quantum computing ================================================================== -.. image:: https://travis-ci.org/ProjectQ-Framework/ProjectQ.svg?branch=master - :target: https://travis-ci.org/ProjectQ-Framework/ProjectQ +.. image:: https://img.shields.io/pypi/pyversions/projectq?label=Python + :alt: PyPI - Python Version + +.. image:: https://badge.fury.io/py/projectq.svg + :target: https://badge.fury.io/py/projectq + +.. image:: https://github.com/ProjectQ-Framework/ProjectQ/actions/workflows/ci.yml/badge.svg + :alt: CI Status + :target: https://github.com/ProjectQ-Framework/ProjectQ/actions/workflows/ci.yml .. image:: https://coveralls.io/repos/github/ProjectQ-Framework/ProjectQ/badge.svg - :target: https://coveralls.io/github/ProjectQ-Framework/ProjectQ + :alt: Coverage Status + :target: https://coveralls.io/github/ProjectQ-Framework/ProjectQ .. image:: https://readthedocs.org/projects/projectq/badge/?version=latest - :target: http://projectq.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -.. image:: https://badge.fury.io/py/projectq.svg - :target: https://badge.fury.io/py/projectq - -.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-brightgreen.svg + :target: http://projectq.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status ProjectQ is an open source effort for quantum computing. @@ -24,7 +27,8 @@ targeting various types of hardware, a high-performance quantum computer simulator with emulation capabilities, and various compiler plug-ins. This allows users to -- run quantum programs on the IBM Quantum Experience chip +- run quantum programs on the IBM Quantum Experience chip, AQT devices, AWS Braket, Azure Quantum, or IonQ service + provided devices - simulate quantum programs on classical computers - emulate quantum programs at a higher level of abstraction (e.g., mimicking the action of large oracles instead of compiling them to @@ -32,44 +36,303 @@ This allows users to - export quantum programs as circuits (using TikZ) - get resource estimates +Examples +-------- + +**First quantum program** + +.. code-block:: python + + from projectq import MainEngine # import the main compiler engine + from projectq.ops import ( + H, + Measure, + ) # import the operations we want to perform (Hadamard and measurement) + + eng = MainEngine() # create a default compiler (the back-end is a simulator) + qubit = eng.allocate_qubit() # allocate a quantum register with 1 qubit + + H | qubit # apply a Hadamard gate + Measure | qubit # measure the qubit + + eng.flush() # flush all gates (and execute measurements) + print(f"Measured {int(qubit)}") # converting a qubit to int or bool gives access to the measurement result + + +ProjectQ features a lean syntax which is close to the mathematical notation used in quantum physics. For example, a +rotation of a qubit around the x-axis is usually specified as: + +.. image:: docs/images/braket_notation.svg + :alt: Rx(theta)|qubit> + :width: 100px + +The same statement in ProjectQ's syntax is: + +.. code-block:: python + + Rx(theta) | qubit + +The **|**-operator separates the specification of the gate operation (left-hand side) from the quantum bits to which the +operation is applied (right-hand side). + +**Changing the compiler and using a resource counter as a back-end** + +Instead of simulating a quantum program, one can use our resource counter (as a back-end) to determine how many +operations it would take on a future quantum computer with a given architecture. Suppose the qubits are arranged on a +linear chain and the architecture supports any single-qubit gate as well as the two-qubit CNOT and Swap operations: + +.. code-block:: python + + from projectq import MainEngine + from projectq.backends import ResourceCounter + from projectq.ops import QFT, CNOT, Swap + from projectq.setups import linear + + compiler_engines = linear.get_engine_list(num_qubits=16, one_qubit_gates='any', two_qubit_gates=(CNOT, Swap)) + resource_counter = ResourceCounter() + eng = MainEngine(backend=resource_counter, engine_list=compiler_engines) + qureg = eng.allocate_qureg(16) + QFT | qureg + eng.flush() + + print(resource_counter) + + # This will output, among other information, + # how many operations are needed to perform + # this quantum fourier transform (QFT), i.e., + # Gate class counts: + # AllocateQubitGate : 16 + # CXGate : 240 + # HGate : 16 + # R : 120 + # Rz : 240 + # SwapGate : 262 + + +**Running a quantum program on IBM's QE chips** + +To run a program on the IBM Quantum Experience chips, all one has to do is choose the `IBMBackend` and the corresponding +setup: + +.. code-block:: python + + import projectq.setups.ibm + from projectq.backends import IBMBackend + + token = 'MY_TOKEN' + device = 'ibmq_16_melbourne' + compiler_engines = projectq.setups.ibm.get_engine_list(token=token, device=device) + eng = MainEngine( + IBMBackend(token=token, use_hardware=True, num_runs=1024, verbose=False, device=device), + engine_list=compiler_engines, + ) + + +**Running a quantum program on AQT devices** + +To run a program on the AQT trapped ion quantum computer, choose the `AQTBackend` and the corresponding setup: + +.. code-block:: python + + import projectq.setups.aqt + from projectq.backends import AQTBackend + + token = 'MY_TOKEN' + device = 'aqt_device' + compiler_engines = projectq.setups.aqt.get_engine_list(token=token, device=device) + eng = MainEngine( + AQTBackend(token=token, use_hardware=True, num_runs=1024, verbose=False, device=device), + engine_list=compiler_engines, + ) + + +**Running a quantum program on a AWS Braket provided device** + +To run a program on some of the devices provided by the AWS Braket service, choose the `AWSBraketBackend`. The currend +devices supported are Aspen-8 from Rigetti, IonQ from IonQ and the state vector simulator SV1: + +.. code-block:: python + + from projectq.backends import AWSBraketBackend + + creds = { + 'AWS_ACCESS_KEY_ID': 'your_aws_access_key_id', + 'AWS_SECRET_KEY': 'your_aws_secret_key', + } + + s3_folder = ['S3Bucket', 'S3Directory'] + device = 'IonQ' + eng = MainEngine( + AWSBraketBackend( + use_hardware=True, + credentials=creds, + s3_folder=s3_folder, + num_runs=1024, + verbose=False, + device=device, + ), + engine_list=[], + ) + + +.. note:: + + In order to use the AWSBraketBackend, you need to install ProjectQ with the 'braket' extra requirement: + + .. code-block:: bash + + python3 -m pip install projectq[braket] + + or + + .. code-block:: bash + + cd /path/to/projectq/source/code + python3 -m pip install -ve .[braket] + + +**Running a quantum program on a Azure Quantum provided device** + +To run a program on devices provided by the `Azure Quantum `_. + +Use `AzureQuantumBackend` to run ProjectQ circuits on hardware devices and simulator devices from providers `IonQ` and +`Quantinuum`. + +.. code-block:: python + + from projectq.backends import AzureQuantumBackend + + azure_quantum_backend = AzureQuantumBackend( + use_hardware=False, target_name='ionq.simulator', resource_id='', location='', verbose=True + ) + +.. note:: + + In order to use the AzureQuantumBackend, you need to install ProjectQ with the 'azure-quantum' extra requirement: + + .. code-block:: bash + + python3 -m pip install projectq[azure-quantum] + + or + + .. code-block:: bash + + cd /path/to/projectq/source/code + python3 -m pip install -ve .[azure-quantum] + +**Running a quantum program on IonQ devices** + +To run a program on the IonQ trapped ion hardware, use the `IonQBackend` and its corresponding setup. + +Currently available devices are: + +* `ionq_simulator`: A 29-qubit simulator. +* `ionq_qpu`: A 11-qubit trapped ion system. + +.. code-block:: python + + import projectq.setups.ionq + from projectq import MainEngine + from projectq.backends import IonQBackend + + token = 'MY_TOKEN' + device = 'ionq_qpu' + backend = IonQBackend( + token=token, + use_hardware=True, + num_runs=1024, + verbose=False, + device=device, + ) + compiler_engines = projectq.setups.ionq.get_engine_list( + token=token, + device=device, + ) + eng = MainEngine(backend, engine_list=compiler_engines) + + +**Classically simulate a quantum program** + +ProjectQ has a high-performance simulator which allows simulating up to about 30 qubits on a regular laptop. See the +`simulator tutorial +`__ for +more information. Using the emulation features of our simulator (fast classical shortcuts), one can easily emulate +Shor's algorithm for problem sizes for which a quantum computer would require above 50 qubits, see our `example codes +`__. + + +The advanced features of the simulator are also particularly useful to investigate algorithms for the simulation of +quantum systems. For example, the simulator can evolve a quantum system in time (without Trotter errors) and it gives +direct access to expectation values of Hamiltonians leading to extremely fast simulations of VQE type algorithms: + +.. code-block:: python + + from projectq import MainEngine + from projectq.ops import All, Measure, QubitOperator, TimeEvolution + + eng = MainEngine() + wavefunction = eng.allocate_qureg(2) + # Specify a Hamiltonian in terms of Pauli operators: + hamiltonian = QubitOperator("X0 X1") + 0.5 * QubitOperator("Y0 Y1") + # Apply exp(-i * Hamiltonian * time) (without Trotter error) + TimeEvolution(time=1, hamiltonian=hamiltonian) | wavefunction + # Measure the expectation value using the simulator shortcut: + eng.flush() + value = eng.backend.get_expectation_value(hamiltonian, wavefunction) + + # Last operation in any program should be measuring all qubits + All(Measure) | qureg + eng.flush() + + + Getting started --------------- -To start using ProjectQ, simply follow the installation instructions in the `tutorials `__. There, you will also find OS-specific hints, a small introduction to the ProjectQ syntax, and a few `code examples `__. More example codes and tutorials can be found in the examples folder `here `__ on GitHub. +To start using ProjectQ, simply follow the installation instructions in the `tutorials +`__. There, you will also find OS-specific hints, a small +introduction to the ProjectQ syntax, and a few `code examples +`__. More example codes and tutorials can be found in the +examples folder `here `__ on GitHub. -Also, make sure to check out the `ProjectQ -website `__ and the detailed `code documentation `__. +Also, make sure to check out the `ProjectQ website `__ and the detailed `code documentation +`__. How to contribute ----------------- -For information on how to contribute, please visit the `ProjectQ -website `__ or send an e-mail to -info@projectq.ch. +For information on how to contribute, please visit the `ProjectQ website `__ or send an e-mail +to info@projectq.ch. Please cite ----------- When using ProjectQ for research projects, please cite -- Damian S. Steiger, Thomas Häner, and Matthias Troyer "ProjectQ: An +- Damian S. Steiger, Thomas Haener, and Matthias Troyer "ProjectQ: An Open Source Software Framework for Quantum Computing" - `[arxiv:1612.08091] `__ -- Thomas Häner, Damian S. Steiger, Krysta M. Svore, and Matthias Troyer - "A Software Methodology for Compiling Quantum Programs" - `[arxiv:1604.01401] `__ + `Quantum 2, 49 (2018) `__ + (published on `arXiv `__ on 23 Dec 2016) +- Thomas Haener, Damian S. Steiger, Krysta M. Svore, and Matthias Troyer + "A Software Methodology for Compiling Quantum Programs" `Quantum Sci. Technol. 3 (2018) 020501 + `__ (published on `arXiv `__ on 5 + Apr 2016) Authors ------- The first release of ProjectQ (v0.1) was developed by `Thomas -Häner `__ +Haener `__ and `Damian S. Steiger `__ in the group of `Prof. Dr. Matthias Troyer `__ at ETH Zurich. +ProjectQ is constantly growing and `many other people +`__ have already contributed to it in the meantime. + License ------- diff --git a/docs/README.rst b/docs/README.rst index 8edb90a73..2521e47ff 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -1,4 +1,4 @@ -Documentation +Documentation ============= .. image:: https://readthedocs.org/projects/projectq/badge/?version=latest @@ -6,12 +6,15 @@ Documentation :alt: Documentation Status -Our detailed code documentation can be found online at `Read the Docs `__ and gets updated automatically. Besides the latest code documentation, there are also previous and offline versions available for download. +Our detailed code documentation can be found online at `Read the Docs `__ and +gets updated automatically. Besides the latest code documentation, there are also previous and offline versions +available for download. Building the docs locally ------------------------- -Before submitting new code, please make sure that the new or changed docstrings render nicely by building the docs manually. To this end, one has to install sphinx and the Read the Docs theme: +Before submitting new code, please make sure that the new or changed docstrings render nicely by building the docs +manually. To this end, one has to install sphinx and the Read the Docs theme: .. code-block:: bash diff --git a/docs/conf.py b/docs/conf.py index 971cefd8d..382a41366 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # projectq documentation build configuration file, created by # sphinx-quickstart on Tue Nov 29 11:51:46 2016. @@ -16,10 +15,20 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# + +"""Configuration file for generating the documentation for ProjectQ.""" + +# pylint: disable=invalid-name + +import functools +import importlib +import inspect import os import sys -sys.path.insert(0, os.path.abspath('..')) +from importlib.metadata import version + +sys.path.insert(0, os.path.abspath('..')) # for projectq +sys.path.append(os.path.abspath('.')) # for package_description # -- General configuration ------------------------------------------------ @@ -27,14 +36,13 @@ # # needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -import sphinx_rtd_theme extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.mathjax', - 'sphinx.ext.autosummary', 'sphinx.ext.linkcode', + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.mathjax', + 'sphinx.ext.autosummary', + 'sphinx.ext.linkcode', ] autosummary_generate = True @@ -57,7 +65,7 @@ # General information about the project. project = 'ProjectQ' -copyright = '2017, ProjectQ' +copyright = '2017-2021, ProjectQ' # pylint: disable=redefined-builtin author = 'ProjectQ' # The version info for the project you're documenting, acts as replacement for @@ -66,12 +74,8 @@ # # The short X.Y version. -# This reads the __version__ variable from projectq/_version.py -exec(open('../projectq/_version.py').read()) - -version = __version__ -# The full version, including alpha/beta/rc tags. -release = __version__ +release = version('projectq') # Full version string +version = '.'.join(release.split('.')[:3]) # X.Y.Z # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -125,7 +129,6 @@ # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -253,15 +256,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -271,8 +271,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'projectq.tex', 'projectq Documentation', - 'a', 'manual'), + (master_doc, 'projectq.tex', 'projectq Documentation', 'a', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -307,30 +306,31 @@ # # latex_domain_indices = True - # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'projectq', 'projectq Documentation', - [author], 1) -] +man_pages = [(master_doc, 'projectq', 'projectq Documentation', [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False - # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'projectq', 'projectq Documentation', - author, 'projectq', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'projectq', + 'projectq Documentation', + author, + 'projectq', + 'One line description of project.', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. @@ -350,14 +350,23 @@ # texinfo_no_detailmenu = False # -- Options for sphinx.ext.linkcode -------------------------------------- -import inspect -import projectq + + +def recursive_getattr(obj, attr, *args): + """Recursively get the attributes of a Python object.""" + + def _getattr(obj, attr): + return getattr(obj, attr, *args) + + return functools.reduce(_getattr, [obj] + attr.split('.')) def linkcode_resolve(domain, info): + """Change URLs in documentation on the fly.""" # Copyright 2018 ProjectQ (www.projectq.ch), all rights reserved. on_rtd = os.environ.get('READTHEDOCS') == 'True' github_url = "https://github.com/ProjectQ-Framework/ProjectQ/tree/" + github_tag = 'v' + version if on_rtd: rtd_tag = os.environ.get('READTHEDOCS_VERSION') if rtd_tag == 'latest': @@ -375,40 +384,190 @@ def linkcode_resolve(domain, info): github_tag = ''.join(github_tag) else: github_tag = rtd_tag - else: - github_tag = 'v' + __version__ + if domain != 'py': return None - else: - try: - obj = eval(info['module'] + '.' + info['fullname']) - except AttributeError: - # Object might be a non-static attribute of a class, e.g., - # self.num_qubits, which would only exist after init was called. - # For the moment we don't need a link for that as there is a link - # for the class already - return None + try: + module = importlib.import_module(info['module']) + obj = recursive_getattr(module, info['fullname']) + except (AttributeError, ValueError): + # AttributeError: + # Object might be a non-static attribute of a class, e.g., self.num_qubits, which would only exist after init + # was called. + # For the moment we don't need a link for that as there is a link for the class already + # + # ValueError: + # info['module'] is empty + return None + try: + filepath = inspect.getsourcefile(obj) + line_number = inspect.getsourcelines(obj)[1] + except TypeError: + # obj might be a property or a static class variable, e.g., + # loop_tag_id in which case obj is an int and inspect will fail try: + # load obj one hierarchy higher (either class or module) + new_higher_name = info['fullname'].split('.') + module = importlib.import_module(info['module']) + if len(new_higher_name) > 1: + obj = module + else: + obj = recursive_getattr(module, '.' + '.'.join(new_higher_name[:-1])) + filepath = inspect.getsourcefile(obj) line_number = inspect.getsourcelines(obj)[1] - except: - # obj might be a property or a static class variable, e.g., - # loop_tag_id in which case obj is an int and inspect will fail - try: - # load obj one hierarchy higher (either class or module) - new_higher_name = info['fullname'].split('.') - if len(new_higher_name) <= 1: - obj = eval(info['module']) - else: - obj = eval(info['module'] + '.' + - '.'.join(new_higher_name[:-1])) - filepath = inspect.getsourcefile(obj) - line_number = inspect.getsourcelines(obj)[1] - except: - return None - # Only require relative path projectq/relative_path - projectq_path = inspect.getsourcefile(projectq)[:-11] - relative_path = os.path.relpath(filepath, projectq_path) - url = (github_url + github_tag + "/projectq/" + relative_path + "#L" + - str(line_number)) - return url + except AttributeError: + return None + + # Calculate the relative path of the object with respect to the root directory (ie. projectq/some/path/to/a/file.py) + projectq_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) + os.path.sep + idx = len(projectq_path) + relative_path = filepath[idx:] + + url = github_url + github_tag + "/" + relative_path + "#L" + str(line_number) + return url + + +# ------------------------------------------------------------------------------ + +desc = importlib.import_module('package_description') +PackageDescription = desc.PackageDescription + +# ------------------------------------------------------------------------------ +# Define the description of ProjectQ packages and their submodules below. +# +# In order for the automatic package recognition to work properly, it is +# important that PackageDescription of sub-packages appear earlier in the list +# than their parent package (see for example libs.math and libs.revkit +# compared to libs). +# +# It is also possible to customize the presentation of submodules (see for +# example the setups and setups.decompositions) or even to have private +# sub-modules listed in the documentation page of a parent packages (see for +# example the cengines package) + +descriptions = [ + PackageDescription('backends'), + PackageDescription( + 'cengines', + desc=''' +The ProjectQ compiler engines package. +''', + ), + PackageDescription( + 'libs.math', + desc=''' +A tiny math library which will be extended throughout the next weeks. Right now, it only contains the math functions +necessary to run Beauregard's implementation of Shor's algorithm. +''', + ), + PackageDescription( + 'libs.revkit', + desc=''' +This library integrates `RevKit `_ into +ProjectQ to allow some automatic synthesis routines for reversible logic. The +library adds the following operations that can be used to construct quantum +circuits: + +- :class:`~projectq.libs.revkit.ControlFunctionOracle`: Synthesizes a reversible circuit from Boolean control function +- :class:`~projectq.libs.revkit.PermutationOracle`: Synthesizes a reversible circuit for a permutation +- :class:`~projectq.libs.revkit.PhaseOracle`: Synthesizes phase circuit from an arbitrary Boolean function + +RevKit can be installed from PyPi with `pip install revkit`. + +.. note:: + + The RevKit Python module must be installed in order to use this ProjectQ library. + + There exist precompiled binaries in PyPi, as well as a source distribution. + Note that a C++ compiler with C++17 support is required to build the RevKit + python module from source. Examples for compatible compilers are Clang + 6.0, GCC 7.3, and GCC 8.1. + +The integration of RevKit into ProjectQ and other quantum programming languages is described in the paper + + * Mathias Soeken, Thomas Haener, and Martin Roetteler "Programming Quantum Computers Using Design Automation," + in: Design Automation and Test in Europe (2018) [`arXiv:1803.01022 `_] +''', + module_special_members='__init__,__or__', + ), + PackageDescription( + 'libs', + desc=''' +The library collection of ProjectQ which, for now, consists of a tiny math library and an interface library to RevKit. +Soon, more libraries will be added. +''', + ), + PackageDescription( + 'meta', + desc=''' +Contains meta statements which allow more optimal code while making it easier for users to write their code. +Examples are `with Compute`, followed by an automatic uncompute or `with Control`, which allows the user to condition +an entire code block upon the state of a qubit. +''', + ), + PackageDescription( + 'ops', + desc=''' +The operations collection consists of various default gates and is a work-in-progress, as users start to work with +ProjectQ. +''', + module_special_members='__init__,__or__', + ), + PackageDescription( + 'setups.decompositions', + desc=''' +The decomposition package is a collection of gate decomposition / replacement rules which can be used by, +e.g., the AutoReplacer engine. +''', + ), + PackageDescription( + 'setups', + desc=''' +The setups package contains a collection of setups which can be loaded by the `MainEngine`. +Each setup contains a `get_engine_list` function which returns a list of compiler engines: + +Example: + .. code-block:: python + + import projectq.setups.ibm as ibm_setup + from projectq import MainEngine + + eng = MainEngine(engine_list=ibm_setup.get_engine_list()) + # eng uses the default Simulator backend + +The subpackage decompositions contains all the individual decomposition rules +which can be given to, e.g., an `AutoReplacer`. +''', + submodules_desc=''' +Each of the submodules contains a setup which can be used to specify the +`engine_list` used by the `MainEngine` :''', + submodule_special_members='__init__', + ), + PackageDescription( + 'types', + ( + 'The types package contains quantum types such as Qubit, Qureg, and WeakQubitRef. With further development ' + 'of the math library, also quantum integers, quantum fixed point numbers etc. will be added.' + ), + ), +] +# ------------------------------------------------------------------------------ +# Automatically generate ReST files for each package of ProjectQ + +docgen_path = os.path.join(os.path.dirname(os.path.abspath('__file__')), '_doc_gen') +if not os.path.isdir(docgen_path): + os.mkdir(docgen_path) + +for desc in descriptions: + fname = os.path.join(docgen_path, f'projectq.{desc.name}.rst') + lines = None + if os.path.exists(fname): + with open(fname) as fd: + lines = [line[:-1] for line in fd.readlines()] + + new_lines = desc.get_ReST() + + if new_lines != lines: + with open(fname, 'w') as fd: + fd.write('\n'.join(desc.get_ReST())) diff --git a/docs/examples.rst b/docs/examples.rst index c337344eb..667f4e7f0 100755 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -6,44 +6,46 @@ Examples All of these example codes **and more** can be found on `GitHub `_. .. toctree:: - :maxdepth: 2 + :maxdepth: 2 Quantum Random Numbers ---------------------- -The most basic example is a quantum random number generator (QRNG). It can be found in the examples-folder of ProjectQ. The code looks as follows +The most basic example is a quantum random number generator (QRNG). It can be found in the examples-folder of +ProjectQ. The code looks as follows .. literalinclude:: ../examples/quantum_random_numbers.py - :tab-width: 2 + :tab-width: 2 Running this code three times may yield, e.g., .. code-block:: bash - $ python examples/quantum_random_numbers.py - Measured: 0 - $ python examples/quantum_random_numbers.py - Measured: 0 - $ python examples/quantum_random_numbers.py - Measured: 1 + $ python examples/quantum_random_numbers.py + Measured: 0 + $ python examples/quantum_random_numbers.py + Measured: 0 + $ python examples/quantum_random_numbers.py + Measured: 1 -These values are obtained by simulating this quantum algorithm classically. By changing three lines of code, we can run an actual quantum random number generator using the IBM Quantum Experience back-end: +These values are obtained by simulating this quantum algorithm classically. By changing three lines of code, we can run +an actual quantum random number generator using the IBM Quantum Experience back-end: .. code-block:: bash - $ python examples/quantum_random_numbers_ibm.py - Measured: 1 - $ python examples/quantum_random_numbers_ibm.py - Measured: 0 + $ python examples/quantum_random_numbers_ibm.py + Measured: 1 + $ python examples/quantum_random_numbers_ibm.py + Measured: 0 All you need to do is: - * Create an account for `IBM's Quantum Experience `_ - * And perform these minor changes: + * Create an account for `IBM's Quantum Experience `_ + * And perform these minor changes: - .. literalinclude:: ../examples/quantum_random_numbers_ibm.py - :diff: ../examples/quantum_random_numbers.py - :tab-width: 2 + .. literalinclude:: ../examples/quantum_random_numbers_ibm.py + :diff: ../examples/quantum_random_numbers.py + :tab-width: 2 @@ -51,57 +53,64 @@ All you need to do is: Quantum Teleportation --------------------- -Alice has a qubit in some interesting state :math:`|\psi\rangle`, which she would like to show to Bob. This does not really make sense, since Bob would not be able to look at the qubit without collapsing the superposition; but let's just assume Alice wants to send her state to Bob for some reason. -What she can do is use quantum teleportation to achieve this task. Yet, this only works if Alice and Bob share a Bell-pair (which luckily happens to be the case). A Bell-pair is a pair of qubits in the state +Alice has a qubit in some interesting state :math:`|\psi\rangle`, which she would like to show to Bob. This does not +really make sense, since Bob would not be able to look at the qubit without collapsing the superposition; but let's just +assume Alice wants to send her state to Bob for some reason. What she can do is use quantum teleportation to achieve +this task. Yet, this only works if Alice and Bob share a Bell-pair (which luckily happens to be the case). A Bell-pair +is a pair of qubits in the state .. math:: - |A\rangle \otimes |B\rangle = \frac 1{\sqrt 2} \left( |0\rangle\otimes|0\rangle + |1\rangle\otimes|1\rangle \right) + |A\rangle \otimes |B\rangle = \frac 1{\sqrt 2} \left( |0\rangle\otimes|0\rangle + |1\rangle\otimes|1\rangle \right) -They can create a Bell-pair using a very simple circuit which first applies a Hadamard gate to the first qubit, and then flips the second qubit conditional on the first qubit being in :math:`|1\rangle`. The circuit diagram can be generated by calling the function +They can create a Bell-pair using a very simple circuit which first applies a Hadamard gate to the first qubit, and then +flips the second qubit conditional on the first qubit being in :math:`|1\rangle`. The circuit diagram can be generated +by calling the function .. literalinclude:: ../examples/teleport.py - :lines: 6,18-25 - :tab-width: 2 + :lines: 6,18-25 + :tab-width: 2 with a main compiler engine which has a CircuitDrawer back-end, i.e., .. literalinclude:: ../examples/bellpair_circuit.py - :tab-width: 2 + :tab-width: 2 The resulting LaTeX code can be compiled to produce the circuit diagram: .. code-block:: bash - $ python examples/bellpair_circuit.py > bellpair_circuit.tex - $ pdflatex bellpair_circuit.tex - + $ python examples/bellpair_circuit.py > bellpair_circuit.tex + $ pdflatex bellpair_circuit.tex + The output looks as follows: .. image:: images/bellpair_circuit.png - :align: center + :align: center -Now, this Bell-pair can be used to achieve the quantum teleportation: Alice entangles her qubit with her share of the Bell-pair. Then, she measures both qubits; one in the Z-basis (Measure) and one in the Hadamard basis (Hadamard, then Measure). She then sends her measurement results to Bob who, depending on these outcomes, applies a Pauli-X or -Z gate. +Now, this Bell-pair can be used to achieve the quantum teleportation: Alice entangles her qubit with her share of the +Bell-pair. Then, she measures both qubits; one in the Z-basis (Measure) and one in the Hadamard basis (Hadamard, then +Measure). She then sends her measurement results to Bob who, depending on these outcomes, applies a Pauli-X or -Z gate. The complete example looks as follows: .. literalinclude:: ../examples/teleport.py - :linenos: - :lines: 1-6,18-27,44-100 - :tab-width: 2 + :linenos: + :lines: 1-6,18-27,44-100 + :tab-width: 2 and the corresponding circuit can be generated using .. code-block:: bash - $ python examples/teleport_circuit.py > teleport_circuit.tex - $ pdflatex teleport_circuit.tex + $ python examples/teleport_circuit.py > teleport_circuit.tex + $ pdflatex teleport_circuit.tex which produces (after renaming of the qubits inside the tex-file): .. image:: images/teleport_circuit.png - :align: center + :align: center @@ -109,74 +118,96 @@ which produces (after renaming of the qubits inside the tex-file): Shor's algorithm for factoring ------------------------------ -As a third example, consider Shor's algorithm for factoring, which for a given (large) number :math:`N` determines the two prime factor :math:`p_1` and :math:`p_2` such that -:math:`p_1\cdot p_2 = N` in polynomial time! This is a superpolynomial speed-up over the best known classical algorithm (which is the number field sieve) and enables the breaking of modern encryption schemes such as RSA on a future quantum computer. +As a third example, consider Shor's algorithm for factoring, which for a given (large) number :math:`N` determines the +two prime factor :math:`p_1` and :math:`p_2` such that :math:`p_1\cdot p_2 = N` in polynomial time! This is a +superpolynomial speed-up over the best known classical algorithm (which is the number field sieve) and enables the +breaking of modern encryption schemes such as RSA on a future quantum computer. **A tiny bit of number theory** - There is a small amount of number theory involved, which reduces the problem of factoring to period-finding of the function + There is a small amount of number theory involved, which reduces the problem of factoring to period-finding of the + function - .. math:: - f(x) = a^x\operatorname{mod} N + .. math:: + f(x) = a^x\operatorname{mod} N - for some `a` (relative prime to N, otherwise we get a factor right away anyway by calling `gcd(a,N)`). The period `r` for a function `f(x)` is the number for which :math:`f(x) = f(x+r)\forall x` holds. In this case, this means that :math:`a^x = a^{x+r}\;\; (\operatorname{mod} N)\;\forall x`. Therefore, :math:`a^r = 1 + qN` for some integer q and hence, :math:`a^r - 1 = (a^{r/2} - 1)(a^{r/2}+1) = qN`. This suggests that using the gcd on `N` and :math:`a^{r/2} \pm 1` we may find a factor of `N`! + for some `a` (relative prime to N, otherwise we get a factor right away anyway by calling `gcd(a,N)`). The period + `r` for a function `f(x)` is the number for which :math:`f(x) = f(x+r)\forall x` holds. In this case, this means + that :math:`a^x = a^{x+r}\;\; (\operatorname{mod} N)\;\forall x`. Therefore, :math:`a^r = 1 + qN` for some integer q + and hence, :math:`a^r - 1 = (a^{r/2} - 1)(a^{r/2}+1) = qN`. This suggests that using the gcd on `N` and + :math:`a^{r/2} \pm 1` we may find a factor of `N`! **Factoring on a quantum computer: An example** - At the heart of Shor's algorithm lies modular exponentiation of a classically known constant (denoted by `a` in the code) by a quantum superposition of numbers :math:`x`, i.e., + At the heart of Shor's algorithm lies modular exponentiation of a classically known constant (denoted by `a` in the + code) by a quantum superposition of numbers :math:`x`, i.e., - .. math:: + .. math:: - |x\rangle|0\rangle \mapsto |x\rangle|a^x\operatorname{mod} N\rangle + |x\rangle|0\rangle \mapsto |x\rangle|a^x\operatorname{mod} N\rangle - Using :math:`N=15` and :math:`a=2`, and applying this operation to the uniform superposition over all :math:`x` leads to the superposition (modulo renormalization) + Using :math:`N=15` and :math:`a=2`, and applying this operation to the uniform superposition over all :math:`x` + leads to the superposition (modulo renormalization) - .. math:: + .. math:: - |0\rangle|1\rangle + |1\rangle|2\rangle + |2\rangle|4\rangle + |3\rangle|8\rangle + |4\rangle|1\rangle + |5\rangle|2\rangle + |6\rangle|4\rangle + \cdots + |0\rangle|1\rangle + |1\rangle|2\rangle + |2\rangle|4\rangle + |3\rangle|8\rangle + |4\rangle|1\rangle + |5\rangle|2\rangle + |6\rangle|4\rangle + \cdots - In Shor's algorithm, the second register will not be touched again before the end of the quantum program, which means it might as well be measured now. Let's assume we measure 2; this collapses the state above to + In Shor's algorithm, the second register will not be touched again before the end of the quantum program, which + means it might as well be measured now. Let's assume we measure 2; this collapses the state above to - .. math:: + .. math:: - |1\rangle|2\rangle + |5\rangle|2\rangle + |9\rangle|2\rangle + \cdots + |1\rangle|2\rangle + |5\rangle|2\rangle + |9\rangle|2\rangle + \cdots - The period of `a` modulo `N` can now be read off. On a quantum computer, this information can be accessed by applying an inverse quantum Fourier transform to the x-register, followed by a measurement of x. + The period of `a` modulo `N` can now be read off. On a quantum computer, this information can be accessed by + applying an inverse quantum Fourier transform to the x-register, followed by a measurement of x. **Implementation** - There is an implementation of Shor's algorithm in the examples folder. It uses the implementation by Beauregard, `arxiv:0205095 `_ to factor an n-bit number using 2n+3 qubits. In this implementation, the modular exponentiation is carried out using modular multiplication and shift. Furthermore it uses the semi-classical quantum Fourier transform [see `arxiv:9511007 `_]: Pulling the final measurement of the `x`-register through the final inverse quantum Fourier transform allows to run the 2n modular multiplications serially, which keeps one from having to store the 2n qubits of x. - - Let's run it using the ProjectQ simulator: - - .. code-block:: text - - $ python3 examples/shor.py - - projectq - -------- - Implementation of Shor's algorithm. - Number to factor: 15 - - Factoring N = 15: 00000001 - - Factors found :-) : 3 * 5 = 15 - - Simulating Shor's algorithm at the level of single-qubit gates and CNOTs already takes quite a bit of time for larger numbers than 15. To turn on our **emulation feature**, which does not decompose the modular arithmetic to low-level gates, but carries it out directly instead, we can change the line - - .. literalinclude:: ../examples/shor.py - :lineno-start: 86 - :lines: 86-99 - :emphasize-lines: 8 - :linenos: - :tab-width: 2 - - in examples/shor.py to `return True`. This allows to factor, e.g. :math:`N=4,028,033` in under 3 minutes on a regular laptop! - - The most important part of the code is - - .. literalinclude:: ../examples/shor.py - :lines: 50-69 - :lineno-start: 50 - :linenos: - :dedent: 1 - :tab-width: 2 - - which executes the 2n modular multiplications conditioned on a control qubit `ctrl_qubit` in a uniform superposition of 0 and 1. The control qubit is then measured after performing the semi-classical inverse quantum Fourier transform and the measurement outcome is saved in the list `measurements`, followed by a reset of the control qubit to state 0. + There is an implementation of Shor's algorithm in the examples folder. It uses the implementation by Beauregard, + `arxiv:0205095 `_ to factor an n-bit number using 2n+3 qubits. In this + implementation, the modular exponentiation is carried out using modular multiplication and shift. Furthermore it + uses the semi-classical quantum Fourier transform [see `arxiv:9511007 `_]: + Pulling the final measurement of the `x`-register through the final inverse quantum Fourier transform allows to run + the 2n modular multiplications serially, which keeps one from having to store the 2n qubits of x. + + Let's run it using the ProjectQ simulator: + + .. code-block:: text + + $ python3 examples/shor.py + + projectq + -------- + Implementation of Shor's algorithm. + Number to factor: 15 + + Factoring N = 15: 00000001 + + Factors found :-) : 3 * 5 = 15 + + Simulating Shor's algorithm at the level of single-qubit gates and CNOTs already takes quite a bit of time for + larger numbers than 15. To turn on our **emulation feature**, which does not decompose the modular arithmetic to + low-level gates, but carries it out directly instead, we can change the line + + .. literalinclude:: ../examples/shor.py + :lineno-start: 86 + :lines: 86-99 + :emphasize-lines: 8 + :linenos: + :tab-width: 2 + + in examples/shor.py to `return True`. This allows to factor, e.g. :math:`N=4,028,033` in under 3 minutes on a + regular laptop! + + The most important part of the code is + + .. literalinclude:: ../examples/shor.py + :lines: 50-69 + :lineno-start: 50 + :linenos: + :dedent: 1 + :tab-width: 2 + + which executes the 2n modular multiplications conditioned on a control qubit `ctrl_qubit` in a uniform superposition + of 0 and 1. The control qubit is then measured after performing the semi-classical inverse quantum Fourier transform + and the measurement outcome is saved in the list `measurements`, followed by a reset of the control qubit to + state 0. diff --git a/docs/images/braket_notation.svg b/docs/images/braket_notation.svg new file mode 100644 index 000000000..f4f711d36 --- /dev/null +++ b/docs/images/braket_notation.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index 4b7239693..6a71e8dc9 100755 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,9 @@ ProjectQ ======== -ProjectQ is an open-source software framework for quantum computing. It aims at providing tools which facilitate **inventing**, **implementing**, **testing**, **debugging**, and **running** quantum algorithms using either classical hardware or actual quantum devices. +ProjectQ is an open-source software framework for quantum computing. It aims at providing tools which facilitate +**inventing**, **implementing**, **testing**, **debugging**, and **running** quantum algorithms using either classical +hardware or actual quantum devices. The **four core principles** of this open-source effort are @@ -14,20 +16,24 @@ The **four core principles** of this open-source effort are Please cite - * Damian S. Steiger, Thomas Häner, and Matthias Troyer "ProjectQ: An Open Source Software Framework for Quantum Computing" [`arxiv:1612.08091 `_] - * Thomas Häner, Damian S. Steiger, Krysta M. Svore, and Matthias Troyer "A Software Methodology for Compiling Quantum Programs" [`arxiv:1604.01401 `_] + * Damian S. Steiger, Thomas Häner, and Matthias Troyer "ProjectQ: An Open Source Software Framework for Quantum + Computing" `Quantum 2, 49 (2018) `__ (published on `arXiv + `__ on 23 Dec 2016) + * Thomas Häner, Damian S. Steiger, Krysta M. Svore, and Matthias Troyer "A Software Methodology for Compiling + Quantum Programs" `Quantum Sci. Technol. 3 (2018) 020501 `__ (published + on `arXiv `__ on 5 Apr 2016) Contents - * :ref:`tutorial`: Tutorial containing instructions on how to get started with ProjectQ. - * :ref:`examples`: Example implementations of few quantum algorithms - * :ref:`code_doc`: The code documentation of ProjectQ. + * :ref:`tutorial`: Tutorial containing instructions on how to get started with ProjectQ. + * :ref:`examples`: Example implementations of few quantum algorithms + * :ref:`code_doc`: The code documentation of ProjectQ. .. toctree:: - :maxdepth: 2 - :hidden: - - tutorials - examples - projectq + :maxdepth: 2 + :hidden: + + tutorials + examples + projectq diff --git a/docs/make.bat b/docs/make.bat index 7d0ce4919..8058b20d2 100755 --- a/docs/make.bat +++ b/docs/make.bat @@ -3,50 +3,50 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build + set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. epub3 to make an epub3 - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - echo. dummy to check syntax errors of document sources - goto end + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end ) if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end ) @@ -60,222 +60,222 @@ goto sphinx_ok set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 ) :sphinx_ok if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end ) if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end ) if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end ) if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end ) if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end ) if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. - goto end + goto end ) if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\projectq.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\projectq.ghc - goto end + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\projectq.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\projectq.ghc + goto end ) if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end ) if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end ) if "%1" == "epub3" ( - %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. - goto end + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end ) if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end ) if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end ) if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end ) if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end ) if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end ) if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end ) if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end ) if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end ) if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. - goto end + goto end ) if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. - goto end + goto end ) if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. - goto end + goto end ) if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end ) if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end ) if "%1" == "dummy" ( - %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. Dummy builder generates no files. - goto end + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end ) :end diff --git a/docs/package_description.py b/docs/package_description.py new file mode 100644 index 000000000..d04ba67c3 --- /dev/null +++ b/docs/package_description.py @@ -0,0 +1,181 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module containing some helper classes for generating the documentation.""" + +import importlib +import inspect +import pkgutil + + +class PackageDescription: # pylint: disable=too-many-instance-attributes,too-few-public-methods + """A package description class.""" + + package_list = [] + + def __init__( # pylint: disable=too-many-arguments + self, + pkg_name, + desc='', + module_special_members='__init__', + submodule_special_members='', + submodules_desc='', + helper_submodules=None, + ): + """ + Initialize a PackageDescription object. + + Args: + name (str): Name of ProjectQ module + desc (str): (optional) Description of module + module_special_members (str): (optional) Special members to include in the documentation of the module + submodule_special_members (str): (optional) Special members to include in the documentation of submodules + submodules_desc (str): (optional) Description to print out before the list of submodules + helper_submodules (list): (optional) List of tuples for helper sub-modules to include in the + documentation. + Tuples are (section_title, submodukle_name, automodule_properties) + """ + self.name = pkg_name + self.desc = desc + if pkg_name not in PackageDescription.package_list: + PackageDescription.package_list.append(pkg_name) + + self.module = importlib.import_module(f'projectq.{self.name}') + self.module_special_members = module_special_members + + self.submodule_special_members = submodule_special_members + self.submodules_desc = submodules_desc + + self.helper_submodules = helper_submodules + + sub = [] + for _, module_name, _ in pkgutil.iter_modules(self.module.__path__, self.module.__name__ + '.'): + if not module_name.endswith('_test'): + try: + idx = len(self.module.__name__) + 1 + sub.append((module_name[idx:], importlib.import_module(module_name))) + except ImportError: + pass + + self.subpackages = [] + self.submodules = [] + for name, obj in sub: + if f'{self.name}.{name}' in PackageDescription.package_list: + self.subpackages.append((name, obj)) + else: + self.submodules.append((name, obj)) + + self.subpackages.sort(key=lambda x: x[0].lower()) + self.submodules.sort(key=lambda x: x[0].lower()) + + self.members = [ + (name, obj) + for name, obj in inspect.getmembers( + self.module, + lambda obj: ( + inspect.isclass(obj) + or inspect.isfunction(obj) + or isinstance(obj, (int, float, tuple, list, dict, set, frozenset, str)) + ), + ) + if name[0] != '_' + ] + self.members.sort(key=lambda x: x[0].lower()) + + def get_ReST(self): # pylint: disable=invalid-name,too-many-branches,too-many-statements + """Conversion to ReST formatted string.""" + new_lines = [] + new_lines.append(self.name) + new_lines.append('=' * len(self.name)) + new_lines.append('') + + if self.desc: + new_lines.append(self.desc.strip()) + new_lines.append('') + + submodule_has_index = False + + if self.subpackages: + new_lines.append('Subpackages') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append('.. toctree::') + new_lines.append(' :maxdepth: 1') + new_lines.append('') + for name, _ in self.subpackages: + new_lines.append(f' projectq.{self.name}.{name}') + new_lines.append('') + else: + submodule_has_index = True + new_lines.append('.. autosummary::') + new_lines.append('') + if self.submodules: + for name, _ in self.submodules: + new_lines.append(f'\tprojectq.{self.name}.{name}') + new_lines.append('') + if self.members: + for name, _ in self.members: + new_lines.append(f'\tprojectq.{self.name}.{name}') + new_lines.append('') + + if self.submodules: + new_lines.append('Submodules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + if self.submodules_desc: + new_lines.append(self.submodules_desc.strip()) + new_lines.append('') + + if not submodule_has_index: + new_lines.append('.. autosummary::') + new_lines.append('') + for name, _ in self.submodules: + new_lines.append(f' projectq.{self.name}.{name}') + new_lines.append('') + + for name, _ in self.submodules: + new_lines.append(name) + new_lines.append('^' * len(new_lines[-1])) + new_lines.append('') + new_lines.append(f'.. automodule:: projectq.{self.name}.{name}') + new_lines.append(' :members:') + if self.submodule_special_members: + new_lines.append(f' :special-members: {self.submodule_special_members}') + new_lines.append(' :undoc-members:') + new_lines.append('') + + new_lines.append('Module contents') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + new_lines.append(f'.. automodule:: projectq.{self.name}') + new_lines.append(' :members:') + new_lines.append(' :undoc-members:') + new_lines.append(f' :special-members: {self.module_special_members}') + new_lines.append(' :imported-members:') + new_lines.append('') + + if self.helper_submodules: + new_lines.append('Helper sub-modules') + new_lines.append('-' * len(new_lines[-1])) + new_lines.append('') + for title, name, params in self.helper_submodules: + new_lines.append(title) + new_lines.append('^' * len(title)) + new_lines.append('') + new_lines.append(f'.. automodule:: projectq.{self.name}.{name}') + for param in params: + new_lines.append(f' {param}') + new_lines.append('') + + return new_lines[:-1] diff --git a/docs/projectq.backends.rst b/docs/projectq.backends.rst deleted file mode 100755 index 621f7ce86..000000000 --- a/docs/projectq.backends.rst +++ /dev/null @@ -1,20 +0,0 @@ -backends -======== - -.. autosummary:: - - projectq.backends.CommandPrinter - projectq.backends.CircuitDrawer - projectq.backends.Simulator - projectq.backends.ClassicalSimulator - projectq.backends.ResourceCounter - projectq.backends.IBMBackend - - -Module contents ---------------- - -.. automodule:: projectq.backends - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.cengines.rst b/docs/projectq.cengines.rst deleted file mode 100755 index 5a3c963a6..000000000 --- a/docs/projectq.cengines.rst +++ /dev/null @@ -1,33 +0,0 @@ -cengines -======== - -The ProjectQ compiler engines package. - -.. autosummary:: - projectq.cengines.AutoReplacer - projectq.cengines.BasicEngine - projectq.cengines.BasicMapper - projectq.cengines.CommandModifier - projectq.cengines.CompareEngine - projectq.cengines.DecompositionRule - projectq.cengines.DecompositionRuleSet - projectq.cengines.DummyEngine - projectq.cengines.ForwarderEngine - projectq.cengines.GridMapper - projectq.cengines.InstructionFilter - projectq.cengines.IBM5QubitMapper - projectq.cengines.LinearMapper - projectq.cengines.LocalOptimizer - projectq.cengines.ManualMapper - projectq.cengines.MainEngine - projectq.cengines.SwapAndCNOTFlipper - projectq.cengines.TagRemover - - -Module contents ---------------- - -.. automodule:: projectq.cengines - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.math.rst b/docs/projectq.libs.math.rst deleted file mode 100755 index 1567978b5..000000000 --- a/docs/projectq.libs.math.rst +++ /dev/null @@ -1,21 +0,0 @@ -math -==== - -A tiny math library which will be extended thoughout the next weeks. Right now, it only contains the math functions necessary to run Beauregard's implementation of Shor's algorithm. - -.. autosummary:: - - projectq.libs.math.all_defined_decomposition_rules - projectq.libs.math.AddConstant - projectq.libs.math.SubConstant - projectq.libs.math.AddConstantModN - projectq.libs.math.SubConstantModN - projectq.libs.math.MultiplyByConstantModN - -Module contents ---------------- - -.. automodule:: projectq.libs.math - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.libs.revkit.rst b/docs/projectq.libs.revkit.rst deleted file mode 100644 index 90a2dbb18..000000000 --- a/docs/projectq.libs.revkit.rst +++ /dev/null @@ -1,34 +0,0 @@ -revkit -====== - -This library integrates `RevKit `_ into -ProjectQ to allow some automatic synthesis routines for reversible logic. The -library adds the following operations that can be used to construct quantum -circuits: - -- :class:`~projectq.libs.revkit.ControlFunctionOracle`: Synthesizes a reversible circuit from Boolean control function -- :class:`~projectq.libs.revkit.PermutationOracle`: Synthesizes a reversible circuit for a permutation -- :class:`~projectq.libs.revkit.PhaseOracle`: Synthesizes phase circuit from an arbitrary Boolean function - -RevKit can be installed from PyPi with `pip install revkit`. - -.. note:: - - The RevKit Python module must be installed in order to use this ProjectQ library. - - There exist precompiled binaries in PyPi, as well as a source distribution. - Note that a C++ compiler with C++17 support is required to build the RevKit - python module from source. Examples for compatible compilers are Clang - 6.0, GCC 7.3, and GCC 8.1. - -The integration of RevKit into ProjectQ and other quantum programming languages is described in the paper - - * Mathias Soeken, Thomas Haener, and Martin Roetteler "Programming Quantum Computers Using Design Automation," in: Design Automation and Test in Europe (2018) [`arXiv:1803.01022 `_] - -Module contents ---------------- - -.. automodule:: projectq.libs.revkit - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.libs.rst b/docs/projectq.libs.rst deleted file mode 100755 index 9f2c8cd4b..000000000 --- a/docs/projectq.libs.rst +++ /dev/null @@ -1,20 +0,0 @@ -libs -==== - -The library collection of ProjectQ which, for now, consists of a tiny math library and an interface library to RevKit. Soon, more libraries will be added. - -Subpackages ------------ - -.. toctree:: - - projectq.libs.math - projectq.libs.revkit - -Module contents ---------------- - -.. automodule:: projectq.libs - :members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.meta.rst b/docs/projectq.meta.rst deleted file mode 100755 index 14c3d9eea..000000000 --- a/docs/projectq.meta.rst +++ /dev/null @@ -1,32 +0,0 @@ -meta -==== - -Contains meta statements which allow more optimal code while making it easier for users to write their code. -Examples are `with Compute`, followed by an automatic uncompute or `with Control`, which allows the user to condition an entire code block upon the state of a qubit. - - -.. autosummary:: - - projectq.meta.DirtyQubitTag - projectq.meta.LogicalQubitIDTag - projectq.meta.LoopTag - projectq.meta.Loop - projectq.meta.Compute - projectq.meta.Uncompute - projectq.meta.CustomUncompute - projectq.meta.ComputeTag - projectq.meta.UncomputeTag - projectq.meta.Control - projectq.meta.get_control_count - projectq.meta.Dagger - projectq.meta.insert_engine - projectq.meta.drop_engine_after - -Module contents ---------------- - -.. automodule:: projectq.meta - :members: - :undoc-members: - :special-members: __init__ - :imported-members: diff --git a/docs/projectq.ops.rst b/docs/projectq.ops.rst deleted file mode 100755 index e04ca2e91..000000000 --- a/docs/projectq.ops.rst +++ /dev/null @@ -1,60 +0,0 @@ -ops -=== - -The operations collection consists of various default gates and is a work-in-progress, as users start to work with ProjectQ. - -.. autosummary:: - - projectq.ops.BasicGate - projectq.ops.SelfInverseGate - projectq.ops.BasicRotationGate - projectq.ops.BasicPhaseGate - projectq.ops.ClassicalInstructionGate - projectq.ops.FastForwardingGate - projectq.ops.BasicMathGate - projectq.ops.apply_command - projectq.ops.Command - projectq.ops.H - projectq.ops.X - projectq.ops.Y - projectq.ops.Z - projectq.ops.S - projectq.ops.Sdag - projectq.ops.T - projectq.ops.Tdag - projectq.ops.SqrtX - projectq.ops.Swap - projectq.ops.SqrtSwap - projectq.ops.Entangle - projectq.ops.Ph - projectq.ops.Rx - projectq.ops.Ry - projectq.ops.Rz - projectq.ops.R - projectq.ops.FlushGate - projectq.ops.MeasureGate - projectq.ops.Allocate - projectq.ops.Deallocate - projectq.ops.AllocateDirty - projectq.ops.Barrier - projectq.ops.DaggeredGate - projectq.ops.ControlledGate - projectq.ops.C - projectq.ops.All - projectq.ops.Tensor - projectq.ops.QFT - projectq.ops.QubitOperator - projectq.ops.CRz - projectq.ops.CNOT - projectq.ops.CZ - projectq.ops.Toffoli - projectq.ops.TimeEvolution - - -Module contents ---------------- - -.. automodule:: projectq.ops - :members: - :special-members: __init__,__or__ - :imported-members: diff --git a/docs/projectq.rst b/docs/projectq.rst index cf69c7ab8..ffcb10c26 100755 --- a/docs/projectq.rst +++ b/docs/projectq.rst @@ -3,20 +3,19 @@ Code Documentation ================== -Welcome to the package documentation of ProjectQ. You may now browse through the entire documentation and discover the capabilities of the ProjectQ framework. +Welcome to the package documentation of ProjectQ. You may now browse through the entire documentation and discover the +capabilities of the ProjectQ framework. For a detailed documentation of a subpackage or module, click on its name below: .. toctree:: :maxdepth: 1 :titlesonly: - - projectq.backends - projectq.cengines - projectq.libs - projectq.meta - projectq.ops - projectq.setups - projectq.types - + _doc_gen/projectq.backends + _doc_gen/projectq.cengines + _doc_gen/projectq.libs + _doc_gen/projectq.meta + _doc_gen/projectq.ops + _doc_gen/projectq.setups + _doc_gen/projectq.types diff --git a/docs/projectq.setups.decompositions.rst b/docs/projectq.setups.decompositions.rst deleted file mode 100755 index a17e3d1bd..000000000 --- a/docs/projectq.setups.decompositions.rst +++ /dev/null @@ -1,142 +0,0 @@ -decompositions -============== - -The decomposition package is a collection of gate decomposition / replacement rules which can be used by, e.g., the AutoReplacer engine. - - -.. autosummary:: - - projectq.setups.decompositions.arb1qubit2rzandry - projectq.setups.decompositions.barrier - projectq.setups.decompositions.carb1qubit2cnotrzandry - projectq.setups.decompositions.cnu2toffoliandcu - projectq.setups.decompositions.crz2cxandrz - projectq.setups.decompositions.entangle - projectq.setups.decompositions.globalphase - projectq.setups.decompositions.ph2r - projectq.setups.decompositions.qft2crandhadamard - projectq.setups.decompositions.r2rzandph - projectq.setups.decompositions.rx2rz - projectq.setups.decompositions.ry2rz - projectq.setups.decompositions.swap2cnot - projectq.setups.decompositions.time_evolution - projectq.setups.decompositions.toffoli2cnotandtgate - - -Submodules ----------- - -projectq.setups.decompositions.arb1qubit2rzandry module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.arb1qubit2rzandry - :members: - :undoc-members: - - -projectq.setups.decompositions.barrier module ---------------------------------------------- - -.. automodule:: projectq.setups.decompositions.barrier - :members: - :undoc-members: - -projectq.setups.decompositions.carb1qubit2cnotrzandry module ------------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.carb1qubit2cnotrzandry - :members: - :undoc-members: - -projectq.setups.decompositions.cnu2toffoliandcu module ------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.cnu2toffoliandcu - :members: - :undoc-members: - -projectq.setups.decompositions.crz2cxandrz module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.crz2cxandrz - :members: - :undoc-members: - -projectq.setups.decompositions.entangle module ----------------------------------------------- - -.. automodule:: projectq.setups.decompositions.entangle - :members: - :undoc-members: - -projectq.setups.decompositions.globalphase module -------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.globalphase - :members: - :undoc-members: - -projectq.setups.decompositions.ph2r module ------------------------------------------- - -.. automodule:: projectq.setups.decompositions.ph2r - :members: - :undoc-members: - -projectq.setups.decompositions.qft2crandhadamard module -------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.qft2crandhadamard - :members: - :undoc-members: - -projectq.setups.decompositions.r2rzandph module ------------------------------------------------ - -.. automodule:: projectq.setups.decompositions.r2rzandph - :members: - :undoc-members: - -projectq.setups.decompositions.rx2rz module -------------------------------------------- - -.. automodule:: projectq.setups.decompositions.rx2rz - :members: - :undoc-members: - -projectq.setups.decompositions.ry2rz module -------------------------------------------- - -.. automodule:: projectq.setups.decompositions.ry2rz - :members: - :undoc-members: - -projectq.setups.decompositions.swap2cnot module ------------------------------------------------ - -.. automodule:: projectq.setups.decompositions.swap2cnot - :members: - :undoc-members: - -projectq.setups.decompositions.time_evolution module ----------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.time_evolution - :members: - :undoc-members: - -projectq.setups.decompositions.toffoli2cnotandtgate module ----------------------------------------------------------- - -.. automodule:: projectq.setups.decompositions.toffoli2cnotandtgate - :members: - :undoc-members: - - -Module contents ---------------- - -.. automodule:: projectq.setups.decompositions - :members: - :undoc-members: - :imported-members: diff --git a/docs/projectq.setups.rst b/docs/projectq.setups.rst deleted file mode 100755 index 058469f07..000000000 --- a/docs/projectq.setups.rst +++ /dev/null @@ -1,94 +0,0 @@ -setups -====== - -The setups package contains a collection of setups which can be loaded by the `MainEngine`. Each setup contains a `get_engine_list` function which returns a list of compiler engines: - -Example: - .. code-block:: python - - import projectq.setups.ibm as ibm_setup - from projectq import MainEngine - eng = MainEngine(engine_list=ibm_setup.get_engine_list()) - # eng uses the default Simulator backend - -The subpackage decompositions contains all the individual decomposition rules -which can be given to, e.g., an `AutoReplacer`. - - -Subpackages ------------ - -.. toctree:: - :maxdepth: 1 - - projectq.setups.decompositions - -Submodules ----------- - -Each of the submodules contains a setup which can be used to specify the -`engine_list` used by the `MainEngine` : - -.. autosummary:: - - projectq.setups.default - projectq.setups.grid - projectq.setups.ibm - projectq.setups.ibm16 - projectq.setups.linear - projectq.setups.restrictedgateset - -default -------- - -.. automodule:: projectq.setups.default - :members: - :special-members: __init__ - :undoc-members: - -grid ----- - -.. automodule:: projectq.setups.grid - :members: - :special-members: __init__ - :undoc-members: - -ibm ---- - -.. automodule:: projectq.setups.ibm - :members: - :special-members: __init__ - :undoc-members: - -ibm16 ------ - -.. automodule:: projectq.setups.ibm16 - :members: - :special-members: __init__ - :undoc-members: - -linear ------- - -.. automodule:: projectq.setups.linear - :members: - :special-members: __init__ - :undoc-members: - -restrictedgateset ------------------ - -.. automodule:: projectq.setups.restrictedgateset - :members: - :special-members: __init__ - :undoc-members: - -Module contents ---------------- - -.. automodule:: projectq.setups - :members: - :special-members: __init__ diff --git a/docs/projectq.types.rst b/docs/projectq.types.rst deleted file mode 100755 index 4f26edc9d..000000000 --- a/docs/projectq.types.rst +++ /dev/null @@ -1,18 +0,0 @@ -types -===== - -The types package contains quantum types such as Qubit, Qureg, and WeakQubitRef. With further development of the math library, also quantum integers, quantum fixed point numbers etc. will be added. - -.. autosummary:: - projectq.types.BasicQubit - projectq.types.Qubit - projectq.types.Qureg - projectq.types.WeakQubitRef - -Module contents ---------------- - -.. automodule:: projectq.types - :members: - :special-members: - :imported-members: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 289af1aef..fd6b8c08a 100755 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -4,7 +4,7 @@ Tutorial ======== .. toctree:: - :maxdepth: 2 + :maxdepth: 2 Getting started --------------- @@ -13,40 +13,71 @@ To start using ProjectQ, simply run .. code-block:: bash - python -m pip install --user projectq + python -m pip install --user projectq -or, alternatively, `clone/download `_ this repo (e.g., to your /home directory) and run +Since version 0.6.0, ProjectQ is available as pre-compiled binary wheels in addition to the traditional source +package. These wheels should work on most platforms, provided that your processor supports AVX2 instructions. Should you +encounter any troubles while installation ProjectQ in binary form, you can always try tom compile the project manually +as described below. You may want to pass the `--no-binary projectq` flag to Pip during the installation to make sure +that you are downloading the source package. + +Alternatively, you can also `clone/download `_ this repository (e.g., to +your /home directory) and run .. code-block:: bash - cd /home/projectq - python -m pip install --user . + cd /home/projectq + python -m pip install --user . -ProjectQ comes with a high-performance quantum simulator written in C++. Please see the detailed OS specific installation instructions below to make sure that you are installing the fastest version. +ProjectQ comes with a high-performance quantum simulator written in C++. Please see the detailed OS specific +installation instructions below to make sure that you are installing the fastest version. .. note:: - The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If it fails, you may use the `--without-cppsimulator` parameter, i.e., - - .. code-block:: bash - - python -m pip install --user --global-option=--without-cppsimulator . - - and the framework will use the **slow Python simulator instead**. Note that this only works if the installation has been tried once without the `--without-cppsimulator` parameter and hence all requirements are now installed. See the instructions below if you want to run larger simulations. The Python simulator works perfectly fine for the small examples (e.g., running Shor's algorithm for factoring 15 or 21). + The setup will try to build a C++-Simulator, which is much faster than the Python implementation. If the C++ + compilation were to fail, the setup will install a pure Python implementation of the simulator instead. The Python + simulator should work fine for small examples (e.g., running Shor's algorithm for factoring 15 or 21). + + If you want to skip the installation of the C++-Simulator altogether, you can define the ``PROJECTQ_DISABLE_CEXT`` + environment variable to avoid any compilation steps. .. note:: - If building the C++-Simulator does not work out of the box, consider specifying a different compiler. For example: - - .. code-block:: bash - - env CC=g++-5 python -m pip install --user projectq + If building the C++-Simulator does not work out of the box, consider specifying a different compiler. For example: + + .. code-block:: bash + + env CC=g++-10 python -m pip install --user projectq - Please note that the compiler you specify must support **C++11**! + Please note that the compiler you specify must support at least **C++11**! .. note:: - Please use pip version v6.1.0 or higher as this ensures that dependencies are installed in the `correct order `_. + Please use pip version v6.1.0 or higher as this ensures that dependencies are installed in the `correct order + `_. .. note:: - ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which are optimized for the specific hardware on which it is being installed (potentially using our AVX version and `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware can cause problems. + ProjectQ should be installed on each computer individually as the C++ simulator compilation creates binaries which + are optimized for the specific hardware on which it is being installed (potentially using our AVX version and + `-march=native`). Therefore, sharing the same ProjectQ installation across different hardware may cause some + problems. + +**Install AWS Braket Backend requirement** + +AWS Braket Backend requires the use of the official AWS SDK for Python, Boto3. This is an extra requirement only needed +if you plan to use the AWS Braket Backend. To install ProjectQ including this requirement you can include it in the +installation instruction as + +.. code-block:: bash + + python -m pip install --user projectq[braket] + +**Install Azure Quantum Backend requirement** + +Azure Quantum Backend requires the use of the official `Azure Quantum SDK `_ +for Python. This is an extra requirement only needed if you plan to use the Azure Quantum Backend. To install ProjectQ +including this requirement you can include it in the installation instruction as + +.. code-block:: bash + + python -m pip install --user projectq[azure-quantum] Detailed instructions and OS-specific hints @@ -54,149 +85,216 @@ Detailed instructions and OS-specific hints **Ubuntu**: - After having installed the build tools (for g++): - - .. code-block:: bash - - sudo apt-get install build-essential - - You only need to install Python (and the package manager). For version 3, run - - .. code-block:: bash - - sudo apt-get install python3 python3-pip - - When you then run - - .. code-block:: bash - - sudo pip3 install --user projectq - - all dependencies (such as numpy and pybind11) should be installed automatically. + After having installed the build tools (for g++): + + .. code-block:: bash + + sudo apt-get install build-essential + + You only need to install Python (and the package manager). For version 3, run + + .. code-block:: bash + + sudo apt-get install python3 python3-pip + + When you then run + + .. code-block:: bash + + sudo python3 -m pip install --user projectq + + all dependencies (such as numpy and pybind11) should be installed automatically. + + +**ArchLinux/Manjaro**: + + Make sure that you have a C/C++ compiler installed: + + .. code-block:: bash + + sudo pacman -Syu gcc + + You only need to install Python (and the package manager). For version 3, run + + .. code-block:: bash + + sudo pacman -Syu python python-pip + + When you then run + + .. code-block:: bash + + sudo python3 -m pip install --user projectq + + all dependencies (such as numpy and pybind11) should be installed automatically. **Windows**: - It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to do so is using, e.g., the Python3.5 installers from `python.org `_ or `ANACONDA `_. Installing ProjectQ right away will succeed for the (slow) Python simulator (i.e., with the `--without-cppsimulator` flag). For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support of msvc. + It is easiest to install a pre-compiled version of Python, including numpy and many more useful packages. One way to + do so is using, e.g., the Python 3.8 installers from `python.org `_ or `ANACONDA + `_. Installing ProjectQ right away will succeed for the (slow) Python + simulator. For a compiled version of the simulator, install the Visual C++ Build Tools and the Microsoft Windows SDK + prior to doing a pip install. The built simulator will not support multi-threading due to the limited OpenMP support + of the Visual Studio compiler. + + If the Python executable is added to your PATH (option normally suggested at the end of the Python installation + procedure), you can then open a cmdline window (WIN + R, type "cmd" and click *OK*) and enter the following in order + to install ProjectQ: + + .. code-block:: batch + + python -m pip install --user projectq + - Should you want to run multi-threaded simulations, you can install a compiler which supports newer OpenMP versions, such as MinGW GCC and then manually build the C++ simulator with OpenMP enabled. + Should you want to run multi-threaded simulations, you can install a compiler which supports newer OpenMP versions, + such as MinGW GCC and then manually build the C++ simulator with OpenMP enabled. **macOS**: - These are the steps to install ProjectQ on a new Mac: + Similarly to the other platforms, installing ProjectQ without the C++ simulator is really easy: - In order to install the fast C++ simulator, we require that your system has a C++ compiler (see option 3 below on how to only install the slower Python simulator via the `--without-cppsimulator` parameter) + .. code-block:: bash - Below you will find two options to install the fast C++ simulator. The first one is the easiest and requires only the standard compiler which Apple distributes with XCode. The second option uses macports to install the simulator with additional support for multi-threading by using OpenMP, which makes it slightly faster. We show how to install the required C++ compiler (clang) which supports OpenMP and additionally, we show how to install a newer python version. + python3 -m pip install --user projectq -.. note:: - Depending on your system you might need to use `sudo` for the installation. -1. Installation using XCode and the default python: + In order to install the fast C++ simulator, we require that a C++ compiler is installed on your system. There are + essentially three options you can choose from: + + 1. Using the compiler provided by Apple through the XCode command line tools. + 2. Using Homebrew + 3. Using MacPorts + + For both options 2 and 3, you will be required to first install the XCode command line tools + + + **Apple XCode command line tool** + + Install the XCode command line tools by opening a terminal window and running the following command: - Install XCode by opening a terminal and running the following command: + .. code-block:: bash - .. code-block:: bash + xcode-select --install - xcode-select --install + Next, you will need to install Python and pip. See options 2 and 3 for information on how to install a newer python + version with either Homebrew or MacPorts. Here, we are using the standard python which is preinstalled with + macOS. Pip can be installed by: - Next, you will need to install Python and pip. See option 2 for information on how to install a newer python version with macports. Here, we are using the standard python which is preinstalled with macOS. Pip can be installed by: + .. code-block:: bash - .. code-block:: bash + sudo easy_install pip - sudo easy_install pip + Now, you can install ProjectQ with the C++ simulator using the standard command: - Now, you can install ProjectQ with the C++ simulator using the standard command: + .. code-block:: bash - .. code-block:: bash + python3 -m pip install --user projectq - python -m pip install --user projectq + Note that the compiler provided by Apple is currently not able to compile ProjectQ's multi-threaded code. + **Homebrew** -2. Installation using macports: + First install the XCode command line tools. Then install Homebrew with the following command: - Either use the standard python and install pip as shown in option 1 or better use macports to install a newer python version, e.g., Python 3.5 and the corresponding pip. Visit `macports.org `_ and install the latest version (afterwards open a new terminal). Then, use macports to install Python 3.5 by + .. code-block:: bash - .. code-block:: bash + /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - sudo port install python35 + Then proceed to install Python as well as a C/C++ compiler (note: gcc installed via Homebrew may lead to some issues): - It might show a warning that if you intend to use python from the terminal, you should also install + .. code-block:: bash - .. code-block:: bash + brew install python llvm - sudo port install py35-readline + You should now be able to install ProjectQ with the C++ simulator using the following command: - Install pip by + .. code-block:: bash - .. code-block:: bash + env P=/usr/local/opt/llvm/bin CC=$P/clang CXX=$P/clang++ python3 -m pip install --user projectq - sudo port install py35-pip - Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a suitable compiler with support for **C++11**, OpenMP, and instrinsics. The best option is to install clang 3.9 also using macports (note: gcc installed via macports does not work) + **MacPorts** - .. code-block:: bash + Visit `macports.org `_ and install the latest version that corresponds to your + operating system's version. Afterwards, open a new terminal window. - sudo port install clang-3.9 + Then, use macports to install Python 3.8 by entering the following command - ProjectQ is now installed by: + .. code-block:: bash - .. code-block:: bash + sudo port install python38 - env CC=clang-mp-3.9 env CXX=clang++-mp-3.9 python3.5 -m pip install --user projectq + It might show a warning that if you intend to use python from the terminal. In this case, you should also install -3. Installation with only the slow Python simulator: + .. code-block:: bash - While this simulator works fine for small examples, it is suggested to install the high performance simulator written in C++. + sudo port install py38-gnureadline - If you just want to install ProjectQ with the (slow) Python simulator and no compiler, then first try to install ProjectQ with the default compiler + Install pip by - .. code-block:: bash + .. code-block:: bash - python -m pip install --user projectq + sudo port install py38-pip - which most likely will fail. Then, try again with the flag ``--without-cppsimulator``: + Next, we can install ProjectQ with the high performance simulator written in C++. First, we will need to install a + suitable compiler with support for **C++11**, OpenMP, and intrinsics. The best option is to install clang 9.0 also + using macports (note: gcc installed via macports does not work). - .. code-block:: bash + .. code-block:: bash - python -m pip install --user --global-option=--without-cppsimulator projectq + sudo port install clang-9.0 + + ProjectQ is now installed by: + + .. code-block:: bash + + env CC=clang-mp-9.0 env CXX=clang++-mp-9.0 /opt/local/bin/python3.8 -m pip install --user projectq The ProjectQ syntax ------------------- -Our goal is to have an intuitive syntax in order to enable an easy learning curve. Therefore, ProjectQ features a lean syntax which is close to the mathematical notation used in physics. +Our goal is to have an intuitive syntax in order to enable an easy learning curve. Therefore, ProjectQ features a lean +syntax which is close to the mathematical notation used in physics. For example, consider applying an x-rotation by an angle `theta` to a qubit. In ProjectQ, this looks as follows: .. code-block:: python - Rx(theta) | qubit + Rx(theta) | qubit whereas the corresponding notation in physics would be :math:`R_x(\theta) \; |\text{qubit}\rangle` -Moreover, the `|`-operator separates the classical arguments (on the left) from the quantum arguments (on the right). Next, you will see a basic quantum program using this syntax. Further examples can be found in the docs (`Examples` in the panel on the left) and in the ProjectQ examples folder on `GitHub `_. +Moreover, the `|`-operator separates the classical arguments (on the left) from the quantum arguments (on the +right). Next, you will see a basic quantum program using this syntax. Further examples can be found in the docs +(`Examples` in the panel on the left) and in the ProjectQ examples folder on `GitHub +`_. Basic quantum program --------------------- -To check out the ProjectQ syntax in action and to see whether the installation worked, try to run the following basic example +To check out the ProjectQ syntax in action and to see whether the installation worked, try to run the following basic +example .. code-block:: python - from projectq import MainEngine # import the main compiler engine - from projectq.ops import H, Measure # import the operations we want to perform (Hadamard and measurement) - - eng = MainEngine() # create a default compiler (the back-end is a simulator) - qubit = eng.allocate_qubit() # allocate 1 qubit - - H | qubit # apply a Hadamard gate - Measure | qubit # measure the qubit - - eng.flush() # flush all gates (and execute measurements) - print("Measured {}".format(int(qubit))) # output measurement result + from projectq import MainEngine # import the main compiler engine + from projectq.ops import ( + H, + Measure, + ) # import the operations we want to perform (Hadamard and measurement) + + eng = MainEngine() # create a default compiler (the back-end is a simulator) + qubit = eng.allocate_qubit() # allocate 1 qubit + + H | qubit # apply a Hadamard gate + Measure | qubit # measure the qubit + + eng.flush() # flush all gates (and execute measurements) + print(f"Measured {int(qubit)}") # output measurement result Which creates random bits (0 or 1). diff --git a/examples/README.rst b/examples/README.rst index 6ee67c41b..821663817 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -1,32 +1,48 @@ Examples and Tutorials ====================== -This folder contains a collection of **examples** and **tutorials** for how to use ProjectQ. They offer a great way to get started. While this collection is growing, it will never be possible to cover everything. Therefore, we refer the readers to also have a look at: +This folder contains a collection of **examples** and **tutorials** for how to use ProjectQ. They offer a great way to +get started. While this collection is growing, it will never be possible to cover everything. Therefore, we refer the +readers to also have a look at: -* Our complete **code documentation** which can be found online `here `__. Besides the newest version of the documentation it also provides older versions. Moreover, these docs can be downloaded for offline usage. +* Our complete **code documentation** which can be found online `here + `__. Besides the newest version of the documentation it also provides older + versions. Moreover, these docs can be downloaded for offline usage. -* Our **unit tests**. More than 99% of all lines of code are covered with various unit tests since the first release. Tests are really important to us. Therefore, if you are wondering how a specific feature can be used, have a look at the **unit tests**, where you can find plenty of examples. Finding the unit tests is very easy: E.g., the tests of the simulator implemented in *ProjectQ/projectq/backends/_sim/_simulator.py* can all be found in the same folder in the file *ProjectQ/projectq/backends/_sim/_simulator_test.py*. +* Our **unit tests**. More than 99% of all lines of code are covered with various unit tests since the first + release. Tests are really important to us. Therefore, if you are wondering how a specific feature can be used, have a + look at the **unit tests**, where you can find plenty of examples. Finding the unit tests is very easy: E.g., the + tests of the simulator implemented in *ProjectQ/projectq/backends/_sim/_simulator.py* can all be found in the same + folder in the file *ProjectQ/projectq/backends/_sim/_simulator_test.py*. Getting started / background information ---------------------------------------- -It might be a good starting point to have a look at our paper which explains the goals of the ProjectQ framework and also gives a good overview: +It might be a good starting point to have a look at our paper which explains the goals of the ProjectQ framework and +also gives a good overview: -* Damian S. Steiger, Thomas Häner, and Matthias Troyer "ProjectQ: An Open Source Software Framework for Quantum Computing" `[arxiv:1612.08091] `__ +* Damian S. Steiger, Thomas Häner, and Matthias Troyer "ProjectQ: An Open Source Software Framework for Quantum + Computing" `Quantum 2, 49 (2018) `__ (published on `arXiv + `__ on 23 Dec 2016) Our second paper looks at a few aspects of ProjectQ in more details: -* Damian S. Steiger, Thomas Häner, and Matthias Troyer "Advantages of a modular high-level quantum programming framework" `[arxiv:1806.01861] `__ +* Damian S. Steiger, Thomas Häner, and Matthias Troyer "Advantages of a modular high-level quantum programming + framework" `[arxiv:1806.01861] `__ Examples and tutorials in this folder ------------------------------------- -1. Some of the files in this folder are explained in the `documentation `__. +1. Some of the files in this folder are explained in the `documentation + `__. -2. Take a look at the *simulator_tutorial.ipynb* for a detailed introduction to most of the features of our high performance quantum simulator. +2. Take a look at the *simulator_tutorial.ipynb* for a detailed introduction to most of the features of our high + performance quantum simulator. 3. Running on the IBM QE chip is explained in more details in *ibm_entangle.ipynb*. -4. A small tutorial on the compiler is available in *compiler_tutorial.ipynb* which explains how to compile to a specific gate set. +4. A small tutorial on the compiler is available in *compiler_tutorial.ipynb* which explains how to compile to a + specific gate set. -5. A small tutorial on the mappers is available in *mapper_tutorial.ipynb* which explains how to map a quantum circuit to a linear chain or grid of physical qubits. +5. A small tutorial on the mappers is available in *mapper_tutorial.ipynb* which explains how to map a quantum circuit + to a linear chain or grid of physical qubits. diff --git a/examples/aqt.py b/examples/aqt.py new file mode 100644 index 000000000..1be765189 --- /dev/null +++ b/examples/aqt.py @@ -0,0 +1,72 @@ +# pylint: skip-file + +"""Example of running a quantum circuit using the AQT APIs.""" + +import getpass + +import matplotlib.pyplot as plt + +import projectq.setups.aqt +from projectq import MainEngine +from projectq.backends import AQTBackend +from projectq.libs.hist import histogram +from projectq.ops import All, Entangle, Measure + + +def run_entangle(eng, num_qubits=3): + """ + Run an entangling operation on the provided compiler engine. + + Args: + eng (MainEngine): Main compiler engine to use. + num_qubits (int): Number of qubits to entangle. + + Returns: + measurement (list): List of measurement outcomes. + """ + # allocate the quantum register to entangle + qureg = eng.allocate_qureg(num_qubits) + + # entangle the qureg + Entangle | qureg + + # measure; should be all-0 or all-1 + All(Measure) | qureg + + # run the circuit + eng.flush() + + # access the probabilities via the back-end: + # results = eng.backend.get_probabilities(qureg) + # for state in results: + # print(f"Measured {state} with p = {results[state]}.") + # or plot them directly: + histogram(eng.backend, qureg) + plt.show() + + # return one (random) measurement outcome. + return [int(q) for q in qureg] + + +if __name__ == "__main__": + # devices available to subscription: + # aqt_simulator (11 qubits) + # aqt_simulator_noise (11 qubits) + # aqt_device (4 qubits) + # + # To get a subscription, create a profile at : + # https://gateway-portal.aqt.eu/ + # + device = None # replace by the AQT device name you want to use + token = None # replace by the token given by AQT + if token is None: + token = getpass.getpass(prompt='AQT token > ') + if device is None: + device = getpass.getpass(prompt='AQT device > ') + # create main compiler engine for the AQT back-end + eng = MainEngine( + AQTBackend(use_hardware=True, token=token, num_runs=200, verbose=False, device=device), + engine_list=projectq.setups.aqt.get_engine_list(token=token, device=device), + ) + # run the circuit and print the result + print(run_entangle(eng)) diff --git a/examples/awsbraket.ipynb b/examples/awsbraket.ipynb new file mode 100644 index 000000000..0e3a1935a --- /dev/null +++ b/examples/awsbraket.ipynb @@ -0,0 +1,210 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10-final" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.7.10 64-bit", + "metadata": { + "interpreter": { + "hash": "fd69f43f58546b570e94fd7eba7b65e6bcc7a5bbc4eab0408017d18902915d69" + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "source": [ + "# Running ProjectQ code on AWS Braket service provided devices\n", + "## Compiling code for AWS Braket Service\n", + "\n", + "In this tutorial we will see how to run code on some of the devices provided by the Amazon AWS Braket service. The AWS Braket devices supported are: the State Vector Simulator 'SV1', the Rigetti device 'Aspen-8' and the IonQ device 'IonQ'\n", + "\n", + "You need to have a valid AWS account, created a pair of access key/secret key, and have activated the braket service. As part of the activation of the service, a specific S3 bucket and folder associated to the service should be configured.\n", + "\n", + "First we need to do the required imports. That includes the mail compiler engine (MainEngine), the backend (AWSBraketBackend in this case) and the operations to be used in the cicuit" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from projectq import MainEngine\n", + "from projectq.backends import AWSBraketBackend\n", + "from projectq.ops import Measure, H, C, X, All\n" + ] + }, + { + "source": [ + "Prior to the instantiation of the backend we need to configure the credentials, the S3 storage folder and the device to be used (in the example the State Vector Simulator SV1)" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "creds = {\n", + " 'AWS_ACCESS_KEY_ID': 'aws_access_key_id',\n", + " 'AWS_SECRET_KEY': 'aws_secret_key',\n", + " } # replace with your Access key and Secret key\n", + "\n", + "s3_folder = ['S3Bucket', 'S3Directory'] # replace with your S3 bucket and directory\n", + "\n", + "device = 'SV1' # replace by the device you want to use" + ] + }, + { + "source": [ + "Next we instantiate the engine with the AWSBraketBackend including the credentials and S3 configuration. By setting the 'use_hardware' parameter to False we indicate the use of the Simulator. In addition we set the number of times we want to run the circuit and the interval in secons to ask for the results. For a complete list of parameters and descriptions, please check the documentation." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eng = MainEngine(AWSBraketBackend(use_hardware=False,\n", + " credentials=creds,\n", + " s3_folder=s3_folder,\n", + " num_runs=10,\n", + " interval=10))" + ] + }, + { + "source": [ + "We can now allocate the required qubits and create the circuit to be run. With the last instruction we ask the backend to run the circuit." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Allocate the required qubits\n", + "qureg = eng.allocate_qureg(3)\n", + "\n", + "# Create the circuit. In this example a quantum teleportation algorithms that teleports the first qubit to the third one.\n", + "H | qureg[0]\n", + "H | qureg[1]\n", + "C(X) | (qureg[1], qureg[2])\n", + "C(X) | (qureg[0], qureg[1])\n", + "H | qureg[0]\n", + "C(X) | (qureg[1], qureg[2])\n", + "\n", + "# At the end we measure the qubits to get the results; should be all-0 or all-1\n", + "All(Measure) | qureg\n", + "\n", + "# And run the circuit\n", + "eng.flush()\n" + ] + }, + { + "source": [ + "The backend will automatically create the task and generate a unique identifier (the task Arn) that can be used to recover the status of the task and results later on.\n", + "\n", + "Once the circuit is executed the indicated number of times, the results are stored in the S3 folder configured previously and can be recovered to obtain the probabilities of each of the states." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Obtain and print the probabilies of the states\n", + "prob_dict = eng.backend.get_probabilities(qureg)\n", + "print(\"Probabilites for each of the results: \", prob_dict)" + ] + }, + { + "source": [ + "## Retrieve results form a previous execution\n", + "\n", + "We can retrieve the result later on (of this job or a previously executed one) using the task Arn provided when it was run. In addition, you have to remember the amount of qubits involved in the job and the order you used. The latter is required since we need to set up a mapping for the qubits when retrieving results of a previously executed job.\n", + "\n", + "To retrieve the results we need to configure the backend including the parameter 'retrieve_execution' set to the Task Arn of the job. To be able to get the probabilities of each state we need to configure the qubits and ask the backend to get the results." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set the Task Arn of the job to be retrieved and instantiate the engine with the AWSBraketBackend\n", + "task_arn = 'your_task_arn' # replace with the actual TaskArn you want to use\n", + "\n", + "eng1 = MainEngine(AWSBraketBackend(retrieve_execution=task_arn, credentials=creds, num_retries=2, verbose=True))\n", + "\n", + "# Configure the qubits to get the states probabilies\n", + "qureg1 = eng1.allocate_qureg(3)\n", + "\n", + "# Ask the backend to retrieve the results\n", + "eng1.flush()\n", + "\n", + "# Obtain and print the probabilities of the states\n", + "prob_dict1 = eng1.backend.get_probabilities(qureg1)\n", + "print(\"Probabilities \", prob_dict1)\n" + ] + }, + { + "source": [ + "We can plot an histogram with the probabilities as well." + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "%matplotlib inline\n", + "from projectq.libs.hist import histogram\n", + "\n", + "histogram(eng1.backend, qureg1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ] +} diff --git a/examples/azure-quantum.ipynb b/examples/azure-quantum.ipynb new file mode 100644 index 000000000..bd126815d --- /dev/null +++ b/examples/azure-quantum.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2f4b2787-f008-4331-83ee-e743c64017aa", + "metadata": {}, + "source": [ + "# Azure Quantum Backend" + ] + }, + { + "cell_type": "markdown", + "id": "2bc9b3a4-23d8-4638-b875-386295010a3d", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- An Azure account with active subcription.\n", + "- An Azure Quantum workspace. ([How to create this?](https://docs.microsoft.com/en-us/azure/quantum/how-to-create-workspace?tabs=tabid-quick))\n", + "- Resource ID and location of Azure Quantum workspace.\n", + "- Install Azure Quantum dependencies for ProjectQ.\n", + "```\n", + "python -m pip install --user projectq[azure-quantum]\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "e8e87193-fc98-4718-a62d-cbbde662b82f", + "metadata": {}, + "source": [ + "## Load Imports\n", + "\n", + "Run following cell to load requried imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a239f2e4-dcc1-4c9f-b8db-0a775a9e2863", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from projectq import MainEngine\n", + "from projectq.ops import H, CX, All, Measure\n", + "from projectq.cengines import BasicMapperEngine\n", + "from projectq.backends import AzureQuantumBackend\n", + "from projectq.libs.hist import histogram" + ] + }, + { + "cell_type": "markdown", + "id": "e98cedd2-3d13-4b81-8776-59ef74a0ca2c", + "metadata": {}, + "source": [ + "## Initialize Azure Quantum backend\n", + "\n", + "Update `resource_id` and `location` of your Azure Quantum workspace in below cell and run to initialize Azure Quantum workspace.\n", + "\n", + "Following are valid `target_names`:\n", + "- ionq.simulator\n", + "- ionq.qpu\n", + "- quantinuum.hqs-lt-s1-apival\n", + "- quantinuum.hqs-lt-s1-sim\n", + "- quantinuum.hqs-lt-s1\n", + "\n", + "Flag `use_hardware` represents wheather or not use real hardware or just a simulator. If False regardless of the value of `target_name`, simulator will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e160b42-039e-4d5b-8333-7169bc77c57f", + "metadata": {}, + "outputs": [], + "source": [ + "azure_quantum_backend = AzureQuantumBackend(\n", + " use_hardware=False,\n", + " target_name='ionq.simulator',\n", + " resource_id=\"\", # resource id of workspace\n", + " location=\"\", # location of workspace\n", + " verbose=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "321876bd-dbbf-446d-9732-3627037d49f0", + "metadata": {}, + "source": [ + "## Create ProjectQ Engine\n", + "\n", + "Initialize ProjectQ `MainEngine` using Azure Quantum as backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fbc7650-fa8a-4047-8739-ee79aa2e6c2a", + "metadata": {}, + "outputs": [], + "source": [ + "mapper = BasicMapperEngine()\n", + "max_qubits = 3\n", + "\n", + "mapping = {}\n", + "for i in range(max_qubits):\n", + " mapping[i] = i\n", + "\n", + "mapper.current_mapping = mapping\n", + "\n", + "main_engine = MainEngine(\n", + " backend=azure_quantum_backend,\n", + " engine_list=[mapper]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d2665b04-0903-4176-b62d-b8fed5d5e041", + "metadata": {}, + "source": [ + "## Create circuit using ProjectQ lean syntax!\n", + "\n", + "Allocate qubits, build circuit and measure qubits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a8e84c3-cf1c-44db-80f1-e0a229a38eb9", + "metadata": {}, + "outputs": [], + "source": [ + "circuit = main_engine.allocate_qureg(3)\n", + "q0, q1, q2 = circuit\n", + "\n", + "H | q0\n", + "CX | (q0, q1)\n", + "CX | (q1, q2)\n", + "All(Measure) | circuit" + ] + }, + { + "cell_type": "markdown", + "id": "4a9901e8-ed74-4859-a004-8f1b9b72dd45", + "metadata": {}, + "source": [ + "## Run circuit and get result\n", + "\n", + "Flush down circuit to Azure Quantum backend and wait for results. It prints `job-id` for the reference (in case this operation timed-out). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9af5ea10-1edc-4fe9-9d8d-c021c196779d", + "metadata": {}, + "outputs": [], + "source": [ + "main_engine.flush()\n", + "\n", + "print(azure_quantum_backend.get_probabilities(circuit))" + ] + }, + { + "cell_type": "markdown", + "id": "b7dfc230-d4fc-4460-8acb-c42ebccc1659", + "metadata": {}, + "source": [ + "## Timed out! Re-run the circuit with retrieve_execution argument\n", + "\n", + "If job execution timed-out, use `retrieve_execution` argument retrive result instead of re-running the circuit. Use `job-id` from previous cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b05001b5-25ca-4dfd-a165-123ac7e83b38", + "metadata": {}, + "outputs": [], + "source": [ + "azure_quantum_backend = AzureQuantumBackend(\n", + " use_hardware=False,\n", + " target_name='ionq.simulator',\n", + " resource_id=\"\", # resource id of workspace\n", + " location=\"\", # location of workspace\n", + " retrieve_execution=\"\", # job-id of Azure Quantum job\n", + " verbose=True\n", + ")\n", + "\n", + "mapper = BasicMapperEngine()\n", + "max_qubits = 10\n", + "\n", + "mapping = {}\n", + "for i in range(max_qubits):\n", + " mapping[i] = i\n", + "\n", + "mapper.current_mapping = mapping\n", + "\n", + "main_engine = MainEngine(\n", + " backend=azure_quantum_backend,\n", + " engine_list=[mapper]\n", + ")\n", + "\n", + "circuit = main_engine.allocate_qureg(3)\n", + "q0, q1, q2 = circuit\n", + "\n", + "H | q0\n", + "CX | (q0, q1)\n", + "CX | (q1, q2)\n", + "All(Measure) | circuit\n", + "\n", + "main_engine.flush()\n", + "\n", + "print(azure_quantum_backend.get_probabilities(circuit))" + ] + }, + { + "cell_type": "markdown", + "id": "b658cb79-15c8-4e25-acbd-77788af08f9e", + "metadata": {}, + "source": [ + "# Plot Histogram\n", + "\n", + "Now, let's plot histogram with above result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68b3fa8d-a06d-4049-8350-d179304bf045", + "metadata": {}, + "outputs": [], + "source": [ + "histogram(main_engine.backend, circuit)\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:projectq] *", + "language": "python", + "name": "conda-env-projectq-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/bellpair_circuit.py b/examples/bellpair_circuit.py index 108e224fa..611e1ffe5 100755 --- a/examples/bellpair_circuit.py +++ b/examples/bellpair_circuit.py @@ -1,13 +1,23 @@ -from projectq import MainEngine -from projectq.backends import CircuitDrawer +# pylint: skip-file +"""Example implementation of a quantum circuit generating a Bell pair state.""" + +import matplotlib.pyplot as plt from teleport import create_bell_pair +from projectq import MainEngine +from projectq.backends import CircuitDrawer +from projectq.libs.hist import histogram +from projectq.setups.default import get_engine_list + # create a main compiler engine drawing_engine = CircuitDrawer() -eng = MainEngine(drawing_engine) +eng = MainEngine(engine_list=get_engine_list() + [drawing_engine]) -create_bell_pair(eng) +qb0, qb1 = create_bell_pair(eng) eng.flush() print(drawing_engine.get_latex()) + +histogram(eng.backend, [qb0, qb1]) +plt.show() diff --git a/examples/compiler_tutorial.ipynb b/examples/compiler_tutorial.ipynb index 92cab21d7..68419fc4e 100644 --- a/examples/compiler_tutorial.ipynb +++ b/examples/compiler_tutorial.ipynb @@ -231,7 +231,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Please have a look at the documention of the [restrictedgateset](http://projectq.readthedocs.io/en/latest/projectq.setups.html#module-projectq.setups.restrictedgateset) for details. The above compiler compiles the circuit to gates consisting of any single qubit gate, the `CNOT` and `Toffoli` gate. The gate specifications can either be a gate class, e.g., `Rz` or a specific instance `Rz(math.pi)`. A smaller but still universal gate set would be for example `CNOT` and `Rz, Ry`:" + "Please have a look at the documentation of the [restrictedgateset](http://projectq.readthedocs.io/en/latest/projectq.setups.html#module-projectq.setups.restrictedgateset) for details. The above compiler compiles the circuit to gates consisting of any single qubit gate, the `CNOT` and `Toffoli` gate. The gate specifications can either be a gate class, e.g., `Rz` or a specific instance `Rz(math.pi)`. A smaller but still universal gate set would be for example `CNOT` and `Rz, Ry`:" ] }, { @@ -292,7 +292,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As mentioned in the documention of [this setup](http://projectq.readthedocs.io/en/latest/projectq.setups.html#module-projectq.setups.restrictedgateset), one cannot (yet) choose an arbitrary gate set but there is a limited choice. If it doesn't work for a specified gate set, the compiler will either raises a `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...` which means that for this particular choice of gate set, one would be required to write more [decomposition rules](https://github.com/ProjectQ-Framework/ProjectQ/tree/develop/projectq/setups/decompositions) to make it work. Also for some choice of gate set there might be compiler engines producing more optimal code." + "As mentioned in the documentation of [this setup](http://projectq.readthedocs.io/en/latest/projectq.setups.html#module-projectq.setups.restrictedgateset), one cannot (yet) choose an arbitrary gate set but there is a limited choice. If it doesn't work for a specified gate set, the compiler will either raises a `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...` which means that for this particular choice of gate set, one would be required to write more [decomposition rules](https://github.com/ProjectQ-Framework/ProjectQ/tree/develop/projectq/setups/decompositions) to make it work. Also for some choice of gate set there might be compiler engines producing more optimal code." ] }, { diff --git a/examples/control_tester.py b/examples/control_tester.py new file mode 100755 index 000000000..3175404a7 --- /dev/null +++ b/examples/control_tester.py @@ -0,0 +1,92 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: skip-file + +"""Example of using the control state for control qubits.""" + +from projectq.cengines import MainEngine +from projectq.meta import Control +from projectq.ops import All, CtrlAll, Measure, X + + +def run_circuit(eng, circuit_num): + """Run the quantum circuit.""" + qubit = eng.allocate_qureg(2) + ctrl_fail = eng.allocate_qureg(3) + ctrl_success = eng.allocate_qureg(3) + + if circuit_num == 1: + with Control(eng, ctrl_fail): + X | qubit[0] + All(X) | ctrl_success + with Control(eng, ctrl_success): + X | qubit[1] + + elif circuit_num == 2: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state=CtrlAll.Zero): + X | qubit[0] + with Control(eng, ctrl_success, ctrl_state=CtrlAll.Zero): + X | qubit[1] + + elif circuit_num == 3: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state='101'): + X | qubit[0] + + X | ctrl_success[0] + X | ctrl_success[2] + with Control(eng, ctrl_success, ctrl_state='101'): + X | qubit[1] + + elif circuit_num == 4: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state=5): + X | qubit[0] + + X | ctrl_success[0] + X | ctrl_success[2] + with Control(eng, ctrl_success, ctrl_state=5): + X | qubit[1] + + All(Measure) | qubit + All(Measure) | ctrl_fail + All(Measure) | ctrl_success + eng.flush() + return qubit, ctrl_fail, ctrl_success + + +if __name__ == '__main__': + # Create a MainEngine with a unitary simulator backend + eng = MainEngine() + + # Run out quantum circuit + # 1 - Default behaviour of the control: all control qubits should be 1 + # 2 - Off-control: all control qubits should remain 0 + # 3 - Specific state given by a string + # 4 - Specific state given by an integer + + qubit, ctrl_fail, ctrl_success = run_circuit(eng, 4) + + # Measured value of the failed qubit should be 0 in all cases + print('The final value of the qubit with failed control is:') + print(int(qubit[0])) + print('with the state of control qubits are:') + print([int(qubit) for qubit in ctrl_fail], '\n') + + # Measured value of the success qubit should be 1 in all cases + print('The final value of the qubit with successful control is:') + print(int(qubit[1])) + print('with the state of control qubits are:') + print([int(qubit) for qubit in ctrl_success], '\n') diff --git a/examples/gate_zoo.py b/examples/gate_zoo.py index bce118994..684addfa4 100644 --- a/examples/gate_zoo.py +++ b/examples/gate_zoo.py @@ -1,16 +1,44 @@ +# pylint: skip-file + +"""Showcase most of the quantum gates available in ProjectQ.""" + import os import sys -import projectq.setups.default from projectq import MainEngine from projectq.backends import CircuitDrawer -from projectq.ops import * +from projectq.ops import ( + CNOT, + QFT, + All, + Barrier, + BasicMathGate, + C, + Entangle, + H, + Measure, + Ph, + QubitOperator, + Rx, + Ry, + Rz, + S, + SqrtSwap, + SqrtX, + Swap, + T, + Tensor, + TimeEvolution, + Toffoli, + X, + Y, + Z, + get_inverse, +) def zoo_profile(): - ''' - Generate and display the zoo of quantum gates. - ''' + """Generate and display the zoo of quantum gates.""" # create a main compiler engine with a drawing backend drawing_engine = CircuitDrawer() locations = {0: 1, 1: 2, 2: 0, 3: 3} @@ -23,14 +51,32 @@ def zoo_profile(): def add(x, y): return x, y + 1 + zoo = [ - (X, 3), (Y, 2), (Z, 0), (Rx(0.5), 2), (Ry(0.5), 1), - (Rz(0.5), 1), (Ph(0.5), 0), (S, 3), (T, 2), (H, 1), - (Toffoli, (0, 1, 2)), (Barrier, None), (Swap, (0, 3)), - (SqrtSwap, (0, 1)), (get_inverse(SqrtSwap), (2, 3)), - (SqrtX, 2), (C(get_inverse(SqrtX)), (0, 2)), (C(Ry(0.5)), (2, 3)), - (CNOT, (2, 1)), (Entangle, None), (te_gate, None), (QFT, None), - (Tensor(H), None), (BasicMathGate(add), (2, 3)), + (X, 3), + (Y, 2), + (Z, 0), + (Rx(0.5), 2), + (Ry(0.5), 1), + (Rz(0.5), 1), + (Ph(0.5), 0), + (S, 3), + (T, 2), + (H, 1), + (Toffoli, (0, 1, 2)), + (Barrier, None), + (Swap, (0, 3)), + (SqrtSwap, (0, 1)), + (get_inverse(SqrtSwap), (2, 3)), + (SqrtX, 2), + (C(get_inverse(SqrtX)), (0, 2)), + (C(Ry(0.5)), (2, 3)), + (CNOT, (2, 1)), + (Entangle, None), + (te_gate, None), + (QFT, None), + (Tensor(H), None), + (BasicMathGate(add), (2, 3)), (All(Measure), None), ] @@ -48,16 +94,16 @@ def add(x, y): # generate latex code to draw the circuit s = drawing_engine.get_latex() prefix = 'zoo' - with open('{}.tex'.format(prefix), 'w') as f: + with open(f'{prefix}.tex', 'w') as f: f.write(s) # compile latex source code and open pdf file - os.system('pdflatex {}.tex'.format(prefix)) - openfile('{}.pdf'.format(prefix)) + os.system(f'pdflatex {prefix}.tex') + openfile(f'{prefix}.pdf') def openfile(filename): - ''' + """ Open a file. Args: @@ -65,7 +111,7 @@ def openfile(filename): Return: bool: succeed if True. - ''' + """ platform = sys.platform if platform == "linux" or platform == "linux2": os.system('xdg-open %s' % filename) diff --git a/examples/grover.py b/examples/grover.py index b9823efb8..1d1511db1 100755 --- a/examples/grover.py +++ b/examples/grover.py @@ -1,13 +1,17 @@ +# pylint: skip-file + +"""Example implementation of Grover's algorithm.""" + import math from projectq import MainEngine -from projectq.ops import H, Z, X, Measure, All -from projectq.meta import Loop, Compute, Uncompute, Control +from projectq.meta import Compute, Control, Loop, Uncompute +from projectq.ops import All, H, Measure, X, Z def run_grover(eng, n, oracle): """ - Runs Grover's algorithm on n qubit using the provided quantum oracle. + Run Grover's algorithm on n qubit using the provided quantum oracle. Args: eng (MainEngine): Main compiler engine to run Grover on. @@ -25,7 +29,7 @@ def run_grover(eng, n, oracle): All(H) | x # number of iterations we have to run: - num_it = int(math.pi/4.*math.sqrt(1 << n)) + num_it = int(math.pi / 4.0 * math.sqrt(1 << n)) # prepare the oracle output qubit (the one that is flipped to indicate the # solution. start in state 1/sqrt(2) * (|0> - |1>) s.t. a bit-flip turns @@ -59,8 +63,10 @@ def run_grover(eng, n, oracle): def alternating_bits_oracle(eng, qubits, output): """ - Marks the solution string 1,0,1,0,...,0,1 by flipping the output qubit, - conditioned on qubits being equal to the alternating bit-string. + Alternating bit oracle. + + Mark the solution string 1,0,1,0,...,0,1 by flipping the output qubit, conditioned on qubits being equal to the + alternating bit-string. Args: eng (MainEngine): Main compiler engine the algorithm is being run on. diff --git a/examples/hws4.py b/examples/hws4.py index 12178fda5..4225480b2 100644 --- a/examples/hws4.py +++ b/examples/hws4.py @@ -1,12 +1,19 @@ +# pylint: skip-file + +"""Example of a 4-qubit phase function.""" + from projectq.cengines import MainEngine -from projectq.ops import All, H, X, Measure -from projectq.meta import Compute, Uncompute from projectq.libs.revkit import PhaseOracle +from projectq.meta import Compute, Uncompute +from projectq.ops import All, H, Measure, X + # phase function def f(a, b, c, d): + """Phase function.""" return (a and b) ^ (c and d) + eng = MainEngine() x1, x2, x3, x4 = qubits = eng.allocate_qureg(4) @@ -22,4 +29,4 @@ def f(a, b, c, d): eng.flush() -print("Shift is {}".format(8 * int(x4) + 4 * int(x3) + 2 * int(x2) + int(x1))) +print(f"Shift is {8 * int(x4) + 4 * int(x3) + 2 * int(x2) + int(x1)}") diff --git a/examples/hws6.py b/examples/hws6.py index c8becc20c..4aa9a30e4 100644 --- a/examples/hws6.py +++ b/examples/hws6.py @@ -1,21 +1,28 @@ -from projectq.cengines import MainEngine -from projectq.ops import All, H, X, CNOT, Measure -from projectq.meta import Compute, Uncompute, Dagger -from projectq.libs.revkit import PhaseOracle, PermutationOracle +# pylint: skip-file + +"""Example of a 6-qubit phase function.""" import revkit +from projectq.cengines import MainEngine +from projectq.libs.revkit import PermutationOracle, PhaseOracle +from projectq.meta import Compute, Dagger, Uncompute +from projectq.ops import All, H, Measure, X + + # phase function def f(a, b, c, d, e, f): + """Phase function.""" return (a and b) ^ (c and d) ^ (e and f) + # permutation pi = [0, 2, 3, 5, 7, 1, 4, 6] eng = MainEngine() qubits = eng.allocate_qureg(6) x = qubits[::2] # qubits on odd lines -y = qubits[1::2] # qubits on even lines +y = qubits[1::2] # qubits on even lines # circuit with Compute(eng): @@ -27,7 +34,7 @@ def f(a, b, c, d, e, f): with Compute(eng): with Dagger(eng): - PermutationOracle(pi, synth = revkit.dbs) | x + PermutationOracle(pi, synth=revkit.dbs) | x PhaseOracle(f) | qubits Uncompute(eng) @@ -36,4 +43,4 @@ def f(a, b, c, d, e, f): All(Measure) | qubits # measurement result -print("Shift is {}".format(sum(int(q) << i for i, q in enumerate(qubits)))) +print(f"Shift is {sum(int(q) << i for i, q in enumerate(qubits))}") diff --git a/examples/ibm.py b/examples/ibm.py index 05e042230..ce19e5491 100755 --- a/examples/ibm.py +++ b/examples/ibm.py @@ -1,12 +1,21 @@ +# pylint: skip-file + +"""Example of running a quantum circuit using the IBM QE APIs.""" + +import getpass + +import matplotlib.pyplot as plt + import projectq.setups.ibm -from projectq.backends import IBMBackend -from projectq.ops import Measure, Entangle, All from projectq import MainEngine +from projectq.backends import IBMBackend +from projectq.libs.hist import histogram +from projectq.ops import All, Entangle, Measure -def run_entangle(eng, num_qubits=5): +def run_entangle(eng, num_qubits=3): """ - Runs an entangling operation on the provided compiler engine. + Run an entangling operation on the provided compiler engine. Args: eng (MainEngine): Main compiler engine to use. @@ -28,18 +37,37 @@ def run_entangle(eng, num_qubits=5): eng.flush() # access the probabilities via the back-end: - results = eng.backend.get_probabilities(qureg) - for state in results: - print("Measured {} with p = {}.".format(state, results[state])) + # results = eng.backend.get_probabilities(qureg) + # for state in results: + # print(f"Measured {state} with p = {results[state]}.") + # or plot them directly: + histogram(eng.backend, qureg) + plt.show() # return one (random) measurement outcome. return [int(q) for q in qureg] if __name__ == "__main__": + # devices commonly available : + # ibmq_16_melbourne (15 qubit) + # ibmq_essex (5 qubit) + # ibmq_qasm_simulator (32 qubits) + # and plenty of other 5 qubits devices! + # + # To get a token, create a profile at: + # https://quantum-computing.ibm.com/ + # + device = None # replace by the IBM device name you want to use + token = None # replace by the token given by IBMQ + if token is None: + token = getpass.getpass(prompt='IBM Q token > ') + if device is None: + device = getpass.getpass(prompt='IBM device > ') # create main compiler engine for the IBM back-end - eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024, - verbose=False, device='ibmqx4'), - engine_list=projectq.setups.ibm.get_engine_list()) + eng = MainEngine( + IBMBackend(use_hardware=True, token=token, num_runs=1024, verbose=False, device=device), + engine_list=projectq.setups.ibm.get_engine_list(token=token, device=device), + ) # run the circuit and print the result print(run_entangle(eng)) diff --git a/examples/ibm16.py b/examples/ibm16.py deleted file mode 100755 index 894c644ce..000000000 --- a/examples/ibm16.py +++ /dev/null @@ -1,43 +0,0 @@ -import projectq.setups.ibm16 -from projectq.backends import IBMBackend -from projectq.ops import All, Entangle, Measure -from projectq import MainEngine - - -def run_test(eng): - """ - Runs a test circuit on the provided compiler engine. - - Args: - eng (MainEngine): Main compiler engine to use. - - Returns: - measurement (list): List of measurement outcomes. - """ - # allocate the quantum register to entangle - qureg = eng.allocate_qureg(8) - - Entangle | qureg - - # measure; should be all-0 or all-1 - All(Measure) | qureg - - # run the circuit - eng.flush() - - # access the probabilities via the back-end: - results = eng.backend.get_probabilities(qureg) - for state, probability in sorted(list(results.items())): - print("Measured {} with p = {}.".format(state, probability)) - - # return one (random) measurement outcome. - return [int(q) for q in qureg] - - -if __name__ == "__main__": - # create main compiler engine for the 16-qubit IBM back-end - eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024, - verbose=False, device='ibmqx5'), - engine_list=projectq.setups.ibm16.get_engine_list()) - # run the circuit and print the result - print(run_test(eng)) diff --git a/examples/ibmq_tutorial.ipynb b/examples/ibmq_tutorial.ipynb new file mode 100644 index 000000000..9721b71fb --- /dev/null +++ b/examples/ibmq_tutorial.ipynb @@ -0,0 +1,564 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Running ProjectQ code on IBM Q devices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "In this tutorial, we will see how to run code on IBM Q devices directly from within ProjectQ. All that is needed is an IBM Q Experience user account. To sign up, visit https://quantumexperience.ng.bluemix.net/.\n", + "\n", + "ProjectQ supports two IBM Q devices called `ibmqx4` and `ibmqx5` which feature 5 and 16 qubits, respectively. Let us start with entangling the qubits of the 5-qubit device:\n", + "\n", + "## Entangling 5 qubits\n", + "First, we import all necessary operations (`Entangle`, measurement), the back-end (`IBMBackend`), and the main compiler engine (`MainEngine`). The Entangle operation is defined as a Hadamard gate on the first qubit (creates an equal superposition of |0> and |1>), followed by controlled NOT gates acting on all other qubits controlled on the first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import projectq.setups.ibm\n", + "from projectq.backends import IBMBackend\n", + "from projectq.ops import Measure, Entangle, All\n", + "from projectq import MainEngine" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Next, we instantiate a main compiler engine using the IBM Q back-end and the predefined compiler engines which take care of the qubit placement, translation of operations, etc.:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,\n", + " verbose=False, device='ibmqx4'),\n", + " engine_list=projectq.setups.ibm.get_engine_list())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "If `use_hardware` is set to `False`, it will use the IBM Q simulator instead. `num_runs` specifies the number of samples to collect for statistics, `verbose=True` would output additional information which may be helpful for debugging, and the device parameter lets users choose between the two devices (\"ibmqx4\" and \"ibmqx5\").\n", + "\n", + "With our compiler set up, we can now allocate our qubits, entangle them, measure the outcome, and then flush the entire circuit down the compilation pipeline such that it is executed (and measurements are registered). Note that there are many jobs queued for execution on the IBM Q device and, as a result, our execution times out. We will learn how to retrieve our results despite this time out." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IBM QE user (e-mail) > haenert@phys.ethz.ch\n", + "IBM QE password > \n", + "Waiting for results. [Job ID: 5b557df2306393003b746da2]\n", + "Currently there are 49 jobs queued for execution on ibmqx4.\n", + "Currently there are 48 jobs queued for execution on ibmqx4.\n" + ] + }, + { + "ename": "Exception", + "evalue": "Timeout. The ID of your submitted job is 5b557df2306393003b746da2.\n raised in:\n' File \"/home/thomas/ProjectQ/projectq/backends/_ibm/_ibm_http_client.py\", line 174, in _get_result'\n' .format(execution_id))'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mq\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mq\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mqureg\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 21\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 22\u001b[0;31m \u001b[0mrun_entangle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0meng\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnum_qubits\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# run it\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mrun_entangle\u001b[0;34m(eng, num_qubits)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0;31m# run the circuit\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 12\u001b[0;31m \u001b[0meng\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mflush\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 13\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;31m# access the probabilities via the back-end:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/home/thomas/ProjectQ/projectq/cengines/_main.py\u001b[0m in \u001b[0;36mflush\u001b[0;34m(self, deallocate_qubits)\u001b[0m\n\u001b[1;32m 302\u001b[0m \u001b[0mqb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mactive_qubits\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 303\u001b[0m \u001b[0mqb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__del__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 304\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreceive\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mCommand\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mFlushGate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mWeakQubitRef\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/home/thomas/ProjectQ/projectq/cengines/_main.py\u001b[0m in \u001b[0;36mreceive\u001b[0;34m(self, command_list)\u001b[0m\n\u001b[1;32m 264\u001b[0m then send on)\n\u001b[1;32m 265\u001b[0m \"\"\"\n\u001b[0;32m--> 266\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcommand_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 267\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 268\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcommand_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/home/thomas/ProjectQ/projectq/cengines/_main.py\u001b[0m in \u001b[0;36msend\u001b[0;34m(self, command_list)\u001b[0m\n\u001b[1;32m 286\u001b[0m \"\\n\" + repr(last_line[-2]))\n\u001b[1;32m 287\u001b[0m \u001b[0mcompact_exception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 288\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mcompact_exception\u001b[0m \u001b[0;31m# use verbose=True for more info\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 289\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mflush\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdeallocate_qubits\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mException\u001b[0m: Timeout. The ID of your submitted job is 5b557df2306393003b746da2.\n raised in:\n' File \"/home/thomas/ProjectQ/projectq/backends/_ibm/_ibm_http_client.py\", line 174, in _get_result'\n' .format(execution_id))'" + ] + } + ], + "source": [ + "def run_entangle(eng, num_qubits):\n", + " # allocate a quantum register of 5 qubits\n", + " qureg = eng.allocate_qureg(num_qubits)\n", + "\n", + " # entangle the qureg\n", + " Entangle | qureg\n", + "\n", + " # measure; should be all-0 or all-1\n", + " All(Measure) | qureg\n", + "\n", + " # run the circuit\n", + " eng.flush()\n", + "\n", + " # access the probabilities via the back-end:\n", + " # results = eng.backend.get_probabilities(qureg)\n", + " # for state in results:\n", + " # print(f\"Measured {state} with p = {results[state]}.\")\n", + " # or plot them directly:\n", + " histogram(eng.backend, qureg)\n", + " plt.show()\n", + "\n", + " # return one (random) measurement outcome.\n", + " return [int(q) for q in qureg]\n", + "\n", + "run_entangle(eng, num_qubits=5) # run it" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Retrieving a timed-out execution\n", + "Sometimes, the queue is very long and the waiting times may exceed the limit of 5 minutes. In this case, ProjectQ will raise an exception which contains the job ID, as could be seen above, where the job ID was `5b557df2306393003b746da2`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "In order to still retrieve all results at a later point in time, one can simply re-run the entire program using a slightly modified back-end:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IBM QE user (e-mail) > haenert@phys.ethz.ch\n", + "IBM QE password > \n", + "Waiting for results. [Job ID: 5b557df2306393003b746da2]\n", + "Measured 00001 with p = 0.0185546875.\n", + "Measured 01101 with p = 0.00390625.\n", + "Measured 10001 with p = 0.0107421875.\n", + "Measured 11001 with p = 0.0029296875.\n", + "Measured 10101 with p = 0.0107421875.\n", + "Measured 11101 with p = 0.0419921875.\n", + "Measured 00011 with p = 0.005859375.\n", + "Measured 01011 with p = 0.00390625.\n", + "Measured 00111 with p = 0.0029296875.\n", + "Measured 01111 with p = 0.0107421875.\n", + "Measured 10011 with p = 0.0322265625.\n", + "Measured 11011 with p = 0.0419921875.\n", + "Measured 10111 with p = 0.056640625.\n", + "Measured 11111 with p = 0.2744140625.\n", + "Measured 00000 with p = 0.392578125.\n", + "Measured 01000 with p = 0.0029296875.\n", + "Measured 00100 with p = 0.01171875.\n", + "Measured 01100 with p = 0.0126953125.\n", + "Measured 10000 with p = 0.0009765625.\n", + "Measured 00010 with p = 0.009765625.\n", + "Measured 01010 with p = 0.0009765625.\n", + "Measured 00110 with p = 0.0029296875.\n", + "Measured 01110 with p = 0.0087890625.\n", + "Measured 10010 with p = 0.0029296875.\n", + "Measured 11010 with p = 0.0068359375.\n", + "Measured 10110 with p = 0.00390625.\n", + "Measured 11110 with p = 0.025390625.\n" + ] + }, + { + "data": { + "text/plain": [ + "[0, 0, 0, 0, 0]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,\n", + " verbose=False, device='ibmqx4',\n", + " retrieve_execution=\"5b557df2306393003b746da2\"), # provide job ID\n", + " engine_list=projectq.setups.ibm.get_engine_list())\n", + "\n", + "run_entangle(eng, num_qubits=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Entangling more qubits: Using ibmqx5\n", + "\n", + "If you have access to the 16-qubit device as well, you can also use ProjectQ to run your quantum programs on this device. ProjectQ contains a 2D grid mapper, which takes care of the mapping for you. We only have to change two things in order to use the 16-qubit chip as opposed to the 5-qubit chip:\n", + "\n", + "1) Import the new 16-qubit setup which contains the compiler engines for this device\n", + "\n", + "2) Modify the device parameter in the IBMBackend to \"ibmqx5\"\n", + "\n", + "Therefore, in order to entangle more than 5 qubits, we can simply write" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import projectq.setups.ibm16 # import setup which contains the grid mapper\n", + "eng = MainEngine(IBMBackend(use_hardware=True, num_runs=1024,\n", + " verbose=False, device='ibmqx5'), # use ibmqx5 now\n", + " engine_list=projectq.setups.ibm16.get_engine_list()) # note: ibm16 setup" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "and then re-run the example from before via `run_entangle(eng, num_qubits)`. If an execution times out, it can also be retrieved at a later point by providing the additional `retrieve_execution=\"execution_id\"` parameter to the IBMBackend (but this time with `device='ibmqx5'`)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IBM QE user (e-mail) > haenert@phys.ethz.ch\n", + "IBM QE password > \n", + "Waiting for results. [Job ID: 5b5580e0e291fd003ea62acf]\n", + "Currently there are 12 jobs queued for execution on ibmqx5.\n", + "Currently there are 12 jobs queued for execution on ibmqx5.\n", + "Measured 00000000 with p = 0.0234375.\n", + "Measured 00100000 with p = 0.017578125.\n", + "Measured 01000000 with p = 0.0234375.\n", + "Measured 01100000 with p = 0.0087890625.\n", + "Measured 00010000 with p = 0.013671875.\n", + "Measured 00110000 with p = 0.0126953125.\n", + "Measured 01010000 with p = 0.0146484375.\n", + "Measured 01110000 with p = 0.013671875.\n", + "Measured 00000010 with p = 0.013671875.\n", + "Measured 00100010 with p = 0.009765625.\n", + "Measured 01000010 with p = 0.0107421875.\n", + "Measured 01100010 with p = 0.0068359375.\n", + "Measured 00010010 with p = 0.0048828125.\n", + "Measured 00110010 with p = 0.0078125.\n", + "Measured 01010010 with p = 0.0068359375.\n", + "Measured 01110010 with p = 0.0078125.\n", + "Measured 00000100 with p = 0.001953125.\n", + "Measured 00100100 with p = 0.009765625.\n", + "Measured 01000100 with p = 0.0068359375.\n", + "Measured 01100100 with p = 0.0048828125.\n", + "Measured 00010100 with p = 0.005859375.\n", + "Measured 00110100 with p = 0.005859375.\n", + "Measured 01010100 with p = 0.001953125.\n", + "Measured 01110100 with p = 0.005859375.\n", + "Measured 00000110 with p = 0.001953125.\n", + "Measured 00100110 with p = 0.005859375.\n", + "Measured 01000110 with p = 0.00390625.\n", + "Measured 01100110 with p = 0.005859375.\n", + "Measured 00010110 with p = 0.0107421875.\n", + "Measured 00110110 with p = 0.0009765625.\n", + "Measured 01010110 with p = 0.001953125.\n", + "Measured 01110110 with p = 0.0029296875.\n", + "Measured 10000000 with p = 0.0009765625.\n", + "Measured 10100000 with p = 0.0087890625.\n", + "Measured 11000000 with p = 0.001953125.\n", + "Measured 11100000 with p = 0.0029296875.\n", + "Measured 10010000 with p = 0.0029296875.\n", + "Measured 10110000 with p = 0.001953125.\n", + "Measured 11010000 with p = 0.001953125.\n", + "Measured 11110000 with p = 0.0009765625.\n", + "Measured 10000010 with p = 0.0009765625.\n", + "Measured 10100010 with p = 0.0048828125.\n", + "Measured 11000010 with p = 0.0009765625.\n", + "Measured 11100010 with p = 0.0029296875.\n", + "Measured 10010010 with p = 0.001953125.\n", + "Measured 10110010 with p = 0.0087890625.\n", + "Measured 11010010 with p = 0.0009765625.\n", + "Measured 11110010 with p = 0.0009765625.\n", + "Measured 10000100 with p = 0.0009765625.\n", + "Measured 10100100 with p = 0.0009765625.\n", + "Measured 11000100 with p = 0.0048828125.\n", + "Measured 10010100 with p = 0.0048828125.\n", + "Measured 10110100 with p = 0.001953125.\n", + "Measured 11010100 with p = 0.0029296875.\n", + "Measured 11110100 with p = 0.001953125.\n", + "Measured 10000110 with p = 0.001953125.\n", + "Measured 10100110 with p = 0.001953125.\n", + "Measured 11100110 with p = 0.0029296875.\n", + "Measured 10010110 with p = 0.001953125.\n", + "Measured 10110110 with p = 0.001953125.\n", + "Measured 11110110 with p = 0.0048828125.\n", + "Measured 00000001 with p = 0.0029296875.\n", + "Measured 00100001 with p = 0.0029296875.\n", + "Measured 01000001 with p = 0.0029296875.\n", + "Measured 01100001 with p = 0.0009765625.\n", + "Measured 00110001 with p = 0.00390625.\n", + "Measured 01010001 with p = 0.005859375.\n", + "Measured 01110001 with p = 0.0009765625.\n", + "Measured 00000011 with p = 0.0029296875.\n", + "Measured 00100011 with p = 0.0029296875.\n", + "Measured 01000011 with p = 0.0009765625.\n", + "Measured 01100011 with p = 0.0009765625.\n", + "Measured 00010011 with p = 0.0029296875.\n", + "Measured 00000101 with p = 0.0029296875.\n", + "Measured 00100101 with p = 0.0029296875.\n", + "Measured 01000101 with p = 0.001953125.\n", + "Measured 01100101 with p = 0.0029296875.\n", + "Measured 00110101 with p = 0.001953125.\n", + "Measured 01010101 with p = 0.001953125.\n", + "Measured 01110101 with p = 0.0029296875.\n", + "Measured 00000111 with p = 0.0029296875.\n", + "Measured 01100111 with p = 0.0009765625.\n", + "Measured 00010111 with p = 0.001953125.\n", + "Measured 00110111 with p = 0.0009765625.\n", + "Measured 01010111 with p = 0.0009765625.\n", + "Measured 01110111 with p = 0.001953125.\n", + "Measured 10000001 with p = 0.00390625.\n", + "Measured 10100001 with p = 0.001953125.\n", + "Measured 11000001 with p = 0.0029296875.\n", + "Measured 11100001 with p = 0.0048828125.\n", + "Measured 10010001 with p = 0.0048828125.\n", + "Measured 10110001 with p = 0.0029296875.\n", + "Measured 11010001 with p = 0.001953125.\n", + "Measured 11110001 with p = 0.0029296875.\n", + "Measured 10000011 with p = 0.0029296875.\n", + "Measured 10100011 with p = 0.0048828125.\n", + "Measured 11000011 with p = 0.0048828125.\n", + "Measured 11100011 with p = 0.0029296875.\n", + "Measured 10010011 with p = 0.001953125.\n", + "Measured 10110011 with p = 0.001953125.\n", + "Measured 11010011 with p = 0.001953125.\n", + "Measured 11110011 with p = 0.0029296875.\n", + "Measured 10000101 with p = 0.005859375.\n", + "Measured 10100101 with p = 0.0107421875.\n", + "Measured 11000101 with p = 0.009765625.\n", + "Measured 11100101 with p = 0.0029296875.\n", + "Measured 10010101 with p = 0.0078125.\n", + "Measured 10110101 with p = 0.0068359375.\n", + "Measured 11010101 with p = 0.0078125.\n", + "Measured 11110101 with p = 0.00390625.\n", + "Measured 10000111 with p = 0.0078125.\n", + "Measured 10100111 with p = 0.005859375.\n", + "Measured 11000111 with p = 0.001953125.\n", + "Measured 11100111 with p = 0.0048828125.\n", + "Measured 10010111 with p = 0.0048828125.\n", + "Measured 10110111 with p = 0.001953125.\n", + "Measured 11010111 with p = 0.00390625.\n", + "Measured 11110111 with p = 0.0068359375.\n", + "Measured 00001000 with p = 0.0087890625.\n", + "Measured 00101000 with p = 0.017578125.\n", + "Measured 01001000 with p = 0.0107421875.\n", + "Measured 01101000 with p = 0.0146484375.\n", + "Measured 00011000 with p = 0.0048828125.\n", + "Measured 00111000 with p = 0.01171875.\n", + "Measured 01011000 with p = 0.0126953125.\n", + "Measured 01111000 with p = 0.0146484375.\n", + "Measured 00001010 with p = 0.009765625.\n", + "Measured 00101010 with p = 0.005859375.\n", + "Measured 01001010 with p = 0.0029296875.\n", + "Measured 01101010 with p = 0.017578125.\n", + "Measured 00011010 with p = 0.0087890625.\n", + "Measured 00111010 with p = 0.01171875.\n", + "Measured 01011010 with p = 0.0029296875.\n", + "Measured 01111010 with p = 0.00390625.\n", + "Measured 00001100 with p = 0.0068359375.\n", + "Measured 00101100 with p = 0.001953125.\n", + "Measured 01001100 with p = 0.005859375.\n", + "Measured 01101100 with p = 0.0078125.\n", + "Measured 00011100 with p = 0.005859375.\n", + "Measured 00111100 with p = 0.00390625.\n", + "Measured 01011100 with p = 0.00390625.\n", + "Measured 01111100 with p = 0.0068359375.\n", + "Measured 00001110 with p = 0.0029296875.\n", + "Measured 00101110 with p = 0.00390625.\n", + "Measured 01101110 with p = 0.0029296875.\n", + "Measured 00011110 with p = 0.0048828125.\n", + "Measured 00111110 with p = 0.00390625.\n", + "Measured 01011110 with p = 0.001953125.\n", + "Measured 01111110 with p = 0.0009765625.\n", + "Measured 10001000 with p = 0.001953125.\n", + "Measured 10101000 with p = 0.001953125.\n", + "Measured 11101000 with p = 0.00390625.\n", + "Measured 10011000 with p = 0.0048828125.\n", + "Measured 10111000 with p = 0.001953125.\n", + "Measured 11011000 with p = 0.001953125.\n", + "Measured 10001010 with p = 0.0029296875.\n", + "Measured 10101010 with p = 0.001953125.\n", + "Measured 11101010 with p = 0.001953125.\n", + "Measured 10011010 with p = 0.0009765625.\n", + "Measured 10111010 with p = 0.001953125.\n", + "Measured 11011010 with p = 0.001953125.\n", + "Measured 11111010 with p = 0.001953125.\n", + "Measured 10001100 with p = 0.0029296875.\n", + "Measured 10101100 with p = 0.00390625.\n", + "Measured 11001100 with p = 0.0009765625.\n", + "Measured 11101100 with p = 0.0009765625.\n", + "Measured 10011100 with p = 0.0029296875.\n", + "Measured 10111100 with p = 0.001953125.\n", + "Measured 10001110 with p = 0.005859375.\n", + "Measured 10101110 with p = 0.001953125.\n", + "Measured 11001110 with p = 0.0029296875.\n", + "Measured 11101110 with p = 0.001953125.\n", + "Measured 10011110 with p = 0.0029296875.\n", + "Measured 10111110 with p = 0.001953125.\n", + "Measured 11011110 with p = 0.001953125.\n", + "Measured 11111110 with p = 0.0029296875.\n", + "Measured 00001001 with p = 0.001953125.\n", + "Measured 00101001 with p = 0.0029296875.\n", + "Measured 01001001 with p = 0.0029296875.\n", + "Measured 01101001 with p = 0.0048828125.\n", + "Measured 00011001 with p = 0.0048828125.\n", + "Measured 01011001 with p = 0.0009765625.\n", + "Measured 01111001 with p = 0.00390625.\n", + "Measured 00001011 with p = 0.001953125.\n", + "Measured 00101011 with p = 0.0029296875.\n", + "Measured 01001011 with p = 0.001953125.\n", + "Measured 00011011 with p = 0.0009765625.\n", + "Measured 01111011 with p = 0.0009765625.\n", + "Measured 00001101 with p = 0.0009765625.\n", + "Measured 01001101 with p = 0.0009765625.\n", + "Measured 00011101 with p = 0.0009765625.\n", + "Measured 01011101 with p = 0.0029296875.\n", + "Measured 00001111 with p = 0.0009765625.\n", + "Measured 00101111 with p = 0.0029296875.\n", + "Measured 01001111 with p = 0.0009765625.\n", + "Measured 00011111 with p = 0.001953125.\n", + "Measured 00111111 with p = 0.0009765625.\n", + "Measured 01111111 with p = 0.0009765625.\n", + "Measured 10001001 with p = 0.0029296875.\n", + "Measured 10101001 with p = 0.00390625.\n", + "Measured 11001001 with p = 0.001953125.\n", + "Measured 11101001 with p = 0.0068359375.\n", + "Measured 10011001 with p = 0.001953125.\n", + "Measured 10111001 with p = 0.0029296875.\n", + "Measured 11011001 with p = 0.00390625.\n", + "Measured 11111001 with p = 0.0068359375.\n", + "Measured 10001011 with p = 0.0048828125.\n", + "Measured 10101011 with p = 0.00390625.\n", + "Measured 11001011 with p = 0.00390625.\n", + "Measured 11101011 with p = 0.0029296875.\n", + "Measured 10011011 with p = 0.0009765625.\n", + "Measured 10111011 with p = 0.001953125.\n", + "Measured 11011011 with p = 0.001953125.\n", + "Measured 11111011 with p = 0.001953125.\n", + "Measured 10001101 with p = 0.0009765625.\n", + "Measured 10101101 with p = 0.00390625.\n", + "Measured 11101101 with p = 0.0029296875.\n", + "Measured 10011101 with p = 0.00390625.\n", + "Measured 10111101 with p = 0.0078125.\n", + "Measured 11011101 with p = 0.005859375.\n", + "Measured 11111101 with p = 0.0048828125.\n", + "Measured 10001111 with p = 0.0009765625.\n", + "Measured 10101111 with p = 0.001953125.\n", + "Measured 11001111 with p = 0.001953125.\n", + "Measured 11101111 with p = 0.0078125.\n", + "Measured 10011111 with p = 0.0009765625.\n", + "Measured 10111111 with p = 0.005859375.\n", + "Measured 11011111 with p = 0.00390625.\n", + "Measured 11111111 with p = 0.0009765625.\n" + ] + }, + { + "data": { + "text/plain": [ + "[0, 0, 1, 0, 0, 0, 1, 0]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run_entangle(eng, num_qubits=8)" + ] + } + ], + "metadata": { + "kernelspec": { + "argv": [ + "python", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "Python 3", + "env": null, + "interrupt_mode": "signal", + "language": "python", + "metadata": null, + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + }, + "name": "ibmq_tutorial.ipynb" + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/ionq.ipynb b/examples/ionq.ipynb new file mode 100644 index 000000000..a324c35a9 --- /dev/null +++ b/examples/ionq.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# IonQ ProjectQ Backend Example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook will walk you through a basic example of using IonQ hardware to run ProjectQ circuits." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "The only requirement to run ProjectQ circuits on IonQ hardware is an IonQ API token.\n", + "\n", + "Once you have acquired a token, please try out the examples in this notebook!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage & Examples\n", + "\n", + "\n", + "**NOTE**: The `IonQBackend` expects an API key to be supplied via the `token` keyword argument to its constructor. If no token is directly provided, the backend will prompt you for one.\n", + "\n", + "The `IonQBackend` currently supports two device types:\n", + "* `ionq_simulator`: IonQ's simulator backend.\n", + "* `ionq_qpu`: IonQ's QPU backend.\n", + "\n", + "To view the latest list of available devices, you can run the `show_devices` function in the `projectq.backends._ionq._ionq_http_client` module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# NOTE: Optional! This ignores warnings emitted from ProjectQ imports.\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# Import ProjectQ and IonQBackend objects, the setup an engine\n", + "import projectq.setups.ionq\n", + "from projectq import MainEngine\n", + "from projectq.backends import IonQBackend\n", + "\n", + "# REPLACE WITH YOUR API TOKEN\n", + "token = 'your api token'\n", + "device = 'ionq_simulator'\n", + "\n", + "# Create an IonQBackend\n", + "backend = IonQBackend(\n", + " use_hardware=True,\n", + " token=token,\n", + " num_runs=200,\n", + " device=device,\n", + ")\n", + "\n", + "# Make sure to get an engine_list from the ionq setup module\n", + "engine_list = projectq.setups.ionq.get_engine_list(\n", + " token=token,\n", + " device=device,\n", + ")\n", + "\n", + "# Create a ProjectQ engine\n", + "engine = MainEngine(backend, engine_list)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example — Bell Pair" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Notes about running circuits on IonQ backends\n", + "Circuit building and visualization should feel identical to building a circuit using any other backend with ProjectQ. \n", + "\n", + "That said, there are a couple of things to note when running on IonQ backends: \n", + " \n", + "- IonQ backends do not allow arbitrary unitaries, mid-circuit resets or measurements, or multi-experiment jobs. In practice, this means using `reset`, `initialize`, `u` `u1`, `u2`, `u3`, `cu`, `cu1`, `cu2`, or `cu3` gates will throw an exception on submission, as will measuring mid-circuit, and submmitting jobs with multiple experiments.\n", + "- While `barrier` is allowed for organizational and visualization purposes, the IonQ compiler does not see it as a compiler directive." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's make a simple Bell pair circuit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import gates to apply:\n", + "from projectq.ops import All, H, CNOT, Measure\n", + "\n", + "# Allocate two qubits\n", + "circuit = engine.allocate_qureg(2)\n", + "qubit0, qubit1 = circuit\n", + "\n", + "# add gates — here we're creating a simple bell pair\n", + "H | qubit0\n", + "CNOT | (qubit0, qubit1)\n", + "All(Measure) | circuit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the bell pair circuit\n", + "Now, let's run our bell pair circuit on the simulator. \n", + "\n", + "All that is left is to call the main engine's `flush` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Flush the circuit, which will submit the circuit to IonQ's API for processing\n", + "engine.flush()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# If all went well, we can view results from the circuit execution\n", + "probabilities = engine.backend.get_probabilities(circuit)\n", + "print(probabilities)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also use the built-in matplotlib support to plot the histogram of results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# show a plot of result probabilities\n", + "import matplotlib.pyplot as plt\n", + "from projectq.libs.hist import histogram\n", + "\n", + "# Show the histogram\n", + "histogram(engine.backend, circuit)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example - Bernstein-Vazirani\n", + "\n", + "\n", + "For our second example, let's build a Bernstein-Vazirani circuit and run it on a real IonQ quantum computer.\n", + "\n", + "Rather than manually building the BV circuit every time, we'll create a method that can build one for any oracle $s$, and any register size." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from projectq.ops import All, H, Z, CX, Measure\n", + "\n", + "\n", + "def oracle(qureg, input_size, s_int):\n", + " \"\"\"Apply the 'oracle'.\"\"\"\n", + "\n", + " s = ('{0:0' + str(input_size) + 'b}').format(s_int)\n", + "\n", + " for bit in range(input_size):\n", + " if s[input_size - 1 - bit] == '1':\n", + " CX | (qureg[bit], qureg[input_size])\n", + "\n", + " \n", + "def run_bv_circuit(eng, s_int, input_size):\n", + " \"\"\"build the Bernstein-Vazirani circuit\n", + " \n", + " Args:\n", + " eng (MainEngine): A ProjectQ engine instance with an IonQBackend.\n", + " s_int (int): value of s, the secret bitstring, as an integer\n", + " input_size (int): size of the input register, \n", + " i.e. the number of (qu)bits to use for the binary \n", + " representation of s\n", + " \"\"\"\n", + " # confirm the bitstring of S is what we think it should be\n", + " s = ('{0:0' + str(input_size) + 'b}').format(s_int)\n", + " print('s: ', s)\n", + " \n", + " # We need a circuit with `input_size` qubits, plus one ancilla qubit\n", + " # Also need `input_size` classical bits to write the output to\n", + " circuit = eng.allocate_qureg(input_size + 1)\n", + " qubits = circuit[:-1]\n", + " output = circuit[input_size]\n", + "\n", + " # put ancilla in state |-⟩\n", + " H | output\n", + " Z | output\n", + " \n", + " # Apply Hadamard gates before querying the oracle\n", + " All(H) | qubits\n", + " \n", + " # Apply the inner-product oracle\n", + " oracle(circuit, input_size, s_int)\n", + "\n", + " # Apply Hadamard gates after querying the oracle\n", + " All(H) | qubits\n", + "\n", + " # Measurement\n", + " All(Measure) | qubits\n", + "\n", + " return qubits\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's use that method to create a BV circuit to submit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# Run a BV circuit:\n", + "s_int = 3\n", + "input_size = 3\n", + "\n", + "circuit = run_bv_circuit(engine, s_int, input_size)\n", + "engine.flush()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Time to run it on an IonQ QPU!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create an IonQBackend set to use the 'ionq_qpu' device\n", + "device = 'ionq_qpu'\n", + "backend = IonQBackend(\n", + " use_hardware=True,\n", + " token=token,\n", + " num_runs=100,\n", + " device=device,\n", + ")\n", + "\n", + "# Make sure to get an engine_list from the ionq setup module\n", + "engine_list = projectq.setups.ionq.get_engine_list(\n", + " token=token,\n", + " device=device,\n", + ")\n", + "\n", + "# Create a ProjectQ engine\n", + "engine = MainEngine(backend, engine_list)\n", + "\n", + "# Setup another BV circuit\n", + "circuit = run_bv_circuit(engine, s_int, input_size)\n", + "\n", + "# Run the circuit!\n", + "engine.flush()\n", + "\n", + "# Show the histogram\n", + "histogram(engine.backend, circuit)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because QPU time is a limited resource, QPU jobs are handled in a queue and may take a while to complete. The IonQ backend accounts for this delay by providing basic attributes which may be used to tweak the behavior of the backend while it waits on job results: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create an IonQ backend with custom job fetch/wait settings\n", + "backend = IonQBackend(\n", + " token=token,\n", + " device=device,\n", + " num_runs=100,\n", + " use_hardware=True,\n", + " # Number of times to check for results before giving up\n", + " num_retries=3000,\n", + " # The number of seconds to wait between attempts\n", + " interval=1,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "name": "python379jvsc74a57bd083bb9cfe1c33ba3c1386f3a99c53663f4ea55973353f0ef3c6be0ff58dd42d14", + "display_name": "Python 3.7.9 64-bit ('projectq': pyenv)" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/ionq.py b/examples/ionq.py new file mode 100644 index 000000000..472f45281 --- /dev/null +++ b/examples/ionq.py @@ -0,0 +1,86 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: skip-file + +"""Example of a basic entangling operation using an IonQBackend.""" + +import getpass + +import matplotlib.pyplot as plt + +import projectq.setups.ionq +from projectq import MainEngine +from projectq.backends import IonQBackend +from projectq.libs.hist import histogram +from projectq.ops import All, Entangle, Measure + + +def run_entangle(eng, num_qubits=3): + """ + Run an entangling operation on the provided compiler engine. + + Args: + eng (MainEngine): Main compiler engine to use. + num_qubits (int): Number of qubits to entangle. + + Returns: + measurement (list): List of measurement outcomes. + """ + # allocate the quantum register to entangle + qureg = eng.allocate_qureg(num_qubits) + + # entangle the qureg + Entangle | qureg + + # measure; should be all-0 or all-1 + All(Measure) | qureg + + # run the circuit + eng.flush() + + # access the probabilities via the back-end: + # results = eng.backend.get_probabilities(qureg) + # for state in results: + # print(f"Measured {state} with p = {results[state]}.") + # or plot them directly: + histogram(eng.backend, qureg) + plt.show() + + # return one (random) measurement outcome. + return [int(q) for q in qureg] + + +if __name__ == '__main__': + token = None + device = None + if token is None: + token = getpass.getpass(prompt='IonQ apiKey > ') + if device is None: + device = input('IonQ device > ') + + # create an IonQBackend + backend = IonQBackend( + use_hardware=True, + token=token, + num_runs=200, + verbose=True, + device=device, + ) + engine_list = projectq.setups.ionq.get_engine_list( + token=token, + device=device, + ) + engine = MainEngine(backend, engine_list) + # run the circuit and print the result + print(run_entangle(engine)) diff --git a/examples/ionq_bv.py b/examples/ionq_bv.py new file mode 100644 index 000000000..5cbe839fa --- /dev/null +++ b/examples/ionq_bv.py @@ -0,0 +1,91 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: skip-file + +"""Example of a basic Bernstein-Vazirani circuit using an IonQBackend.""" + +import getpass +import random + +import matplotlib.pyplot as plt + +import projectq.setups.ionq +from projectq import MainEngine +from projectq.backends import IonQBackend +from projectq.libs.hist import histogram +from projectq.ops import CX, All, Barrier, H, Measure, Z + + +def oracle(qureg, input_size, s): + """Apply the 'oracle'.""" + for bit in range(input_size): + if s[input_size - 1 - bit] == '1': + CX | (qureg[bit], qureg[input_size]) + + +def run_bv_circuit(eng, input_size, s_int): + """Run the quantum circuit.""" + s = f"{s_int:0{input_size}b}" + print("Secret string: ", s) + print("Number of qubits: ", str(input_size + 1)) + circuit = eng.allocate_qureg(input_size + 1) + All(H) | circuit + Z | circuit[input_size] + + Barrier | circuit + + oracle(circuit, input_size, s) + + Barrier | circuit + + qubits = circuit[:input_size] + All(H) | qubits + All(Measure) | qubits + eng.flush() + + # return a random answer from our results + histogram(eng.backend, qubits) + plt.show() + + # return a random answer from our results + probabilities = eng.backend.get_probabilities(qubits) + random_answer = random.choice(list(probabilities.keys())) + print("Probability of getting correct string: ", probabilities[s[::-1]]) + return [int(s) for s in random_answer] + + +if __name__ == '__main__': + token = None + device = None + if token is None: + token = getpass.getpass(prompt='IonQ apiKey > ') + if device is None: + device = input('IonQ device > ') + + # create main compiler engine for the IonQ back-end + backend = IonQBackend( + use_hardware=True, + token=token, + num_runs=1, + verbose=False, + device=device, + ) + engine_list = projectq.setups.ionq.get_engine_list( + token=token, + device=device, + ) + engine = MainEngine(backend, engine_list) + + # run the circuit and print the result + print(run_bv_circuit(engine, 3, 3)) diff --git a/examples/ionq_half_adder.py b/examples/ionq_half_adder.py new file mode 100644 index 000000000..2efbe7a45 --- /dev/null +++ b/examples/ionq_half_adder.py @@ -0,0 +1,91 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: skip-file + +"""Example of a basic 'half-adder' circuit using an IonQBackend.""" + +import getpass +import random + +import matplotlib.pyplot as plt + +import projectq.setups.default +import projectq.setups.ionq +from projectq import MainEngine +from projectq.backends import IonQBackend +from projectq.libs.hist import histogram +from projectq.ops import CNOT, All, Barrier, Measure, Toffoli, X + + +def run_half_adder(eng): + """Run the half-adder circuit.""" + # allocate the quantum register to entangle + circuit = eng.allocate_qureg(4) + qubit1, qubit2, qubit3, qubit4 = circuit + result_qubits = [qubit3, qubit4] + + # X gates on the first two qubits + All(X) | [qubit1, qubit2] + + # Barrier + Barrier | circuit + + # Cx gates + CNOT | (qubit1, qubit3) + CNOT | (qubit2, qubit3) + + # CCNOT + Toffoli | (qubit1, qubit2, qubit4) + + # Barrier + Barrier | circuit + + # Measure result qubits + All(Measure) | result_qubits + + # Flush the circuit (this submits a job to the IonQ API) + eng.flush() + + # Show the histogram + histogram(eng.backend, result_qubits) + plt.show() + + # return a random answer from our results + probabilities = eng.backend.get_probabilities(result_qubits) + random_answer = random.choice(list(probabilities.keys())) + return [int(s) for s in random_answer] + + +if __name__ == '__main__': + token = None + device = None + if token is None: + token = getpass.getpass(prompt='IonQ apiKey > ') + if device is None: + device = input('IonQ device > ') + + backend = IonQBackend( + use_hardware=True, + token=token, + num_runs=200, + verbose=True, + device=device, + ) + engine_list = projectq.setups.ionq.get_engine_list( + token=token, + device=device, + ) + engine = MainEngine(backend, engine_list) + # run the circuit and print the result + print(run_half_adder(engine)) diff --git a/examples/mapper_tutorial.ipynb b/examples/mapper_tutorial.ipynb index 5027819b2..b5c50881d 100644 --- a/examples/mapper_tutorial.ipynb +++ b/examples/mapper_tutorial.ipynb @@ -361,9 +361,9 @@ "\n", "# Remember that allocate_qubit returns a quantum register (Qureg) of size 1,\n", "# so accessing the qubit requires qubit[0]\n", - "print(\"This logical qubit0 has the unique ID: {}\".format(qubit0[0].id))\n", - "print(\"This logical qubit1 has the unique ID: {}\".format(qubit1[0].id))\n", - "print(\"This logical qubit2 has the unique ID: {}\".format(qubit2[0].id)) \n", + "print(f\"This logical qubit0 has the unique ID: {qubit0[0].id}\")\n", + "print(f\"This logical qubit1 has the unique ID: {qubit1[0].id}\")\n", + "print(f\"This logical qubit2 has the unique ID: {qubit2[0].id}\") \n", "\n", "eng4.flush()" ] @@ -400,9 +400,9 @@ "# current_mapping is a dictionary with keys being the\n", "# logical qubit ids and the values being the physical ids on\n", "# on the linear chain\n", - "print(\"Physical location of qubit0: {}\".format(current_mapping[qubit0[0].id]))\n", - "print(\"Physical location of qubit1: {}\".format(current_mapping[qubit1[0].id]))\n", - "print(\"Physical location of qubit2: {}\".format(current_mapping[qubit2[0].id]))" + "print(f\"Physical location of qubit0: {current_mapping[qubit0[0].id]}\")\n", + "print(f\"Physical location of qubit1: {current_mapping[qubit1[0].id]}\")\n", + "print(f\"Physical location of qubit2: {current_mapping[qubit2[0].id]}\")" ] }, { @@ -435,9 +435,9 @@ "eng4.flush()\n", "# Get current mapping:\n", "current_mapping = eng4.mapper.current_mapping\n", - "print(\"\\nPhysical location of qubit0: {}\".format(current_mapping[qubit0[0].id]))\n", - "print(\"Physical location of qubit1: {}\".format(current_mapping[qubit1[0].id]))\n", - "print(\"Physical location of qubit2: {}\".format(current_mapping[qubit2[0].id]))" + "print(f\"\\nPhysical location of qubit0: {current_mapping[qubit0[0].id]}\")\n", + "print(f\"Physical location of qubit1: {current_mapping[qubit1[0].id]}\")\n", + "print(f\"Physical location of qubit2: {current_mapping[qubit2[0].id]}\")" ] }, { @@ -491,7 +491,7 @@ "Measure | qubit0\n", "eng5.flush()\n", "\n", - "print(\"qubit0 was measured in state: {}\".format(int(qubit0)))" + "print(f\"qubit0 was measured in state: {int(qubit0)}\")" ] }, { diff --git a/examples/quantum_random_numbers.py b/examples/quantum_random_numbers.py index 9ccb679c8..34dc61acc 100755 --- a/examples/quantum_random_numbers.py +++ b/examples/quantum_random_numbers.py @@ -1,5 +1,9 @@ -from projectq.ops import H, Measure +# pylint: skip-file + +"""Example of a simple quantum random number generator.""" + from projectq import MainEngine +from projectq.ops import H, Measure # create a main compiler engine eng = MainEngine() @@ -15,4 +19,4 @@ eng.flush() # print the result: -print("Measured: {}".format(int(q1))) +print(f"Measured: {int(q1)}") diff --git a/examples/quantum_random_numbers_ibm.py b/examples/quantum_random_numbers_ibm.py index a8289a68d..2cbf35b93 100755 --- a/examples/quantum_random_numbers_ibm.py +++ b/examples/quantum_random_numbers_ibm.py @@ -1,11 +1,14 @@ +# pylint: skip-file + +"""Example of a simple quantum random number generator using IBM's API.""" + import projectq.setups.ibm -from projectq.ops import H, Measure from projectq import MainEngine from projectq.backends import IBMBackend +from projectq.ops import H, Measure # create a main compiler engine -eng = MainEngine(IBMBackend(), - engine_list=projectq.setups.ibm.get_engine_list()) +eng = MainEngine(IBMBackend(), engine_list=projectq.setups.ibm.get_engine_list()) # allocate one qubit q1 = eng.allocate_qubit() @@ -18,4 +21,4 @@ eng.flush() # print the result: -print("Measured: {}".format(int(q1))) +print(f"Measured: {int(q1)}") diff --git a/examples/shor.py b/examples/shor.py index 949f804c1..cb5d41bd8 100755 --- a/examples/shor.py +++ b/examples/shor.py @@ -1,32 +1,36 @@ -from __future__ import print_function +# pylint: skip-file + +"""Example implementation of Shor's algorithm.""" import math import random import sys from fractions import Fraction + try: from math import gcd except ImportError: from fractions import gcd -from builtins import input - import projectq.libs.math import projectq.setups.decompositions -from projectq.backends import Simulator, ResourceCounter -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, LocalOptimizer, - MainEngine, TagRemover) -from projectq.libs.math import (AddConstant, AddConstantModN, - MultiplyByConstantModN) +from projectq.backends import ResourceCounter, Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + LocalOptimizer, + MainEngine, + TagRemover, +) +from projectq.libs.math import AddConstant, AddConstantModN, MultiplyByConstantModN from projectq.meta import Control -from projectq.ops import (All, BasicMathGate, get_inverse, H, Measure, QFT, R, - Swap, X) +from projectq.ops import QFT, All, BasicMathGate, H, Measure, R, Swap, X, get_inverse def run_shor(eng, N, a, verbose=False): """ - Runs the quantum subroutine of Shor's algorithm for factoring. + Run the quantum subroutine of Shor's algorithm for factoring. Args: eng (MainEngine): Main compiler engine to use. @@ -57,7 +61,7 @@ def run_shor(eng, N, a, verbose=False): # perform inverse QFT --> Rotations conditioned on previous outcomes for i in range(k): if measurements[i]: - R(-math.pi/(1 << (k - i))) | ctrl_qubit + R(-math.pi / (1 << (k - i))) | ctrl_qubit H | ctrl_qubit # and measure @@ -68,16 +72,15 @@ def run_shor(eng, N, a, verbose=False): X | ctrl_qubit if verbose: - print("\033[95m{}\033[0m".format(measurements[k]), end="") + print(f"\033[95m{measurements[k]}\033[0m", end="") sys.stdout.flush() All(Measure) | x # turn the measured values into a number in [0,1) - y = sum([(measurements[2 * n - 1 - i]*1. / (1 << (i + 1))) - for i in range(2 * n)]) + y = sum((measurements[2 * n - 1 - i] * 1.0 / (1 << (i + 1))) for i in range(2 * n)) # continued fraction expansion to get denominator (the period?) - r = Fraction(y).limit_denominator(N-1).denominator + r = Fraction(y).limit_denominator(N - 1).denominator # return the (potential) period return r @@ -86,6 +89,7 @@ def run_shor(eng, N, a, verbose=False): # Filter function, which defines the gate set for the first optimization # (don't decompose QFTs and iQFTs to make cancellation easier) def high_level_gates(eng, cmd): + """Filter high-level gates.""" g = cmd.gate if g == QFT or get_inverse(g) == QFT or g == Swap: return True @@ -102,32 +106,34 @@ def high_level_gates(eng, cmd): if __name__ == "__main__": # build compilation engine list resource_counter = ResourceCounter() - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - compilerengines = [AutoReplacer(rule_set), - InstructionFilter(high_level_gates), - TagRemover(), - LocalOptimizer(3), - AutoReplacer(rule_set), - TagRemover(), - LocalOptimizer(3), - resource_counter] + rule_set = DecompositionRuleSet(modules=[projectq.libs.math, projectq.setups.decompositions]) + compilerengines = [ + AutoReplacer(rule_set), + InstructionFilter(high_level_gates), + TagRemover(), + LocalOptimizer(3), + AutoReplacer(rule_set), + TagRemover(), + LocalOptimizer(3), + resource_counter, + ] # make the compiler and run the circuit on the simulator backend eng = MainEngine(Simulator(), compilerengines) # print welcome message and ask the user for the number to factor - print("\n\t\033[37mprojectq\033[0m\n\t--------\n\tImplementation of Shor" - "\'s algorithm.", end="") + print( + "\n\t\033[37mprojectq\033[0m\n\t--------\n\tImplementation of Shor" "\'s algorithm.", + end="", + ) N = int(input('\n\tNumber to factor: ')) - print("\n\tFactoring N = {}: \033[0m".format(N), end="") + print(f"\n\tFactoring N = {N}: \033[0m", end="") # choose a base at random: - a = int(random.random()*N) + a = int(random.random() * N) if not gcd(a, N) == 1: - print("\n\n\t\033[92mOoops, we were lucky: Chose non relative prime" - " by accident :)") - print("\tFactor: {}\033[0m".format(gcd(a, N))) + print("\n\n\t\033[92mOoops, we were lucky: Chose non relative prime" " by accident :)") + print(f"\tFactor: {gcd(a, N)}\033[0m") else: # run the quantum subroutine r = run_shor(eng, N, a, True) @@ -138,14 +144,11 @@ def high_level_gates(eng, cmd): apowrhalf = pow(a, r >> 1, N) f1 = gcd(apowrhalf + 1, N) f2 = gcd(apowrhalf - 1, N) - if ((not f1 * f2 == N) and f1 * f2 > 1 and - int(1. * N / (f1 * f2)) * f1 * f2 == N): - f1, f2 = f1*f2, int(N/(f1*f2)) + if (not f1 * f2 == N) and f1 * f2 > 1 and int(1.0 * N / (f1 * f2)) * f1 * f2 == N: + f1, f2 = f1 * f2, int(N / (f1 * f2)) if f1 * f2 == N and f1 > 1 and f2 > 1: - print("\n\n\t\033[92mFactors found :-) : {} * {} = {}\033[0m" - .format(f1, f2, N)) + print(f"\n\n\t\033[92mFactors found :-) : {f1} * {f2} = {N}\033[0m") else: - print("\n\n\t\033[91mBad luck: Found {} and {}\033[0m".format(f1, - f2)) + print(f"\n\n\t\033[91mBad luck: Found {f1} and {f2}\033[0m") print(resource_counter) # print resource usage diff --git a/examples/simulator_tutorial.ipynb b/examples/simulator_tutorial.ipynb index f95ac662b..437011ad8 100644 --- a/examples/simulator_tutorial.ipynb +++ b/examples/simulator_tutorial.ipynb @@ -301,14 +301,14 @@ "\n", "# Amplitude will be 1 as Hadamard gate is not yet executed on the simulator backend\n", "# We forgot the eng.flush()!\n", - "print(\"Amplitude saved in amp_before: {}\".format(amp_before))\n", + "print(f\"Amplitude saved in amp_before: {amp_before}\")\n", "\n", "eng.flush() # Makes sure that all the gates are sent to the backend and executed\n", "\n", "amp_after = eng.backend.get_amplitude('00', qubit + qubit2)\n", "\n", "# Amplitude will be 1/sqrt(2) as Hadamard gate was executed on the simulator backend\n", - "print(\"Amplitude saved in amp_after: {}\".format(amp_after))\n", + "print(f\"Amplitude saved in amp_after: {amp_after}\")\n", "\n", "# To avoid triggering the warning of deallocating qubits which are in a superposition\n", "Measure | qubit\n", @@ -363,11 +363,11 @@ "prob00 = eng.backend.get_probability('00', qureg)\n", "prob_second_0 = eng.backend.get_probability('0', [qureg[1]])\n", "\n", - "print(\"Probability to measure 11: {}\".format(prob11))\n", - "print(\"Probability to measure 00: {}\".format(prob00))\n", - "print(\"Probability to measure 01: {}\".format(prob01))\n", - "print(\"Probability to measure 10: {}\".format(prob10))\n", - "print(\"Probability that second qubit is in state 0: {}\".format(prob_second_0))\n", + "print(f\"Probability to measure 11: {prob11}\")\n", + "print(f\"Probability to measure 00: {prob00}\")\n", + "print(f\"Probability to measure 01: {prob01}\")\n", + "print(f\"Probability to measure 10: {prob10}\")\n", + "print(f\"Probability that second qubit is in state 0: {prob_second_0}\")\n", "\n", "All(Measure) | qureg" ] @@ -406,11 +406,11 @@ "eng.flush()\n", "op0 = QubitOperator('Z0') # Z applied to qureg[0] tensor identity on qureg[1], qureg[2]\n", "expectation = eng.backend.get_expectation_value(op0, qureg)\n", - "print(\"Expectation value = = {}\".format(expectation))\n", + "print(f\"Expectation value = = {expectation}\")\n", "\n", "op_sum = QubitOperator('Z0 X1') - 0.5 * QubitOperator('X1')\n", "expectation2 = eng.backend.get_expectation_value(op_sum, qureg)\n", - "print(\"Expectation value = = {}\".format(expectation2))\n", + "print(f\"Expectation value = = {expectation2}\")\n", "\n", "All(Measure) | qureg # To avoid error message of deallocating qubits in a superposition" ] @@ -452,7 +452,7 @@ "Measure | qureg[1]\n", "eng.flush() # required such that all above gates are executed before accessing the measurement result\n", "\n", - "print(\"First qubit measured in state: {} and second qubit in state: {}\".format(int(qureg[0]), int(qureg[1])))" + "print(f\"First qubit measured in state: {int(qureg[0])} and second qubit in state: {int(qureg[1])}\")" ] }, { @@ -496,7 +496,7 @@ "prob_0 = eng.backend.get_probability('0', [qureg[1]])\n", "\n", "print(\"After forcing a measurement outcome of the first qubit to be 0, \\n\"\n", - " \"the second qubit is in state 0 with probability: {}\".format(prob_0))" + " f\"the second qubit is in state 0 with probability: {prob_0}\")" ] }, { @@ -573,18 +573,18 @@ "# also if the Python simulator is used, one should make a deepcopy:\n", "mapping, wavefunction = copy.deepcopy(eng.backend.cheat())\n", "\n", - "print(\"The full wavefunction is: {}\".format(wavefunction))\n", + "print(f\"The full wavefunction is: {wavefunction}\")\n", "# Note: qubit1 is a qureg of length 1, i.e. a list containing one qubit objects, therefore the\n", "# unique qubit id can be accessed via qubit1[0].id\n", - "print(\"qubit1 has bit-location {}\".format(mapping[qubit1[0].id]))\n", - "print(\"qubit2 has bit-location {}\".format(mapping[qubit2[0].id]))\n", + "print(f\"qubit1 has bit-location {mapping[qubit1[0].id]}\")\n", + "print(f\"qubit2 has bit-location {mapping[qubit2[0].id]}\")\n", "\n", "# Suppose we want to know the amplitude of the qubit1 in state 0 and qubit2 in state 1:\n", "state = 0 + (1 << mapping[qubit2[0].id])\n", - "print(\"Amplitude of state qubit1 in state 0 and qubit2 in state 1: {}\".format(wavefunction[state]))\n", + "print(f\"Amplitude of state qubit1 in state 0 and qubit2 in state 1: {wavefunction[state]}\")\n", "# If one only wants to access one (or a few) amplitudes, get_amplitude provides an easier interface:\n", "amplitude = eng.backend.get_amplitude('01', qubit1 + qubit2)\n", - "print(\"Accessing same amplitude but using get_amplitude instead: {}\".format(amplitude))\n", + "print(f\"Accessing same amplitude but using get_amplitude instead: {amplitude}\")\n", "\n", "All(Measure) | qubit1 + qubit2 # In order to not deallocate a qubit in a superposition state" ] diff --git a/examples/spectral_measurement.ipynb b/examples/spectral_measurement.ipynb new file mode 100644 index 000000000..825d2c7a5 --- /dev/null +++ b/examples/spectral_measurement.ipynb @@ -0,0 +1,467 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Algorithm for Spectral Measurement with Lower Gate Count\n", + "\n", + "This tutorial shows how to implement the algorithm introduced in the following paper:\n", + "\n", + "**Quantum Algorithm for Spectral Measurement with Lower Gate Count**\n", + "by David Poulin, Alexei Kitaev, Damian S. Steiger, Matthew B. Hastings, Matthias Troyer\n", + "[Phys. Rev. Lett. 121, 010501 (2018)](https://doi.org/10.1103/PhysRevLett.121.010501)\n", + "([arXiv:1711.11025](https://arxiv.org/abs/1711.11025))\n", + "\n", + "For details please see the above paper. The implementation in ProjectQ is discussed in the PhD thesis of Damian S. Steiger (soon available online). A more detailed discussion will be uploaded soon.\n", + "Here we only show a small part of the paper, namely the implementation of W and how it can be used with iterative phase estimation to obtain eigenvalues and eigenstates." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from copy import deepcopy\n", + "import math\n", + "\n", + "import scipy.sparse.linalg as spsl\n", + "\n", + "import projectq\n", + "from projectq.backends import Simulator\n", + "from projectq.meta import Compute, Control, Dagger, Uncompute\n", + "from projectq.ops import All, H, Measure, Ph, QubitOperator, R, StatePreparation, X, Z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. Let's use a simple Hamiltonian acting on 3 qubits for which we want to know the eigenvalues:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 3\n", + "\n", + "hamiltonian = QubitOperator()\n", + "hamiltonian += QubitOperator(\"X0\", -1/12.)\n", + "hamiltonian += QubitOperator(\"X1\", -1/12.)\n", + "hamiltonian += QubitOperator(\"X2\", -1/12.)\n", + "hamiltonian += QubitOperator(\"Z0 Z1\", -1/12.)\n", + "hamiltonian += QubitOperator(\"Z0 Z2\", -1/12.)\n", + "hamiltonian += QubitOperator(\"\", 7/12.)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this quantum algorithm, we need to normalize the hamiltonian:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "hamiltonian_norm = 0.\n", + "for term in hamiltonian.terms:\n", + " hamiltonian_norm += abs(hamiltonian.terms[term])\n", + "normalized_hamiltonian = deepcopy(hamiltonian)\n", + "normalized_hamiltonian /= hamiltonian_norm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**1.** We implement a short helper function which uses the ProjectQ simulator to numerically calculate some eigenvalues and eigenvectors of Hamiltonians stored in ProjectQ's `QubitOperator` in order to check our implemenation of the quantum algorithm. This function is particularly fast because it doesn't need to build the matrix of the hamiltonian but instead uses implicit matrix vector multiplication by using our simulator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def get_eigenvalue_and_eigenvector(n_sites, hamiltonian, k, which='SA'):\n", + " \"\"\"\n", + " Returns k eigenvalues and eigenvectors of the hamiltonian.\n", + " \n", + " Args:\n", + " n_sites(int): Number of qubits/sites in the hamiltonian\n", + " hamiltonian(QubitOperator): QubitOperator representating the Hamiltonian\n", + " k: num of eigenvalue and eigenvector pairs (see spsl.eigsh k)\n", + " which: see spsl.eigsh which\n", + " \n", + " \"\"\"\n", + " def mv(v):\n", + " eng = projectq.MainEngine(backend=Simulator(), engine_list=[])\n", + " qureg = eng.allocate_qureg(n_sites)\n", + " eng.flush()\n", + " eng.backend.set_wavefunction(v, qureg)\n", + " eng.backend.apply_qubit_operator(hamiltonian, qureg)\n", + " order, output = deepcopy(eng.backend.cheat())\n", + " for i in order:\n", + " assert i == order[i]\n", + " eng.backend.set_wavefunction([1]+[0]*(2**n_sites-1), qureg)\n", + " return output\n", + "\n", + " A = spsl.LinearOperator((2**n_sites,2**n_sites), matvec=mv)\n", + "\n", + " eigenvalues, eigenvectormatrix = spsl.eigsh(A, k=k, which=which)\n", + " eigenvectors = []\n", + " for i in range(k):\n", + " eigenvectors.append(list(eigenvectormatrix[:, i]))\n", + " return eigenvalues, eigenvectors" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use this function to find the 4 lowest eigenstates of the normalized hamiltonian:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.29217007 0.36634371 0.5 0.57417364]\n" + ] + } + ], + "source": [ + "eigenvalues, eigenvectors = get_eigenvalue_and_eigenvector(\n", + " n_sites=num_qubits,\n", + " hamiltonian=normalized_hamiltonian,\n", + " k=4)\n", + "print(eigenvalues)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that the eigenvalues are all positive as required (otherwise increase identity term in hamiltonian)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**2.** Let's define the W operator:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def W(eng, individual_terms, initial_wavefunction, ancilla_qubits, system_qubits):\n", + " \"\"\"\n", + " Applies the W operator as defined in arXiv:1711.11025.\n", + " \n", + " Args:\n", + " eng(MainEngine): compiler engine\n", + " individual_terms(list): list of individual unitary\n", + " QubitOperators. It applies\n", + " individual_terms[0] if ancilla\n", + " qubits are in state |0> where\n", + " ancilla_qubits[0] is the least\n", + " significant bit.\n", + " initial_wavefunction: Initial wavefunction of the ancilla qubits\n", + " ancilla_qubits(Qureg): ancilla quantum register in state |0>\n", + " system_qubits(Qureg): system quantum register\n", + " \"\"\"\n", + " # Apply V:\n", + " for ancilla_state in range(len(individual_terms)):\n", + " with Compute(eng):\n", + " for bit_pos in range(len(ancilla_qubits)):\n", + " if not (ancilla_state >> bit_pos) & 1:\n", + " X | ancilla_qubits[bit_pos]\n", + " with Control(eng, ancilla_qubits):\n", + " individual_terms[ancilla_state] | system_qubits\n", + " Uncompute(eng)\n", + " # Apply S: 1) Apply B^dagger\n", + " with Compute(eng):\n", + " with Dagger(eng):\n", + " StatePreparation(initial_wavefunction) | ancilla_qubits\n", + " # Apply S: 2) Apply I-2|0><0|\n", + " with Compute(eng):\n", + " All(X) | ancilla_qubits\n", + " with Control(eng, ancilla_qubits[:-1]):\n", + " Z | ancilla_qubits[-1]\n", + " Uncompute(eng)\n", + " # Apply S: 3) Apply B\n", + " Uncompute(eng)\n", + " # Could also be omitted and added when calculating the eigenvalues:\n", + " Ph(math.pi) | system_qubits[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**3.** For testing this algorithm, let's initialize the qubits in a superposition state of the lowest and second lowest eigenstate of the hamiltonian:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "eng = projectq.MainEngine()\n", + "system_qubits = eng.allocate_qureg(num_qubits)\n", + "\n", + "# Create a normalized equal superposition of the two eigenstates for numerical testing:\n", + "initial_state_norm =0.\n", + "initial_state = [i+j for i,j in zip(eigenvectors[0], eigenvectors[1])]\n", + "for amplitude in initial_state:\n", + " initial_state_norm += abs(amplitude)**2\n", + "normalized_initial_state = [amp / math.sqrt(initial_state_norm) for amp in initial_state]\n", + "\n", + "#initialize system qubits in this state:\n", + "StatePreparation(normalized_initial_state) | system_qubits" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**4.** Split the normalized_hamiltonian into individual terms and build the wavefunction for the ancilla qubits:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "individual_terms = []\n", + "initial_ancilla_wavefunction = []\n", + "for term in normalized_hamiltonian.terms:\n", + " coefficient = normalized_hamiltonian.terms[term]\n", + " initial_ancilla_wavefunction.append(math.sqrt(abs(coefficient)))\n", + " if coefficient < 0:\n", + " individual_terms.append(QubitOperator(term, -1))\n", + " else:\n", + " individual_terms.append(QubitOperator(term))\n", + "\n", + "# Calculate the number of ancilla qubits required and pad\n", + "# the ancilla wavefunction with zeros:\n", + "num_ancilla_qubits = int(math.ceil(math.log(len(individual_terms), 2)))\n", + "required_padding = 2**num_ancilla_qubits - len(initial_ancilla_wavefunction)\n", + "initial_ancilla_wavefunction.extend([0]*required_padding)\n", + "\n", + "# Initialize ancillas by applying B\n", + "ancillas = eng.allocate_qureg(num_ancilla_qubits)\n", + "StatePreparation(initial_ancilla_wavefunction) | ancillas" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**5.** Perform an iterative phase estimation of the unitary W to collapse to one of the eigenvalues of the `normalized_hamiltonian`:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Semiclassical iterative phase estimation\n", + "bits_of_precision = 8\n", + "pe_ancilla = eng.allocate_qubit()\n", + "\n", + "measurements = [0] * bits_of_precision\n", + "\n", + "for k in range(bits_of_precision):\n", + " H | pe_ancilla\n", + " with Control(eng, pe_ancilla):\n", + " for i in range(2**(bits_of_precision-k-1)):\n", + " W(eng=eng,\n", + " individual_terms=individual_terms,\n", + " initial_wavefunction=initial_ancilla_wavefunction,\n", + " ancilla_qubits=ancillas,\n", + " system_qubits=system_qubits)\n", + "\n", + " #inverse QFT using one qubit\n", + " for i in range(k):\n", + " if measurements[i]:\n", + " R(-math.pi/(1 << (k - i))) | pe_ancilla\n", + "\n", + " H | pe_ancilla\n", + " Measure | pe_ancilla\n", + " eng.flush()\n", + " measurements[k] = int(pe_ancilla)\n", + " # put the ancilla in state |0> again\n", + " if measurements[k]:\n", + " X | pe_ancilla\n", + "\n", + "est_phase = sum(\n", + " [(measurements[bits_of_precision - 1 - i]*1. / (1 << (i + 1)))\n", + " for i in range(bits_of_precision)])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We measured 0.203125 corresponding to energy 0.290284677254\n" + ] + } + ], + "source": [ + "print(f\"We measured {est_phase} corresponding to energy {math.cos(2*math.pi*est_phase)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**6.** We measured the lowest eigenstate. You can verify that this happens with 50% probability as we chose our initial state to have 50% overlap with the ground state. As the paper notes, the `system_qubits` are not in an eigenstate and one can easily test that using our simulator to get the energy of the current state:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.33236578253447085" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng.backend.get_expectation_value(normalized_hamiltonian, system_qubits)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**7.** As explained in the paper, one can change this state into an eigenstate by undoing the `StatePreparation` of the ancillas and then by measuring if the ancilla qubits in are state 0. The paper says that this should be the case with 50% probability. So let's check this (we require an ancilla to measure this):" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.5004522593645913" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "with Dagger(eng):\n", + " StatePreparation(initial_ancilla_wavefunction) | ancillas\n", + "measure_qb = eng.allocate_qubit()\n", + "with Compute(eng):\n", + " All(X) | ancillas\n", + "with Control(eng, ancillas):\n", + " X | measure_qb\n", + "Uncompute(eng)\n", + "eng.flush()\n", + "eng.backend.get_probability('1', measure_qb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, we would measure 1 (corresponding to the ancilla qubits in state 0) with probability 50% as explained in the paper. Let's assume we measure 1, then we can easily check that we are in an eigenstate of the `normalized_hamiltonian` by numerically calculating its energy:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.29263140625433537" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "eng.backend.collapse_wavefunction(measure_qb, [1])\n", + "eng.backend.get_expectation_value(normalized_hamiltonian, system_qubits)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Indeed we are in the ground state of the `normalized_hamiltonian`. Have a look at the paper on how to recover, when the ancilla qubits are not in state 0." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/teleport.py b/examples/teleport.py index d5f24ef76..e662aef49 100755 --- a/examples/teleport.py +++ b/examples/teleport.py @@ -1,13 +1,18 @@ -from projectq.ops import All, CNOT, H, Measure, Rz, X, Z +# pylint: skip-file + +"""Example of a quantum teleportation circuit.""" + from projectq import MainEngine -from projectq.meta import Dagger, Control +from projectq.meta import Control, Dagger +from projectq.ops import CNOT, H, Measure, Rz, X, Z def create_bell_pair(eng): - """ - Returns a Bell-pair (two qubits in state :math:`|A\rangle \otimes |B - \rangle = \frac 1{\sqrt 2} \left( |0\rangle\otimes|0\rangle + |1\rangle - \otimes|1\rangle \right)`). + r""" + Create a Bell pair state with two qubits. + + Returns a Bell-pair (two qubits in state :math:`|A\rangle \otimes |B \rangle = \frac 1{\sqrt 2} \left( + |0\rangle\otimes|0\rangle + |1\rangle \otimes|1\rangle \right)`). Args: eng (MainEngine): MainEngine from which to allocate the qubits. @@ -26,18 +31,16 @@ def create_bell_pair(eng): def run_teleport(eng, state_creation_function, verbose=False): """ - Runs quantum teleportation on the provided main compiler engine. + Run quantum teleportation on the provided main compiler engine. - Creates a state from |0> using the state_creation_function, teleports this - state to Bob who then tries to uncompute his qubit using the inverse of - the state_creation_function. If successful, deleting the qubit won't raise - an error in the underlying Simulator back-end (else it will). + Creates a state from |0> using the state_creation_function, teleports this state to Bob who then tries to + uncompute his qubit using the inverse of the state_creation_function. If successful, deleting the qubit won't + raise an error in the underlying Simulator back-end (else it will). Args: eng (MainEngine): Main compiler engine to run the circuit on. - state_creation_function (function): Function which accepts the main - engine and a qubit in state |0>, which it then transforms to the - state that Alice would like to send to Bob. + state_creation_function (function): Function which accepts the main engine and a qubit in state |0>, which it + then transforms to the state that Alice would like to send to Bob. verbose (bool): If True, info messages will be printed. """ @@ -61,7 +64,7 @@ def run_teleport(eng, state_creation_function, verbose=False): Measure | b1 msg_to_bob = [int(psi), int(b1)] if verbose: - print("Alice is sending the message {} to Bob.".format(msg_to_bob)) + print(f"Alice is sending the message {msg_to_bob} to Bob.") # Bob may have to apply up to two operation depending on the message sent # by Alice: @@ -93,6 +96,7 @@ def run_teleport(eng, state_creation_function, verbose=False): # we would like to send. Bob can then try to uncompute it and, if he # arrives back at |0>, we know that the teleportation worked. def create_state(eng, qb): + """Create a quantum state.""" H | qb Rz(1.21) | qb diff --git a/examples/teleport_circuit.py b/examples/teleport_circuit.py index 6910d0582..f626f595e 100755 --- a/examples/teleport_circuit.py +++ b/examples/teleport_circuit.py @@ -1,8 +1,12 @@ -from projectq import MainEngine -from projectq.backends import CircuitDrawer +# pylint: skip-file + +"""Example if drawing of a quantum teleportation circuit.""" import teleport +from projectq import MainEngine +from projectq.backends import CircuitDrawer + if __name__ == "__main__": # create a main compiler engine with a simulator backend: drawing_engine = CircuitDrawer() @@ -12,7 +16,7 @@ # we just want to draw the teleportation circuit def create_state(eng, qb): - pass + """Create a quantum state.""" # run the teleport and then, let Bob try to uncompute his qubit: teleport.run_teleport(eng, create_state, verbose=False) diff --git a/examples/unitary_simulator.py b/examples/unitary_simulator.py new file mode 100644 index 000000000..d91b0ede9 --- /dev/null +++ b/examples/unitary_simulator.py @@ -0,0 +1,81 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: skip-file + +"""Example of using the UnitarySimulator.""" + + +import numpy as np + +from projectq.backends import UnitarySimulator +from projectq.cengines import MainEngine +from projectq.meta import Control +from projectq.ops import QFT, All, CtrlAll, Measure, X + + +def run_circuit(eng, n_qubits, circuit_num, gate_after_measure=False): + """Run a quantum circuit demonstrating the capabilities of the UnitarySimulator.""" + qureg = eng.allocate_qureg(n_qubits) + + if circuit_num == 1: + All(X) | qureg + elif circuit_num == 2: + X | qureg[0] + with Control(eng, qureg[:2]): + All(X) | qureg[2:] + elif circuit_num == 3: + with Control(eng, qureg[:2], ctrl_state=CtrlAll.Zero): + All(X) | qureg[2:] + elif circuit_num == 4: + QFT | qureg + + eng.flush() + All(Measure) | qureg + + if gate_after_measure: + QFT | qureg + eng.flush() + All(Measure) | qureg + + +def main(): + """Definition of the main function of this example.""" + # Create a MainEngine with a unitary simulator backend + eng = MainEngine(backend=UnitarySimulator()) + + n_qubits = 3 + + # Run out quantum circuit + # 1 - circuit applying X on all qubits + # 2 - circuit applying an X gate followed by a controlled-X gate + # 3 - circuit applying a off-controlled-X gate + # 4 - circuit applying a QFT on all qubits (QFT will get decomposed) + run_circuit(eng, n_qubits, 3, gate_after_measure=True) + + # Output the unitary transformation of the circuit + print('The unitary of the circuit is:') + print(eng.backend.unitary) + + # Output the final state of the qubits (assuming they all start in state |0>) + print('The final state of the qubits is:') + print(eng.backend.unitary @ np.array([1] + ([0] * (2**n_qubits - 1)))) + print('\n') + + # Show the unitaries separated by measurement: + for history in eng.backend.history: + print('Previous unitary is: \n', history, '\n') + + +if __name__ == '__main__': + main() diff --git a/examples/variational_quantum_eigensolver.ipynb b/examples/variational_quantum_eigensolver.ipynb new file mode 100644 index 000000000..b893a8698 --- /dev/null +++ b/examples/variational_quantum_eigensolver.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Implementation of a Variational Quantum Eigensolver (VQE).\n", + "\n", + "The example shown here is from the paper \"Scalable Quantum Simulation of\n", + "Molecular Energies\" by P.J.J. O'Malley et al. [arXiv:1512.06860v2](https://arxiv.org/abs/1512.06860v2)\n", + "(Note that only the latest arXiv version contains the correct coefficients of\n", + " the Hamiltonian)\n", + "\n", + "Eq. 2 of the paper shows the functional which one needs to minimize and Eq. 3\n", + "shows the coupled cluster ansatz for the trial wavefunction (using the unitary\n", + "coupled cluster approach). The Hamiltonian is given in Eq. 1. The coefficients\n", + "can be found in Table 1. Note that both the ansatz and the Hamiltonian can be\n", + "calculated using FermiLib which is a library for simulating quantum systems\n", + "on top of ProjectQ." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import projectq\n", + "from projectq.ops import All, Measure, QubitOperator, TimeEvolution, X\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from scipy.optimize import minimize_scalar\n", + "\n", + "# Data from paper (arXiv:1512.06860v2) table 1: R, I, Z0, Z1, Z0Z1, X0X1, Y0Y1\n", + "raw_data_table_1 = [\n", + " [0.20, 2.8489, 0.5678, -1.4508, 0.6799, 0.0791, 0.0791],\n", + " [0.25, 2.1868, 0.5449, -1.2870, 0.6719, 0.0798, 0.0798],\n", + " [0.30, 1.7252, 0.5215, -1.1458, 0.6631, 0.0806, 0.0806],\n", + " [0.35, 1.3827, 0.4982, -1.0226, 0.6537, 0.0815, 0.0815],\n", + " [0.40, 1.1182, 0.4754, -0.9145, 0.6438, 0.0825, 0.0825],\n", + " [0.45, 0.9083, 0.4534, -0.8194, 0.6336, 0.0835, 0.0835],\n", + " [0.50, 0.7381, 0.4325, -0.7355, 0.6233, 0.0846, 0.0846],\n", + " [0.55, 0.5979, 0.4125, -0.6612, 0.6129, 0.0858, 0.0858],\n", + " [0.60, 0.4808, 0.3937, -0.5950, 0.6025, 0.0870, 0.0870],\n", + " [0.65, 0.3819, 0.3760, -0.5358, 0.5921, 0.0883, 0.0883],\n", + " [0.70, 0.2976, 0.3593, -0.4826, 0.5818, 0.0896, 0.0896],\n", + " [0.75, 0.2252, 0.3435, -0.4347, 0.5716, 0.0910, 0.0910],\n", + " [0.80, 0.1626, 0.3288, -0.3915, 0.5616, 0.0925, 0.0925],\n", + " [0.85, 0.1083, 0.3149, -0.3523, 0.5518, 0.0939, 0.0939],\n", + " [0.90, 0.0609, 0.3018, -0.3168, 0.5421, 0.0954, 0.0954],\n", + " [0.95, 0.0193, 0.2895, -0.2845, 0.5327, 0.0970, 0.0970],\n", + " [1.00, -0.0172, 0.2779, -0.2550, 0.5235, 0.0986, 0.0986],\n", + " [1.05, -0.0493, 0.2669, -0.2282, 0.5146, 0.1002, 0.1002],\n", + " [1.10, -0.0778, 0.2565, -0.2036, 0.5059, 0.1018, 0.1018],\n", + " [1.15, -0.1029, 0.2467, -0.1810, 0.4974, 0.1034, 0.1034],\n", + " [1.20, -0.1253, 0.2374, -0.1603, 0.4892, 0.1050, 0.1050],\n", + " [1.25, -0.1452, 0.2286, -0.1413, 0.4812, 0.1067, 0.1067],\n", + " [1.30, -0.1629, 0.2203, -0.1238, 0.4735, 0.1083, 0.1083],\n", + " [1.35, -0.1786, 0.2123, -0.1077, 0.4660, 0.1100, 0.1100],\n", + " [1.40, -0.1927, 0.2048, -0.0929, 0.4588, 0.1116, 0.1116],\n", + " [1.45, -0.2053, 0.1976, -0.0792, 0.4518, 0.1133, 0.1133],\n", + " [1.50, -0.2165, 0.1908, -0.0666, 0.4451, 0.1149, 0.1149],\n", + " [1.55, -0.2265, 0.1843, -0.0549, 0.4386, 0.1165, 0.1165],\n", + " [1.60, -0.2355, 0.1782, -0.0442, 0.4323, 0.1181, 0.1181],\n", + " [1.65, -0.2436, 0.1723, -0.0342, 0.4262, 0.1196, 0.1196],\n", + " [1.70, -0.2508, 0.1667, -0.0251, 0.4204, 0.1211, 0.1211],\n", + " [1.75, -0.2573, 0.1615, -0.0166, 0.4148, 0.1226, 0.1226],\n", + " [1.80, -0.2632, 0.1565, -0.0088, 0.4094, 0.1241, 0.1241],\n", + " [1.85, -0.2684, 0.1517, -0.0015, 0.4042, 0.1256, 0.1256],\n", + " [1.90, -0.2731, 0.1472, 0.0052, 0.3992, 0.1270, 0.1270],\n", + " [1.95, -0.2774, 0.1430, 0.0114, 0.3944, 0.1284, 0.1284],\n", + " [2.00, -0.2812, 0.1390, 0.0171, 0.3898, 0.1297, 0.1297],\n", + " [2.05, -0.2847, 0.1352, 0.0223, 0.3853, 0.1310, 0.1310],\n", + " [2.10, -0.2879, 0.1316, 0.0272, 0.3811, 0.1323, 0.1323],\n", + " [2.15, -0.2908, 0.1282, 0.0317, 0.3769, 0.1335, 0.1335],\n", + " [2.20, -0.2934, 0.1251, 0.0359, 0.3730, 0.1347, 0.1347],\n", + " [2.25, -0.2958, 0.1221, 0.0397, 0.3692, 0.1359, 0.1359],\n", + " [2.30, -0.2980, 0.1193, 0.0432, 0.3655, 0.1370, 0.1370],\n", + " [2.35, -0.3000, 0.1167, 0.0465, 0.3620, 0.1381, 0.1381],\n", + " [2.40, -0.3018, 0.1142, 0.0495, 0.3586, 0.1392, 0.1392],\n", + " [2.45, -0.3035, 0.1119, 0.0523, 0.3553, 0.1402, 0.1402],\n", + " [2.50, -0.3051, 0.1098, 0.0549, 0.3521, 0.1412, 0.1412],\n", + " [2.55, -0.3066, 0.1078, 0.0572, 0.3491, 0.1422, 0.1422],\n", + " [2.60, -0.3079, 0.1059, 0.0594, 0.3461, 0.1432, 0.1432],\n", + " [2.65, -0.3092, 0.1042, 0.0614, 0.3433, 0.1441, 0.1441],\n", + " [2.70, -0.3104, 0.1026, 0.0632, 0.3406, 0.1450, 0.1450],\n", + " [2.75, -0.3115, 0.1011, 0.0649, 0.3379, 0.1458, 0.1458],\n", + " [2.80, -0.3125, 0.0997, 0.0665, 0.3354, 0.1467, 0.1467],\n", + " [2.85, -0.3135, 0.0984, 0.0679, 0.3329, 0.1475, 0.1475]]\n", + "\n", + "\n", + "def variational_quantum_eigensolver(theta, hamiltonian):\n", + " \"\"\"\n", + " Args:\n", + " theta (float): variational parameter for ansatz wavefunction\n", + " hamiltonian (QubitOperator): Hamiltonian of the system\n", + " Returns:\n", + " energy of the wavefunction for parameter theta\n", + " \"\"\"\n", + " # Create a ProjectQ compiler with a simulator as a backend\n", + " eng = projectq.MainEngine()\n", + " # Allocate 2 qubits in state |00>\n", + " wavefunction = eng.allocate_qureg(2)\n", + " # Initialize the Hartree Fock state |01>\n", + " X | wavefunction[0]\n", + " # build the operator for ansatz wavefunction\n", + " ansatz_op = QubitOperator('X0 Y1')\n", + " # Apply the unitary e^{-i * ansatz_op * t}\n", + " TimeEvolution(theta, ansatz_op) | wavefunction\n", + " # flush all gates\n", + " eng.flush()\n", + " # Calculate the energy.\n", + " # The simulator can directly return expectation values, while on a\n", + " # real quantum devices one would have to measure each term of the\n", + " # Hamiltonian.\n", + " energy = eng.backend.get_expectation_value(hamiltonian, wavefunction)\n", + " # Measure in order to return to return to a classical state\n", + " # (as otherwise the simulator will give an error)\n", + " All(Measure) | wavefunction\n", + " return energy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAEWCAYAAABxMXBSAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvNQv5yAAAIABJREFUeJzt3Xl4FFXWwOHfSSDgKItCQJAl4Ma+JUSjIquIA4qooLgMODKoqJ+OMw7MOKO4jBvOqDCO+wKKG7jhLiCISHAABVRUVETZZAdZBLKc749bnXSS7k6FpNOd9Hmfp59UV1VXneru1Ol7b9W9oqoYY4wxfiXFOgBjjDFViyUOY4wxZWKJwxhjTJlY4jDGGFMmljiMMcaUiSUOY4wxZWKJw5SZiLQQkd0iknyQr/+biDxe0XGF2M9cERkV7f1UByLypYj0inUcoYhILxFZG+s4TCFLHNWciLwrIreGmD9YRH4WkRpl3aaq/qSqh6lqno/9l/inV9U7VDXmJ3QRaSciM0Rkp4jsEpEPROTEStr3SBGZXxn7Ctpfnpfwgx9NAVS1varOrax4TNVmiaP6mwxcLCJSbP4lwFRVzS3Lxg4m0cQjETka+Bj4HGgFNAVeA2aKSGYsY4uibC/hBz/WxzqoWKku3+VYsMRR/b0GNAB6BGaIyOHAIGCK93ygiHwmIr+IyBoRGR+0bpqIqIhcJiI/AR8EzavhrXOpiHzl/WpfJSKXe/MPBd4Bmgb/whWR8SLybNA+zvKqSnZ41Uttg5atFpE/i8hyr2TwoojUDhyHiLwpIptFZLs33czn+zIedyK9UVW3qeouVZ0IPAvc7W2/RGnJi6efN50pItle3BtE5D8ikhK0rorIFSLyrbfOg+K0BR4Gsrz3ZIe3fpGqteKlEm97Y7zt7RKR20TkaBFZ4H12LwXvvyyKHdchIjLZe0+/EpG/BL8P3mf4sve+/yAi/xe0bLwXxxQvxi9FJCNo+VgRWect+0ZE+nrza4nI/SKy3nvcLyK1QsQ5VkSmF5v3gIhM9KbricgT3uexTkRuF69K1Xs/PxaR+0RkK+47YA6CJY5qTlV/BV4Cfhc0exjwtaou857v8ZbXBwYCV4rI2cU21RNoC5weYjebcImoLnApcJ+IdFPVPcAZwPpwv3BF5DjgeeA6IBV4G3ij2AlwGDAAVzLoBIz05icBTwEtgRbAr8B/SntPPKcB00LMfwnoEUhOpcgD/gg0BLKAvsCYYusMArp7cQ8DTlfVr4ArKCwB1PcZM7j3Px04EfgL8ChwMdAc6AAML8O2wrkZSANa496niwMLRCQJeANYBhyFO+brRCT4e3EW8ALu+zQD7zMRkeOBq4HuqlrHO5bV3mtu9I6pC9AZyAT+HiK2F4Dfikgdb5vJuPf1OW/500AucAzQFegPBFeLngCsAhoD//T5fphiLHEkhsnAeUEnw9958wBQ1bmq+rmq5qvqctyJvGexbYxX1T1eIipCVd9S1e/V+RB4n6ASTinOB95S1ZmqmgPcCxwCnBS0zkRVXa+q23AnrS7efreq6suquldVd+FOBMXjDqchsCHE/A1AMnBEaRtQ1SWqulBVc1V1NfBIiP3fpao7VPUnYE4g9nK4R1V/UdUvgS+A91V1laruxJXuukZ47YleySfw+D7MesOAO1R1u6quBSYGLesOpKrqrap6QFVXAY8BFwStM19V3/bawJ7BJQJwibYW0E5EaqrqalUNxHARcKuqblLVzcAtuOrUIlT1R+BTYIg3qw+wV1UXikhj4LfAdd53dRNwX7HY1qvqJO8zK/FdNv5YHV8CUNX5IrIFOFtEFuF+zZ0TWC4iJwB34X6xpuD+uYv/Gl8TbvsicgbuV+pxuB8jv8G1HfjRFPgxKNZ8EVmD+zUb8HPQ9F7vNYjIb3AnhgHA4d7yOiKS7KPhfgvQJMT8JoACW0sL3Cst/RvIwB1zDWBJsdWKx35Yadstxcag6V9DPD8ywmsXquopPvbRlKKfd/B0S1zV446gecnAR0HPix9zbRGpoarfich1uCqi9iLyHnC9Vwot8j3wppuGie85XMlqCnAhhaWNlkBNYIMUNuklRTgWc5CsxJE4puBKGhcD76lq8AnnOVyVQnNVrYerfy/emB6yG2WvHvplXEmhsVft8nbQ60vrfnk97h8+sD3BVbus83FMfwKOB05Q1brAqYHN+HjtLGBoiPnDcCfY/bgqvN8ExZaMq04LeAj4GjjW2//ffO4bQr8vRfZH5CQQTRuA4Lai5kHTa4AfVLV+0KOOqv7Wz4ZV9TkvebXEvQd3e4uKfA9wVY/hGu6nAb289qwhFCaONcB+oGFQbHVVtX1wCH7iNJFZ4kgcU4B+wB8Iqqby1AG2qeo+cVcUXViG7QZKKJuBXK/00T9o+UaggYjUC/P6l4CBItJXRGriksF+YIGPfdfB/creISJH4Eo9ft0CnCQi/xSRI0Skjohcg2ujuclbZyXu1/JAL7a/e8cavP9fgN0i0ga4sgz73wg0K9aWsxQ4R0R+IyLHAJeVYXsV6SXgr+IuPjgK1y4R8D9gl9dIfYiIJItIBxHpXtpGReR4Eenj/djYh/vs8r3FzwN/F5FUEWmI+wyeDbUdryprLq596wevzQhV3YCrJv2XiNQVkSTv4gG/1ZfGJ0scCcKrg18AHIorXQQbA9wqIrtw/7AvlWG7u4D/816zHZd0ZgQt/xp3Uljl1as3Lfb6b3CloEm46qMzgTNV9YCP3d+Paw/ZAiwE3i1D3N8Cp+Dq31cDO4DbgCGqOstbZyfuvXkcVwLaAwRfZfVn73h34er5X/S7f+AD4EvgZ68aEVy12wFcUpkMTC3D9vwIXMUV/Ah1wr8Vd5w/4Epm03HJHK8KcBCureYH3Hv/OBDuh0GwWrgq0S246qxGwF+9ZbcDi4HluGrOT7154TyH+yH0XLH5v8P9mFmB+z5OJ3SVpCkHsYGcjAGv2mMhcLOqPhHreOKJiFwJXKCq9svdAFbiMAYA7+qhM4AmIlLeBuwqTUSaiMjJXlXP8bjqw1djHZeJH1biMMYUISItgbdw983swN078Vef1YcmAVjiMMYYUyYxraoSkQFetwPfici4EMuvF5EV4rqbmO39EjLGGBNDMStxeNfEr8R1abAWWAQMV9UVQev0Bj5R1b1eA10vVT2/tG03bNhQ09LSohO4McZUQ0uWLNmiqqmlrxnbO8czge+8LgsQkReAwbjL6ABQ1TlB6y8kqM+cSNLS0li8eHEFhmqMMdWbiPxY+lpOLKuqjqLo7f9rKdrNRHGX4friCUlERovIYhFZvHnz5goK0RhjTHFV4nJcEbkY1x/QhHDrqOqjqpqhqhmpqb5KW8YYYw5CLKuq1lG0D5xmhOifSNwYATcCPb3+g4wxxsRQLBPHIuBYEWmFSxgXUKyPJBHpiuuqeoDXRbKpIDk5Oaxdu5Z9+/bFOhRjTCWqXbs2zZo1o2bNmge9jZglDlXNFZGrgfdw3TI/qapfihsfe7GqzsBVTR0GTPO6Sf5JVc+KVczVydq1a6lTpw5paWlIiVFljTHVkaqydetW1q5dS6tWrQ56OzEdj0NV38Z1wR0876ag6X6VHlSC2LdvnyUNYxKMiNCgQQPKewFRlWgcryzZ2XDnne5vIrCkYUziqYj/exsB0LNgAfTsCfn5UKsWzJ4NWVmxjsoYY+KPlTg8H34IubkucRw4AHPnxjqi6m/jxo1ceOGFtG7dmvT0dLKysnj11crvhDUtLY0tW7aUviKwevVqnnuu+BAQB79eVTdt2jTatm1L7969K3S748eP5957763QbY4cOZLp06cf9OvLG1N59x9PLHF4evUCEfdISXHPTfSoKmeffTannnoqq1atYsmSJbzwwgusXbu2xLq5ubkxiDC0qpg4ovn+PfHEEzz22GPMmTOn9JWrmMr63uXl5VXKfiqSJQ5PVha0bQtHH23VVOFUZBvQBx98QEpKCldccUXBvJYtW3LNNdcA8PTTT3PWWWfRp08f+vbti6pyww030KFDBzp27MiLL7rB9ubOncugQYMKtnH11Vfz9NNPA64kcfPNN9OtWzc6duzI119/DcDWrVvp378/7du3Z9SoUYTrr+3DDz+kS5cudOnSha5du7Jr1y7GjRvHRx99RJcuXbjvvvtYvXo1PXr0oFu3bnTr1o0FC9yIt8XXy8vL44YbbqB79+506tSJRx55JOQ+n332WTIzM+nSpQuXX355wUnlsMMO48Ybb6Rz586ceOKJbNzohozfvHkz5557Lt27d6d79+58/PHHgPt1fMkll3DyySdzySWXsHfvXoYNG0a7du0YMmQIJ5xwAosXL+bJJ5/kuuuuK9j/Y489xh//+McScT3//PN07NiRDh06MHbsWABuvfVW5s+fz2WXXcYNN9xQZP25c+fSs2dPBg8eTOvWrRk3bhxTp04lMzOTjh078v333wMuwfbp04dOnTrRt29ffvrppxL7/v777xkwYADp6en06NGj4HPcuHEjQ4YMoXPnznTu3JkFCxawevVqOnToUPDae++9l/Hjx5fY5q233kr37t3p0KEDo0ePLvgO9OrVi+uuu46MjAweeOCBEq9bsWIFvXr1onXr1kycOBGAm266ifvvv79gnRtvvJEHHngAVeXqq6/m+OOPp1+/fmzaVHhHQVpaGmPHjqVbt25MmzaNpUuXcuKJJ9KpUyeGDBnC9u3bAVi0aBGdOnWiS5cuBd9/IOz3ae7cufTq1YvzzjuPNm3acNFFF4X9fpeLqla7R3p6uh6MgQNVu3Y9qJdWOStWrCiYvvZa1Z49Iz+6dFFNSlIF97dLl8jrX3tt5P0/8MADet1114Vd/tRTT+lRRx2lW7duVVXV6dOna79+/TQ3N1d//vlnbd68ua5fv17nzJmjAwcOLHjdVVddpU899ZSqqrZs2VInTpyoqqoPPvigXnbZZaqqes011+gtt9yiqqpvvvmmArp58+YSMQwaNEjnz5+vqqq7du3SnJycEvvbs2eP/vrrr6qqunLlSg1894qv98gjj+htt92mqqr79u3T9PR0XbVqVZH9rVixQgcNGqQHDhxQVdUrr7xSJ0+erKqqgM6YMUNVVW+44YaCbQ0fPlw/+ugjVVX98ccftU2bNqqqevPNN2u3bt107969qqo6YcIEHT16tKqqfv7555qcnKyLFi3SXbt2aevWrQv2mZWVpcuXLy8S17p167R58+a6adMmzcnJ0d69e+urr76qqqo9e/bURYsWlXjv5syZo/Xq1dP169frvn37tGnTpnrTTTepqur999+v13pfkEGDBunTTz+tqqpPPPGEDh48uCD+CRMmqKpqnz59dOXKlaqqunDhQu3du7eqqg4bNkzvu+8+VVXNzc3VHTt26A8//KDt27cviGPChAl68803q6rqiBEjdNq0aaqqBd8rVdWLL7644L3t2bOnXnnllSWOJxBTVlaW7tu3Tzdv3qxHHHGEHjhwQH/44Qft6p048vLytHXr1rplyxZ9+eWXC76z69at03r16hXsv2XLlnr33XcXbLtjx446d+5cVVX9xz/+UfD+tG/fXhcsWKCqqmPHji04tnDfpzlz5mjdunV1zZo1mpeXpyeeeGLB9yNY8P9/AO42CF/nWGscD5KaCsuXxzqK+LRzp2v/Afd3506o52eUaZ+uuuoq5s+fT0pKCosWLQLgtNNO44gjjgBg/vz5DB8+nOTkZBo3bkzPnj1ZtGgRdevWjbjdc845B4D09HReeeUVAObNm1cwPXDgQA4//PCQrz355JO5/vrrueiiizjnnHNo1qxZiXVycnK4+uqrWbp0KcnJyaxcuTLktt5//32WL19eUMe9c+dOvv322yLX0s+ePZslS5bQvbsbBvzXX3+lUaNGAKSkpBSUrNLT05k5cyYAs2bNYsWKgn5B+eWXX9i9ezcAZ511FoccckjB+3fttdcC0KFDBzp16gS4kkyfPn148803adu2LTk5OXTs2LFI7IsWLaJXr14EuvK56KKLmDdvHmeffXbIYw3o3r07TZq44b6PPvpo+vfvD0DHjh0Lqrays7MLPotLLrmEv/zlL0W2sXv3bhYsWMDQoUML5u3f7zqQ+OCDD5gyZQoAycnJ1KtXr+CXemnmzJnDPffcw969e9m2bRvt27fnzDPPBOD888N3wD1w4EBq1apFrVq1aNSoERs3biQtLY0GDRrw2WefsXHjRrp27UqDBg2YN29ewXe2adOm9OnTp8i2AvvZuXMnO3bsoGdPNzLviBEjGDp0KDt27GDXrl1kedUfF154IW+++SYQ/vuUkpJCZmZmwXe1S5curF69mlNOOcXX++KXJY4gDRvC5s2g6to6EkVQKTus7Gzo29ddOJCSAlOnlq86r3379rz88ssFzx988EG2bNlCRkZGwbxDDz201O3UqFGD/EBGgxJ3wteqVQtwJ5bS6qwffPBBHnvsMQDefvttxo0bx8CBA3n77bc5+eSTee+990q85r777qNx48YsW7aM/Px8ateuHXLbqsqkSZM4/fTTw+5fVRkxYgR33nlniWU1a9YsuIwy+Fjy8/NZuHBhyP36ef8ARo0axR133EGbNm249NJLfb3Gj8B7D5CUlFTwPCkpyXf7QX5+PvXr12fp0qW+1i/t+xCYN2bMGBYvXkzz5s0ZP358kfUivW/BxxT8OYwaNYqnn36an3/+md///ve+YvX7+YQS7vs0d+7csDFWJGvjCJKaCvv2wd69sY4k/mRlubaf226rmDagPn36sG/fPh566KGCeXsjvPE9evTgxRdfJC8vj82bNzNv3jwyMzNp2bIlK1asYP/+/ezYsYPZs2eXuu9TTz21oOH6nXfeKfiVetVVV7F06VKWLl1K06ZN+f777+nYsSNjx46le/fufP3119SpU4ddu3YVbGvnzp00adKEpKQknnnmmYI2ieLrnX766Tz00EPk5OQAsHLlSvbs2VMkrr59+zJ9+vSCuvBt27bx44+Re7ru378/kyZNKnge7gR78skn89JLLwGunv7zzz8vWHbCCSewZs0annvuOYYPH17itZmZmXz44Yds2bKFvLw8nn/++YJfx+V10kkn8cILLwAwdepUevToUWR53bp1adWqFdOmTQPcCXPZsmWAe78C35+8vDx27txJ48aN2bRpE1u3bmX//v0Fv9CDBZJEw4YN2b17d4Vc6TRkyBDeffddFi1aVHAyP/XUUwu+sxs2bAh7AUG9evU4/PDD+eijjwB45pln6NmzJ/Xr16dOnTp88sknAAXvE/j7PkWTlTiCNGzo/m7eDOX4MVBtZWVV3EUDIsJrr73GH//4R+655x5SU1M59NBDufvuu0OuP2TIELKzs+ncuTMiwj333MORRx4JwLBhw+jQoQOtWrWia9eupe775ptvZvjw4bRv356TTjqJFi1ahFzv/vvvZ86cOSQlJdG+fXvOOOMMkpKSSE5OpnPnzowcOZIxY8Zw7rnnMmXKFAYMGFDwK7JTp05F1rv22mtZvXo13bp1Q1VJTU3ltddeK7K/du3acfvtt9O/f3/y8/OpWbMmDz74IC1bhh/4cuLEiVx11VV06tSJ3NxcTj31VB5++OES640ZM4YRI0bQrl072rRpQ/v27akXVNc4bNgwli5dGrLarkmTJtx111307t0bVWXgwIEMHjy41PfZj0mTJnHppZcyYcIEUlNTeeqpp0qsM3XqVK688kpuv/12cnJyuOCCC+jcuTMPPPAAo0eP5oknniA5OZmHHnqIrKwsbrrpJjIzMznqqKNo06ZNie3Vr1+fP/zhD3To0IEjjzyyoGqwPFJSUujduzf169cnOTkZcN/ZDz74gHbt2tGiRYuCKqdQJk+ezBVXXMHevXtp3bp1wfvwxBNP8Ic//IGkpCR69uxZ8JmNGjWq1O9TVPltDKlKj4NtHJ8xwzX+hmjrq3ZCNY6Z6is3N7egEf+7777TtLQ03b9/f8HygQMH6qxZs2IVXpWXl5ennTt3LmjEryi7du0qmL7zzjv1//7v/ypku9Y4XoGCSxzGVCd79+6ld+/e5OTkoKr897//JSUlhR07dpCZmUnnzp3p27dvrMOsklasWMGgQYMYMmQIxx57bIVu+6233uLOO+8kNzeXli1bFlxqHmuWOIIExn/yeROxMVVGnTp1Qg6nXL9+/bBXghl/2rVrx6pVq6Ky7fPPPz/iVV6xYo3jQRKtxOFKp8aYRFIR//eWOILUqwc1aiRGiaN27dps3brVkocxCUTVjccR7rJxv6yqKohI4b0c1V2zZs1Yu3ZtufvlN8ZULYERAMvDEkcxqamJUeKoWbNmuUYAM8YkLquqKiZRShzGGHOwLHEU07BhYpQ4jDHmYFniKCY11UocxhgTiSWOYho2hO3b3WiAxhhjSopp4hCRASLyjYh8JyLjQiyvJSIvess/EZG0aMeUmup6x922Ldp7MsaYqilmiUNEkoEHgTOAdsBwEWlXbLXLgO2qegxwHxC6B7wKFLgJ0No5jDEmtFiWODKB71R1laoeAF4Aine5ORiY7E1PB/qKRHekjEC3I9bOYYwxocUycRwFrAl6vtabF3IdVc0FdgINQm1MREaLyGIRWVyem9qsxGGMMZFVm8ZxVX1UVTNUNSMwxOXBsBKHMcZEFsvEsQ5oHvS8mTcv5DoiUgOoB2yNZlANvPKMlTiMMSa0WCaORcCxItJKRFKAC4AZxdaZAYzwps8DPtAo98pXqxbUrWslDmOMCSdmfVWpaq6IXA28ByQDT6rqlyJyK24kqhnAE8AzIvIdsA2XXKLO7h43xpjwYtrJoaq+DbxdbN5NQdP7gKGVHZfdPW6MMeFVm8bximQlDmOMCc8SRwhW4jDGmPAscYQQKHHY4HjGGFOSJY4QUlNh3z7YsyfWkRhjTPyxxBGC3T1ujDHhWeIIwe4eN8aY8CxxhGAlDmOMCc8SRwhW4jDGmPAscYRgJQ5jjAnPEkcI9epBjRpW4jDGmFAscYQgYnePG2NMOJY4wrC7x40xJjRLHGFYicMYY0KzxBGGlTiMMSY0SxxhWInDGGNCs8QRRmoqbNsGubmxjsQYY+KLJY4wAvdybNsW2ziMMSbehB0BUETO8vH6far6fgXGEzeC7x5v1Ci2sRhjTDyJNHTsU8BbgERY5yTg6AqNKE7Y3ePGGBNapMQxU1V/F+nFIvJCBccTN6y/KmOMCS1sG4eqXlDai/2sU1VZicMYY0IrtXFcRA4Rkb+KyMPe82NE5IzohxZbljiMMSY0P1dVPYlr5zjFe74euKM8OxWRI0Rkpoh86/09PMQ6XUQkW0S+FJHlInJ+efZZVikpULeuVVUZY0xxfhLHsap6B5ADoKp7idxg7sc4YLaqHgvM9p4Xtxf4naq2BwYA94tI/XLut0xSU63EYYwxxflJHAdEpDagACLSCjhQzv0OBiZ705OBs4uvoKorVfVbb3o9sAlILed+y6RhQytxGGNMcX4Sx63Au0AzEZkMzAH+Ws79NlbVDd70z0DjSCuLSCaQAnwfYZ3RIrJYRBZvrqCzvZU4jDGmpEiX4wKgqu+KyBLcPRsC3KCqm0p7nYjMAo4MsejGYttXEdEI22kCPAOMUNX8CHE+CjwKkJGREXZ7ZdGwISxdWhFbMsaY6qPUxOHpCxytqv8UkeYikq6qSyK9QFX7hVsmIhtFpImqbvASQ8hEJCJ1cTch3qiqC33GWmECJQ5VN7iTMcYYf5fj/gfoDVzszdoDPFzO/c4ARnjTI4DXQ+w3BXgVmKKq08u5v4PSsCHs2wd79sRi78YYE5/8tHGcpKqXA/sAVHUbrr2hPO4CThORb4F+3nNEJENEHvfWGQacCowUkaXeo0s591smgbvHrZ3DGGMK+amqyhGRJAqvqmoAhG1r8ENVt+Kqv4rPXwyM8qafBZ4tz37KK3AT4ObNkJYWy0iMMSZ++ClxPAi8DKSKyC3AfODuqEYVJ6zEYYwxJfm5qmqKd1VVP9xVVUNV9YuoRxYHgkscxhhjnIiJQ0SSgeXe3dtfVk5I8cNKHMYYU1LEqipVzQNWichRlRRPXKlbF2rWtBKHMcYE89M4fhjwlYhk4y7FBUBVz4laVHFCxFVXWYnDGGMK+Ukct0c9ijhm/VUZY0xRfhJHX1X9W/AMEbkD16tttWf9VRljTFF+LscdEGLewIoOJF5ZicMYY4oKW+IQkcuBK4DjReTToEV1gIj9VFUnVuIwxpiiIlVVvYSrjrqTogMt7fLTO2510bAhbNsGublQw2+XkMYYU42FPRWq6nYR+QVop6phx8Go7gL3cmzbBo0axTYWY4yJB3YfRykCd4/fcQdkZ8c2FmOMiQd+GscD93G8JyKvBB7RDixebPIq5SZNgr59LXkYY4zdx1GKVavc3/x8OHAA5s6FrKyYhmSMMTHlp5PDhLhfI5zBg+H++91d5Ckp0KtXrCMyxpjY8jMCYHcRWSgiO0Vkn4js9xrNE0KvXtCgAXTrBrNnW2nDGGP8VFX9Fzds7AtAJjASaBnFmOLO8ce70oYlDWOM8dc4nqSq3wA1VDVHVR8jge4cB2jVCn74IdZRGGNMfPCTOPaISAqwTETuEJFrgOQoxxVXWrWCNWsgJyfWkRhjTOz5SRwjvfWuBvKAY4HzohhT3Gnd2l1V9dNPsY7EGGNiz89VVd4FqewD/hHdcOJTq1bu7w8/wNFHxzYWY4yJtUidHH4GaLjlqtqtPDsWkSOAF4E0YDUwTFW3h1m3LrACeE1Vry7Pfg9GcOIwxphEF6nEEaiOEuB14KwK3vc4YLaq3iUi47znY8Osexswr4L371uzZq6DQ0scxhgTuZPDgo4NRWR/FDo6HAz08qYnA3MJkThEJB1oDLwLZFRwDL4kJ0OLFpY4jDEG/DWOR0tjVd3gTf+MSw5FiEgS8C/gz5UZWCitWxd2P2KMMYksUhtHp6Cnh4hIR1y1FQCqury0jYvILODIEItuDH6iqioiodpTxgBvq+paEQmxuMi+RgOjAVq0aFFaaGXWqhW89lqFb9YYY6qcSG0cDwZNb8HdQR6gwKmlbVxV+4VbJiIbRaSJqm4QkSZAqMGhsoAeIjIG10tviojsVtWhMv6gAAAdcklEQVRxxVdU1UeBRwEyMjLCNuofrFat3BCyu3fDYYdV9NaNMabqiNTG0SPK+54BjADu8v6+HiKGiwLTIjISyAiVNCpD4Mqq1auhQ4dYRGCMMfEhbBtHsaqqg14ngruA00TkW6Cf9xwRyRCRx8ux3aiwS3KNMcaJVFX1jIicQlC7RgiTga4Hs2NV3Qr0DTF/MTAqxPyngacPZl8VoXVr99cayI0xiS5S4mgAfEnkxBGqXaJaatgQDj3UShzGGBOpjaNZZQYS70Ssl1xjjIHY3sdR5VjiMMYYSxxlEkgcWuEX+xpjTNVhiaMMWrd293Fs2RLrSIwxJnb8jDn+ooicLqXdup0A7JJcY4zxV+J4Cvg9sFJEbheRY6IcU9yyxGGMMT4Sh6q+q6rnA5m4zgjniMg8EblEREodCKo6scRhjDE+2zhE5HDgQuASYDnwCHASrqvzhHHYYe5+DkscxphEVmqJQUSmAR2BqcC5qrrWWzTVGyUwobRubYnDGJPY/FQ1PQrMUi15EaqqHlR3I1VZq1aweHGsozDGmNjxkzgOAc4sdlHVTuALr7+phNKqFbzyCuTluZEBjTEm0fhJHFfixsX40Ht+KvAp0FJEblLV56IVXDxq1QpycmDdOjecrDHGJBo/jeNJQFtVHayqg4F2wAHgROBv0QwuHtmVVcaYROcncTQPGhscb7qlqm4BcqMWWZwKdK9uicMYk6j8VFXNE5HXgZe85+cBH4nIocAvUYssTrVoAUlJNi6HMSZx+UkcY4ChwCne8xeBl1Q1Hx/jjlc3NWtCs2ZW4jDGJK6IiUNEkoF3VfU0XMIwWPfqxpjEFrGNQ1XzgGQRqVtJ8VQJljiMMYnMT1XVTmCZiLwP7AnMVNXroxZVnGvVCtavh337oHbtWEdjjDGVy0/ieNN7GE/gyqrVq6FNm5iGYowxla7UxKGqT4hICtBCVb+rhJjiXvC9HJY4jDGJxs9ATgOBz4GZ3vMuIvJqeXYqIkeIyEwR+db7e3iY9VqIyPsi8pWIrBCRtPLst6LYTYDGmETm5wbAW4ETgB0AqroUKO9gTuOA2ap6LDDbex7KFGCCqrbFjQeyqZz7rRBHHgm1alniMMYkJj+JI0dVdxSbV6Kn3DIaDEz2picDZxdfQUTaATVUdSaAqu5W1b3l3G+FSEqCtDRLHMaYxOQncXwlIsOAJBFpJSL3AQvLud/GQd2Y/Aw0DrHOccAOEXlFRD4TkQnefSUhichoEVksIos3b95czvBKd/jhsGABZGdHfVfGGBNX/CSOq4F0IB94FdfB4XWlvUhEZonIFyEeg4PX88b5CFWCqQH0AP4MdAdaAyPD7U9VH1XVDFXNSE1N9XFYBy87243JsWED9O1rycMYk1j8XFW1BxjrPXxT1X7hlonIRhFpoqobRKQJodsu1gJLVXWV95rXcD3yPlGWOKJh7lw3HgfAgQPueVZWLCMyxpjK42fo2GOA64G04PVVtX859jsDGAHc5f19PcQ6i4D6IpKqqpuBPkBcjL3XqxekpMD+/W4wp169Yh2RMcZUHj83AE7H/cp/FsiroP3eBbwkIpcBPwLDAEQkA7hCVUepap6I/BmYLW74wSXAYxW0/3LJyoI334TTToMRI6y0YYxJLH4SR76qTqrInXpDzvYNMX8xMCro+UygU0Xuu6L06wfHHQeb4uICYWOMqTx+Gsdf965YShWRuoFH1COrAtLTYcmSWEdhjDGVy0/iGAX8AzfO+Jfe44toBlVVpKfD2rVW6jDGJBY/V1U1r4xAqqL0dPd3yRI444zYxmKMMZUlbIlDRP4UNH1OsWW3RTOoqqJbN/fXqquMMYkkUlXVRUHTfy+2bGAUYqly6tZ1DeSWOIwxiSRS4pAw06GeJyxrIDfGJJpIiUPDTId6nrDS02HNGmsgN8YkjkiJo7OIbBOR7UAnbzrwvGMlxRf3ghvIjTEmEURKHClAKtAQqOVNB57bSNuerl3dX0scxphEEfZyXFWtqO5FqrV69eDYYy1xGGMSh58bAE0pMjIscRhjEocljgoQaCCvhPGjjDEm5ixxVABrIDfGJJJId45vD7qSKvixXUS2VWaQ8S7QQL44LkYLMcaY6IrUV1XDSouiirMGcmNMIvF9VZWIHEHRy3DXRyuoqig9HT7+ONZRGGNM9JXaxiEiA0VkJW4M8E+8vx9EO7CqxhrIjTGJwk/j+D+Bk4FvvC7WTwc+impUVVBGhvtr1VXGmOrOT+LIVdXNQJKIiDeca2aU46py7A5yY0yi8DPm+E4ROQyYD0wRkU3Ar9ENq+qxBnJjTKLwU+I4G5corgPmAuuAQVGMqcpKT7dLco0x1Z+fxPFXVc1T1RxVfUJV/w1cH+3AqiJrIDfGJAI/iWNAiHnlHgFQRI4QkZki8q339/Aw690jIl+KyFciMlFE4nYQKbuD3BiTCCLdOX65iHwGHC8inwY9vgW+qoB9jwNmq+qxwGzvefEYTsJd0dUJ6AB0B3pWwL6jIjAG+b//DdnZsY3FGGOiJVLj+Eu4E/qdFD2p71LVihjvbjDQy5uejGs/GVtsHcXddJiCG662JrCxAvYdFStWgAjMnAnz58Ps2ZCVFeuojDGmYoUtcajqdlX9TlWH4k7ep3mP1Arad2NV3eBN/ww0DhFDNjAH2OA93lPVkKUdERktIotFZPHmGDUyzJ0L6g2qe+CAe26MMdWNnzvHrwKmAS28x0siMsbPxkVkloh8EeIxOHg9VVVCjGMuIscAbYFmwFFAHxHpEWpfqvqoqmaoakZqakXltrLp1QtSUtx0jRruuTHGVDd+7uO4HMhU1d0AInIHsAD4b2kvVNV+4ZaJyEYRaaKqG0SkCRCq+msIsDBo3+8AWcTpnetZWfDWWzBgAJxzjlVTGWOqJz9XVQlwIOh5jjevvGYAI7zpEcDrIdb5CegpIjVEpCauYbwiGuajpl8/6NvXrqwyxlRfka6qCpRGngE+EZG/i8jfcaWNyRWw77uA07yrtPp5zxGRDBF53FtnOvA98DmwDFimqm9UwL6j6swzYeVK9zDGmOpGVEs0LbgFIp+qajdvOhM4xVv0kaouqqT4DkpGRoYujuEt3D/+CGlpcO+98Kc/xSwMY4zxTUSWqGqGn3UjtXEUVEep6v+A/5U3sETRsiV07AhvvmmJwxhT/URKHKkiErZrEa/rERPGmWfC3XfD9u1weMh74o0xpmqK1DieDBwG1AnzMBGceSbk5cG778Y6EmOMqViRShwbVPXWSoukmsnMhEaN4I03YPjwWEdjjDEVJ1KJI247E6wKkpJg4EB45x3IyYl1NMYYU3EiJY6+lRZFNTVoEOzYAR9/HOtIjDGm4kTqq2pbZQZSHfXv77ogeSPu7zwxxhj//Nw5bg7SYYdB796WOIwx1Ysljig780z49lv45ptYR2KMMRXDEkeUDfJGZ7dShzGmurDEEWUtW0KnTu4ucmOMqQ4scVSCM8+Ejz6Cm26yIWWNMVWfJY5KkJYG+fnwz3+6LtcteRhjqjJLHJVgkzdEVX6+DSlrjKn6LHFUgt69oWZNN21DyhpT+bKz4c47i5b2Q82rqPnxtO1o8DN0rCmnrCzXOH7mmS5p2JCyJpFlZ7tSd/H/hbLMD8w79VRIT4d9+9xjwQKYPx+6dYO2bV0J/7PP4PrrXdc/NWq4tsacHHeSDcwbMwaaNYPvv4fHH3cdlCYnw7Bhrs+5NWvgtddcrUFSkhseukED2LgRZs0qXP/kk0HV9RYRWDcjA+rWdT1lf/ZZ4fz27aFOHdi5E776ys0XgdatoXZt2L0bfvrJbU8EmjZ1x75+feG8xo2hVi137Js2ufmHHAKzZ0f3PGOJo5L07w9XXgkPPgjr1sFRR8U6ImMqTriT+wcfuBNnu3awa5ebd9VVhSfsa6+F1FR34nzmGXcCTkqCHj3gN7+BDRtg2bLCk2qjRi4ZbN9+cHEeOAB//3vReTk58MADJdfNzYVp09yJOCfHxQbu7/z5UL8+/PJL0fnffOPizM938/Lz3THUqAFbthSdv2ePO/Fv2lQ4X9X1NnH88W4E0eBx9ho0cNtet65w3pFHuqs2v/iisEo8UB0e1R+oqlrtHunp6RqPVq1STUpSHTs21pEYE9mCBap33OH+5uer/vKL+/4+/rjqyJGqN9+sOnGi+3vuuarJyargvt9paaoNG7rnB/to1Eg1I8NtKzBPRLVbN9XMTDcdmHf66ar33ac6eHDh/KQk1UsuUX33XdVJk1Rr1XIx1qql+swzqi+8oFq7tptXu7Zbb9cu1blzVQ85xM0/5BB3/IH3w+/8sqwb7W2XBbBYfZ5jww4dW5XFeujYSIYOdUXbNWtclyTGVJbipYLdu10V6uzZrpqmbl336/jzz+H99wt/5deoEbmH59q1XVVJQPv27lf6kiWFVSpnn+2qfdaudb/4c3Ndu99LL7k2wGXL4LTT3K/llJTCqpbsbHclYvB8KDkv3LrFq7ZCVXeVp8qsKmzbr7IMHWuJo5ItXOg+1IkT4ZprYh2NqY6ys92J/7jjXPXGTz+5uv8pU1x1iog7se/dW/K1tWq5ZTt2uOcirtpo0CD45BN49VWXUJKTYdw4GD8eFi3yf3IPxBevJ89EZokjjhMHwEknuUa1lSvdP6AxkYQ6Gebnw+uvw1tvuTYCEVi1CpYvd+0FpcnKciWMmTMLE8Hf/ga33OJ+3ETzF72JT2VJHDFpgwCGAl8C+UBGhPUGAN8A3wHj/G4/Xts4AqZPd/WwL78c60hMvAluW1BVnTPH1csnJanWqKHap49q586qKSlF2wSSk1WPOcY9guv5R49WXb1add48//XloeIobb6p+oj3Ng4RaesljUeAP6tqieKBiCQDK4HTgLXAImC4qq4obfvxXuLIy4Njj3WX182fH+toTCwU/zUeuIJn5EjXnpCUBE2auCtogv9FjzgCTjzRXcK5YIFblpzsSgo33milAnPwylLiiMnluKr6FYBIxNFpM4HvVHWVt+4LwGCg1MQR75KT4brr3KWIn3wCJ5wQ64hMZZo1y7UZHDjgEsQxx8CPPxZtYM7Lg3r13GXczz7rnqekuMbsUNVGffq412VluWQRKhlkZZVMDqHmGVOaeL6P4yhgTdDztUC1OcVeeqm7Eelvf4N+/ewXX3U1Zw68+KJrcN6yBRYvhq+/Llyel+dKG2PGwKGHwj33uOcpKfDYY+47MWpUyURQ1gRhTEWKWuIQkVnAkSEW3aiqr0dhf6OB0QAtWrSo6M1XuDp13K/OqVPdP3+tWtG/29NET3a2SxLHHAP797sG5lmz3AUQAQ0auDuLTzml8AqnlBR341vgcz/jjNBJItT3whKEiZWoJQ5V7VfOTawDmgc9b+bNC7e/R4FHwbVxlHPflaK5d3TBnR/aiSD+BdoFTjrJ3eMwdSo88kjh3b/g7tFp1Mhd7RRoh/jTn+Cvf3XLf/97Ky2Yqiueq6oWAceKSCtcwrgAuDC2IVWss86Cf/2rsDHUOj+Mb7t2uX6M/vIXV50USlKSuz/nX/+C//2vaDtE8OdrCcJUZTHpHVdEhojIWiALeEtE3vPmNxWRtwFUNRe4GngP+Ap4SVW/jEW80ZKV5X51HnOM+0V6ZKiKPRMzs2a5tqhLLnFXMh1+uOssL5A0RODii929FIcc4j7DWrXg/PPddKAd4rbbrBrSVC92A2Ac+Okn6NDBdQY3a5b71WoqT6Dq6ZRT3Al/1ix4+WV3M11Ax46uhNiokbtjOtTNcXZZq6nK4v5yXFNUixauamP0aHj0UbjiilhHlDimTYOLLiraF1OgC+vg9onhwwvbJ7p399+AbUx1ZCWOOKHqrtlfuNB1MpeWFuuIqp/sbHjvPXfZ648/uunvvitcLgIXXACTJrmrocLdSGdMdWR9VVXBxAHuZNaxI2Rmuj6EIt8faSIJVB317OnaJh56yI2FErjyqVYtd//Mcce5ZTk5/jvjM6Y6sqqqKqplS7j3Xrj8cjjnHHf1jp2wym7uXDj99MLqp+K/jZKSXPcc//iHez50qF0aa0xZWOKIMx06uBPba6/BO++4m8rs5BVeoFTQrh38/LPrkuPdd4teLjt4MPzud+4KqEDVU7+gu4wsQRhTNpY44syHHxZWUe3fD08+aSe1UPLz4amn3IUEwUmiVSs3aNAbbxR23TF2bOQuOowxZWOJI8706uVOdgcOuJPjlCmuwbZv31hHFlvZ2e4y2d/8Br791iWG9esLl4u4TiP//W83Hap9wkoWxlQMSxxxJviXcadO7hLQQYPcoD39+8c6usq3caMbLfHuu13fTuCGKh040FVPTZhQ2LA9bFhhac2ShDHRY4kjDgWf9E44wdXHn3UW3Hmn63q7Ole1LFgAL7zgjnP5ctdtR3DjdlKS61E40LAdqlNAY0x02eW4VcDWra5DvZUr3YmzuvSkG6hOysyE3btde84bbxQmirZt4cILXWeQV15p91QYE012OW4106CB6//otttcu8evv8KMGVX35KnqShUjRhS9YzslpTBpJCe7PqICd2sfd5yVLIyJF5Y4qogzznD3eOzb506uEyfCUUe5X+LJybGOLrLsbHdpca1asHq1u2N7TdAQXSJw2WUuUQwYYL3JGhPvrKqqCglU7RxzjBsdbuZM12/SmDGwYUPsf40HX8nUvr0bT/255+D55wvv2D70UHdz3jHHuORX/I5tu1vbmNiwLkeqaeIIFqjuueoq2L7dzUtJgfffd91sVLYZM9wd2MFVT6quTSaQNJKS4NZb3V3bYEnCmHhibRwJQMT12PrVV3D77e4kfeCAq+q5+GLo0gV27oTevct3Ug51cn/vPdfteEqKu1v7k09g7dqir+vTxyUIEXfpbKD6qU+fwnWs+smYqslKHFVcdnZhL641arhE8eGHrgEd3K/8Cy5wl/S2betGsVu8uOSv/OAE0bmzu5Jr9mx3Z/aBA64dpWNH10YRKOGA6368Z083TsXDDxferW2dBRpTtVhVVQIlDih5Yr7lFlclFKgiSk4uvHkuWMOG7k7s3FzXRlLaV6F5c5cgPv20cJyK224rvPLJEoQxVZcljgRLHMUFl0IC7R5HHumSybPPupO+CKSnu0bspUth2TL3WhF3h/p558GWLTB+fNFSBNg4FcZUR5Y4EjxxQOhf/8UTSvCVTOGSQbjtWMnCmOrFEocljrDCnfQtGRiT2CxxWOIwxpgyKUviSIp2MMYYY6qXmCQOERkqIl+KSL6IhMxwItJcROaIyApv3WsrO05jjDElxarE8QVwDjAvwjq5wJ9UtR1wInCViLSrjOCMMcaEF5M7x1X1KwAJjLoTep0NwAZvepeIfAUcBayojBiNMcaEViXaOEQkDegKfBJhndEislhEFm/evLmyQjPGmIQTtRKHiMwCjgyx6EZVfb0M2zkMeBm4TlV/Cbeeqj4KPAruqqoyhmuMMcanqCUOVe1X3m2ISE1c0piqqq/4fd2SJUu2iMiP5d1/jDUEtsQ6iEqQCMeZCMcIiXGc1fkYW/pdMW57xxXXAPIE8JWq/rssr1XV1OhEVXlEZLHfa6qrskQ4zkQ4RkiM40yEY/QjVpfjDhGRtUAW8JaIvOfNbyoib3urnQxcAvQRkaXe47exiNcYY0yhWF1V9Srwaoj564HfetPzgfCXXRljjImJKnFVVYJ6NNYBVJJEOM5EOEZIjONMhGMsVbXsq8oYY0z0WInDGGNMmVjiMMYYUyaWOGJMRAaIyDci8p2IjAuxfKSIbA66smxULOIsDxF5UkQ2icgXYZaLiEz03oPlItKtsmMsLx/H2EtEdgZ9jjdVdozl5afj0WryWfo5zir/eZaLqtojRg8gGfgeaA2kAMuAdsXWGQn8J9axlvM4TwW6AV+EWf5b4B3cVXQnAp/EOuYoHGMv4M1Yx1nOY2wCdPOm6wArQ3xfq8Nn6ec4q/znWZ6HlThiKxP4TlVXqeoB4AVgcIxjqnCqOg/YFmGVwcAUdRYC9UWkSeVEVzF8HGOVp6obVPVTb3oXEOh4NFh1+Cz9HGdCs8QRW0cBa4KeryX0F/Rcr9g/XUSaV05olcrv+1DVZYnIMhF5R0TaxzqY8ojQ8Wi1+ixL6WC12nyeZWWJI/69AaSpaidgJjA5xvGYg/Mp0FJVOwOTgNdiHM9B89vxaFVXynFWm8/zYFjiiK11QHAJopk3r4CqblXV/d7Tx4H0SoqtMpX6PlR1qvqLqu72pt8GaopIwxiHVWY+Oh6tFp9lacdZXT7Pg2WJI7YWAceKSCsRSQEuAGYEr1CsfvgsXH1rdTMD+J13Rc6JwE51A3lVGyJypNdxJyKSifvf2xrbqMrGZ8ejVf6z9HOc1eHzLI+47R03EahqrohcDbyHu8LqSVX9UkRuBRar6gzg/0TkLNxQuttwV1lVKSLyPO4qlIZe55Y3AzUBVPVh4G3c1TjfAXuBS2MT6cHzcYznAVeKSC7wK3CBepfnVCGBjkc/F5Gl3ry/AS2g+nyW+DvO6vB5HjTrcsQYY0yZWFWVMcaYMrHEYYwxpkwscRhjjCkTSxzGGGPKxBKHMcaYMrHEYWJORPK8HkaXicinInJSBW23l4i86Xd+BezvbBFpF/R8rohk+Ihxp4i8XWz+dSKyT0TqRSHOLiLy24rebtD2J4jIzyLy52jtw8SWJQ4TD35V1S5e9w1/Be6MdUAH6WygXalrlfSRqhY/kQ/H3SB6TrmjKqkL7l6LEkSk3Pd2qeoNwMPl3Y6JX5Y4TLypC2yHgrEdJojIFyLyuYic783v5f2any4iX4vI1KC7eAd48z7Fx0lXRA4VN5bG/0TkMxEZ7M0fKSKviMi7IvKtiNwT9JrLRGSl95rHROQ/XinpLGCCV3o62lt9qLfeShHp4ecN8F57GPB3XAIJzC9TTN78od77t0xE5nk9FNwKnO/Feb6IjBeRZ0TkY+AZEaktIk957/lnItI7aP+vichMEVktIleLyPXeOgtF5Ag/x2eqPrtz3MSDQ7w7dGvjxkLo480/B/fruDPQEFgkIvO8ZV2B9sB64GPgZBFZDDzmvf474EUf+74R+EBVfy8i9YH/icgsb1kXbz/7gW9EZBKQB/wDN/bGLuADYJmqLhCRGbgxGqYDeLmshqpmelVDNwP9fMR0Aa6L/Y+A40WksapuLGtM3vo3Aaer6joRqa+qB8QNOpShqld7cY7HlZROUdVfReRPgKpqRxFpA7wvIsd52+vg7b827j0eq6pdReQ+4HfA/T6Oz1RxVuIw8SBQVdUGGABM8UoQpwDPq2qed+L8EOjuveZ/qrpWVfOBpUAa0Ab4QVW/9bp/eNbHvvsD47zENRd3QmzhLZutqjtVdR+wAmiJG0PlQ1Xdpqo5wLRSth/oIG+JF6Mfw4EXvGN7GRgatKysMX0MPC0if8B1axPODFX91Zs+Be+9U9WvgR+BQOKYo6q7VHUzsBPXezPA52U4PlPFWYnDxBVVzRbXy2hqKavuD5rO4+C/ywKcq6rfFJkpckIF7SOwDV+vF5GOwLHATK/EkgL8APyn2PZ8bVNVr/COZSCwRETC9a68p7TYQuw/P+h5fmmxmOrDShwmrnhVI8m4nkY/wtXFJ4tIKm541v9FePnXQFpQ+8LwCOsGvAdcE9RG0rWU9RcBPUXkcK8h+dygZbtwQ42Wx3BgvKqmeY+mQFMRaXkwMYnI0ar6iareBGzGdXleWpwfARd5rz8OVwL7JsL6JsFY4jDx4BCvoXYprl1ihKrmAa8Cy3H19R8Af1HVn8NtxKu+GQ285TWOb/Kx79twvdguF5Evvedhqeo64A5cAvsYWI2rsgHXLnGD11h8dOgtlOoC3HEHe9WbfzAxTfAaub8AFuDeyzlAu0DjeIhN/hdIEpHPcZ/HyKAxYYyx3nGNKSsROUxVd3u/7l/FdYdf/GTvd1u9gD+r6qB4iakieA3uu1X13ljFYKLHShzGlN14r3T0Ba79oTzDhh4AOkixGwBjHFO5iMgE4GL8t5uYKsZKHMYYY8rEShzGGGPKxBKHMcaYMrHEYYwxpkwscRhjjCkTSxzGGGPK5P8BDP+TWmm69e4AAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "lowest_energies = []\n", + "bond_distances = []\n", + "for i in range(len(raw_data_table_1)):\n", + " # Use data of paper to construct the Hamiltonian\n", + " bond_distances.append(raw_data_table_1[i][0])\n", + " hamiltonian = raw_data_table_1[i][1] * QubitOperator(()) # == identity\n", + " hamiltonian += raw_data_table_1[i][2] * QubitOperator(\"Z0\")\n", + " hamiltonian += raw_data_table_1[i][3] * QubitOperator(\"Z1\")\n", + " hamiltonian += raw_data_table_1[i][4] * QubitOperator(\"Z0 Z1\")\n", + " hamiltonian += raw_data_table_1[i][5] * QubitOperator(\"X0 X1\")\n", + " hamiltonian += raw_data_table_1[i][6] * QubitOperator(\"Y0 Y1\")\n", + "\n", + " # Use Scipy to perform the classical outerloop of the variational\n", + " # eigensolver, i.e., the minimization of the parameter theta.\n", + " # See documentation of Scipy for different optimizers.\n", + " minimum = minimize_scalar(lambda theta: variational_quantum_eigensolver(theta, hamiltonian))\n", + " lowest_energies.append(minimum.fun)\n", + "\n", + "# print result\n", + "plt.xlabel(\"Bond length [Angstrom]\")\n", + "plt.ylabel(\"Total Energy [Hartree]\")\n", + "plt.title(\"Variational Quantum Eigensolver\")\n", + "plt.plot(bond_distances, lowest_energies, \"b.-\",\n", + " label=\"Ground-state energy of molecular hydrogen\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/projectq/__init__.py b/projectq/__init__.py index 8dff84e6c..430859a3a 100755 --- a/projectq/__init__.py +++ b/projectq/__init__.py @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -ProjectQ - An open source software framework for quantum computing +ProjectQ - An open source software framework for quantum computing. Get started: Simply import the main compiler engine (from projectq import MainEngine) @@ -25,5 +24,4 @@ Shor's algorithm for factoring. """ -from ._version import __version__ from projectq.cengines import MainEngine diff --git a/projectq/backends/__init__.py b/projectq/backends/__init__.py index 6a3319779..e531ed9f2 100755 --- a/projectq/backends/__init__.py +++ b/projectq/backends/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains back-ends for ProjectQ. @@ -24,9 +23,19 @@ * a resource counter (counts gates and keeps track of the maximal width of the circuit) * an interface to the IBM Quantum Experience chip (and simulator). +* an interface to the AQT trapped ion system (and simulator). +* an interface to the AWS Braket service decives (and simulators) +* an interface to the Azure Quantum service devices (and simulators) +* an interface to the IonQ trapped ionq hardware (and simulator). """ +from ._aqt import AQTBackend +from ._awsbraket import AWSBraketBackend +from ._azure import AzureQuantumBackend +from ._circuits import CircuitDrawer, CircuitDrawerMatplotlib +from ._exceptions import DeviceNotHandledError, DeviceOfflineError, DeviceTooSmall +from ._ibm import IBMBackend +from ._ionq import IonQBackend from ._printer import CommandPrinter -from ._circuits import CircuitDrawer -from ._sim import Simulator, ClassicalSimulator from ._resource import ResourceCounter -from ._ibm import IBMBackend +from ._sim import ClassicalSimulator, Simulator +from ._unitary import UnitarySimulator diff --git a/projectq/_version.py b/projectq/backends/_aqt/__init__.py old mode 100755 new mode 100644 similarity index 78% rename from projectq/_version.py rename to projectq/backends/_aqt/__init__.py index 297633d91..37d42fcd9 --- a/projectq/_version.py +++ b/projectq/backends/_aqt/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,5 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Define version number here and read it from setup.py automatically""" -__version__ = "0.3.6" +"""ProjectQ module for supporting the AQT platform.""" + +from ._aqt import AQTBackend diff --git a/projectq/backends/_aqt/_aqt.py b/projectq/backends/_aqt/_aqt.py new file mode 100644 index 000000000..9d5e5e2fe --- /dev/null +++ b/projectq/backends/_aqt/_aqt.py @@ -0,0 +1,309 @@ +# Copyright 2020, 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Back-end to run quantum program on AQT's API.""" + +import math +import random + +from projectq.cengines import BasicEngine +from projectq.meta import LogicalQubitIDTag, get_control_count +from projectq.ops import Allocate, Barrier, Deallocate, FlushGate, Measure, Rx, Rxx, Ry +from projectq.types import WeakQubitRef + +from .._exceptions import InvalidCommandError +from .._utils import _rearrange_result +from ._aqt_http_client import retrieve, send + + +def _format_counts(samples, length): + counts = {} + for result in samples: + h_result = _rearrange_result(result, length) + if h_result not in counts: + counts[h_result] = 1 + else: + counts[h_result] += 1 + return dict(sorted(counts.items(), key=lambda item: item[0])) + + +class AQTBackend(BasicEngine): # pylint: disable=too-many-instance-attributes + """ + Backend for building circuits and submitting them to the AQT API. + + The AQT Backend class, which stores the circuit, transforms it to the appropriate data format, and sends the + circuit through the AQT API. + """ + + def __init__( + self, + use_hardware=False, + num_runs=100, + verbose=False, + token='', + device='simulator', + num_retries=3000, + interval=1, + retrieve_execution=None, + ): # pylint: disable=too-many-arguments + """ + Initialize the Backend object. + + Args: + use_hardware (bool): If True, the code is run on the AQT quantum chip (instead of using the AQT simulator) + num_runs (int): Number of runs to collect statistics. (default is 100, max is usually around 200) + verbose (bool): If True, statistics are printed, in addition to the measurement result being registered + (at the end of the circuit). + token (str): AQT user API token. + device (str): name of the AQT device to use. simulator By default + num_retries (int): Number of times to retry to obtain results from the AQT API. (default is 3000) + interval (float, int): Number of seconds between successive attempts to obtain results from the AQT API. + (default is 1) + retrieve_execution (int): Job ID to retrieve instead of re- running the circuit (e.g., if previous run + timed out). + """ + super().__init__() + self._reset() + if use_hardware: + self.device = device + else: + self.device = 'simulator' + self._clear = True + self._num_runs = num_runs + self._verbose = verbose + self._token = token + self._num_retries = num_retries + self._interval = interval + self._probabilities = {} + self._circuit = [] + self._mapper = [] + self._measured_ids = [] + self._allocated_qubits = set() + self._retrieve_execution = retrieve_execution + + def is_available(self, cmd): + """ + Return true if the command can be executed. + + The AQT ion trap can only do Rx,Ry and Rxx. + + Args: + cmd (Command): Command for which to check availability + """ + if get_control_count(cmd) == 0: + if isinstance(cmd.gate, (Rx, Ry, Rxx)): + return True + if cmd.gate in (Measure, Allocate, Deallocate, Barrier): + return True + return False + + def _reset(self): + """Reset all temporary variables (after flush gate).""" + self._clear = True + self._measured_ids = [] + + def _store(self, cmd): + """ + Temporarily store the command cmd. + + Translates the command and stores it in a local variable (self._cmds). + + Args: + cmd: Command to store + """ + if self._clear: + self._probabilities = {} + self._clear = False + self._circuit = [] + self._allocated_qubits = set() + + gate = cmd.gate + if gate == Allocate: + self._allocated_qubits.add(cmd.qubits[0][0].id) + return + if gate == Deallocate: + return + if gate == Measure: + qb_id = cmd.qubits[0][0].id + logical_id = None + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + logical_id = tag.logical_qubit_id + break + if logical_id is None: + logical_id = qb_id + self._mapper.append(qb_id) + self._measured_ids += [logical_id] + return + if isinstance(gate, (Rx, Ry, Rxx)): + qubits = [] + qubits.append(cmd.qubits[0][0].id) + if len(cmd.qubits) == 2: + qubits.append(cmd.qubits[1][0].id) + angle = gate.angle / math.pi + instruction = [] + u_name = {'Rx': "X", 'Ry': "Y", 'Rxx': "MS"} + instruction.append(u_name[str(gate)[0 : int(len(cmd.qubits) + 1)]]) # noqa: E203 + instruction.append(round(angle, 2)) + instruction.append(qubits) + self._circuit.append(instruction) + return + if gate == Barrier: + return + raise InvalidCommandError(f"Invalid command: {str(cmd)}") + + def _logical_to_physical(self, qb_id): + """ + Return the physical location of the qubit with the given logical id. + + If no mapper is present then simply returns the qubit ID. + + Args: + qb_id (int): ID of the logical qubit whose position should be returned. + """ + try: + mapping = self.main_engine.mapper.current_mapping + if qb_id not in mapping: + raise RuntimeError( + f"Unknown qubit id {qb_id}. " + "Please make sure eng.flush() was called and that the qubit was eliminated during optimization." + ) + return mapping[qb_id] + except AttributeError as err: + if qb_id not in self._mapper: + raise RuntimeError( + f"Unknown qubit id {qb_id}. Please make sure eng.flush() was called and that the qubit was " + "eliminated during optimization." + ) from err + return qb_id + + def get_probabilities(self, qureg): + """ + Return the probability of the outcome `bit_string` when measuring the quantum register `qureg`. + + Return the list of basis states with corresponding probabilities. If input qureg is a subset of the register + used for the experiment, then returns the projected probabilities over the other states. The measured bits + are ordered according to the supplied quantum register, i.e., the left-most bit in the state-string + corresponds to the first qubit in the supplied quantum register. + + Warning: + Only call this function after the circuit has been executed! + + Args: + qureg (list): Quantum register determining the order of the qubits. + + Returns: + probability_dict (dict): Dictionary mapping n-bit strings to probabilities. + + Raises: + RuntimeError: If no data is available (i.e., if the circuit has not been executed). Or if a qubit was + supplied which was not present in the circuit (might have gotten optimized away). + """ + if len(self._probabilities) == 0: + raise RuntimeError("Please, run the circuit first!") + + probability_dict = {} + for state, probability in self._probabilities.items(): + mapped_state = ['0'] * len(qureg) + for i, qubit in enumerate(qureg): + mapped_state[i] = state[self._logical_to_physical(qubit.id)] + mapped_state = "".join(mapped_state) + + probability_dict[mapped_state] = probability_dict.get(mapped_state, 0) + probability + return probability_dict + + def _run(self): + """ + Run the circuit. + + Send the circuit via the AQT API using the provided user token / ask for the user token. + """ + # finally: measurements + # NOTE AQT DOESN'T SEEM TO HAVE MEASUREMENT INSTRUCTIONS (no + # intermediate measurements are allowed, so implicit at the end) + # return if no operations. + if not self._circuit: + return + + n_qubit = max(self._allocated_qubits) + 1 + info = {} + # Hack: AQT instructions specifically need "GATE" string representation + # instead of 'GATE' + info['circuit'] = str(self._circuit).replace("'", '"') + info['nq'] = n_qubit + info['shots'] = self._num_runs + info['backend'] = {'name': self.device} + if self._num_runs > 200: + raise Exception("Number of shots limited to 200") + try: + if self._retrieve_execution is None: + res = send( + info, + device=self.device, + token=self._token, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + else: + res = retrieve( + device=self.device, + token=self._token, + jobid=self._retrieve_execution, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + self._num_runs = len(res) + counts = _format_counts(res, n_qubit) + # Determine random outcome + random_outcome = random.random() + p_sum = 0.0 + measured = "" + for state in counts: + probability = counts[state] * 1.0 / self._num_runs + p_sum += probability + star = "" + if p_sum >= random_outcome and measured == "": + measured = state + star = "*" + self._probabilities[state] = probability + if self._verbose and probability > 0: + print(f"{str(state)} with p = {probability}{star}") + + # register measurement result from AQT + for qubit_id in self._measured_ids: + location = self._logical_to_physical(qubit_id) + result = int(measured[location]) + self.main_engine.set_measurement_result(WeakQubitRef(self, qubit_id), result) + self._reset() + except TypeError as err: + raise Exception("Failed to run the circuit. Aborting.") from err + + def receive(self, command_list): + """ + Receive a list of commands. + + Receive a command list and, for each command, stores it until completion. Upon flush, send the data to the + AQT API. + + Args: + command_list: List of commands to execute + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._store(cmd) + else: + self._run() + self._reset() diff --git a/projectq/backends/_aqt/_aqt_http_client.py b/projectq/backends/_aqt/_aqt_http_client.py new file mode 100644 index 000000000..b2c8d9048 --- /dev/null +++ b/projectq/backends/_aqt/_aqt_http_client.py @@ -0,0 +1,270 @@ +# Copyright 2020, 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Back-end to run quantum program on AQT cloud platform.""" + +import getpass +import signal +import time + +import requests +from requests import Session +from requests.compat import urljoin + +from .._exceptions import DeviceOfflineError, DeviceTooSmall, RequestTimeoutError + +# An AQT token can be requested at: +# https://gateway-portal.aqt.eu/ + + +_API_URL = 'https://gateway.aqt.eu/marmot/' + + +class AQT(Session): + """Class managing the session to AQT's APIs.""" + + def __init__(self): + """Initialize an AQT session with AQT's APIs.""" + super().__init__() + self.backends = {} + self.timeout = 5.0 + self.token = None + + def update_devices_list(self, verbose=False): + """ + Update the internal device list. + + Returns: + (list): list of available devices + + Note: + Up to my knowledge there is no proper API call for online devices, so we just assume that the list from + AQT portal always up to date + """ + # TODO: update once the API for getting online devices is available + self.backends = {} + self.backends['aqt_simulator'] = {'nq': 11, 'version': '0.0.1', 'url': 'sim/'} + self.backends['aqt_simulator_noise'] = { + 'nq': 11, + 'version': '0.0.1', + 'url': 'sim/noise-model-1', + } + self.backends['aqt_device'] = {'nq': 4, 'version': '0.0.1', 'url': 'lint/'} + if verbose: + print('- List of AQT devices available:') + print(self.backends) + + def is_online(self, device): + """ + Check whether a device is currently online. + + Args: + device (str): name of the aqt device to use + + Note: + Useless at the moment, may change if the API evolves + """ + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Check if the device is big enough to run the code. + + Args: + info (dict): dictionary sent by the backend containing the code to + run + device (str): name of the aqt device to use + Returns: + (bool): True if device is big enough, False otherwise + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed + + def authenticate(self, token=None): + """ + Authenticate with the AQT Web API. + + Args: + token (str): AQT user API token. + """ + if token is None: + token = getpass.getpass(prompt='AQT token > ') + self.headers.update({'Ocp-Apim-Subscription-Key': token, 'SDK': 'ProjectQ'}) + self.token = token + + def run(self, info, device): + """Run a quantum circuit.""" + argument = { + 'data': info['circuit'], + 'access_token': self.token, + 'repetitions': info['shots'], + 'no_qubits': info['nq'], + } + req = super().put(urljoin(_API_URL, self.backends[device]['url']), data=argument) + req.raise_for_status() + r_json = req.json() + if r_json['status'] != 'queued': + raise Exception('Error in sending the code online') + execution_id = r_json["id"] + return execution_id + + def get_result( # pylint: disable=too-many-arguments + self, device, execution_id, num_retries=3000, interval=1, verbose=False + ): + """Get the result of an execution.""" + if verbose: + print(f"Waiting for results. [Job ID: {execution_id}]") + + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): # pragma: no cover + raise Exception(f"Interrupted. The ID of your submitted job is {execution_id}.") + + try: + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + + for retries in range(num_retries): + argument = {'id': execution_id, 'access_token': self.token} + req = super().put(urljoin(_API_URL, self.backends[device]['url']), data=argument) + req.raise_for_status() + r_json = req.json() + if r_json['status'] == 'finished' or 'samples' in r_json: + return r_json['samples'] + if r_json['status'] != 'running': + raise Exception(f"Error while running the code: {r_json['status']}.") + time.sleep(interval) + if self.is_online(device) and retries % 60 == 0: + self.update_devices_list() + + # TODO: update once the API for getting online devices is + # available + if not self.is_online(device): # pragma: no cover + raise DeviceOfflineError( + f"Device went offline. The ID of your submitted job is {execution_id}." + ) + + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise RequestTimeoutError(f"Timeout. The ID of your submitted job is {execution_id}.") + + +def show_devices(verbose=False): + """ + Access the list of available devices and their properties (ex: for setup configuration). + + Args: + verbose (bool): If True, additional information is printed + + Returns: + (list) list of available devices and their properties + """ + aqt_session = AQT() + aqt_session.update_devices_list(verbose=verbose) + return aqt_session.backends + + +def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): # pylint: disable=too-many-arguments + """ + Retrieve a previously run job by its ID. + + Args: + device (str): Device on which the code was run / is running. + token (str): AQT user API token. + jobid (str): Id of the job to retrieve + + Returns: + (list) samples form the AQT server + """ + aqt_session = AQT() + aqt_session.authenticate(token) + aqt_session.update_devices_list(verbose) + res = aqt_session.get_result(device, jobid, num_retries=num_retries, interval=interval, verbose=verbose) + return res + + +def send( + info, + device='aqt_simulator', + token=None, + num_retries=100, + interval=1, + verbose=False, +): # pylint: disable=too-many-arguments + """ + Send circuit through the AQT API and runs the quantum circuit. + + Args: + info(dict): Contains representation of the circuit to run. + device (str): name of the aqt device. Simulator chosen by default + token (str): AQT user API token. + verbose (bool): If True, additional information is printed, such as measurement statistics. Otherwise, the + backend simply registers one measurement result (same behavior as the projectq Simulator). + + Returns: + (list) samples form the AQT server + + """ + try: + aqt_session = AQT() + + if verbose: + print("- Authenticating...") + if token is not None: + print(f"user API token: {token}") + aqt_session.authenticate(token) + + # check if the device is online + aqt_session.update_devices_list(verbose) + online = aqt_session.is_online(device) + # useless for the moment + if not online: # pragma: no cover + print("The device is offline (for maintenance?). Use the simulator instead or try again later.") + raise DeviceOfflineError("Device is offline.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = aqt_session.can_run_experiment(info, device) + if not runnable: + print( + f"The device is too small ({qmax} qubits available) for the code requested({qneeded} qubits needed).", + "Try to look for another device with more qubits", + ) + raise DeviceTooSmall("Device is too small.") + if verbose: + print(f"- Running code: {info}") + execution_id = aqt_session.run(info, device) + if verbose: + print("- Waiting for results...") + res = aqt_session.get_result( + device, + execution_id, + num_retries=num_retries, + interval=interval, + verbose=verbose, + ) + if verbose: + print("- Done.") + return res + except requests.exceptions.HTTPError as err: + print("- There was an error running your code:") + print(err) + except requests.exceptions.RequestException as err: + print("- Looks like something is wrong with server:") + print(err) + except KeyError as err: + print("- Failed to parse response:") + print(err) + return None diff --git a/projectq/backends/_aqt/_aqt_http_client_test.py b/projectq/backends/_aqt/_aqt_http_client_test.py new file mode 100644 index 000000000..1159bc02b --- /dev/null +++ b/projectq/backends/_aqt/_aqt_http_client_test.py @@ -0,0 +1,446 @@ +# Copyright 2020, 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.backends._aqt._aqt_http_client.py.""" + +import pytest +import requests +from requests.compat import urljoin + +from projectq.backends._aqt import _aqt_http_client + + +# Insure that no HTTP request can be made in all tests in this module +@pytest.fixture(autouse=True) +def no_requests(monkeypatch): + monkeypatch.delattr("requests.sessions.Session.request") + + +_api_url = 'https://gateway.aqt.eu/marmot/' + + +def test_is_online(): + token = 'access' + + aqt_session = _aqt_http_client.AQT() + aqt_session.authenticate(token) + aqt_session.update_devices_list() + assert aqt_session.is_online('aqt_simulator') + assert aqt_session.is_online('aqt_simulator_noise') + assert aqt_session.is_online('aqt_device') + assert not aqt_session.is_online('aqt_unknown') + + +def test_show_devices(): + device_list = _aqt_http_client.show_devices(verbose=True) + # TODO: update once the API for getting online devices is available + assert len(device_list) == 3 + + +def test_send_too_many_qubits(): + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 100, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + token = "access" + + # Code to test: + with pytest.raises(_aqt_http_client.DeviceTooSmall): + _aqt_http_client.send(info, device="aqt_simulator", token=token, verbose=True) + + +def test_send_real_device_online_verbose(monkeypatch): + json_aqt = { + 'data': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'access_token': 'access', + 'repetitions': 1, + 'no_qubits': 3, + } + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + token = "access" + execution_id = '3' + result_ready = [False] + result = "my_result" + request_num = [0] # To assert correct order of calls + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPutResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Run code + if args[1] == urljoin(_api_url, "sim/") and kwargs["data"] == json_aqt and request_num[0] == 0: + request_num[0] += 1 + return MockPutResponse({"id": execution_id, "status": "queued"}, 200) + elif ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and not result_ready[0] + and request_num[0] == 1 + ): + result_ready[0] = True + request_num[0] += 1 + return MockPutResponse({"status": 'running'}, 200) + elif ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and result_ready[0] + and request_num[0] == 2 + ): + return MockPutResponse({"samples": result, "status": 'finished'}, 200) + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + # Code to test: + res = _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) + assert res == result + + +def test_send_that_errors_are_caught(monkeypatch): + def mocked_requests_put(*args, **kwargs): + # Test that this error gets caught + raise requests.exceptions.HTTPError + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + # Patch login data + token = 12345 + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) + + +def test_send_that_errors_are_caught2(monkeypatch): + def mocked_requests_put(*args, **kwargs): + # Test that this error gets caught + raise requests.exceptions.RequestException + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + # Patch login data + token = 12345 + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) + + +def test_send_that_errors_are_caught3(monkeypatch): + def mocked_requests_put(*args, **kwargs): + # Test that this error gets caught + raise KeyError + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + # Patch login data + token = 12345 + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) + + +def test_send_that_errors_are_caught4(monkeypatch): + json_aqt = { + 'data': '[]', + 'access_token': 'access', + 'repetitions': 1, + 'no_qubits': 3, + } + info = {'circuit': '[]', 'nq': 3, 'shots': 1, 'backend': {'name': 'aqt_simulator'}} + token = "access" + execution_id = '123e' + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPutResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Run code + if args[1] == urljoin(_api_url, "sim/") and kwargs["data"] == json_aqt: + return MockPutResponse({"id": execution_id, "status": "error"}, 200) + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + + # Code to test: + _aqt_http_client.time.sleep = lambda x: x + with pytest.raises(Exception): + _aqt_http_client.send( + info, + device="aqt_simulator", + token=token, + num_retries=10, + verbose=True, + ) + + +def test_timeout_exception(monkeypatch): + json_aqt = { + 'data': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'access_token': 'access', + 'repetitions': 1, + 'no_qubits': 3, + } + info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 1, + 'backend': {'name': 'aqt_simulator'}, + } + token = "access" + execution_id = '123e' + tries = [0] + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPutResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Run code + if args[1] == urljoin(_api_url, "sim/") and kwargs["data"] == json_aqt: + return MockPutResponse({"id": execution_id, "status": "queued"}, 200) + if ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + ): + tries[0] += 1 + return MockPutResponse({"status": 'running'}, 200) + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + # Code to test: + _aqt_http_client.time.sleep = lambda x: x + for tok in (None, token): + with pytest.raises(Exception) as excinfo: + _aqt_http_client.send( + info, + device="aqt_simulator", + token=tok, + num_retries=10, + verbose=True, + ) + assert "123e" in str(excinfo.value) # check that job id is in exception + assert tries[0] > 0 + + +def test_retrieve(monkeypatch): + token = "access" + execution_id = '123e' + result_ready = [False] + result = "my_result" + request_num = [0] # To assert correct order of calls + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPutResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Run code + if ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and not result_ready[0] + and request_num[0] < 1 + ): + result_ready[0] = True + request_num[0] += 1 + return MockPutResponse({"status": 'running'}, 200) + if ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and result_ready[0] + and request_num[0] == 1 + ): + return MockPutResponse({"samples": result, "status": 'finished'}, 200) + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + + def user_password_input(prompt): + if prompt == "AQT token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + # Code to test: + _aqt_http_client.time.sleep = lambda x: x + res = _aqt_http_client.retrieve(device="aqt_simulator", token=None, verbose=True, jobid="123e") + assert res == result + + +def test_retrieve_that_errors_are_caught(monkeypatch): + token = "access" + execution_id = '123e' + result_ready = [False] + request_num = [0] # To assert correct order of calls + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPutResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Run code + if ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and not result_ready[0] + and request_num[0] < 1 + ): + result_ready[0] = True + request_num[0] += 1 + return MockPutResponse({"status": 'running'}, 200) + if ( + args[1] == urljoin(_api_url, "sim/") + and kwargs["data"]["access_token"] == token + and kwargs["data"]["id"] == execution_id + and result_ready[0] + and request_num[0] == 1 + ): + return MockPutResponse({"status": 'error'}, 200) + + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + + # Code to test: + _aqt_http_client.time.sleep = lambda x: x + with pytest.raises(Exception): + _aqt_http_client.retrieve(device="aqt_simulator", token=token, verbose=True, jobid="123e") diff --git a/projectq/backends/_aqt/_aqt_test.py b/projectq/backends/_aqt/_aqt_test.py new file mode 100644 index 000000000..d35bcaf6a --- /dev/null +++ b/projectq/backends/_aqt/_aqt_test.py @@ -0,0 +1,239 @@ +# Copyright 2020, 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.backends._aqt._aqt.py.""" + +import math + +import pytest + +from projectq import MainEngine +from projectq.backends._aqt import _aqt +from projectq.cengines import BasicMapperEngine, DummyEngine +from projectq.ops import ( + NOT, + All, + Allocate, + Barrier, + Command, + Deallocate, + Entangle, + Measure, + Rx, + Rxx, + Ry, + Rz, + S, + Sdag, + T, + Tdag, + X, + Y, + Z, +) +from projectq.types import WeakQubitRef + + +# Insure that no HTTP request can be made in all tests in this module +@pytest.fixture(autouse=True) +def no_requests(monkeypatch): + monkeypatch.delattr("requests.sessions.Session.request") + + +@pytest.mark.parametrize( + "single_qubit_gate, is_available", + [ + (X, False), + (Y, False), + (Z, False), + (T, False), + (Tdag, False), + (S, False), + (Sdag, False), + (Allocate, True), + (Deallocate, True), + (Measure, True), + (NOT, False), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), False), + (Rxx(0.5), True), + (Barrier, True), + (Entangle, False), + ], +) +def test_aqt_backend_is_available(single_qubit_gate, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + aqt_backend = _aqt.AQTBackend() + cmd = Command(eng, single_qubit_gate, (qubit1,)) + assert aqt_backend.is_available(cmd) == is_available + + +@pytest.mark.parametrize("num_ctrl_qubits, is_available", [(0, True), (1, False), (2, False), (3, False)]) +def test_aqt_backend_is_available_control_not(num_ctrl_qubits, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + aqt_backend = _aqt.AQTBackend() + cmd = Command(eng, Rx(0.5), (qubit1,), controls=qureg) + assert aqt_backend.is_available(cmd) == is_available + cmd = Command(eng, Rxx(0.5), (qubit1,), controls=qureg) + assert aqt_backend.is_available(cmd) == is_available + + +def test_aqt_backend_init(): + backend = _aqt.AQTBackend(verbose=True, use_hardware=True) + assert len(backend._circuit) == 0 + + +def test_aqt_empty_circuit(): + backend = _aqt.AQTBackend(verbose=True) + eng = MainEngine(backend=backend) + eng.flush() + + +def test_aqt_invalid_command(): + backend = _aqt.AQTBackend(verbose=True) + + qb = WeakQubitRef(None, 1) + cmd = Command(None, gate=S, qubits=[(qb,)]) + with pytest.raises(Exception): + backend.receive([cmd]) + + +def test_aqt_sent_error(monkeypatch): + # patch send + def mock_send(*args, **kwargs): + raise TypeError + + monkeypatch.setattr(_aqt, "send", mock_send) + + backend = _aqt.AQTBackend(verbose=True) + eng = MainEngine(backend=backend) + qubit = eng.allocate_qubit() + Rx(0.5) | qubit + with pytest.raises(Exception): + qubit[0].__del__() + eng.flush() + # atexit sends another FlushGate, therefore we remove the backend: + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + +def test_aqt_too_many_runs(): + backend = _aqt.AQTBackend(num_runs=300, verbose=True) + eng = MainEngine(backend=backend, engine_list=[]) + with pytest.raises(Exception): + qubit = eng.allocate_qubit() + Rx(math.pi / 2) | qubit + eng.flush() + + # Avoid exception at deletion + backend._num_runs = 1 + backend._circuit = [] + + +def test_aqt_retrieve(monkeypatch): + # patch send + def mock_retrieve(*args, **kwargs): + return [0, 6, 0, 6, 0, 0, 0, 6, 0, 6] + + monkeypatch.setattr(_aqt, "retrieve", mock_retrieve) + backend = _aqt.AQTBackend(retrieve_execution="a3877d18-314f-46c9-86e7-316bc4dbe968", verbose=True) + + eng = MainEngine(backend=backend, engine_list=[]) + unused_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + # entangle the qureg + Ry(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Ry(math.pi / 2) | qureg[0] + Rxx(math.pi / 2) | (qureg[0], qureg[1]) + Rx(7 * math.pi / 2) | qureg[0] + Ry(7 * math.pi / 2) | qureg[0] + Rx(7 * math.pi / 2) | qureg[1] + del unused_qubit + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[1]]) + assert prob_dict['11'] == pytest.approx(0.4) + assert prob_dict['00'] == pytest.approx(0.6) + + # Unknown qubit and no mapper + invalid_qubit = [WeakQubitRef(eng, 10)] + with pytest.raises(RuntimeError): + eng.backend.get_probabilities(invalid_qubit) + + +def test_aqt_backend_functional_test(monkeypatch): + correct_info = { + 'circuit': '[["Y", 0.5, [1]], ["X", 0.5, [1]], ["X", 0.5, [1]], ' + '["Y", 0.5, [1]], ["MS", 0.5, [1, 2]], ["X", 3.5, [1]], ' + '["Y", 3.5, [1]], ["X", 3.5, [2]]]', + 'nq': 3, + 'shots': 10, + 'backend': {'name': 'simulator'}, + } + + def mock_send(*args, **kwargs): + assert args[0] == correct_info + return [0, 6, 0, 6, 0, 0, 0, 6, 0, 6] + + monkeypatch.setattr(_aqt, "send", mock_send) + + backend = _aqt.AQTBackend(verbose=True, num_runs=10) + # no circuit has been executed -> raises exception + with pytest.raises(RuntimeError): + backend.get_probabilities([]) + + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + + eng = MainEngine(backend=backend, engine_list=[mapper]) + + unused_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + # entangle the qureg + Ry(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Ry(math.pi / 2) | qureg[0] + Rxx(math.pi / 2) | (qureg[0], qureg[1]) + Rx(7 * math.pi / 2) | qureg[0] + Ry(7 * math.pi / 2) | qureg[0] + Rx(7 * math.pi / 2) | qureg[1] + All(Barrier) | qureg + del unused_qubit + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[1]]) + assert prob_dict['11'] == pytest.approx(0.4) + assert prob_dict['00'] == pytest.approx(0.6) + + # Unknown qubit and no mapper + invalid_qubit = [WeakQubitRef(eng, 10)] + with pytest.raises(RuntimeError): + eng.backend.get_probabilities(invalid_qubit) + + with pytest.raises(RuntimeError): + eng.backend.get_probabilities(eng.allocate_qubit()) diff --git a/projectq/backends/_awsbraket/__init__.py b/projectq/backends/_awsbraket/__init__.py new file mode 100644 index 000000000..985fc1b10 --- /dev/null +++ b/projectq/backends/_awsbraket/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ProjectQ module for supporting the AWS Braket platform.""" + +try: + from ._awsbraket import AWSBraketBackend +except ImportError: # pragma: no cover + + class AWSBraketBackend: # pylint: disable=too-few-public-methods + """Dummy class.""" + + def __init__(self, *args, **kwargs): + """Initialize dummy class.""" + raise RuntimeError( + "Failed to import one of the dependencies required to use " + "the Amazon Braket Backend.\n" + "Did you install ProjectQ using the [braket] extra? " + "(python3 -m pip install projectq[braket])" + ) diff --git a/projectq/backends/_awsbraket/_awsbraket.py b/projectq/backends/_awsbraket/_awsbraket.py new file mode 100755 index 000000000..78fe5bd01 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket.py @@ -0,0 +1,459 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Back-end to run quantum program on AWS Braket provided devices.""" + +import json +import random + +from projectq.cengines import BasicEngine +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control +from projectq.ops import ( + Allocate, + Barrier, + DaggeredGate, + Deallocate, + FlushGate, + HGate, + Measure, + R, + Rx, + Ry, + Rz, + Sdag, + SGate, + SqrtXGate, + SwapGate, + Tdag, + TGate, + XGate, + YGate, + ZGate, +) +from projectq.types import WeakQubitRef + +from ._awsbraket_boto3_client import retrieve, send + +# TODO: Add MatrixGate to cover the unitary operation in the SV1 simulator + + +class AWSBraketBackend(BasicEngine): # pylint: disable=too-many-instance-attributes + """ + Compiler engine class implementing support for the AWS Braket framework. + + The AWS Braket Backend class, which stores the circuit, transforms it to Braket compatible, and sends the circuit + through the Boto3 and Amazon Braket SDK. + """ + + def __init__( + self, + use_hardware=False, + num_runs=1000, + verbose=False, + credentials=None, + s3_folder=None, + device='Aspen-8', + num_retries=30, + interval=1, + retrieve_execution=None, + ): # pylint: disable=too-many-arguments + """ + Initialize the Backend object. + + Args: + use_hardware (bool): If True, the code is run on one of the AWS Braket backends, by default on the Rigetti + Aspen-8 chip (instead of using the AWS Braket SV1 Simulator) + num_runs (int): Number of runs to collect statistics. (default is 1000) + verbose (bool): If True, statistics are printed, in addition to the measurement result being registered + (at the end of the circuit). + credentials (dict): mapping the AWS key credentials as the AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. + device (str): name of the device to use. Rigetti Aspen-8 by default. Valid names are "Aspen-8", "IonQ + Device" and "SV1" + num_retries (int): Number of times to retry to obtain results from AWS Braket. (default is 30) + interval (float, int): Number of seconds between successive attempts to obtain results from AWS Braket. + (default is 1) + retrieve_execution (str): TaskArn to retrieve instead of re-running the circuit (e.g., if previous run + timed out). The TaskArns have the form: + "arn:aws:braket:us-east-1:123456789012:quantum-task/5766032b-2b47-4bf9-cg00-f11851g4015b" + """ + super().__init__() + self._reset() + if use_hardware: + self.device = device + else: + self.device = 'SV1' + self._clear = False + self._num_runs = num_runs + self._verbose = verbose + self._credentials = credentials + self._s3_folder = s3_folder + self._num_retries = num_retries + self._interval = interval + self._probabilities = {} + self._circuit = "" + self._measured_ids = [] + self._allocated_qubits = set() + self._retrieve_execution = retrieve_execution + + # Dictionary to translate the gates from ProjectQ to AWSBraket + self._gationary = { + XGate: 'x', + YGate: 'y', + ZGate: 'z', + HGate: 'h', + R: 'phaseshift', + Rx: 'rx', + Ry: 'ry', + Rz: 'rz', + SGate: 's', # NB: Sdag is 'si' + TGate: 't', # NB: Tdag is 'ti' + SwapGate: 'swap', + SqrtXGate: 'v', + } + + # Static head and tail to be added to the circuit + # to build the "action". + self._circuithead = '{"braketSchemaHeader": \ +{"name": "braket.ir.jaqcd.program", "version": "1"}, \ +"results": [], "basis_rotation_instructions": [], \ +"instructions": [' + + self._circuittail = ']}' + + def is_available(self, cmd): # pylint: disable=too-many-return-statements,too-many-branches + """ + Return true if the command can be executed. + + Depending on the device chosen, the operations available differ. + + The operations available for the Aspen-8 Rigetti device are: + - "cz" = Control Z, "xy" = Not available in ProjectQ, "ccnot" = Toffoli (ie. controlled CNOT), "cnot" = + Control X, "cphaseshift" = Control R, "cphaseshift00" "cphaseshift01" "cphaseshift10" = Not available + in ProjectQ, + "cswap" = Control Swap, "h" = H, "i" = Identity, not in ProjectQ, "iswap" = Not available in ProjectQ, + "phaseshift" = R, "pswap" = Not available in ProjectQ, "rx" = Rx, "ry" = Ry, "rz" = Rz, "s" = S, "si" = + Sdag, "swap" = Swap, "t" = T, "ti" = Tdag, "x" = X, "y" = Y, "z" = Z + + The operations available for the IonQ Device are: + - "x" = X, "y" = Y, "z" = Z, "rx" = Rx, "ry" = Ry, "rz" = Rz, "h", H, "cnot" = Control X, "s" = S, "si" = + Sdag, "t" = T, "ti" = Tdag, "v" = SqrtX, "vi" = Not available in ProjectQ, "xx" "yy" "zz" = Not available in + ProjectQ, "swap" = Swap, "i" = Identity, not in ProjectQ + + The operations available for the StateVector simulator (SV1) are the union of the ones for Rigetti Aspen-8 and + IonQ Device plus some more: + - "cy" = Control Y, "unitary" = Arbitrary unitary gate defined as a matrix equivalent to the MatrixGate in + ProjectQ, "xy" = Not available in ProjectQ + + Args: + cmd (Command): Command for which to check availability + """ + gate = cmd.gate + if gate in (Measure, Allocate, Deallocate, Barrier): + return True + + if has_negative_control(cmd): + return False + + if self.device == 'Aspen-8': + if get_control_count(cmd) == 2: + return isinstance(gate, XGate) + if get_control_count(cmd) == 1: + return isinstance(gate, (R, ZGate, XGate, SwapGate)) + if get_control_count(cmd) == 0: + return isinstance( + gate, + ( + R, + Rx, + Ry, + Rz, + XGate, + YGate, + ZGate, + HGate, + SGate, + TGate, + SwapGate, + ), + ) or gate in (Sdag, Tdag) + + if self.device == 'IonQ Device': + if get_control_count(cmd) == 1: + return isinstance(gate, XGate) + if get_control_count(cmd) == 0: + return isinstance( + gate, + ( + Rx, + Ry, + Rz, + XGate, + YGate, + ZGate, + HGate, + SGate, + TGate, + SqrtXGate, + SwapGate, + ), + ) or gate in (Sdag, Tdag) + + if self.device == 'SV1': + if get_control_count(cmd) == 2: + return isinstance(gate, XGate) + if get_control_count(cmd) == 1: + return isinstance(gate, (R, ZGate, YGate, XGate, SwapGate)) + if get_control_count(cmd) == 0: + # TODO: add MatrixGate to cover the unitary operation + # TODO: Missing XY gate in ProjectQ + return isinstance( + gate, + ( + R, + Rx, + Ry, + Rz, + XGate, + YGate, + ZGate, + HGate, + SGate, + TGate, + SqrtXGate, + SwapGate, + ), + ) or gate in (Sdag, Tdag) + return False + + def _reset(self): + """Reset all temporary variables (after flush gate).""" + self._clear = True + self._measured_ids = [] + + def _store(self, cmd): # pylint: disable=too-many-branches + """ + Temporarily store the command cmd. + + Translates the command and stores it in a local variable (self._circuit) in JSON format. + + Args: + cmd: Command to store + """ + gate = cmd.gate + + # Do not clear the self._clear flag for those gates + if gate in (Deallocate, Barrier): + return + + num_controls = get_control_count(cmd) + gate_type = ( + type(gate) if not isinstance(gate, DaggeredGate) else type(gate._gate) # pylint: disable=protected-access + ) + + if self._clear: + self._probabilities = {} + self._clear = False + self._circuit = "" + self._allocated_qubits = set() + + if gate == Allocate: + self._allocated_qubits.add(cmd.qubits[0][0].id) + return + if gate == Measure: + qb_id = cmd.qubits[0][0].id + logical_id = None + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + logical_id = tag.logical_qubit_id + break + self._measured_ids.append(logical_id if logical_id is not None else qb_id) + return + + # All other supported gate types + json_cmd = {} + + if num_controls > 1: + json_cmd['controls'] = [qb.id for qb in cmd.control_qubits] + elif num_controls == 1: + json_cmd['control'] = cmd.control_qubits[0].id + + qubits = [qb.id for qureg in cmd.qubits for qb in qureg] + if len(qubits) > 1: + json_cmd['targets'] = qubits + else: + json_cmd['target'] = qubits[0] + + if isinstance(gate, (R, Rx, Ry, Rz)): + json_cmd['angle'] = gate.angle + + if isinstance(gate, DaggeredGate): + json_cmd['type'] = f"{'c' * num_controls + self._gationary[gate_type]}i" + elif isinstance(gate, (XGate)) and num_controls > 0: + json_cmd['type'] = f"{'c' * (num_controls - 1)}cnot" + else: + json_cmd['type'] = 'c' * num_controls + self._gationary[gate_type] + + self._circuit += f"{json.dumps(json_cmd)}, " + + # TODO: Add unitary for the SV1 simulator as MatrixGate + + def _logical_to_physical(self, qb_id): + """ + Return the physical location of the qubit with the given logical id. + + Args: + qb_id (int): ID of the logical qubit whose position should be returned. + """ + if self.main_engine.mapper is not None: + mapping = self.main_engine.mapper.current_mapping + if qb_id not in mapping: + raise RuntimeError( + f"Unknown qubit id {qb_id} in current mapping. " + "Please make sure eng.flush() was called and that the qubit was eliminated during optimization." + ) + return mapping[qb_id] + return qb_id + + def get_probabilities(self, qureg): + """ + Return the list of basis states with corresponding probabilities. + + If input qureg is a subset of the register used for the experiment, then returns the projected probabilities + over the other states. + + The measured bits are ordered according to the supplied quantum register, i.e., the left-most bit in the + state-string corresponds to the first qubit in the supplied quantum register. + + Args: + qureg (list): Quantum register determining the order of the qubits. + + Returns: + probability_dict (dict): Dictionary mapping n-bit strings to probabilities. + + Raises: + RuntimeError: If no data is available (i.e., if the circuit has not been executed). Or if a qubit was + supplied which was not present in the circuit (might have gotten optimized away). + + Warning: + Only call this function after the circuit has been executed! + + This is maintained in the same form of IBM and AQT for compatibility but in AWSBraket, a previously + executed circuit will store the results in the S3 bucket and it can be retrieved at any point in time + thereafter. + No circuit execution should be required at the time of retrieving the results and probabilities if the + circuit has already been executed. + In order to obtain the probabilities of a previous job you have to get the TaskArn and remember the qubits + and ordering used in the original job. + """ + if len(self._probabilities) == 0: + raise RuntimeError("Please, run the circuit first!") + + probability_dict = {} + for state, probability in self._probabilities.items(): + mapped_state = ['0'] * len(qureg) + for i, qubit in enumerate(qureg): + if self._logical_to_physical(qubit.id) >= len(state): # pragma: no cover + raise IndexError(f'Physical ID {qubit.id} > length of internal probabilities array') + mapped_state[i] = state[self._logical_to_physical(qubit.id)] + mapped_state = "".join(mapped_state) + if mapped_state not in probability_dict: + probability_dict[mapped_state] = probability + else: + probability_dict[mapped_state] += probability + return probability_dict + + def _run(self): + """ + Run the circuit. + + Send the circuit via the AWS Boto3 SDK. Use the provided Access Key and Secret key or ask for them if not + provided + """ + # NB: the AWS Braket API does not require explicit measurement commands at the end of a circuit; after running + # any circuit, all qubits are implicitly measured. Also, AWS Braket currently does not support intermediate + # measurements. + + # If the clear flag is set, nothing to do here... + if self._clear: + return + + # In Braket the results for the jobs are stored in S3. You can recover the results from previous jobs using + # the TaskArn (self._retrieve_execution). + if self._retrieve_execution is not None: + res = retrieve( + credentials=self._credentials, + task_arn=self._retrieve_execution, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + else: + # Return if no operations have been added. + if not self._circuit: + return + + n_qubit = len(self._allocated_qubits) + info = {} + info['circuit'] = self._circuithead + self._circuit.rstrip(', ') + self._circuittail + info['nq'] = n_qubit + info['shots'] = self._num_runs + info['backend'] = {'name': self.device} + res = send( + info, + device=self.device, + credentials=self._credentials, + s3_folder=self._s3_folder, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + + counts = res + + # Determine random outcome + random_outcome = random.random() + p_sum = 0.0 + measured = "" + for state in counts: + probability = counts[state] + p_sum += probability + star = "" + if p_sum >= random_outcome and measured == "": + measured = state + star = "*" + self._probabilities[state] = probability + if self._verbose and probability > 0: + print(f"{state} with p = {probability}{star}") + + # register measurement result + for qubit_id in self._measured_ids: + result = int(measured[self._logical_to_physical(qubit_id)]) + self.main_engine.set_measurement_result(WeakQubitRef(self.main_engine, qubit_id), result) + self._reset() + + def receive(self, command_list): + """ + Receives a command list and, for each command, stores it until completion. + + Args: + command_list: List of commands to execute + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._store(cmd) + else: + self._run() + self._reset() + if not self.is_last_engine: + self.send([cmd]) diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client.py new file mode 100755 index 000000000..7924a40b8 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client.py @@ -0,0 +1,432 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Back-end to run quantum program on AWS Braket supported devices. + +This backend requires the official AWS SDK for Python, Boto3. +The installation is very simple +> pip install boto3 +""" + +import getpass +import json +import re +import signal +import time + +import boto3 +import botocore + +from .._exceptions import DeviceOfflineError, DeviceTooSmall, RequestTimeoutError + + +class AWSBraket: + """Manage a session between ProjectQ and AWS Braket service.""" + + def __init__(self): + """Initialize a session with the AWS Braket Web APIs.""" + self.backends = {} + self.timeout = 5.0 + self._credentials = {} + self._s3_folder = [] + + def authenticate(self, credentials=None): + """ + Authenticate with AWSBraket Web APIs. + + Args: + credentials (dict): mapping the AWS key credentials as the AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. + """ + if credentials is None: # pragma: no cover + credentials['AWS_ACCESS_KEY_ID'] = getpass.getpass(prompt="Enter AWS_ACCESS_KEY_ID: ") + credentials['AWS_SECRET_KEY'] = getpass.getpass(prompt="Enter AWS_SECRET_KEY: ") + + self._credentials = credentials + + def get_s3_folder(self, s3_folder=None): + """ + Get the S3 bucket that contains the results. + + Args: + s3_folder (list): contains the S3 bucket and directory to store the results. + """ + if s3_folder is None: # pragma: no cover + s3_bucket = input("Enter the S3 Bucket configured in Braket: ") + s3_directory = input("Enter the Directory created in the S3 Bucket: ") + s3_folder = [s3_bucket, s3_directory] + + self._s3_folder = s3_folder + + def get_list_devices(self, verbose=False): + """ + Get the list of available devices with their basic properties. + + Args: + verbose (bool): print the returned dictionary if True + + Returns: + (dict) backends dictionary by deviceName, containing the qubit size 'nq', the coupling map 'coupling_map' + if applicable (IonQ Device as an ion device is having full connectivity) and the Schema Header + version 'version', because it seems that no device version is available by now + """ + # TODO: refresh region_names if more regions get devices available + self.backends = {} + region_names = ['us-west-1', 'us-east-1'] + for region in region_names: + client = boto3.client( + 'braket', + region_name=region, + aws_access_key_id=self._credentials['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=self._credentials['AWS_SECRET_KEY'], + ) + filters = [] + devicelist = client.search_devices(filters=filters) + for result in devicelist['devices']: + if result['deviceType'] not in ['QPU', 'SIMULATOR']: + continue + if result['deviceType'] == 'QPU': + device_capabilities = json.loads( + client.get_device(deviceArn=result['deviceArn'])['deviceCapabilities'] + ) + self.backends[result['deviceName']] = { + 'nq': device_capabilities['paradigm']['qubitCount'], + 'coupling_map': device_capabilities['paradigm']['connectivity']['connectivityGraph'], + 'version': device_capabilities['braketSchemaHeader']['version'], + 'location': region, # deviceCapabilities['service']['deviceLocation'], + 'deviceArn': result['deviceArn'], + 'deviceParameters': device_capabilities['deviceParameters']['properties']['braketSchemaHeader'][ + 'const' + ], + 'deviceModelParameters': device_capabilities['deviceParameters']['definitions'][ + 'GateModelParameters' + ]['properties']['braketSchemaHeader']['const'], + } + # Unfortunately the Capabilities schemas are not homogeneus for real devices and simulators + elif result['deviceType'] == 'SIMULATOR': + device_capabilities = json.loads( + client.get_device(deviceArn=result['deviceArn'])['deviceCapabilities'] + ) + self.backends[result['deviceName']] = { + 'nq': device_capabilities['paradigm']['qubitCount'], + 'coupling_map': {}, + 'version': device_capabilities['braketSchemaHeader']['version'], + 'location': 'us-east-1', + 'deviceArn': result['deviceArn'], + 'deviceParameters': device_capabilities['deviceParameters']['properties']['braketSchemaHeader'][ + 'const' + ], + 'deviceModelParameters': device_capabilities['deviceParameters']['definitions'][ + 'GateModelParameters' + ]['properties']['braketSchemaHeader']['const'], + } + + if verbose: + print('- List of AWSBraket devices available:') + print(list(self.backends)) + + return self.backends + + def is_online(self, device): + """ + Check if the device is in the list of available backends. + + Args: + device (str): name of the device to check + + Returns: + (bool) True if device is available, False otherwise + """ + # TODO: Add info for the device if it is actually ONLINE + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Check if the device is big enough to run the code. + + Args: + info (dict): dictionary sent by the backend containing the code to run + device (str): name of the device to use + + Returns: + (tuple): (bool) True if device is big enough, False otherwise (int) + maximum number of qubit available on the device (int) + number of qubit needed for the circuit + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed + + def run(self, info, device): + """ + Run the quantum code to the AWS Braket selected device. + + Args: + info (dict): dictionary sent by the backend containing the code to run + device (str): name of the device to use + + Returns: + task_arn (str): The Arn of the task + """ + argument = { + 'circ': info['circuit'], + 's3_folder': self._s3_folder, + 'shots': info['shots'], + } + + region_name = self.backends[device]['location'] + device_parameters = { + 'braketSchemaHeader': self.backends[device]['deviceParameters'], + 'paradigmParameters': { + 'braketSchemaHeader': self.backends[device]['deviceModelParameters'], + 'qubitCount': info['nq'], + 'disableQubitRewiring': False, + }, + } + device_parameters = json.dumps(device_parameters) + + client_braket = boto3.client( + 'braket', + region_name=region_name, + aws_access_key_id=self._credentials['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=self._credentials['AWS_SECRET_KEY'], + ) + + response = client_braket.create_quantum_task( + action=argument['circ'], + deviceArn=self.backends[device]['deviceArn'], + deviceParameters=device_parameters, + outputS3Bucket=argument['s3_folder'][0], + outputS3KeyPrefix=argument['s3_folder'][1], + shots=argument['shots'], + ) + + return response['quantumTaskArn'] + + def get_result(self, execution_id, num_retries=30, interval=1, verbose=False): # pylint: disable=too-many-locals + """Get the result of an execution.""" + if verbose: + print(f"Waiting for results. [Job Arn: {execution_id}]") + + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): # pragma: no cover + raise Exception(f"Interrupted. The Arn of your submitted job is {execution_id}.") + + def _calculate_measurement_probs(measurements): + """ + Calculate the measurement probabilities . + + Calculate the measurement probabilities based on the list of measurements for a job sent to a SV1 Braket + simulator. + + Args: + measurements (list): list of measurements + + Returns: + measurementsProbabilities (dict): The measurements with their probabilities + """ + total_mes = len(measurements) + unique_mes = [list(x) for x in {tuple(x) for x in measurements}] + total_unique_mes = len(unique_mes) + len_qubits = len(unique_mes[0]) + measurements_probabilities = {} + for i in range(total_unique_mes): + strqubits = '' + for qubit_idx in range(len_qubits): + strqubits += str(unique_mes[i][qubit_idx]) + prob = measurements.count(unique_mes[i]) / total_mes + measurements_probabilities[strqubits] = prob + + return measurements_probabilities + + # The region_name is obtained from the task_arn itself + region_name = re.split(':', execution_id)[3] + client_braket = boto3.client( + 'braket', + region_name=region_name, + aws_access_key_id=self._credentials['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=self._credentials['AWS_SECRET_KEY'], + ) + + try: + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + + for _ in range(num_retries): + quantum_task = client_braket.get_quantum_task(quantumTaskArn=execution_id) + status = quantum_task['status'] + bucket = quantum_task['outputS3Bucket'] + directory = quantum_task['outputS3Directory'] + resultsojectname = f"{directory}/results.json" + if status == 'COMPLETED': + # Get the device type to obtian the correct measurement + # structure + devicetype_used = client_braket.get_device(deviceArn=quantum_task['deviceArn'])['deviceType'] + # Get the results from S3 + client_s3 = boto3.client( + 's3', + aws_access_key_id=self._credentials['AWS_ACCESS_KEY_ID'], + aws_secret_access_key=self._credentials['AWS_SECRET_KEY'], + ) + s3result = client_s3.get_object(Bucket=bucket, Key=resultsojectname) + if verbose: + print(f"Results obtained. [Status: {status}]") + result_content = json.loads(s3result['Body'].read()) + + if devicetype_used == 'QPU': + return result_content['measurementProbabilities'] + if devicetype_used == 'SIMULATOR': + return _calculate_measurement_probs(result_content['measurements']) + if status == 'FAILED': + raise Exception( + f'Error while running the code: {status}. ' + f'The failure reason was: {quantum_task["failureReason"]}.' + ) + if status == 'CANCELLING': + raise Exception(f"The job received a CANCEL operation: {status}.") + time.sleep(interval) + # NOTE: Be aware that AWS is billing if a lot of API calls are + # executed, therefore the num_repetitions is set to a small + # number by default. + # For QPU devices the job is always queued and there are some + # working hours available. + # In addition the results and state is written in the + # results.json file in the S3 Bucket and does not depend on the + # status of the device + + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise RequestTimeoutError( + f"Timeout. The Arn of your submitted job is {execution_id} and the status of the job is {status}." + ) + + +def show_devices(credentials=None, verbose=False): + """ + Access the list of available devices and their properties (ex: for setup configuration). + + Args: + credentials (dict): Dictionary storing the AWS credentials with keys AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. + verbose (bool): If True, additional information is printed + + Returns: + (list) list of available devices and their properties + """ + awsbraket_session = AWSBraket() + awsbraket_session.authenticate(credentials=credentials) + return awsbraket_session.get_list_devices(verbose=verbose) + + +# TODO: Create a Show Online properties per device + + +def retrieve(credentials, task_arn, num_retries=30, interval=1, verbose=False): + """ + Retrieve a job/task by its Arn. + + Args: + credentials (dict): Dictionary storing the AWS credentials with keys AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. + task_arn (str): The Arn of the task to retrieve + + + Returns: + (dict) measurement probabilities from the result stored in the S3 folder + """ + try: + awsbraket_session = AWSBraket() + if verbose: + print("- Authenticating...") + if credentials is not None: + print(f"AWS credentials: {credentials['AWS_ACCESS_KEY_ID']}, {credentials['AWS_SECRET_KEY']}") + awsbraket_session.authenticate(credentials=credentials) + res = awsbraket_session.get_result(task_arn, num_retries=num_retries, interval=interval, verbose=verbose) + return res + except botocore.exceptions.ClientError as error: + error_code = error.response['Error']['Code'] + if error_code == 'ResourceNotFoundException': + print("- Unable to locate the job with Arn ", task_arn) + print(error, error_code) + raise + + +def send( # pylint: disable=too-many-branches,too-many-arguments,too-many-locals + info, device, credentials, s3_folder, num_retries=30, interval=1, verbose=False +): + """ + Send circuit through the Boto3 SDK and runs the quantum circuit. + + Args: + info(dict): Contains representation of the circuit to run. + device (str): name of the AWS Braket device. + credentials (dict): Dictionary storing the AWS credentials with keys AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. + s3_folder (list): Contains the S3 bucket and directory to store the results. + verbose (bool): If True, additional information is printed, such as measurement statistics. Otherwise, the + backend simply registers one measurement result (same behavior as the projectq Simulator). + + Returns: + (list) samples from the AWS Braket device + """ + try: + awsbraket_session = AWSBraket() + if verbose: + print("- Authenticating...") + if credentials is not None: + print(f"AWS credentials: {credentials['AWS_ACCESS_KEY_ID']}, {credentials['AWS_SECRET_KEY']}") + awsbraket_session.authenticate(credentials=credentials) + awsbraket_session.get_s3_folder(s3_folder=s3_folder) + + # check if the device is online/is available + awsbraket_session.get_list_devices(verbose) + online = awsbraket_session.is_online(device) + if online: + print("The job will be queued in any case, please take this into account") + else: + print("The device is not available. Use the simulator instead or try another device.") + raise DeviceOfflineError("Device is not available.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = awsbraket_session.can_run_experiment(info, device) + if not runnable: + print( + f"The device is too small ({qmax} qubits available) for the code", + f"requested({qneeded} qubits needed). Try to look for another device with more qubits", + ) + raise DeviceTooSmall("Device is too small.") + if verbose: + print(f"- Running code: {info}") + task_arn = awsbraket_session.run(info, device) + print(f"Your task Arn is: {task_arn}. Make note of that for future reference") + + if verbose: + print("- Waiting for results...") + res = awsbraket_session.get_result(task_arn, num_retries=num_retries, interval=interval, verbose=verbose) + if verbose: + print("- Done.") + return res + + except botocore.exceptions.ClientError as error: + error_code = error.response['Error']['Code'] + if error_code == 'AccessDeniedException': + print("- There was an error: the access to Braket was denied") + if error_code == 'DeviceOfflineException': + print("- There was an error: the device is offline") + if error_code == 'InternalServiceException': + print("- There was an internal Bracket service error") + if error_code == 'ServiceQuotaExceededException': + print("- There was an error: the quota on Braket was exceed") + if error_code == 'ValidationException': + print("- There was a Validation error") + print(error, error_code) + raise diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py new file mode 100644 index 000000000..2b6e33e05 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py @@ -0,0 +1,298 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Test for projectq.backends._awsbraket._awsbraket_boto3_client.py """ + +import pytest + +from ._awsbraket_boto3_client_test_fixtures import * # noqa: F401,F403 + +# ============================================================================== + +_has_boto3 = True +try: + import botocore + + from projectq.backends._awsbraket import _awsbraket_boto3_client +except ImportError: + _has_boto3 = False + +has_boto3 = pytest.mark.skipif(not _has_boto3, reason="boto3 package is not installed") + +# ============================================================================== + + +@has_boto3 +def test_show_devices(mocker, show_devices_setup): + creds, search_value, device_value, devicelist_result = show_devices_setup + + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device']) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + devicelist = _awsbraket_boto3_client.show_devices(credentials=creds) + assert devicelist == devicelist_result + + +# ============================================================================== + +completed_value = { + 'deviceArn': 'arndevice', + 'deviceParameters': 'parameters', + 'failureReason': 'None', + 'outputS3Bucket': 'amazon-braket-bucket', + 'outputS3Directory': 'complete/directory', + 'quantumTaskArn': 'arntask', + 'shots': 123, + 'status': 'COMPLETED', + 'tags': {'tagkey': 'tagvalue'}, +} + +failed_value = { + 'failureReason': 'This is a failure reason', + 'outputS3Bucket': 'amazon-braket-bucket', + 'outputS3Directory': 'complete/directory', + 'status': 'FAILED', +} + +cancelling_value = { + 'failureReason': 'None', + 'outputS3Bucket': 'amazon-braket-bucket', + 'outputS3Directory': 'complete/directory', + 'status': 'CANCELLING', +} + +other_value = { + 'failureReason': 'None', + 'outputS3Bucket': 'amazon-braket-bucket', + 'outputS3Directory': 'complete/directory', + 'status': 'OTHER', +} + +# ------------------------------------------------------------------------------ + + +@has_boto3 +@pytest.mark.parametrize( + "var_status, var_result", + [ + ('completed', completed_value), + ('failed', failed_value), + ('cancelling', cancelling_value), + ('other', other_value), + ], +) +def test_retrieve(mocker, var_status, var_result, retrieve_setup): + arntask, creds, device_value, res_completed, results_dict = retrieve_setup + + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) + mock_boto3_client.get_quantum_task.return_value = var_result + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + if var_status == 'completed': + res = _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask) + assert res == res_completed + else: + with pytest.raises(Exception) as exinfo: + _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask, num_retries=2) + print(exinfo.value) + if var_status == 'failed': + assert ( + str(exinfo.value) + == "Error while running the code: FAILED. \ +The failure reason was: This is a failure reason." + ) + + if var_status == 'cancelling': + assert str(exinfo.value) == "The job received a CANCEL operation: CANCELLING." + if var_status == 'other': + assert ( + str(exinfo.value) + == "Timeout. The Arn of your submitted job \ +is arn:aws:braket:us-east-1:id:taskuuid \ +and the status of the job is OTHER." + ) + + +# ============================================================================== + + +@has_boto3 +def test_retrieve_devicetypes(mocker, retrieve_devicetypes_setup): + ( + arntask, + creds, + device_value, + results_dict, + res_completed, + ) = retrieve_devicetypes_setup + + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) + mock_boto3_client.get_quantum_task.return_value = completed_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + res = _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask) + assert res == res_completed + + +# ============================================================================== + + +@has_boto3 +def test_send_too_many_qubits(mocker, send_too_many_setup): + (creds, s3_folder, search_value, device_value, info_too_much) = send_too_many_setup + + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device']) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + with pytest.raises(_awsbraket_boto3_client.DeviceTooSmall): + _awsbraket_boto3_client.send(info_too_much, device='name2', credentials=creds, s3_folder=s3_folder) + + +# ============================================================================== + + +@has_boto3 +@pytest.mark.parametrize( + "var_status, var_result", + [ + ('completed', completed_value), + ('failed', failed_value), + ('cancelling', cancelling_value), + ('other', other_value), + ], +) +def test_send_real_device_online_verbose(mocker, var_status, var_result, real_device_online_setup): + ( + qtarntask, + creds, + s3_folder, + info, + search_value, + device_value, + res_completed, + results_dict, + ) = real_device_online_setup + + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.create_quantum_task.return_value = qtarntask + mock_boto3_client.get_quantum_task.return_value = var_result + mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + # This is a ficticios situation because the job will be always queued + # at the beginning. After that the status will change at some point in time + # If the status change while the _get_result loop with num_retries, is + # active the result will change. We mock this using some preconfigured + # statuses in var_status for the tests + + if var_status == 'completed': + res = _awsbraket_boto3_client.send(info, device='name2', credentials=creds, s3_folder=s3_folder, verbose=True) + assert res == res_completed + else: + with pytest.raises(Exception) as exinfo: + _awsbraket_boto3_client.send( + info, + device='name2', + credentials=creds, + s3_folder=s3_folder, + verbose=True, + num_retries=2, + ) + print(exinfo.value) + if var_status == 'failed': + assert ( + str(exinfo.value) + == "Error while running the code: FAILED. The failure \ +reason was: This is a failure reason." + ) + + if var_status == 'cancelling': + assert str(exinfo.value) == "The job received a CANCEL operation: CANCELLING." + if var_status == 'other': + assert ( + str(exinfo.value) + == "Timeout. The Arn of your submitted job \ +is arn:aws:braket:us-east-1:id:taskuuid \ +and the status of the job is OTHER." + ) + + +# ============================================================================== + + +@has_boto3 +@pytest.mark.parametrize( + "var_error", + [ + ('AccessDeniedException'), + ('DeviceOfflineException'), + ('InternalServiceException'), + ('ServiceQuotaExceededException'), + ('ValidationException'), + ], +) +def test_send_that_errors_are_caught(mocker, var_error, send_that_error_setup): + creds, s3_folder, info, search_value, device_value = send_that_error_setup + + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device', 'create_quantum_task']) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.create_quantum_task.side_effect = botocore.exceptions.ClientError( + {"Error": {"Code": var_error, "Message": f"Msg error for {var_error}"}}, + "create_quantum_task", + ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + with pytest.raises(botocore.exceptions.ClientError): + _awsbraket_boto3_client.send(info, device='name2', credentials=creds, s3_folder=s3_folder, num_retries=2) + + with pytest.raises(_awsbraket_boto3_client.DeviceOfflineError): + _awsbraket_boto3_client.send( + info, + device='unknown', + credentials=creds, + s3_folder=s3_folder, + num_retries=2, + ) + + +# ============================================================================== + + +@has_boto3 +@pytest.mark.parametrize("var_error", [('ResourceNotFoundException')]) +def test_retrieve_error_arn_not_exist(mocker, var_error, arntask, creds): + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task']) + mock_boto3_client.get_quantum_task.side_effect = botocore.exceptions.ClientError( + {"Error": {"Code": var_error, "Message": f"Msg error for {var_error}"}}, + "get_quantum_task", + ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + with pytest.raises(botocore.exceptions.ClientError): + _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask) + + +# ============================================================================== diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test_fixtures.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test_fixtures.py new file mode 100644 index 000000000..ba38906b3 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test_fixtures.py @@ -0,0 +1,431 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== +# This file contains: +# +# - Helper fixtures: +# * arntask +# * creds +# * s3_folder +# * info +# * results_json +# * results_dict +# * res_completed +# * search_value +# * device_value +# * devicelist_result +# - Setup fixtures for specific tests: +# * show_devices_setup +# * retrieve_setup +# * retrieve_devicetypes_setup +# * send_too_many_setup +# * real_device_online_setup +# ============================================================================== + +"""Define test fixtures for the AWSBraket HTTP client.""" + +import json +from io import StringIO + +import pytest + +try: + from botocore.response import StreamingBody +except ImportError: + + class StreamingBody: + """Dummy implementation of a StreamingBody.""" + + def __init__(self, raw_stream, content_length): + """Initialize a dummy StreamingBody.""" + + +# ============================================================================== + + +@pytest.fixture +def arntask(): + """Define an ARNTask test setup.""" + return 'arn:aws:braket:us-east-1:id:taskuuid' + + +@pytest.fixture +def creds(): + """Credentials test setup.""" + return { + 'AWS_ACCESS_KEY_ID': 'aws_access_key_id', + 'AWS_SECRET_KEY': 'aws_secret_key', + } + + +@pytest.fixture +def s3_folder(): + """S3 folder value test setup.""" + return ['S3Bucket', 'S3Directory'] + + +@pytest.fixture +def info(): + """Info value test setup.""" + return { + 'circuit': '{"braketSchemaHeader":' + '{"name": "braket.ir.jaqcd.program", "version": "1"}, ' + '"results": [], "basis_rotation_instructions": [], ' + '"instructions": [{"target": 0, "type": "h"}, {\ + "target": 1, "type": "h"}, {\ + "control": 1, "target": 2, "type": "cnot"}]}', + 'nq': 10, + 'shots': 1, + 'backend': {'name': 'name2'}, + } + + +@pytest.fixture +def results_json(): + """Results test setup.""" + return json.dumps( + { + "braketSchemaHeader": { + "name": "braket.task_result.gate_model_task_result", + "version": "1", + }, + "measurementProbabilities": { + "000": 0.1, + "010": 0.4, + "110": 0.1, + "001": 0.1, + "111": 0.3, + }, + "measuredQubits": [0, 1, 2], + } + ) + + +@pytest.fixture +def results_dict(results_json): + """Results dict test setup.""" + body = StreamingBody(StringIO(results_json), len(results_json)) + return { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body, + } + + +@pytest.fixture +def res_completed(): + """Completed results test setup.""" + return {"000": 0.1, "010": 0.4, "110": 0.1, "001": 0.1, "111": 0.3} + + +@pytest.fixture +def search_value(): + """Search value test setup.""" + return { + "devices": [ + { + "deviceArn": "arn1", + "deviceName": "name1", + "deviceType": "SIMULATOR", + "deviceStatus": "ONLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn2", + "deviceName": "name2", + "deviceType": "QPU", + "deviceStatus": "OFFLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn3", + "deviceName": "name3", + "deviceType": "QPU", + "deviceStatus": "ONLINE", + "providerName": "pname2", + }, + { + "deviceArn": "invalid", + "deviceName": "invalid", + "deviceType": "BLABLA", + "deviceStatus": "ONLINE", + "providerName": "pname3", + }, + ] + } + + +@pytest.fixture +def device_value_devicecapabilities(): + """Device capabilities value test setup.""" + return json.dumps( + { + "braketSchemaHeader": { + "name": "braket.device_schema.rigetti.rigetti_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + "deviceLocation": "us-east-1", + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": { + "fullyConnected": False, + "connectivityGraph": {"1": ["2", "3"]}, + }, + }, + "deviceParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.rigetti.rigetti_device_parameters", + "version": "1", + } + } + }, + "definitions": { + "GateModelParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.gate_model_parameters", + "version": "1", + } + } + } + } + }, + }, + } + ) + + +@pytest.fixture +def device_value(device_value_devicecapabilities): + """Device value test setup.""" + return { + "deviceName": "Aspen-8", + "deviceType": "QPU", + "providerName": "provider1", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_value_devicecapabilities, + } + + +@pytest.fixture +def devicelist_result(): + """Device list value test setup.""" + return { + 'name1': { + 'coupling_map': {}, + 'deviceArn': 'arn1', + 'location': 'us-east-1', + 'nq': 30, + 'version': '1', + 'deviceParameters': { + 'name': 'braket.device_schema.rigetti.rigetti_device_parameters', + 'version': '1', + }, + 'deviceModelParameters': { + 'name': 'braket.device_schema.gate_model_parameters', + 'version': '1', + }, + }, + 'name2': { + 'coupling_map': {'1': ['2', '3']}, + 'deviceArn': 'arn2', + 'location': 'us-east-1', + 'nq': 30, + 'version': '1', + 'deviceParameters': { + 'name': 'braket.device_schema.rigetti.rigetti_device_parameters', + 'version': '1', + }, + 'deviceModelParameters': { + 'name': 'braket.device_schema.gate_model_parameters', + 'version': '1', + }, + }, + 'name3': { + 'coupling_map': {'1': ['2', '3']}, + 'deviceArn': 'arn3', + 'location': 'us-east-1', + 'nq': 30, + 'version': '1', + 'deviceParameters': { + 'name': 'braket.device_schema.rigetti.rigetti_device_parameters', + 'version': '1', + }, + 'deviceModelParameters': { + 'name': 'braket.device_schema.gate_model_parameters', + 'version': '1', + }, + }, + } + + +# ============================================================================== + + +@pytest.fixture +def show_devices_setup(creds, search_value, device_value, devicelist_result): + """Show devices value test setup.""" + return creds, search_value, device_value, devicelist_result + + +@pytest.fixture +def retrieve_setup(arntask, creds, device_value, res_completed, results_dict): + """Retrieve value test setup.""" + return arntask, creds, device_value, res_completed, results_dict + + +@pytest.fixture(params=["qpu", "sim"]) +def retrieve_devicetypes_setup(request, arntask, creds, results_json, device_value_devicecapabilities): + """Retrieve device types value test setup.""" + if request.param == "qpu": + body_qpu = StreamingBody(StringIO(results_json), len(results_json)) + results_dict = { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body_qpu, + } + + device_value = { + "deviceName": "Aspen-8", + "deviceType": "QPU", + "providerName": "provider1", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_value_devicecapabilities, + } + + res_completed = {"000": 0.1, "010": 0.4, "110": 0.1, "001": 0.1, "111": 0.3} + else: + results_json_simulator = json.dumps( + { + "braketSchemaHeader": { + "name": "braket.task_result.gate_model_task_result", + "version": "1", + }, + "measurements": [ + [0, 0], + [0, 1], + [1, 1], + [0, 1], + [0, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + [1, 1], + ], + "measuredQubits": [0, 1], + } + ) + body_simulator = StreamingBody(StringIO(results_json_simulator), len(results_json_simulator)) + results_dict = { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body_simulator, + } + + device_value = { + "deviceName": "SV1", + "deviceType": "SIMULATOR", + "providerName": "providerA", + "deviceStatus": "ONLINE", + "deviceCapabilities": device_value_devicecapabilities, + } + + res_completed = {"00": 0.1, "01": 0.3, "11": 0.6} + return arntask, creds, device_value, results_dict, res_completed + + +@pytest.fixture +def send_too_many_setup(creds, s3_folder, search_value, device_value): + """Send too many value test setup.""" + info_too_much = { + 'circuit': '{"braketSchemaHeader":' + '{"name": "braket.ir.jaqcd.program", "version": "1"}, ' + '"results": [], "basis_rotation_instructions": [], ' + '"instructions": [{"target": 0, "type": "h"}, {\ + "target": 1, "type": "h"}, {\ + "control": 1, "target": 2, "type": "cnot"}]}', + 'nq': 100, + 'shots': 1, + 'backend': {'name': 'name2'}, + } + return creds, s3_folder, search_value, device_value, info_too_much + + +@pytest.fixture +def real_device_online_setup( + arntask, + creds, + s3_folder, + info, + search_value, + device_value, + res_completed, + results_json, +): + """Real device online value test setup.""" + qtarntask = {'quantumTaskArn': arntask} + body = StreamingBody(StringIO(results_json), len(results_json)) + results_dict = { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body, + } + + return ( + qtarntask, + creds, + s3_folder, + info, + search_value, + device_value, + res_completed, + results_dict, + ) + + +@pytest.fixture +def send_that_error_setup(creds, s3_folder, info, search_value, device_value): + """Send error value test setup.""" + return creds, s3_folder, info, search_value, device_value diff --git a/projectq/backends/_awsbraket/_awsbraket_test.py b/projectq/backends/_awsbraket/_awsbraket_test.py new file mode 100644 index 000000000..1a5288c88 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket_test.py @@ -0,0 +1,663 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Test for projectq.backends._awsbraket._awsbraket.py""" + +import copy +import math + +import pytest + +from projectq import MainEngine +from projectq.cengines import ( + AutoReplacer, + BasicMapperEngine, + DecompositionRuleSet, + DummyEngine, +) +from projectq.cengines._replacer import NoGateDecompositionError +from projectq.ops import ( + CNOT, + NOT, + All, + Allocate, + Barrier, + C, + Command, + Deallocate, + Entangle, + H, + MatrixGate, + Measure, + Ph, + R, + Rx, + Ry, + Rz, + S, + Sdag, + SqrtX, + Swap, + T, + Tdag, + X, + Y, + Z, +) +from projectq.types import WeakQubitRef + +from ._awsbraket_test_fixtures import * # noqa: F401,F403 + +# ============================================================================== + +_has_boto3 = True +try: + import botocore + + from projectq.backends._awsbraket import _awsbraket +except ImportError: + _has_boto3 = False + +has_boto3 = pytest.mark.skipif(not _has_boto3, reason="boto3 package is not installed") + +# ============================================================================== + + +@pytest.fixture(params=["mapper", "no_mapper"]) +def mapper(request): + """ + Adds a mapper which changes qubit ids by adding 1 + """ + if request.param == "mapper": + + class TrivialMapper(BasicMapperEngine): + def __init__(self): + super().__init__() + self.current_mapping = {} + + def receive(self, command_list): + for cmd in command_list: + for qureg in cmd.all_qubits: + for qubit in qureg: + if qubit.id == -1: + continue + elif qubit.id not in self.current_mapping: + previous_map = self.current_mapping + previous_map[qubit.id] = qubit.id + self.current_mapping = previous_map + self._send_cmd_with_mapped_ids(cmd) + + return TrivialMapper() + if request.param == "no_mapper": + return None + + +# ============================================================================== +''' +Gate availability Tests +''' + + +@has_boto3 +@pytest.mark.parametrize( + "single_qubit_gate_aspen, is_available_aspen", + [ + (X, True), + (Y, True), + (Z, True), + (H, True), + (T, True), + (Tdag, True), + (S, True), + (Sdag, True), + (Allocate, True), + (Deallocate, True), + (SqrtX, False), + (Measure, True), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), True), + (Ph(0.5), False), + (R(0.5), True), + (Barrier, True), + (Entangle, False), + ], +) +def test_awsbraket_backend_is_available_aspen(single_qubit_gate_aspen, is_available_aspen): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='Aspen-8') + cmd = Command(eng, single_qubit_gate_aspen, (qubit1,)) + assert aws_backend.is_available(cmd) == is_available_aspen + + +@has_boto3 +@pytest.mark.parametrize( + "single_qubit_gate_ionq, is_available_ionq", + [ + (X, True), + (Y, True), + (Z, True), + (H, True), + (T, True), + (Tdag, True), + (S, True), + (Sdag, True), + (Allocate, True), + (Deallocate, True), + (SqrtX, True), + (Measure, True), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), True), + (Ph(0.5), False), + (R(0.5), False), + (Barrier, True), + (Entangle, False), + ], +) +def test_awsbraket_backend_is_available_ionq(single_qubit_gate_ionq, is_available_ionq): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='IonQ Device') + cmd = Command(eng, single_qubit_gate_ionq, (qubit1,)) + assert aws_backend.is_available(cmd) == is_available_ionq + + +@has_boto3 +@pytest.mark.parametrize( + "single_qubit_gate_sv1, is_available_sv1", + [ + (X, True), + (Y, True), + (Z, True), + (H, True), + (T, True), + (Tdag, True), + (S, True), + (Sdag, True), + (Allocate, True), + (Deallocate, True), + (SqrtX, True), + (Measure, True), + (Rx(0.5), True), + # use MatrixGate as unitary gate + (MatrixGate([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]), False), + (Ry(0.5), True), + (Rz(0.5), True), + (Ph(0.5), False), + (R(0.5), True), + (Barrier, True), + (Entangle, False), + ], +) +def test_awsbraket_backend_is_available_sv1(single_qubit_gate_sv1, is_available_sv1): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=False) + cmd = Command(eng, single_qubit_gate_sv1, (qubit1,)) + assert aws_backend.is_available(cmd) == is_available_sv1 + + +@has_boto3 +@pytest.mark.parametrize( + "num_ctrl_qubits_aspen, is_available_aspen", + [(0, True), (1, True), (2, True), (3, False)], +) +def test_awsbraket_backend_is_available_control_not_aspen(num_ctrl_qubits_aspen, is_available_aspen): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits_aspen) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='Aspen-8') + cmd = Command(eng, X, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_aspen + + +@has_boto3 +@pytest.mark.parametrize( + "num_ctrl_qubits_ionq, is_available_ionq", + [(0, True), (1, True), (2, False), (3, False)], +) +def test_awsbraket_backend_is_available_control_not_ionq(num_ctrl_qubits_ionq, is_available_ionq): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits_ionq) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='IonQ Device') + cmd = Command(eng, X, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_ionq + + +@has_boto3 +@pytest.mark.parametrize( + "num_ctrl_qubits_sv1, is_available_sv1", + [(0, True), (1, True), (2, True), (3, False)], +) +def test_awsbraket_backend_is_available_control_not_sv1(num_ctrl_qubits_sv1, is_available_sv1): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits_sv1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=False) + cmd = Command(eng, X, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_sv1 + + +@has_boto3 +@pytest.mark.parametrize( + "ctrl_singlequbit_aspen, is_available_aspen", + [ + (X, True), + (Y, False), + (Z, True), + (R(0.5), True), + (Rx(0.5), False), + (Ry(0.5), False), + (Rz(0.5), False), + (NOT, True), + ], +) +def test_awsbraket_backend_is_available_control_singlequbit_aspen(ctrl_singlequbit_aspen, is_available_aspen): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='Aspen-8') + cmd = Command(eng, ctrl_singlequbit_aspen, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_aspen + + +@has_boto3 +@pytest.mark.parametrize( + "ctrl_singlequbit_ionq, is_available_ionq", + [ + (X, True), + (Y, False), + (Z, False), + (R(0.5), False), + (Rx(0.5), False), + (Ry(0.5), False), + (Rz(0.5), False), + ], +) +def test_awsbraket_backend_is_available_control_singlequbit_ionq(ctrl_singlequbit_ionq, is_available_ionq): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='IonQ Device') + cmd = Command(eng, ctrl_singlequbit_ionq, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_ionq + + +@has_boto3 +@pytest.mark.parametrize( + "ctrl_singlequbit_sv1, is_available_sv1", + [ + (X, True), + (Y, True), + (Z, True), + (R(0.5), True), + (Rx(0.5), False), + (Ry(0.5), False), + (Rz(0.5), False), + ], +) +def test_awsbraket_backend_is_available_control_singlequbit_sv1(ctrl_singlequbit_sv1, is_available_sv1): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=False) + cmd = Command(eng, ctrl_singlequbit_sv1, (qubit1,), controls=qureg) + assert aws_backend.is_available(cmd) == is_available_sv1 + + +@has_boto3 +def test_awsbraket_backend_is_available_negative_control(): + backend = _awsbraket.AWSBraketBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) + + +@has_boto3 +def test_awsbraket_backend_is_available_swap_aspen(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='Aspen-8') + cmd = Command(eng, Swap, (qubit1, qubit2)) + assert aws_backend.is_available(cmd) + + +@has_boto3 +def test_awsbraket_backend_is_available_swap_ionq(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='IonQ Device') + cmd = Command(eng, Swap, (qubit1, qubit2)) + assert aws_backend.is_available(cmd) + + +@has_boto3 +def test_awsbraket_backend_is_available_swap_sv1(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=False) + cmd = Command(eng, Swap, (qubit1, qubit2)) + assert aws_backend.is_available(cmd) + + +@has_boto3 +def test_awsbraket_backend_is_available_control_swap_aspen(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=True, device='Aspen-8') + cmd = Command(eng, Swap, (qubit1, qubit2), controls=qureg) + assert aws_backend.is_available(cmd) + + +@has_boto3 +def test_awsbraket_backend_is_available_control_swap_sv1(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + aws_backend = _awsbraket.AWSBraketBackend(use_hardware=False) + cmd = Command(eng, Swap, (qubit1, qubit2), controls=qureg) + assert aws_backend.is_available(cmd) + + +''' +End of Gate availability Tests +''' + + +@has_boto3 +def test_awsbraket_backend_init(): + backend = _awsbraket.AWSBraketBackend(verbose=True, use_hardware=True) + assert len(backend._circuit) == 0 + + +@has_boto3 +def test_awsbraket_empty_circuit(): + backend = _awsbraket.AWSBraketBackend(verbose=True) + eng = MainEngine(backend=backend) + eng.flush() + + +@has_boto3 +def test_awsbraket_invalid_command(): + backend = _awsbraket.AWSBraketBackend(use_hardware=True, verbose=True) + qb = WeakQubitRef(None, 1) + cmd = Command(None, gate=SqrtX, qubits=[(qb,)]) + with pytest.raises(Exception): + backend.receive([cmd]) + + +# ============================================================================== + + +@has_boto3 +def test_awsbraket_sent_error(mocker, sent_error_setup): + creds, s3_folder, search_value, device_value = sent_error_setup + + var_error = 'ServiceQuotaExceededException' + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device', 'create_quantum_task']) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.create_quantum_task.side_effect = botocore.exceptions.ClientError( + {"Error": {"Code": var_error, "Message": f"Msg error for {var_error}"}}, + "create_quantum_task", + ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + backend = _awsbraket.AWSBraketBackend( + verbose=True, + credentials=creds, + s3_folder=s3_folder, + use_hardware=True, + device='Aspen-8', + num_runs=10, + ) + eng = MainEngine(backend=backend, verbose=True) + qubit = eng.allocate_qubit() + Rx(0.5) | qubit + qubit[0].__del__() + with pytest.raises(botocore.exceptions.ClientError): + eng.flush() + + # atexit sends another FlushGate, therefore we remove the backend: + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + +@has_boto3 +def test_awsbraket_sent_error_2(): + backend = _awsbraket.AWSBraketBackend(verbose=True, use_hardware=True, device='Aspen-8') + eng = MainEngine( + backend=backend, + engine_list=[AutoReplacer(DecompositionRuleSet())], + verbose=True, + ) + qubit = eng.allocate_qubit() + Rx(math.pi) | qubit + + with pytest.raises(NoGateDecompositionError): + SqrtX | qubit + # no setup to decompose SqrtX gate for Aspen-8, + # so not accepted by the backend + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + +# ============================================================================== + + +@has_boto3 +def test_awsbraket_retrieve(mocker, retrieve_setup): + (arntask, creds, completed_value, device_value, results_dict) = retrieve_setup + + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) + mock_boto3_client.get_quantum_task.return_value = completed_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + backend = _awsbraket.AWSBraketBackend(retrieve_execution=arntask, credentials=creds, num_retries=2, verbose=True) + + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + + eng = MainEngine(backend=backend, engine_list=[mapper], verbose=True) + + separate_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(3) + del separate_qubit + eng.flush() + + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) + assert prob_dict['000'] == 0.04 + assert prob_dict['101'] == 0.2 + assert prob_dict['010'] == 0.8 + + # Unknown qubit or no mapper + invalid_qubit = [WeakQubitRef(eng, 10)] + with pytest.raises(RuntimeError): + eng.backend.get_probabilities(invalid_qubit) + + +# ============================================================================== + + +@has_boto3 +def test_awsbraket_backend_functional_test(mocker, functional_setup, mapper): + ( + creds, + s3_folder, + search_value, + device_value, + qtarntask, + completed_value, + results_dict, + ) = functional_setup + + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.create_quantum_task.return_value = qtarntask + mock_boto3_client.get_quantum_task.return_value = completed_value + mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + backend = _awsbraket.AWSBraketBackend( + verbose=True, + credentials=creds, + s3_folder=s3_folder, + use_hardware=True, + device='Aspen-8', + num_runs=10, + num_retries=2, + ) + # no circuit has been executed -> raises exception + with pytest.raises(RuntimeError): + backend.get_probabilities([]) + + from projectq.backends import ResourceCounter + + rcount = ResourceCounter() + engine_list = [rcount] + if mapper is not None: + engine_list.append(mapper) + eng = MainEngine(backend=backend, engine_list=engine_list, verbose=True) + + unused_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(3) + + H | qureg[0] + S | qureg[1] + T | qureg[2] + NOT | qureg[0] + Y | qureg[1] + Z | qureg[2] + Rx(0.1) | qureg[0] + Ry(0.2) | qureg[1] + Rz(0.3) | qureg[2] + R(0.6) | qureg[2] + C(X) | (qureg[1], qureg[2]) + C(Swap) | (qureg[0], qureg[1], qureg[2]) + H | qureg[0] + C(Z) | (qureg[1], qureg[2]) + C(R(0.5)) | (qureg[1], qureg[0]) + C(NOT, 2) | ([qureg[2], qureg[1]], qureg[0]) + Swap | (qureg[2], qureg[0]) + Tdag | qureg[1] + Sdag | qureg[0] + + All(Barrier) | qureg + del unused_qubit + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[1]]) + assert prob_dict['00'] == pytest.approx(0.84) + assert prob_dict['01'] == pytest.approx(0.06) + + eng.flush(deallocate_qubits=True) + + +@has_boto3 +def test_awsbraket_functional_test_as_engine(mocker, functional_setup): + ( + creds, + s3_folder, + search_value, + device_value, + qtarntask, + completed_value, + results_dict, + ) = functional_setup + + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + mock_boto3_client.create_quantum_task.return_value = qtarntask + mock_boto3_client.get_quantum_task.return_value = completed_value + mock_boto3_client.get_object.return_value = copy.deepcopy(results_dict) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) + + backend = _awsbraket.AWSBraketBackend( + verbose=True, + credentials=creds, + s3_folder=s3_folder, + use_hardware=True, + device='Aspen-8', + num_runs=10, + num_retries=2, + ) + # no circuit has been executed -> raises exception + with pytest.raises(RuntimeError): + backend.get_probabilities([]) + + eng = MainEngine(backend=DummyEngine(save_commands=True), engine_list=[backend], verbose=True) + + unused_qubit = eng.allocate_qubit() # noqa: F841 + qureg = eng.allocate_qureg(3) + + H | qureg[0] + CNOT | (qureg[0], qureg[1]) + eng.flush() + + assert len(eng.backend.received_commands) == 7 + assert eng.backend.received_commands[4].gate == H + assert eng.backend.received_commands[4].qubits[0][0].id == qureg[0].id + assert eng.backend.received_commands[5].gate == X + assert eng.backend.received_commands[5].control_qubits[0].id == qureg[0].id + assert eng.backend.received_commands[5].qubits[0][0].id == qureg[1].id + + # NB: also test that we can call eng.flush() multiple times + + mock_boto3_client.get_object.return_value = copy.deepcopy(results_dict) + + CNOT | (qureg[1], qureg[0]) + H | qureg[1] + eng.flush() + + assert len(eng.backend.received_commands) == 10 + assert eng.backend.received_commands[7].gate == X + assert eng.backend.received_commands[7].control_qubits[0].id == qureg[1].id + assert eng.backend.received_commands[7].qubits[0][0].id == qureg[0].id + assert eng.backend.received_commands[8].gate == H + assert eng.backend.received_commands[8].qubits[0][0].id == qureg[1].id + + eng.flush(deallocate_qubits=True) diff --git a/projectq/backends/_awsbraket/_awsbraket_test_fixtures.py b/projectq/backends/_awsbraket/_awsbraket_test_fixtures.py new file mode 100644 index 000000000..7ee1dd7b9 --- /dev/null +++ b/projectq/backends/_awsbraket/_awsbraket_test_fixtures.py @@ -0,0 +1,257 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== +# This file contains: +# +# - Helper fixtures: +# * arntask +# * creds +# * s3_folder +# * device_value +# * search_value +# * completed_value +# - Setup fixtures for specific tests: +# * sent_error_setup +# * retrieve_setup +# * functional_setup +# ============================================================================== + +"""Define test fixtures for the AWSBraket backend.""" + +import json +from io import StringIO + +import pytest + +try: + from botocore.response import StreamingBody +except ImportError: + + class StreamingBody: + """Dummy implementation of a StreamingBody.""" + + def __init__(self, raw_stream, content_length): + """Initialize a dummy StreamingBody.""" + + +# ============================================================================== + + +@pytest.fixture +def arntask(): + """Define an ARNTask test setup.""" + return 'arn:aws:braket:us-east-1:id:retrieve_execution' + + +@pytest.fixture +def creds(): + """Credentials test setup.""" + return { + 'AWS_ACCESS_KEY_ID': 'aws_access_key_id', + 'AWS_SECRET_KEY': 'aws_secret_key', + } + + +@pytest.fixture +def s3_folder(): + """S3 folder value test setup.""" + return ['S3Bucket', 'S3Directory'] + + +@pytest.fixture +def device_value(): + """Device value test setup.""" + device_value_devicecapabilities = json.dumps( + { + "braketSchemaHeader": { + "name": "braket.device_schema.rigetti.rigetti_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + "deviceLocation": "us-east-1", + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": { + "fullyConnected": False, + "connectivityGraph": {"1": ["2", "3"]}, + }, + }, + "deviceParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.rigetti.rigetti_device_parameters", + "version": "1", + } + } + }, + "definitions": { + "GateModelParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.gate_model_parameters", + "version": "1", + } + } + } + } + }, + }, + } + ) + + return { + "deviceName": "Aspen-8", + "deviceType": "QPU", + "providerName": "provider1", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_value_devicecapabilities, + } + + +@pytest.fixture +def search_value(): + """Search value test setup.""" + return { + "devices": [ + { + "deviceArn": "arn1", + "deviceName": "name1", + "deviceType": "SIMULATOR", + "deviceStatus": "ONLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn2", + "deviceName": "name2", + "deviceType": "QPU", + "deviceStatus": "OFFLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn3", + "deviceName": "Aspen-8", + "deviceType": "QPU", + "deviceStatus": "ONLINE", + "providerName": "pname2", + }, + ] + } + + +@pytest.fixture +def completed_value(): + """Completed value test setup.""" + return { + 'deviceArn': 'arndevice', + 'deviceParameters': 'parameters', + 'outputS3Bucket': 'amazon-braket-bucket', + 'outputS3Directory': 'complete/directory', + 'quantumTaskArn': 'arntask', + 'shots': 123, + 'status': 'COMPLETED', + 'tags': {'tagkey': 'tagvalue'}, + } + + +# ============================================================================== + + +@pytest.fixture +def sent_error_setup(creds, s3_folder, device_value, search_value): + """Send error test setup.""" + return creds, s3_folder, search_value, device_value + + +@pytest.fixture +def results_json(): + """Results test setup.""" + return json.dumps( + { + "braketSchemaHeader": { + "name": "braket.task_result.gate_model_task_result", + "version": "1", + }, + "measurementProbabilities": { + "0000": 0.04, + "0010": 0.06, + "0110": 0.2, + "0001": 0.3, + "1001": 0.5, + }, + "measuredQubits": [0, 1, 2], + } + ) + + +@pytest.fixture +def retrieve_setup(arntask, creds, device_value, completed_value, results_json): + """Retrieve test setup.""" + body = StreamingBody(StringIO(results_json), len(results_json)) + + results_dict = { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body, + } + + return arntask, creds, completed_value, device_value, results_dict + + +@pytest.fixture +def functional_setup(arntask, creds, s3_folder, search_value, device_value, completed_value, results_json): + """Functional test setup.""" + qtarntask = {'quantumTaskArn': arntask} + body2 = StreamingBody(StringIO(results_json), len(results_json)) + results_dict = { + 'ResponseMetadata': { + 'RequestId': 'CF4CAA48CC18836C', + 'HTTPHeaders': {}, + }, + 'Body': body2, + } + + return ( + creds, + s3_folder, + search_value, + device_value, + qtarntask, + completed_value, + results_dict, + ) + + +# ============================================================================== diff --git a/projectq/backends/_azure/__init__.py b/projectq/backends/_azure/__init__.py new file mode 100644 index 000000000..23d2cb3c8 --- /dev/null +++ b/projectq/backends/_azure/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ProjectQ module for supporting the Azure Quantum platform.""" + +try: + from ._azure_quantum import AzureQuantumBackend +except ImportError: # pragma: no cover + + class AzureQuantumBackend: + """Dummy class.""" + + def __init__(self, *args, **kwargs): + """Initialize dummy class.""" + raise ImportError( + "Missing optional 'azure-quantum' dependencies. To install run: pip install projectq[azure-quantum]" + ) diff --git a/projectq/backends/_azure/_azure_quantum.py b/projectq/backends/_azure/_azure_quantum.py new file mode 100644 index 000000000..682caa69d --- /dev/null +++ b/projectq/backends/_azure/_azure_quantum.py @@ -0,0 +1,387 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Back-end to run quantum programs using Azure Quantum.""" + +from collections import Counter + +import numpy as np + +from projectq.cengines import BasicEngine +from projectq.meta import LogicalQubitIDTag +from projectq.ops import AllocateQubitGate, DeallocateQubitGate, FlushGate, MeasureGate +from projectq.types import WeakQubitRef + +from .._utils import _rearrange_result +from ._azure_quantum_client import retrieve, send +from ._exceptions import AzureQuantumTargetNotFoundError +from ._utils import ( + IONQ_PROVIDER_ID, + QUANTINUUM_PROVIDER_ID, + is_available_ionq, + is_available_quantinuum, + to_json, + to_qasm, +) + +try: + from azure.quantum import Workspace + from azure.quantum.target import IonQ, Quantinuum, Target + from azure.quantum.target.target_factory import TargetFactory +except ImportError: # pragma: no cover + raise ImportError( # pylint: disable=raise-missing-from + "Missing optional 'azure-quantum' dependencies. To install run: pip install projectq[azure-quantum]" + ) + + +class AzureQuantumBackend(BasicEngine): # pylint: disable=too-many-instance-attributes + """Backend for building circuits and submitting them to the Azure Quantum.""" + + DEFAULT_TARGETS = {IONQ_PROVIDER_ID: IonQ, QUANTINUUM_PROVIDER_ID: Quantinuum} + + def __init__( + self, + use_hardware=False, + num_runs=100, + verbose=False, + workspace=None, + target_name='ionq.simulator', + num_retries=100, + interval=1, + retrieve_execution=None, + **kwargs, + ): # pylint: disable=too-many-arguments + """ + Initialize an Azure Quantum Backend object. + + Args: + use_hardware (bool, optional): Whether or not to use real hardware or just a simulator. If False, + regardless of the value of ```target_name```, ```ionq.simulator``` used for IonQ provider and + ```quantinuum.hqs-lt-s1-apival``` used for Quantinuum provider. Defaults to False. + num_runs (int, optional): Number of times to run circuits. Defaults to 100. + verbose (bool, optional): If True, print statistics after job results have been collected. Defaults to + False. + workspace (Workspace, optional): Azure Quantum workspace. If workspace is None, kwargs will be used to + create Workspace object. + target_name (str, optional): Target to run jobs on. Defaults to ```ionq.simulator```. + num_retries (int, optional): Number of times to retry fetching a job after it has been submitted. Defaults + to 100. + interval (int, optional): Number of seconds to wait in between result fetch retries. Defaults to 1. + retrieve_execution (str, optional): An Azure Quantum Job ID. If provided, a job result with this ID will be + fetched. Defaults to None. + """ + super().__init__() + + if target_name in IonQ.target_names: + self._provider_id = IONQ_PROVIDER_ID + elif target_name in Quantinuum.target_names: + self._provider_id = QUANTINUUM_PROVIDER_ID + else: + raise AzureQuantumTargetNotFoundError(f'Target {target_name} does not exit.') + + if use_hardware: + self._target_name = target_name + else: + if self._provider_id == IONQ_PROVIDER_ID: + self._target_name = 'ionq.simulator' + elif self._provider_id == QUANTINUUM_PROVIDER_ID: + if target_name == 'quantinuum.hqs-lt-s1': + self._target_name = 'quantinuum.hqs-lt-s1-apival' + else: + self._target_name = target_name + else: # pragma: no cover + raise RuntimeError("Invalid Azure Quantum target.") + + if workspace is None: + workspace = Workspace(**kwargs) + + workspace.append_user_agent('projectq') + self._workspace = workspace + + self._num_runs = num_runs + self._verbose = verbose + self._num_retries = num_retries + self._interval = interval + self._retrieve_execution = retrieve_execution + self._circuit = None + self._measured_ids = [] + self._probabilities = {} + self._clear = True + + def _reset(self): + """ + Reset this backend. + + Note: + This sets ``_clear = True``, which will trigger state cleanup on the next call to ``_store``. + """ + # Lastly, reset internal state for measured IDs and circuit body. + self._circuit = None + self._clear = True + + def _store(self, cmd): # pylint: disable=too-many-branches + """ + Temporarily store the command cmd. + + Translates the command and stores it in a local variable (self._cmds). + + Args: + cmd (Command): Command to store + """ + if self._clear: + self._probabilities = {} + self._clear = False + self._circuit = None + + gate = cmd.gate + + # No-op/Meta gates + if isinstance(gate, (AllocateQubitGate, DeallocateQubitGate)): + return + + # Measurement + if isinstance(gate, MeasureGate): + logical_id = None + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + logical_id = tag.logical_qubit_id + break + + if logical_id is None: + raise RuntimeError('No LogicalQubitIDTag found in command!') + + self._measured_ids.append(logical_id) + return + + if self._provider_id == IONQ_PROVIDER_ID: + if not self._circuit: + self._circuit = [] + + json_cmd = to_json(cmd) + if json_cmd: + self._circuit.append(json_cmd) + + elif self._provider_id == QUANTINUUM_PROVIDER_ID: + if not self._circuit: + self._circuit = '' + + qasm_cmd = to_qasm(cmd) + if qasm_cmd: + self._circuit += f'\n{qasm_cmd}' + + else: + raise RuntimeError("Invalid Azure Quantum target.") + + def is_available(self, cmd): + """ + Test if this backend is available to process the provided command. + + Args: + cmd (Command): A command to process. + + Returns: + bool: If this backend can process the command. + """ + if self._provider_id == IONQ_PROVIDER_ID: + return is_available_ionq(cmd) + + if self._provider_id == QUANTINUUM_PROVIDER_ID: + return is_available_quantinuum(cmd) + + return False + + @staticmethod + def _target_factory(workspace, target_name, provider_id): # pragma: no cover + target_factory = TargetFactory( + base_cls=Target, workspace=workspace, default_targets=AzureQuantumBackend.DEFAULT_TARGETS + ) + + return target_factory.get_targets(name=target_name, provider_id=provider_id) + + @property + def _target(self): + target = self._target_factory( + workspace=self._workspace, target_name=self._target_name, provider_id=self._provider_id + ) + + if isinstance(target, list) and len(target) == 0: # pragma: no cover + raise AzureQuantumTargetNotFoundError( + f'Target {self._target_name} is not available on workspace {self._workspace.name}.' + ) + + return target + + @property + def current_availability(self): + """Get current availability for given target.""" + return self._target.current_availability + + @property + def average_queue_time(self): + """Get average queue time for given target.""" + return self._target.average_queue_time + + def get_probability(self, state, qureg): + """Shortcut to get a specific state's probability. + + Args: + state (str): A state in bit-string format. + qureg (Qureg): A ProjectQ Qureg object. + + Returns: + float: The probability for the provided state. + """ + if len(state) != len(qureg): + raise ValueError('Desired state and register must be the same length!') + + probs = self.get_probabilities(qureg) + + return probs.get(state, 0.0) + + def get_probabilities(self, qureg): + """ + Given the provided qubit register, determine the probability of each possible outcome. + + Note: + This method should only be called *after* a circuit has been run and its results are available. + + Args: + qureg (Qureg): A ProjectQ Qureg object. + + Returns: + dict: A dict mapping of states -> probability. + """ + if len(self._probabilities) == 0: + raise RuntimeError("Please, run the circuit first!") + + probability_dict = {} + for state in self._probabilities: + mapped_state = ['0'] * len(qureg) + for i, qubit in enumerate(qureg): + try: + meas_idx = self._measured_ids.index(qubit.id) + except ValueError: + continue + mapped_state[i] = state[meas_idx] + probability = self._probabilities[state] + mapped_state = "".join(mapped_state) + probability_dict[mapped_state] = probability_dict.get(mapped_state, 0) + probability + return probability_dict + + @property + def _input_data(self): + qubit_mapping = self.main_engine.mapper.current_mapping + qubits = len(qubit_mapping.keys()) + + if self._provider_id == IONQ_PROVIDER_ID: + return {"qubits": qubits, "circuit": self._circuit} + + if self._provider_id == QUANTINUUM_PROVIDER_ID: + measurement_gates = "" + + for measured_id in self._measured_ids: + qb_loc = self.main_engine.mapper.current_mapping[measured_id] + measurement_gates += f"measure q[{qb_loc}] -> c[{qb_loc}];\n" + + return ( + f"OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{qubits}];\ncreg c[{qubits}];" + f"{self._circuit}\n{measurement_gates}" + ) + + raise RuntimeError("Invalid Azure Quantum target.") + + @property + def _metadata(self): + qubit_mapping = self.main_engine.mapper.current_mapping + num_qubits = len(qubit_mapping.keys()) + meas_map = [qubit_mapping[qubit_id] for qubit_id in self._measured_ids] + + return {"num_qubits": num_qubits, "meas_map": meas_map} + + def estimate_cost(self, **kwargs): + """Estimate cost for the circuit this object has built during engine execution.""" + return self._target.estimate_cost(circuit=self._input_data, num_shots=self._num_runs, **kwargs) + + def _run(self): # pylint: disable=too-many-locals + """Run the circuit this object has built during engine execution.""" + # Nothing to do with an empty circuit. + if not self._circuit: + return + + if self._retrieve_execution is None: + res = send( + input_data=self._input_data, + metadata=self._metadata, + num_shots=self._num_runs, + target=self._target, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + + if res is None: # pragma: no cover + raise RuntimeError('Failed to submit job to the Azure Quantum!') + else: + res = retrieve( + job_id=self._retrieve_execution, + target=self._target, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + + if res is None: + raise RuntimeError( + f"Failed to retrieve job from Azure Quantum with job id: '{self._retrieve_execution}'!" + ) + + if self._provider_id == IONQ_PROVIDER_ID: + self._probabilities = { + _rearrange_result(int(k), len(self._measured_ids)): v for k, v in res["histogram"].items() + } + elif self._provider_id == QUANTINUUM_PROVIDER_ID: + histogram = Counter(res["c"]) + self._probabilities = {k: v / self._num_runs for k, v in histogram.items()} + else: # pragma: no cover + raise RuntimeError("Invalid Azure Quantum target.") + + # Set a single measurement result + bitstring = np.random.choice(list(self._probabilities.keys()), p=list(self._probabilities.values())) + for qid in self._measured_ids: + qubit_ref = WeakQubitRef(self.main_engine, qid) + self.main_engine.set_measurement_result(qubit_ref, bitstring[qid]) + + def receive(self, command_list): + """Receive a command list from the ProjectQ engine pipeline. + + If a given command is a "flush" operation, the pending circuit will be + submitted to Azure Quantum for processing. + + Args: + command_list (list[Command]): A list of ProjectQ Command objects. + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._store(cmd) + else: + # After that, the circuit is ready to be submitted. + try: + self._run() + finally: + # Make sure we always reset engine state so as not to leave + # anything dirty atexit. + self._reset() + + +__all__ = ['AzureQuantumBackend'] diff --git a/projectq/backends/_azure/_azure_quantum_client.py b/projectq/backends/_azure/_azure_quantum_client.py new file mode 100644 index 000000000..86c3f1891 --- /dev/null +++ b/projectq/backends/_azure/_azure_quantum_client.py @@ -0,0 +1,96 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Client methods to run quantum programs using Azure Quantum.""" + +from .._exceptions import DeviceOfflineError, RequestTimeoutError + + +def _get_results(job, num_retries=100, interval=1, verbose=False): + if verbose: # pragma: no cover + print(f"- Waiting for results. [Job ID: {job.id}]") + + try: + return job.get_results(timeout_secs=num_retries * interval) + except TimeoutError: + raise RequestTimeoutError( # pylint: disable=raise-missing-from + f"Timeout. The ID of your submitted job is {job.id}." + ) + + +def send( + input_data, num_shots, target, num_retries=100, interval=1, verbose=False, **kwargs +): # pylint: disable=too-many-arguments + """ + Submit a job to the Azure Quantum. + + Args: + input_data (any): Input data for Quantum job. + num_shots (int): Number of runs. + target (Target), The target to run this on. + num_retries (int, optional): Number of times to retry while the job is + not finished. Defaults to 100. + interval (int, optional): Sleep interval between retries, in seconds. + Defaults to 1. + verbose (bool, optional): Whether to print verbose output. + Defaults to False. + + Raises: + DeviceOfflineError: If the desired device is not available for job + processing. + + Returns: + dict: An intermediate dict representation of an Azure Quantum job result. + """ + if target.current_availability != 'Available': + raise DeviceOfflineError('Device is offline.') + + if verbose: + print(f"- Running code: {input_data}") + + job = target.submit(circuit=input_data, num_shots=num_shots, **kwargs) + + res = _get_results(job=job, num_retries=num_retries, interval=interval, verbose=verbose) + + if verbose: + print("- Done.") + + return res + + +def retrieve(job_id, target, num_retries=100, interval=1, verbose=False): + """ + Retrieve a job from Azure Quantum. + + Args: + job_id (str), Azure Quantum job id. + target (Target), The target job runs on. + num_retries (int, optional): Number of times to retry while the job is + not finished. Defaults to 100. + interval (int, optional): Sleep interval between retries, in seconds. + Defaults to 1. + verbose (bool, optional): Whether to print verbose output. + Defaults to False. + + Returns: + dict: An intermediate dict representation of an Azure Quantum job result. + """ + job = target.workspace.get_job(job_id=job_id) + + res = _get_results(job=job, num_retries=num_retries, interval=interval, verbose=verbose) + + if verbose: + print("- Done.") + + return res diff --git a/projectq/backends/_azure/_azure_quantum_client_test.py b/projectq/backends/_azure/_azure_quantum_client_test.py new file mode 100644 index 000000000..5ce1a6d8f --- /dev/null +++ b/projectq/backends/_azure/_azure_quantum_client_test.py @@ -0,0 +1,420 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.backends._azure._azure_quantum_client.py.""" + +from unittest import mock + +import pytest + +from .._exceptions import DeviceOfflineError, RequestTimeoutError + +_has_azure_quantum = True +try: + import azure.quantum # noqa: F401 + + from projectq.backends._azure._azure_quantum_client import retrieve, send +except ImportError: + _has_azure_quantum = False + +has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") + +ZERO_GUID = '00000000-0000-0000-0000-000000000000' + + +@has_azure_quantum +def test_is_online(): + def get_mock_target(): + mock_target = mock.MagicMock() + mock_target.current_availability = 'Offline' + + return mock_target + + with pytest.raises(DeviceOfflineError): + send( + input_data={}, + metadata={}, + num_shots=100, + target=get_mock_target(), + num_retries=1000, + interval=1, + verbose=True, + ) + + +@has_azure_quantum +@pytest.mark.parametrize('verbose', (False, True)) +def test_send_ionq(verbose): + expected_res = {'0': 0.125, '1': 0.125, '2': 0.125, '3': 0.125, '4': 0.125, '5': 0.125, '6': 0.125, '7': 0.125} + + def get_mock_target(): + mock_job = mock.MagicMock() + mock_job.id = ZERO_GUID + mock_job.get_results = mock.MagicMock(return_value=expected_res) + + mock_target = mock.MagicMock() + mock_target.current_availability = 'Available' + mock_target.submit = mock.MagicMock(return_value=mock_job) + + return mock_target + + input_data = { + 'qubits': 3, + 'circuit': [{'gate': 'h', 'targets': [0]}, {'gate': 'h', 'targets': [1]}, {'gate': 'h', 'targets': [2]}], + } + metadata = {'num_qubits': 3, 'meas_map': [0, 1, 2]} + + actual_res = send( + input_data=input_data, + metadata=metadata, + num_shots=100, + target=get_mock_target(), + num_retries=1000, + interval=1, + verbose=verbose, + ) + + assert actual_res == expected_res + + +@has_azure_quantum +@pytest.mark.parametrize('verbose', (False, True)) +def test_send_quantinuum(verbose): + expected_res = { + 'c': [ + '010', + '100', + '110', + '000', + '101', + '111', + '000', + '100', + '000', + '110', + '111', + '100', + '100', + '000', + '101', + '110', + '111', + '011', + '101', + '100', + '001', + '110', + '001', + '001', + '100', + '011', + '110', + '000', + '101', + '101', + '010', + '100', + '110', + '111', + '010', + '000', + '010', + '110', + '000', + '110', + '001', + '100', + '110', + '011', + '010', + '111', + '100', + '110', + '100', + '100', + '011', + '000', + '001', + '101', + '000', + '011', + '111', + '101', + '101', + '001', + '011', + '110', + '001', + '010', + '001', + '110', + '101', + '000', + '010', + '001', + '011', + '100', + '110', + '100', + '110', + '101', + '110', + '111', + '110', + '001', + '011', + '101', + '111', + '011', + '100', + '111', + '100', + '001', + '111', + '111', + '100', + '100', + '110', + '101', + '100', + '110', + '100', + '000', + '011', + '000', + ] + } + + def get_mock_target(): + mock_job = mock.MagicMock() + mock_job.id = ZERO_GUID + mock_job.get_results = mock.MagicMock(return_value=expected_res) + + mock_target = mock.MagicMock() + mock_target.current_availability = 'Available' + mock_target.submit = mock.MagicMock(return_value=mock_job) + + return mock_target + + input_data = ''''OPENQASM 2.0; + include "qelib1.inc"; + qreg q[3]; + creg c[3]; + h q[0]; + h q[1]; + h q[2]; + measure q[0] -> c[0]; + measure q[1] -> c[1]; + measure q[2] -> c[2]; +''' + + metadata = {'num_qubits': 3, 'meas_map': [0, 1, 2]} + + actual_res = send( + input_data=input_data, + metadata=metadata, + num_shots=100, + target=get_mock_target(), + num_retries=1000, + interval=1, + verbose=verbose, + ) + + assert actual_res == expected_res + + +@has_azure_quantum +@pytest.mark.parametrize('verbose', (False, True)) +def test_retrieve_ionq(verbose): + expected_res = {'0': 0.125, '1': 0.125, '2': 0.125, '3': 0.125, '4': 0.125, '5': 0.125, '6': 0.125, '7': 0.125} + + def get_mock_target(): + mock_job = mock.MagicMock() + mock_job.id = ZERO_GUID + mock_job.get_results = mock.MagicMock(return_value=expected_res) + + mock_workspace = mock.MagicMock() + mock_workspace.get_job = mock.MagicMock(return_value=mock_job) + + mock_target = mock.MagicMock() + mock_target.current_availability = 'Available' + mock_target.workspace = mock_workspace + mock_target.submit = mock.MagicMock(return_value=mock_job) + + return mock_target + + actual_res = retrieve(job_id=ZERO_GUID, target=get_mock_target(), num_retries=1000, interval=1, verbose=verbose) + + assert actual_res == expected_res + + +@has_azure_quantum +@pytest.mark.parametrize('verbose', (False, True)) +def test_retrieve_quantinuum(verbose): + expected_res = { + 'c': [ + '010', + '100', + '110', + '000', + '101', + '111', + '000', + '100', + '000', + '110', + '111', + '100', + '100', + '000', + '101', + '110', + '111', + '011', + '101', + '100', + '001', + '110', + '001', + '001', + '100', + '011', + '110', + '000', + '101', + '101', + '010', + '100', + '110', + '111', + '010', + '000', + '010', + '110', + '000', + '110', + '001', + '100', + '110', + '011', + '010', + '111', + '100', + '110', + '100', + '100', + '011', + '000', + '001', + '101', + '000', + '011', + '111', + '101', + '101', + '001', + '011', + '110', + '001', + '010', + '001', + '110', + '101', + '000', + '010', + '001', + '011', + '100', + '110', + '100', + '110', + '101', + '110', + '111', + '110', + '001', + '011', + '101', + '111', + '011', + '100', + '111', + '100', + '001', + '111', + '111', + '100', + '100', + '110', + '101', + '100', + '110', + '100', + '000', + '011', + '000', + ] + } + + def get_mock_target(): + mock_job = mock.MagicMock() + mock_job.id = ZERO_GUID + mock_job.get_results = mock.MagicMock(return_value=expected_res) + + mock_workspace = mock.MagicMock() + mock_workspace.get_job = mock.MagicMock(return_value=mock_job) + + mock_target = mock.MagicMock() + mock_target.current_availability = 'Available' + mock_target.workspace = mock_workspace + mock_target.submit = mock.MagicMock(return_value=mock_job) + + return mock_target + + actual_res = retrieve(job_id=ZERO_GUID, target=get_mock_target(), num_retries=1000, interval=1, verbose=verbose) + + assert actual_res == expected_res + + +@has_azure_quantum +@pytest.mark.parametrize('verbose', (False, True)) +def test_send_timeout_error(verbose): + def get_mock_target(): + mock_job = mock.MagicMock() + mock_job.id = ZERO_GUID + mock_job.get_results = mock.MagicMock() + mock_job.get_results.side_effect = TimeoutError() + + mock_target = mock.MagicMock() + mock_target.current_availability = 'Available' + mock_target.submit = mock.MagicMock(return_value=mock_job) + + return mock_target + + input_data = { + 'qubits': 3, + 'circuit': [{'gate': 'h', 'targets': [0]}, {'gate': 'h', 'targets': [1]}, {'gate': 'h', 'targets': [2]}], + } + metadata = {'num_qubits': 3, 'meas_map': [0, 1, 2]} + + with pytest.raises(RequestTimeoutError): + _ = send( + input_data=input_data, + metadata=metadata, + num_shots=100, + target=get_mock_target(), + num_retries=1000, + interval=1, + verbose=verbose, + ) diff --git a/projectq/backends/_azure/_azure_quantum_test.py b/projectq/backends/_azure/_azure_quantum_test.py new file mode 100644 index 000000000..f6d2193d2 --- /dev/null +++ b/projectq/backends/_azure/_azure_quantum_test.py @@ -0,0 +1,863 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.backends._azure._azure_quantum.py.""" + +from unittest import mock + +import pytest + +from projectq.cengines import BasicMapperEngine, MainEngine +from projectq.ops import CX, All, Command, H, Measure +from projectq.types import WeakQubitRef + +# NB: temporary workaround until Azure Quantum backend is fixed +_has_azure_quantum = False +try: + from azure.quantum import Workspace + + import projectq.backends._azure._azure_quantum + from projectq.backends import AzureQuantumBackend + from projectq.backends._azure._exceptions import AzureQuantumTargetNotFoundError +except ImportError: + _has_azure_quantum = False + +has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") + +ZERO_GUID = '00000000-0000-0000-0000-000000000000' + + +def mock_target(target_id, current_availability, average_queue_time): + target = mock.MagicMock() + + estimate_cost = mock.Mock() + estimate_cost.estimated_total = 10 + + target.id = target_id + target.current_availability = current_availability + target.average_queue_time = average_queue_time + target.estimate_cost = mock.MagicMock(return_value=estimate_cost) + + return target + + +def mock_providers(): + ionq_provider = mock.MagicMock() + ionq_provider.id = 'ionq' + ionq_provider.targets = [ + mock_target(target_id='ionq.simulator', current_availability='Available', average_queue_time=1000), + mock_target(target_id='ionq.qpu', current_availability='Available', average_queue_time=2000), + ] + + quantinuum_provider = mock.MagicMock() + quantinuum_provider.id = 'quantinuum' + quantinuum_provider.targets = [ + mock_target(target_id='quantinuum.hqs-lt-s1-apival', current_availability='Available', average_queue_time=3000), + mock_target(target_id='quantinuum.hqs-lt-s1-sim', current_availability='Available', average_queue_time=4000), + mock_target(target_id='quantinuum.hqs-lt-s1', current_availability='Degraded', average_queue_time=5000), + ] + + return [ionq_provider, quantinuum_provider] + + +def mock_target_factory(target_name): + for provider in mock_providers(): + for target in provider.targets: + if target.id == target_name: + return target + + return [] + + +def _get_azure_backend(use_hardware, target_name, retrieve_execution=None): + AzureQuantumBackend._target_factory = mock.MagicMock(return_value=mock_target_factory(target_name)) + + workspace = Workspace( + subscription_id=ZERO_GUID, resource_group='testResourceGroup', name='testWorkspace', location='East US' + ) + + backend = AzureQuantumBackend( + use_hardware=use_hardware, + target_name=target_name, + workspace=workspace, + retrieve_execution=retrieve_execution, + verbose=True, + ) + + return backend + + +def _get_main_engine(backend, max_qubits=3): + mapper = BasicMapperEngine() + + mapping = {} + for i in range(max_qubits): + mapping[i] = i + mapper.current_mapping = mapping + + main_engine = MainEngine(backend=backend, engine_list=[mapper], verbose=True) + + return main_engine + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id, expected_target_name", + [ + (False, 'ionq.simulator', 'ionq', 'ionq.simulator'), + (True, 'ionq.qpu', 'ionq', 'ionq.qpu'), + (False, 'ionq.qpu', 'ionq', 'ionq.simulator'), + ], +) +def test_azure_quantum_ionq_target(use_hardware, target_name, provider_id, expected_target_name): + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + + assert backend._target_name == expected_target_name + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id, expected_target_name", + [ + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum', 'quantinuum.hqs-lt-s1-apival'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum', 'quantinuum.hqs-lt-s1-sim'), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum', 'quantinuum.hqs-lt-s1'), + (False, 'quantinuum.hqs-lt-s1', 'quantinuum', 'quantinuum.hqs-lt-s1-apival'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum', 'quantinuum.hqs-lt-s1-sim'), + ], +) +def test_azure_quantum_quantinuum_target(use_hardware, target_name, provider_id, expected_target_name): + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + + assert backend._target_name == expected_target_name + + +@has_azure_quantum +def test_initialize_azure_backend_using_kwargs(): + backend = AzureQuantumBackend( + use_hardware=False, + target_name='ionq.simulator', + subscription_id=ZERO_GUID, + resource_group='testResourceGroup', + name='testWorkspace', + location='East US', + ) + + assert backend._target_name == 'ionq.simulator' + + +@has_azure_quantum +def test_azure_quantum_invalid_target(): + with pytest.raises(AzureQuantumTargetNotFoundError): + _get_azure_backend(use_hardware=False, target_name='invalid-target') + + +@has_azure_quantum +def test_is_available_ionq(): + with mock.patch('projectq.backends._azure._azure_quantum.is_available_ionq') as is_available_ionq_patch: + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend) + + q0 = main_engine.allocate_qubit() + + cmd = Command(main_engine, H, (q0,)) + main_engine.is_available(cmd) + + is_available_ionq_patch.assert_called() + + +@has_azure_quantum +def test_is_available_quantinuum(): + with mock.patch('projectq.backends._azure._azure_quantum.is_available_quantinuum') as is_available_quantinuum_patch: + backend = _get_azure_backend(use_hardware=False, target_name='quantinuum.hqs-lt-s1-sim') + main_engine = _get_main_engine(backend=backend) + + q0 = main_engine.allocate_qubit() + + cmd = Command(main_engine, H, (q0,)) + main_engine.is_available(cmd) + + is_available_quantinuum_patch.assert_called() + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id, current_availability", + [ + (False, 'ionq.simulator', 'ionq', 'Available'), + (True, 'ionq.qpu', 'ionq', 'Available'), + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum', 'Available'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum', 'Available'), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum', 'Degraded'), + ], +) +def test_current_availability(use_hardware, target_name, provider_id, current_availability): + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + + assert backend.current_availability == current_availability + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id, average_queue_time", + [ + (False, 'ionq.simulator', 'ionq', 1000), + (True, 'ionq.qpu', 'ionq', 2000), + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum', 3000), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum', 4000), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum', 5000), + ], +) +def test_average_queue_time(use_hardware, target_name, provider_id, average_queue_time): + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + + assert backend.average_queue_time == average_queue_time + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [(False, 'ionq.simulator', 'ionq'), (True, 'ionq.qpu', 'ionq')], +) +def test_run_ionq_get_probabilities(use_hardware, target_name, provider_id): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}} + ) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + result = backend.get_probabilities(circuit) + + assert len(result) == 8 + assert result['000'] == pytest.approx(0.5) + assert result['001'] == 0.0 + assert result['010'] == 0.0 + assert result['011'] == 0.0 + assert result['100'] == 0.0 + assert result['101'] == 0.0 + assert result['110'] == 0.0 + assert result['111'] == pytest.approx(0.5) + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [ + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum'), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum'), + ], +) +def test_run_quantinuum_get_probabilities(use_hardware, target_name, provider_id): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={ + 'c': [ + '000', + '000', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '000', + '000', + '000', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '000', + '000', + '000', + '000', + '111', + '111', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + ] + } + ) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + result = backend.get_probabilities(circuit) + + assert len(result) == 2 + assert result['000'] == pytest.approx(0.41) + assert result['111'] == pytest.approx(0.59) + + +@has_azure_quantum +def test_run_get_probabilities_unused_qubit(): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}} + ) + + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend, max_qubits=4) + + circuit = main_engine.allocate_qureg(3) + unused_qubit = main_engine.allocate_qubit() + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + result = backend.get_probabilities(unused_qubit) + + assert len(result) == 1 + assert result['0'] == pytest.approx(1) + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [(False, 'ionq.simulator', 'ionq'), (True, 'ionq.qpu', 'ionq')], +) +def test_run_ionq_get_probability(use_hardware, target_name, provider_id): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}} + ) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + assert backend.get_probability('000', circuit) == pytest.approx(0.5) + assert backend.get_probability('001', circuit) == 0.0 + assert backend.get_probability('010', circuit) == 0.0 + assert backend.get_probability('011', circuit) == 0.0 + assert backend.get_probability('100', circuit) == 0.0 + assert backend.get_probability('101', circuit) == 0.0 + assert backend.get_probability('110', circuit) == 0.0 + assert backend.get_probability('111', circuit) == pytest.approx(0.5) + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [ + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum'), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum'), + ], +) +def test_run_quantinuum_get_probability(use_hardware, target_name, provider_id): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={ + 'c': [ + '000', + '000', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '000', + '000', + '000', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '000', + '000', + '000', + '000', + '111', + '111', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + ] + } + ) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + assert backend.get_probability('000', circuit) == pytest.approx(0.41) + assert backend.get_probability('001', circuit) == 0.0 + assert backend.get_probability('010', circuit) == 0.0 + assert backend.get_probability('011', circuit) == 0.0 + assert backend.get_probability('100', circuit) == 0.0 + assert backend.get_probability('101', circuit) == 0.0 + assert backend.get_probability('110', circuit) == 0.0 + assert backend.get_probability('111', circuit) == pytest.approx(0.59) + + +@has_azure_quantum +def test_estimate_cost(): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}} + ) + + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + estimate_cost = backend.estimate_cost() + + assert estimate_cost.estimated_total == 10 + + +@has_azure_quantum +def test_run_get_probability_invalid_state(): + projectq.backends._azure._azure_quantum.send = mock.MagicMock( + return_value={'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}} + ) + + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + with pytest.raises(ValueError): + _ = backend.get_probability('0000', circuit) + + +@has_azure_quantum +def test_run_no_circuit(): + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + + main_engine.flush() + + with pytest.raises(RuntimeError): + _ = backend.get_probabilities(circuit) + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [(False, 'ionq.simulator', 'ionq'), (True, 'ionq.qpu', 'ionq')], +) +@pytest.mark.parametrize( + 'retrieve_retval', + (None, {'histogram': {'0': 0.5, '1': 0.0, '2': 0.0, '3': 0.0, '4': 0.0, '5': 0.0, '6': 0.0, '7': 0.5}}), + ids=('retrieve-FAIL', 'retrieve-SUCESS'), +) +def test_run_ionq_retrieve_execution(use_hardware, target_name, provider_id, retrieve_retval): + projectq.backends._azure._azure_quantum.retrieve = mock.MagicMock(return_value=retrieve_retval) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name, retrieve_execution=ZERO_GUID) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + if retrieve_retval is None: + with pytest.raises(RuntimeError): + main_engine.flush() + else: + main_engine.flush() + result = backend.get_probabilities(circuit) + + assert len(result) == 8 + assert result['000'] == pytest.approx(0.5) + assert result['001'] == 0.0 + assert result['010'] == 0.0 + assert result['011'] == 0.0 + assert result['100'] == 0.0 + assert result['101'] == 0.0 + assert result['110'] == 0.0 + assert result['111'] == pytest.approx(0.5) + + +@has_azure_quantum +@pytest.mark.parametrize( + "use_hardware, target_name, provider_id", + [ + (False, 'quantinuum.hqs-lt-s1-apival', 'quantinuum'), + (False, 'quantinuum.hqs-lt-s1-sim', 'quantinuum'), + (True, 'quantinuum.hqs-lt-s1', 'quantinuum'), + ], +) +def test_run_quantinuum_retrieve_execution(use_hardware, target_name, provider_id): + projectq.backends._azure._azure_quantum.retrieve = mock.MagicMock( + return_value={ + 'c': [ + '000', + '000', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '111', + '000', + '000', + '000', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '111', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '111', + '000', + '000', + '000', + '000', + '111', + '111', + '000', + '111', + '111', + '000', + '000', + '111', + '000', + '111', + '000', + '111', + '000', + '111', + '111', + '000', + '111', + '000', + '111', + '111', + '111', + '000', + '111', + '111', + '000', + ] + } + ) + + backend = _get_azure_backend(use_hardware=use_hardware, target_name=target_name, retrieve_execution=ZERO_GUID) + main_engine = _get_main_engine(backend=backend) + + circuit = main_engine.allocate_qureg(3) + q0, q1, q2 = circuit + + H | q0 + CX | (q0, q1) + CX | (q1, q2) + All(Measure) | circuit + + main_engine.flush() + + result = backend.get_probabilities(circuit) + + assert len(result) == 2 + assert result['000'] == pytest.approx(0.41) + assert result['111'] == pytest.approx(0.59) + + +@has_azure_quantum +def test_error_no_logical_id_tag(): + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + main_engine = _get_main_engine(backend=backend) + + q0 = WeakQubitRef(engine=None, idx=0) + + with pytest.raises(RuntimeError): + main_engine.backend._store(Command(engine=main_engine, gate=Measure, qubits=([q0],))) + + +@has_azure_quantum +def test_error_invalid_provider(): + backend = _get_azure_backend(use_hardware=False, target_name='ionq.simulator') + backend._provider_id = 'INVALID' # NB: this is forcing it... should actually never happen in practice + main_engine = _get_main_engine(backend=backend) + + q0 = WeakQubitRef(engine=None, idx=0) + + cmd = Command(engine=main_engine, gate=H, qubits=([q0],)) + with pytest.raises(RuntimeError): + main_engine.backend._store(cmd) + + with pytest.raises(RuntimeError): + main_engine.backend._input_data + + assert not main_engine.backend.is_available(cmd) diff --git a/projectq/backends/_azure/_exceptions.py b/projectq/backends/_azure/_exceptions.py new file mode 100644 index 000000000..20f8e65c5 --- /dev/null +++ b/projectq/backends/_azure/_exceptions.py @@ -0,0 +1,19 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exception classes for projectq.backends._azure.""" + + +class AzureQuantumTargetNotFoundError(Exception): + """Raised when a Azure Quantum target doesn't exist with given target name.""" diff --git a/projectq/backends/_azure/_utils.py b/projectq/backends/_azure/_utils.py new file mode 100644 index 000000000..0031f8438 --- /dev/null +++ b/projectq/backends/_azure/_utils.py @@ -0,0 +1,307 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for Azure Quantum.""" + +from projectq.meta import get_control_count, has_negative_control +from projectq.ops import ( + AllocateQubitGate, + BarrierGate, + ControlledGate, + DaggeredGate, + DeallocateQubitGate, + HGate, + MeasureGate, + R, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + Sdag, + SGate, + SqrtXGate, + SwapGate, + Tdag, + TGate, + XGate, + YGate, + ZGate, + get_inverse, +) + +from .._exceptions import InvalidCommandError + +IONQ_PROVIDER_ID = 'ionq' +QUANTINUUM_PROVIDER_ID = 'quantinuum' + +# https://docs.ionq.com/#section/Supported-Gates +IONQ_GATE_MAP = { + HGate: 'h', + SGate: 's', + SqrtXGate: 'v', + SwapGate: 'swap', + TGate: 't', + Rx: 'rx', + Rxx: 'xx', + Ry: 'ry', + Ryy: 'yy', + Rz: 'rz', + Rzz: 'zz', + XGate: 'x', + YGate: 'y', + ZGate: 'z', +} # excluding controlled, conjugate-transpose and meta gates + +IONQ_SUPPORTED_GATES = tuple(IONQ_GATE_MAP.keys()) + +QUANTINUUM_GATE_MAP = { + BarrierGate: 'barrier', + HGate: 'h', + Rx: 'rx', + Rxx: 'rxx', + Ry: 'ry', + Ryy: 'ryy', + Rz: 'rz', + Rzz: 'rzz', + SGate: 's', + TGate: 't', + XGate: 'x', + YGate: 'y', + ZGate: 'z', +} # excluding controlled, conjugate-transpose and meta gates + +QUANTINUUM_SUPPORTED_GATES = tuple(QUANTINUUM_GATE_MAP.keys()) + +V = SqrtXGate() +Vdag = get_inverse(V) + + +def is_available_ionq(cmd): + """ + Test if IonQ backend is available to process the provided command. + + Args: + cmd (Command): A command to process. + + Returns: + bool: If this backend can process the command. + """ + gate = cmd.gate + + if has_negative_control(cmd): + return False + + if isinstance(gate, ControlledGate): + num_ctrl_qubits = gate._n # pylint: disable=protected-access + else: + num_ctrl_qubits = get_control_count(cmd) + + # Get base gate wrapped in ControlledGate class + if isinstance(gate, ControlledGate): + gate = gate._gate # pylint: disable=protected-access + + # NOTE: IonQ supports up to 7 control qubits + if 0 < num_ctrl_qubits <= 7: + return isinstance(gate, (XGate,)) + + # Gates without control bits + if num_ctrl_qubits == 0: + supported = isinstance(gate, IONQ_SUPPORTED_GATES) + supported_meta = isinstance(gate, (MeasureGate, AllocateQubitGate, DeallocateQubitGate)) + supported_transpose = gate in (Sdag, Tdag, Vdag) + + return supported or supported_meta or supported_transpose + + return False + + +def is_available_quantinuum(cmd): + """ + Test if Quantinuum backend is available to process the provided command. + + Args: + cmd (Command): A command to process. + + Returns: + bool: If this backend can process the command. + """ + gate = cmd.gate + + if has_negative_control(cmd): + return False + + if isinstance(gate, ControlledGate): + num_ctrl_qubits = gate._n # pylint: disable=protected-access + else: + num_ctrl_qubits = get_control_count(cmd) + + # Get base gate wrapped in ControlledGate class + if isinstance(gate, ControlledGate): + gate = gate._gate # pylint: disable=protected-access + + # TODO: NEEDED CONFIRMATION- Does Quantinuum support more than 2 control gates? + if 0 < num_ctrl_qubits <= 2: + return isinstance(gate, (XGate, ZGate)) + + # Gates without control bits. + if num_ctrl_qubits == 0: + supported = isinstance(gate, QUANTINUUM_SUPPORTED_GATES) + supported_meta = isinstance(gate, (MeasureGate, AllocateQubitGate, DeallocateQubitGate, BarrierGate)) + supported_transpose = gate in (Sdag, Tdag) + return supported or supported_meta or supported_transpose + + return False + + +def to_json(cmd): + """ + Convert ProjectQ command to JSON format. + + Args: + cmd (Command): A command to process. + + Returns: + dict: JSON format of given command. + """ + # Invalid command, raise exception + if not is_available_ionq(cmd): + raise InvalidCommandError('Invalid command:', str(cmd)) + + gate = cmd.gate + + if isinstance(gate, ControlledGate): + inner_gate = gate._gate # pylint: disable=protected-access + gate_type = type(inner_gate) + elif isinstance(gate, DaggeredGate): + gate_type = type(gate.get_inverse()) + else: + gate_type = type(gate) + + gate_name = IONQ_GATE_MAP.get(gate_type) + + # Daggered gates get special treatment + if isinstance(gate, DaggeredGate): + gate_name = gate_name + 'i' + + # Controlled gates get special treatment too + if isinstance(gate, ControlledGate): + all_qubits = [qb.id for qureg in cmd.qubits for qb in qureg] + controls = all_qubits[: gate._n] # pylint: disable=protected-access + targets = all_qubits[gate._n :] # noqa: E203 # pylint: disable=protected-access + else: + controls = [qb.id for qb in cmd.control_qubits] + targets = [qb.id for qureg in cmd.qubits for qb in qureg] + + # Initialize the gate dict + gate_dict = {'gate': gate_name, 'targets': targets} + + # Check if we have a rotation + if isinstance(gate, (R, Rx, Ry, Rz, Rxx, Ryy, Rzz)): + gate_dict['rotation'] = gate.angle + + # Set controls + if len(controls) > 0: + gate_dict['controls'] = controls + + return gate_dict + + +def to_qasm(cmd): # pylint: disable=too-many-return-statements,too-many-branches + """ + Convert ProjectQ command to QASM format. + + Args: + cmd (Command): A command to process. + + Returns: + dict: QASM format of given command. + """ + # Invalid command, raise exception + if not is_available_quantinuum(cmd): + raise InvalidCommandError('Invalid command:', str(cmd)) + + gate = cmd.gate + + if isinstance(gate, ControlledGate): + inner_gate = gate._gate # pylint: disable=protected-access + gate_type = type(inner_gate) + elif isinstance(gate, DaggeredGate): + gate_type = type(gate.get_inverse()) + else: + gate_type = type(gate) + + gate_name = QUANTINUUM_GATE_MAP.get(gate_type) + + # Daggered gates get special treatment + if isinstance(gate, DaggeredGate): + gate_name = gate_name + 'dg' + + # Controlled gates get special treatment too + if isinstance(gate, ControlledGate): + all_qubits = [qb.id for qureg in cmd.qubits for qb in qureg] + controls = all_qubits[: gate._n] # pylint: disable=protected-access + targets = all_qubits[gate._n :] # noqa: E203 # pylint: disable=protected-access + else: + controls = [qb.id for qb in cmd.control_qubits] + targets = [qb.id for qureg in cmd.qubits for qb in qureg] + + # Barrier gate + if isinstance(gate, BarrierGate): + qb_str = "" + for pos in targets: + qb_str += f"q[{pos}], " + return f"{gate_name} {qb_str[:-2]};" + + # Daggered gates + if gate in (Sdag, Tdag): + return f"{gate_name} q[{targets[0]}];" + + # Controlled gates + if len(controls) > 0: + # 1-Controlled gates + if len(controls) == 1: + gate_name = 'c' + gate_name + return f"{gate_name} q[{controls[0]}], q[{targets[0]}];" + + # 2-Controlled gates + if len(controls) == 2: + gate_name = 'cc' + gate_name + return f"{gate_name} q[{controls[0]}], q[{controls[1]}], q[{targets[0]}];" + + raise InvalidCommandError('Invalid command:', str(cmd)) # pragma: no cover + + # Single qubit gates + if len(targets) == 1: + # Standard gates + if isinstance(gate, (HGate, XGate, YGate, ZGate, SGate, TGate)): + return f"{gate_name} q[{targets[0]}];" + + # Rotational gates + if isinstance(gate, (Rx, Ry, Rz)): + return f"{gate_name}({gate.angle}) q[{targets[0]}];" + + raise InvalidCommandError('Invalid command:', str(cmd)) # pragma: no cover + + # Two qubit gates + if len(targets) == 2: + # Rotational gates + if isinstance(gate, (Rxx, Ryy, Rzz)): + return f"{gate_name}({gate.angle}) q[{targets[0]}], q[{targets[1]}];" + + raise InvalidCommandError('Invalid command:', str(cmd)) + + # Invalid command + raise InvalidCommandError('Invalid command:', str(cmd)) # pragma: no cover diff --git a/projectq/backends/_azure/_utils_test.py b/projectq/backends/_azure/_utils_test.py new file mode 100644 index 000000000..8fdf70b48 --- /dev/null +++ b/projectq/backends/_azure/_utils_test.py @@ -0,0 +1,642 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.backends._azure._utils.py.""" + +import math + +import pytest + +from projectq.cengines import DummyEngine, MainEngine +from projectq.ops import ( + CNOT, + CX, + NOT, + Allocate, + Barrier, + C, + Command, + Deallocate, + H, + Measure, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + S, + Sdag, + Sdagger, + SqrtX, + SqrtXGate, + Swap, + T, + Tdag, + Tdagger, + X, + Y, + Z, + get_inverse, +) +from projectq.types import WeakQubitRef + +from .._exceptions import InvalidCommandError + +_has_azure_quantum = True +try: + import azure.quantum # noqa: F401 + + from projectq.backends._azure._utils import ( + is_available_ionq, + is_available_quantinuum, + to_json, + to_qasm, + ) +except ImportError: + _has_azure_quantum = False + +has_azure_quantum = pytest.mark.skipif(not _has_azure_quantum, reason="azure quantum package is not installed") + +V = SqrtXGate() +Vdag = get_inverse(V) + + +@has_azure_quantum +@pytest.mark.parametrize( + "single_qubit_gate, expected_result", + [ + (NOT, True), + (X, True), + (Y, True), + (Z, True), + (H, True), + (S, True), + (T, True), + (SqrtX, True), + (Rx(math.pi / 2), True), + (Ry(math.pi / 2), True), + (Rz(math.pi / 2), True), + (Sdag, True), + (Sdagger, True), + (Tdag, True), + (Tdagger, True), + (Vdag, True), + (Measure, True), + (Allocate, True), + (Deallocate, True), + (Barrier, False), + ], +) +def test_ionq_is_available_single_qubit_gates(single_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + + cmd = Command(eng, single_qubit_gate, (qb0,)) + assert is_available_ionq(cmd) == expected_result, f'Failing on {single_qubit_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "two_qubit_gate, expected_result", + [ + (Swap, True), + (CNOT, True), + (CX, True), + (Rxx(math.pi / 2), True), + (Ryy(math.pi / 2), True), + (Rzz(math.pi / 2), True), + ], +) +def test_ionq_is_available_two_qubit_gates(two_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + + cmd = Command(eng, two_qubit_gate, (qb0, qb1)) + assert is_available_ionq(cmd) == expected_result, f'Failing on {two_qubit_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, True), + (X, 1, True), + (X, 2, True), + (X, 3, True), + (X, 4, True), + (X, 5, True), + (X, 6, True), + (X, 7, True), + (X, 8, False), + (Y, 1, False), + ], +) +def test_ionq_is_available_n_controlled_qubits_type_1(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + # pass controls as parameter + cmd = Command(eng, base_gate, (qb0,), controls=qureg) + assert is_available_ionq(cmd) == expected_result, 'Failing on {}-controlled {} gate'.format( + num_ctrl_qubits, base_gate + ) + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, True), + (X, 1, True), + (X, 2, True), + (X, 3, True), + (X, 4, True), + (X, 5, True), + (X, 6, True), + (X, 7, True), + (X, 8, False), + (Y, 1, False), + ], +) +def test_ionq_is_available_n_controlled_qubits_type_2(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + n_controlled_gate = base_gate + for index in range(num_ctrl_qubits): + n_controlled_gate = C(n_controlled_gate) + + # pass controls as targets + cmd = Command( + eng, + n_controlled_gate, + ( + qureg, + qb0, + ), + ) + assert is_available_ionq(cmd) == expected_result, 'Failing on {}-controlled {} gate'.format( + num_ctrl_qubits, base_gate + ) + + +@has_azure_quantum +def test_ionq_is_available_negative_control(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg) + assert is_available_ionq(cmd), "Failing on negative controlled gate" + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg, control_state='1') + assert is_available_ionq(cmd), "Failing on negative controlled gate" + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg, control_state='0') + assert not is_available_ionq(cmd), "Failing on negative controlled gate" + + +@has_azure_quantum +@pytest.mark.parametrize( + "single_qubit_gate, expected_result", + [ + (NOT, True), + (X, True), + (Y, True), + (Z, True), + (H, True), + (S, True), + (T, True), + (Rx(math.pi / 2), True), + (Ry(math.pi / 2), True), + (Rz(math.pi / 2), True), + (Sdag, True), + (Sdagger, True), + (Tdag, True), + (Tdagger, True), + (Measure, True), + (Allocate, True), + (Deallocate, True), + (Barrier, True), + (SqrtX, False), + (Vdag, False), + ], +) +def test_quantinuum_is_available_single_qubit_gates(single_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + + cmd = Command(eng, single_qubit_gate, (qb0,)) + assert is_available_quantinuum(cmd) == expected_result, f'Failing on {single_qubit_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "two_qubit_gate, expected_result", + [ + (CNOT, True), + (CX, True), + (Rxx(math.pi / 2), True), + (Ryy(math.pi / 2), True), + (Rzz(math.pi / 2), True), + (Swap, False), + ], +) +def test_quantinuum_is_available_two_qubit_gates(two_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + + cmd = Command(eng, two_qubit_gate, (qb0, qb1)) + assert is_available_quantinuum(cmd) == expected_result, f'Failing on {two_qubit_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, True), + (X, 1, True), + (X, 2, True), + (X, 3, False), + (Z, 0, True), + (Z, 1, True), + (Z, 2, True), + (Z, 3, False), + (Y, 1, False), + ], +) +def test_quantinuum_is_available_n_controlled_qubits_type_1(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + cmd = Command(eng, base_gate, (qb0,), controls=qureg) + assert is_available_quantinuum(cmd) == expected_result, 'Failing on {}-controlled {} gate'.format( + num_ctrl_qubits, base_gate + ) + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, True), + (X, 1, True), + (X, 2, True), + (X, 3, False), + (Z, 0, True), + (Z, 1, True), + (Z, 2, True), + (Z, 3, False), + (Y, 1, False), + ], +) +def test_quantinuum_is_available_n_controlled_qubits_type_2(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + n_controlled_gate = base_gate + for index in range(num_ctrl_qubits): + n_controlled_gate = C(n_controlled_gate) + + # pass controls as targets + cmd = Command( + eng, + n_controlled_gate, + ( + qureg, + qb0, + ), + ) + assert is_available_quantinuum(cmd) == expected_result, 'Failing on {}-controlled {} gate'.format( + num_ctrl_qubits, base_gate + ) + + +@has_azure_quantum +def test_quantinuum_is_available_negative_control(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg) + assert is_available_quantinuum(cmd), "Failing on negative controlled gate" + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg, control_state='1') + assert is_available_quantinuum(cmd), "Failing on negative controlled gate" + + cmd = Command(eng, X, qubits=(qb0,), controls=qureg, control_state='0') + assert not is_available_quantinuum(cmd), "Failing on negative controlled gate" + + +@has_azure_quantum +@pytest.mark.parametrize( + "single_qubit_gate, expected_result", + [ + (NOT, {'gate': 'x', 'targets': [0]}), + (X, {'gate': 'x', 'targets': [0]}), + (Y, {'gate': 'y', 'targets': [0]}), + (Z, {'gate': 'z', 'targets': [0]}), + (H, {'gate': 'h', 'targets': [0]}), + (S, {'gate': 's', 'targets': [0]}), + (T, {'gate': 't', 'targets': [0]}), + (Rx(0), {'gate': 'rx', 'rotation': 0.0, 'targets': [0]}), + (Ry(0), {'gate': 'ry', 'rotation': 0.0, 'targets': [0]}), + (Rz(0), {'gate': 'rz', 'rotation': 0.0, 'targets': [0]}), + (Rx(math.pi / 4), {'gate': 'rx', 'rotation': 0.785398163397, 'targets': [0]}), + (Ry(math.pi / 4), {'gate': 'ry', 'rotation': 0.785398163397, 'targets': [0]}), + (Rz(math.pi / 4), {'gate': 'rz', 'rotation': 0.785398163397, 'targets': [0]}), + (Rx(math.pi / 2), {'gate': 'rx', 'rotation': 1.570796326795, 'targets': [0]}), + (Ry(math.pi / 2), {'gate': 'ry', 'rotation': 1.570796326795, 'targets': [0]}), + (Rz(math.pi / 2), {'gate': 'rz', 'rotation': 1.570796326795, 'targets': [0]}), + (Rx(math.pi), {'gate': 'rx', 'rotation': 3.14159265359, 'targets': [0]}), + (Ry(math.pi), {'gate': 'ry', 'rotation': 3.14159265359, 'targets': [0]}), + (Rz(math.pi), {'gate': 'rz', 'rotation': 3.14159265359, 'targets': [0]}), + (Sdag, {'gate': 'si', 'targets': [0]}), + (Sdagger, {'gate': 'si', 'targets': [0]}), + (Tdag, {'gate': 'ti', 'targets': [0]}), + (Tdagger, {'gate': 'ti', 'targets': [0]}), + (SqrtX, {'gate': 'v', 'targets': [0]}), + (Vdag, {'gate': 'vi', 'targets': [0]}), + ], +) +def test_to_json_single_qubit_gates(single_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = WeakQubitRef(engine=eng, idx=0) + + actual_result = to_json(Command(eng, single_qubit_gate, ([qb0],))) + + assert len(actual_result) == len(expected_result) + assert actual_result['gate'] == expected_result['gate'] + assert actual_result['targets'] == expected_result['targets'] + if 'rotation' in expected_result: + assert actual_result['rotation'] == pytest.approx(expected_result['rotation']) + + +@has_azure_quantum +@pytest.mark.parametrize( + "two_qubit_gate, expected_result", + [ + (Swap, {'gate': 'swap', 'targets': [0, 1]}), + (CNOT, {'gate': 'x', 'targets': [1], 'controls': [0]}), + (CX, {'gate': 'x', 'targets': [1], 'controls': [0]}), + (Rxx(0), {'gate': 'xx', 'rotation': 0.0, 'targets': [0, 1]}), + (Ryy(0), {'gate': 'yy', 'rotation': 0.0, 'targets': [0, 1]}), + (Rzz(0), {'gate': 'zz', 'rotation': 0.0, 'targets': [0, 1]}), + (Rxx(math.pi / 4), {'gate': 'xx', 'rotation': 0.785398163397, 'targets': [0, 1]}), + (Ryy(math.pi / 4), {'gate': 'yy', 'rotation': 0.785398163397, 'targets': [0, 1]}), + (Rzz(math.pi / 4), {'gate': 'zz', 'rotation': 0.785398163397, 'targets': [0, 1]}), + (Rxx(math.pi / 2), {'gate': 'xx', 'rotation': 1.570796326795, 'targets': [0, 1]}), + (Ryy(math.pi / 2), {'gate': 'yy', 'rotation': 1.570796326795, 'targets': [0, 1]}), + (Rzz(math.pi / 2), {'gate': 'zz', 'rotation': 1.570796326795, 'targets': [0, 1]}), + (Rxx(math.pi), {'gate': 'xx', 'rotation': 3.14159265359, 'targets': [0, 1]}), + (Ryy(math.pi), {'gate': 'yy', 'rotation': 3.14159265359, 'targets': [0, 1]}), + (Rzz(math.pi), {'gate': 'zz', 'rotation': 3.14159265359, 'targets': [0, 1]}), + ], +) +def test_to_json_two_qubit_gates(two_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = WeakQubitRef(engine=eng, idx=0) + qb1 = WeakQubitRef(engine=eng, idx=1) + + actual_result = to_json(Command(eng, two_qubit_gate, ([qb0], [qb1]))) + + assert len(actual_result) == len(expected_result) + assert actual_result['gate'] == expected_result['gate'] + assert actual_result['targets'] == expected_result['targets'] + if 'rotation' in expected_result: + assert actual_result['rotation'] == pytest.approx(expected_result['rotation']) + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, {'gate': 'x', 'targets': [0]}), + (X, 1, {'gate': 'x', 'targets': [0], 'controls': [1]}), + (X, 2, {'gate': 'x', 'targets': [0], 'controls': [1, 2]}), + (X, 3, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3]}), + (X, 4, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4]}), + (X, 5, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5]}), + (X, 6, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5, 6]}), + (X, 7, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5, 6, 7]}), + ], +) +def test_to_json_n_controlled_qubits_type_1(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + cmd = Command(eng, base_gate, (qb0,), controls=qureg) + assert to_json(cmd) == expected_result, f'Failing on {num_ctrl_qubits}-controlled {base_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, {'gate': 'x', 'targets': [0]}), + (X, 1, {'gate': 'x', 'targets': [0], 'controls': [1]}), + (X, 2, {'gate': 'x', 'targets': [0], 'controls': [1, 2]}), + (X, 3, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3]}), + (X, 4, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4]}), + (X, 5, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5]}), + (X, 6, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5, 6]}), + (X, 7, {'gate': 'x', 'targets': [0], 'controls': [1, 2, 3, 4, 5, 6, 7]}), + ], +) +def test_to_json_n_controlled_qubits_type_2(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + n_controlled_gate = base_gate + for index in range(num_ctrl_qubits): + n_controlled_gate = C(n_controlled_gate) + + # pass controls as targets + cmd = Command( + eng, + n_controlled_gate, + ( + qureg, + qb0, + ), + ) + assert to_json(cmd) == expected_result, f'Failing on {num_ctrl_qubits}-controlled {base_gate} gate' + + +@has_azure_quantum +def test_to_json_invalid_command_gate_not_available(): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + + cmd = Command(eng, Barrier, (qb0,)) + with pytest.raises(InvalidCommandError): + to_json(cmd) + + +@has_azure_quantum +@pytest.mark.parametrize( + "single_qubit_gate, expected_result", + [ + (NOT, 'x q[0];'), + (X, 'x q[0];'), + (Y, 'y q[0];'), + (Z, 'z q[0];'), + (H, 'h q[0];'), + (S, 's q[0];'), + (T, 't q[0];'), + (Rx(0), 'rx(0.0) q[0];'), + (Ry(0), 'ry(0.0) q[0];'), + (Rz(0), 'rz(0.0) q[0];'), + (Rx(math.pi / 4), 'rx(0.785398163397) q[0];'), + (Ry(math.pi / 4), 'ry(0.785398163397) q[0];'), + (Rz(math.pi / 4), 'rz(0.785398163397) q[0];'), + (Rx(math.pi / 2), 'rx(1.570796326795) q[0];'), + (Ry(math.pi / 2), 'ry(1.570796326795) q[0];'), + (Rz(math.pi / 2), 'rz(1.570796326795) q[0];'), + (Rx(math.pi), 'rx(3.14159265359) q[0];'), + (Ry(math.pi), 'ry(3.14159265359) q[0];'), + (Rz(math.pi), 'rz(3.14159265359) q[0];'), + (Sdag, 'sdg q[0];'), + (Sdagger, 'sdg q[0];'), + (Tdag, 'tdg q[0];'), + (Tdagger, 'tdg q[0];'), + ], +) +def test_to_qasm_single_qubit_gates(single_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = WeakQubitRef(engine=eng, idx=0) + + assert to_qasm(Command(eng, single_qubit_gate, ([qb0],))) == expected_result + + +@has_azure_quantum +@pytest.mark.parametrize( + "two_qubit_gate, expected_result", + [ + (CNOT, 'cx q[0], q[1];'), + (CX, 'cx q[0], q[1];'), + (Rxx(0), 'rxx(0.0) q[0], q[1];'), + (Ryy(0), 'ryy(0.0) q[0], q[1];'), + (Rzz(0), 'rzz(0.0) q[0], q[1];'), + (Rxx(math.pi / 4), 'rxx(0.785398163397) q[0], q[1];'), + (Ryy(math.pi / 4), 'ryy(0.785398163397) q[0], q[1];'), + (Rzz(math.pi / 4), 'rzz(0.785398163397) q[0], q[1];'), + (Rxx(math.pi / 2), 'rxx(1.570796326795) q[0], q[1];'), + (Ryy(math.pi / 2), 'ryy(1.570796326795) q[0], q[1];'), + (Rzz(math.pi / 2), 'rzz(1.570796326795) q[0], q[1];'), + (Rxx(math.pi), 'rxx(3.14159265359) q[0], q[1];'), + (Ryy(math.pi), 'ryy(3.14159265359) q[0], q[1];'), + (Rzz(math.pi), 'rzz(3.14159265359) q[0], q[1];'), + ], +) +def test_to_qasm_two_qubit_gates(two_qubit_gate, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = WeakQubitRef(engine=eng, idx=0) + qb1 = WeakQubitRef(engine=eng, idx=1) + + assert to_qasm(Command(eng, two_qubit_gate, ([qb0], [qb1]))) == expected_result + + +@has_azure_quantum +@pytest.mark.parametrize( + "n_qubit_gate, n, expected_result", + [ + (Barrier, 2, 'barrier q[0], q[1];'), + (Barrier, 3, 'barrier q[0], q[1], q[2];'), + (Barrier, 4, 'barrier q[0], q[1], q[2], q[3];'), + ], +) +def test_to_qasm_n_qubit_gates(n_qubit_gate, n, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qureg = eng.allocate_qureg(n) + + assert to_qasm(Command(eng, n_qubit_gate, (qureg,))) == expected_result + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, 'x q[0];'), + (X, 1, 'cx q[1], q[0];'), + (X, 2, 'ccx q[1], q[2], q[0];'), + (Z, 0, 'z q[0];'), + (Z, 1, 'cz q[1], q[0];'), + (Z, 2, 'ccz q[1], q[2], q[0];'), + ], +) +def test_to_qasm_n_controlled_qubits_type_1(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + cmd = Command(eng, base_gate, (qb0,), controls=qureg) + assert to_qasm(cmd) == expected_result, f'Failing on {num_ctrl_qubits}-controlled {base_gate} gate' + + +@has_azure_quantum +@pytest.mark.parametrize( + "base_gate, num_ctrl_qubits, expected_result", + [ + (X, 0, 'x q[0];'), + (X, 1, 'cx q[1], q[0];'), + (X, 2, 'ccx q[1], q[2], q[0];'), + (Z, 0, 'z q[0];'), + (Z, 1, 'cz q[1], q[0];'), + (Z, 2, 'ccz q[1], q[2], q[0];'), + ], +) +def test_to_qasm_n_controlled_qubits_type_2(base_gate, num_ctrl_qubits, expected_result): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qb0 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + + n_controlled_gate = base_gate + for index in range(num_ctrl_qubits): + n_controlled_gate = C(n_controlled_gate) + + # pass controls as targets + cmd = Command( + eng, + n_controlled_gate, + ( + qureg, + qb0, + ), + ) + assert to_qasm(cmd) == expected_result, f'Failing on {num_ctrl_qubits}-controlled {base_gate} gate' + + +@has_azure_quantum +def test_to_qasm_invalid_command_gate_not_available(): + qb0 = WeakQubitRef(None, idx=0) + qb1 = WeakQubitRef(None, idx=1) + + cmd = Command(None, SqrtX, qubits=((qb0,),)) + with pytest.raises(InvalidCommandError): + to_qasm(cmd) + + # NB: unsupported gate for 2 qubits + cmd = Command(None, X, qubits=((qb0, qb1),)) + with pytest.raises(InvalidCommandError): + to_qasm(cmd) diff --git a/projectq/backends/_circuits/__init__.py b/projectq/backends/_circuits/__init__.py index 1f22faec4..62dd8861a 100755 --- a/projectq/backends/_circuits/__init__.py +++ b/projectq/backends/_circuits/__init__.py @@ -12,5 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._to_latex import to_latex +"""ProjectQ module for exporting/printing quantum circuits.""" + from ._drawer import CircuitDrawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib +from ._plot import to_draw +from ._to_latex import to_latex diff --git a/projectq/backends/_circuits/_drawer.py b/projectq/backends/_circuits/_drawer.py index 269f592a2..071ad2cea 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,21 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains a compiler engine which generates TikZ Latex code describing the -circuit. -""" -import sys +"""Contain a compiler engine which generates TikZ Latex code describing the circuit.""" -from builtins import input -from projectq.cengines import LastEngineException, BasicEngine -from projectq.ops import FlushGate, Measure, Allocate, Deallocate +from projectq.cengines import BasicEngine, LastEngineException from projectq.meta import get_control_count -from projectq.backends._circuits import to_latex +from projectq.ops import Allocate, Deallocate, FlushGate, Measure +from ._to_latex import to_latex + + +class CircuitItem: # pylint: disable=too-few-public-methods + """Item of a quantum circuit to draw.""" -class CircuitItem(object): def __init__(self, gate, lines, ctrl_lines): """ Initialize a circuit item. @@ -42,128 +40,127 @@ def __init__(self, gate, lines, ctrl_lines): self.id = -1 def __eq__(self, other): - return (self.gate == other.gate and self.lines == other.lines and - self.ctrl_lines == other.ctrl_lines and - self.id == other.id) - - def __ne__(self, other): - return not self.__eq__(other) + """Equal operator.""" + return ( + self.gate == other.gate + and self.lines == other.lines + and self.ctrl_lines == other.ctrl_lines + and self.id == other.id + ) class CircuitDrawer(BasicEngine): """ - CircuitDrawer is a compiler engine which generates TikZ code for drawing - quantum circuits. + CircuitDrawer is a compiler engine which generates TikZ code for drawing quantum circuits. - The circuit can be modified by editing the settings.json file which is - generated upon first execution. This includes adjusting the gate width, - height, shadowing, line thickness, and many more options. + The circuit can be modified by editing the settings.json file which is generated upon first execution. This + includes adjusting the gate width, height, shadowing, line thickness, and many more options. - After initializing the CircuitDrawer, it can also be given the mapping - from qubit IDs to wire location (via the :meth:`set_qubit_locations` - function): + After initializing the CircuitDrawer, it can also be given the mapping from qubit IDs to wire location (via the + :meth:`set_qubit_locations` function): .. code-block:: python circuit_backend = CircuitDrawer() - circuit_backend.set_qubit_locations({0: 1, 1: 0}) # swap lines 0 and 1 + circuit_backend.set_qubit_locations({0: 1, 1: 0}) # swap lines 0 and 1 eng = MainEngine(circuit_backend) - ... # run quantum algorithm on this main engine + ... # run quantum algorithm on this main engine - print(circuit_backend.get_latex()) # prints LaTeX code + print(circuit_backend.get_latex()) # prints LaTeX code - To see the qubit IDs in the generated circuit, simply set the `draw_id` - option in the settings.json file under "gates":"AllocateQubitGate" to - True: + To see the qubit IDs in the generated circuit, simply set the `draw_id` option in the settings.json file under + "gates":"AllocateQubitGate" to True: .. code-block:: python - "gates": { - "AllocateQubitGate": { - "draw_id": True, - "height": 0.15, - "width": 0.2, - "pre_offset": 0.1, - "offset": 0.1 - }, - ... + { + "gates": { + "AllocateQubitGate": { + "draw_id": True, + "height": 0.15, + "width": 0.2, + "pre_offset": 0.1, + "offset": 0.1, + }, + # ... + } + } The settings.json file has the following structure: .. code-block:: python { - "control": { # settings for control "circle" - "shadow": false, - "size": 0.1 - }, - "gate_shadow": true, # enable/disable shadows for all gates + "control": {"shadow": false, "size": 0.1}, # settings for control "circle" + "gate_shadow": true, # enable/disable shadows for all gates "gates": { - "GateClassString": { - GATE_PROPERTIES - } - "GateClassString2": { - ... + "GateClassString": {GATE_PROPERTIES}, + "GateClassString2": { + # ... + }, + }, + "lines": { # settings for qubit lines + "double_classical": true, # draw double-lines for + # classical bits + "double_lines_sep": 0.04, # gap between the two lines + # for double lines + "init_quantum": true, # start out with quantum bits + "style": "very thin", # line style }, - "lines": { # settings for qubit lines - "double_classical": true, # draw double-lines for - # classical bits - "double_lines_sep": 0.04, # gap between the two lines - # for double lines - "init_quantum": true, # start out with quantum bits - "style": "very thin" # line style - } } - All gates (except for the ones requiring special treatment) support the - following properties: + All gates (except for the ones requiring special treatment) support the following properties: .. code-block:: python - "GateClassString": { - "height": GATE_HEIGHT, - "width": GATE_WIDTH - "pre_offset": OFFSET_BEFORE_PLACEMENT, - "offset": OFFSET_AFTER_PLACEMENT, - }, + { + "GateClassString": { + "height": GATE_HEIGHT, + "width": GATE_WIDTH, + "pre_offset": OFFSET_BEFORE_PLACEMENT, + "offset": OFFSET_AFTER_PLACEMENT, + } + } """ + def __init__(self, accept_input=False, default_measure=0): """ Initialize a circuit drawing engine. - The TikZ code generator uses a settings file (settings.json), which - can be altered by the user. It contains gate widths, heights, offsets, - etc. + The TikZ code generator uses a settings file (settings.json), which can be altered by the user. It contains + gate widths, heights, offsets, etc. Args: - accept_input (bool): If accept_input is true, the printer queries - the user to input measurement results if the CircuitDrawer is - the last engine. Otherwise, all measurements yield the result - default_measure (0 or 1). - default_measure (bool): Default value to use as measurement - results if accept_input is False and there is no underlying - backend to register real measurement results. + accept_input (bool): If accept_input is true, the printer queries the user to input measurement results if + the CircuitDrawer is the last engine. Otherwise, all measurements yield the result default_measure (0 + or 1). + default_measure (bool): Default value to use as measurement results if accept_input is False and there is + no underlying backend to register real measurement results. """ - BasicEngine.__init__(self) + super().__init__() self._accept_input = accept_input self._default_measure = default_measure - self._qubit_lines = dict() + self._qubit_lines = {} self._free_lines = [] - self._map = dict() + self._map = {} + + # Order in which qubit lines are drawn + self._drawing_order = [] def is_available(self, cmd): """ - Specialized implementation of is_available: Returns True if the - CircuitDrawer is the last engine (since it can print any command). + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: Returns True if the CircuitDrawer is the last engine (since it can + print any command). Args: - cmd (Command): Command for which to check availability (all - Commands can be printed). + cmd (Command): Command for which to check availability (all Commands can be printed). + Returns: - availability (bool): True, unless the next engine cannot handle - the Command (if there is a next engine). + availability (bool): True, unless the next engine cannot handle the Command (if there is a next engine). """ try: return BasicEngine.is_available(self, cmd) @@ -172,44 +169,45 @@ def is_available(self, cmd): def set_qubit_locations(self, id_to_loc): """ - Sets the qubit lines to use for the qubits explicitly. + Set the qubit lines to use for the qubits explicitly. - To figure out the qubit IDs, simply use the setting `draw_id` in the - settings file. It is located in "gates":"AllocateQubitGate". - If draw_id is True, the qubit IDs are drawn in red. + To figure out the qubit IDs, simply use the setting `draw_id` in the settings file. It is located in + "gates":"AllocateQubitGate". If draw_id is True, the qubit IDs are drawn in red. Args: - id_to_loc (dict): Dictionary mapping qubit ids to qubit line - numbers. + id_to_loc (dict): Dictionary mapping qubit ids to qubit line numbers. Raises: - RuntimeError: If the mapping has already begun (this function - needs be called before any gates have been received). + RuntimeError: If the mapping has already begun (this function needs be called before any gates have been + received). """ if len(self._map) > 0: - raise RuntimeError("set_qubit_locations() has to be called before" - " applying gates!") + raise RuntimeError("set_qubit_locations() has to be called before applying gates!") - for k in range(min(id_to_loc), max(id_to_loc)+1): + for k in range(min(id_to_loc), max(id_to_loc) + 1): if k not in id_to_loc: - raise RuntimeError("set_qubit_locations(): Invalid id_to_loc " - "mapping provided. All ids in the provided" - " range of qubit ids have to be mapped " - "somewhere.") + raise RuntimeError( + "set_qubit_locations(): Invalid id_to_loc " + "mapping provided. All ids in the provided" + " range of qubit ids have to be mapped " + "somewhere." + ) self._map = id_to_loc def _print_cmd(self, cmd): """ - Add the command cmd to the circuit diagram, taking care of potential - measurements as specified in the __init__ function. + Add a command to the list of commands to be printed. + + Add the command cmd to the circuit diagram, taking care of potential measurements as specified in the __init__ + function. - Queries the user for measurement input if a measurement command - arrives if accept_input was set to True. Otherwise, it uses the - default_measure parameter to register the measurement outcome. + Queries the user for measurement input if a measurement command arrives if accept_input was set to + True. Otherwise, it uses the default_measure parameter to register the measurement outcome. Args: cmd (Command): Command to add to the circuit diagram. """ + # pylint: disable=R0801 if cmd.gate == Allocate: qubit_id = cmd.qubits[0][0].id if qubit_id not in self._map: @@ -221,19 +219,20 @@ def _print_cmd(self, cmd): self._free_lines.append(qubit_id) if self.is_last_engine and cmd.gate == Measure: - assert(get_control_count(cmd) == 0) + if get_control_count(cmd) != 0: + raise ValueError('Cannot have control qubits with a measurement gate!') + for qureg in cmd.qubits: for qubit in qureg: if self._accept_input: - m = None - while m != '0' and m != '1' and m != 1 and m != 0: - prompt = ("Input measurement result (0 or 1) for " - "qubit " + str(qubit) + ": ") - m = input(prompt) + meas = None + while meas not in ('0', '1', 1, 0): + prompt = f"Input measurement result (0 or 1) for qubit {str(qubit)}: " + meas = input(prompt) else: - m = self._default_measure - m = int(m) - self.main_engine.set_measurement_result(qubit, m) + meas = self._default_measure + meas = int(meas) + self.main_engine.set_measurement_result(qubit, meas) all_lines = [qb.id for qr in cmd.all_qubits for qb in qr] @@ -241,10 +240,12 @@ def _print_cmd(self, cmd): lines = [qb.id for qr in cmd.qubits for qb in qr] ctrl_lines = [qb.id for qb in cmd.control_qubits] item = CircuitItem(gate, lines, ctrl_lines) - for l in all_lines: - self._qubit_lines[l].append(item) + for line in all_lines: + self._qubit_lines[line].append(item) + + self._drawing_order.append(all_lines[0]) - def get_latex(self): + def get_latex(self, ordered=False, draw_gates_in_parallel=True): """ Return the latex document string representing the circuit. @@ -256,13 +257,17 @@ def get_latex(self): python3 my_circuit.py | pdflatex where my_circuit.py calls this function and prints it to the terminal. + + Args: + ordered(bool): flag if the gates should be drawn in the order they were added to the circuit + draw_gates_in_parallel(bool): flag if parallel gates should be drawn parallel (True), or not (False) """ - qubit_lines = dict() + qubit_lines = {} - for line in range(len(self._qubit_lines)): + for line, qubit_line in self._qubit_lines.items(): new_line = self._map[line] qubit_lines[new_line] = [] - for cmd in self._qubit_lines[line]: + for cmd in qubit_line: lines = [self._map[qb_id] for qb_id in cmd.lines] ctrl_lines = [self._map[qb_id] for qb_id in cmd.ctrl_lines] gate = cmd.gate @@ -271,19 +276,25 @@ def get_latex(self): new_cmd.id = cmd.lines[0] qubit_lines[new_line].append(new_cmd) - circuit = [] - for lines in qubit_lines: - circuit.append(qubit_lines[lines]) - return to_latex(qubit_lines) + drawing_order = None + if ordered: + drawing_order = self._drawing_order + + return to_latex( + qubit_lines, + drawing_order=drawing_order, + draw_gates_in_parallel=draw_gates_in_parallel, + ) def receive(self, command_list): """ - Receive a list of commands from the previous engine, print the - commands, and then send them on to the next engine. + Receive a list of commands. + + Receive a list of commands from the previous engine, print the commands, and then send them on to the next + engine. Args: - command_list (list): List of Commands to print (and - potentially send on to the next engine). + command_list (list): List of Commands to print (and potentially send on to the next engine). """ for cmd in command_list: if not cmd.gate == FlushGate(): diff --git a/projectq/backends/_circuits/_drawer_matplotlib.py b/projectq/backends/_circuits/_drawer_matplotlib.py new file mode 100644 index 000000000..7c6b58555 --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib.py @@ -0,0 +1,218 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Contain a compiler engine which generates matplotlib figures describing the circuit.""" + +import itertools +import re + +from projectq.cengines import BasicEngine, LastEngineException +from projectq.meta import get_control_count +from projectq.ops import Allocate, Deallocate, FlushGate, Measure + +from ._plot import to_draw + +# ============================================================================== + + +def _format_gate_str(cmd): + param_str = '' + gate_name = str(cmd.gate) + if '(' in gate_name: + (gate_name, param_str) = re.search(r'(.+)\((.*)\)', gate_name).groups() + params = re.findall(r'([^,]+)', param_str) + params_str_list = [] + for param in params: + try: + params_str_list.append(f'{float(param):.2f}') + except ValueError: + if len(param) < 8: + params_str_list.append(param) + else: + params_str_list.append(f"{param[:5]}...") + + gate_name += f"({','.join(params_str_list)})" + return gate_name + + +# ============================================================================== + + +class CircuitDrawerMatplotlib(BasicEngine): + """CircuitDrawerMatplotlib is a compiler engine which using Matplotlib library for drawing quantum circuits.""" + + def __init__(self, accept_input=False, default_measure=0): + """ + Initialize a circuit drawing engine(mpl). + + Args: + accept_input (bool): If accept_input is true, the printer queries the user to input measurement results if + the CircuitDrawerMPL is the last engine. Otherwise, all measurements yield the result default_measure + (0 or 1). + default_measure (bool): Default value to use as measurement results if accept_input is False and there is + no underlying backend to register real measurement results. + """ + super().__init__() + self._accept_input = accept_input + self._default_measure = default_measure + self._map = {} + self._qubit_lines = {} + + def is_available(self, cmd): + """ + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: Returns True if the CircuitDrawerMatplotlib is the last engine + (since it can print any command). + + Args: + cmd (Command): Command for which to check availability (all Commands can be printed). + + Returns: + availability (bool): True, unless the next engine cannot handle the Command (if there is a next engine). + """ + try: + # Multi-qubit gates may fail at drawing time if the target qubits + # are not right next to each other on the output graphic. + return BasicEngine.is_available(self, cmd) + except LastEngineException: + return True + + def _process(self, cmd): # pylint: disable=too-many-branches + """ + Process the command cmd and stores it in the internal storage. + + Queries the user for measurement input if a measurement command arrives if accept_input was set to + True. Otherwise, it uses the default_measure parameter to register the measurement outcome. + + Args: + cmd (Command): Command to add to the circuit diagram. + """ + # pylint: disable=R0801 + if cmd.gate == Allocate: + qb_id = cmd.qubits[0][0].id + if qb_id not in self._map: + self._map[qb_id] = qb_id + self._qubit_lines[qb_id] = [] + return + + if cmd.gate == Deallocate: + return + + if self.is_last_engine and cmd.gate == Measure: + if get_control_count(cmd) != 0: + raise ValueError('Cannot have control qubits with a measurement gate!') + for qureg in cmd.qubits: + for qubit in qureg: + if self._accept_input: + measurement = None + while measurement not in ('0', '1', 1, 0): + prompt = f"Input measurement result (0 or 1) for qubit {qubit}: " + measurement = input(prompt) + else: + measurement = self._default_measure + self.main_engine.set_measurement_result(qubit, int(measurement)) + + targets = [qubit.id for qureg in cmd.qubits for qubit in qureg] + controls = [qubit.id for qubit in cmd.control_qubits] + + ref_qubit_id = targets[0] + gate_str = _format_gate_str(cmd) + + # First find out what is the maximum index that this command might + # have + max_depth = max(len(self._qubit_lines[qubit_id]) for qubit_id in itertools.chain(targets, controls)) + + # If we have a multi-qubit gate, make sure that all the qubit axes + # have the same depth. We do that by recalculating the maximum index + # over all the known qubit axes. + # This is to avoid the possibility of a multi-qubit gate overlapping + # with some other gates. This could potentially be improved by only + # considering the qubit axes that are between the topmost and + # bottommost qubit axes of the current command. + if len(targets) + len(controls) > 1: + max_depth = max(len(line) for qubit_id, line in self._qubit_lines.items()) + + for qb_id in itertools.chain(targets, controls): + depth = len(self._qubit_lines[qb_id]) + self._qubit_lines[qb_id] += [None] * (max_depth - depth) + + if qb_id == ref_qubit_id: + self._qubit_lines[qb_id].append((gate_str, targets, controls)) + else: + self._qubit_lines[qb_id].append(None) + + def receive(self, command_list): + """ + Receive a list of commands. + + Receive a list of commands from the previous engine, print the commands, and then send them on to the next + engine. + + Args: + command_list (list): List of Commands to print (and potentially send on to the next engine). + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._process(cmd) + + if not self.is_last_engine: + self.send([cmd]) + + def draw(self, qubit_labels=None, drawing_order=None, **kwargs): + """ + Generate and returns the plot of the quantum circuit stored so far. + + Args: + qubit_labels (dict): label for each wire in the output figure. Keys: qubit IDs, Values: string to print + out as label for that particular qubit wire. + drawing_order (dict): position of each qubit in the output graphic. Keys: qubit IDs, Values: position of + qubit on the qubit line in the graphic. + **kwargs (dict): additional parameters are used to update the default plot parameters + + Returns: + A tuple containing the matplotlib figure and axes objects + + Note: + Additional keyword arguments can be passed to this function in order to further customize the figure + output by matplotlib (default value in parentheses): + + - fontsize (14): Font size in pt + - column_spacing (.5): Vertical spacing between two + neighbouring gates (roughly in inches) + - control_radius (.015): Radius of the circle for controls + - labels_margin (1): Margin between labels and begin of + wire (roughly in inches) + - linewidth (1): Width of line + - not_radius (.03): Radius of the circle for X/NOT gates + - gate_offset (.05): Inner margins for gates with a text + representation + - mgate_width (.1): Width of the measurement gate + - swap_delta (.02): Half-size of the SWAP gate + - x_offset (.05): Absolute X-offset for drawing within the axes + - wire_height (1): Vertical spacing between two qubit + wires (roughly in inches) + """ + max_depth = max(len(line) for qubit_id, line in self._qubit_lines.items()) + for qubit_id, line in self._qubit_lines.items(): + depth = len(line) + if depth < max_depth: + self._qubit_lines[qubit_id] += [None] * (max_depth - depth) + + return to_draw( + self._qubit_lines, + qubit_labels=qubit_labels, + drawing_order=drawing_order, + **kwargs, + ) diff --git a/projectq/backends/_circuits/_drawer_matplotlib_test.py b/projectq/backends/_circuits/_drawer_matplotlib_test.py new file mode 100644 index 000000000..9744599a4 --- /dev/null +++ b/projectq/backends/_circuits/_drawer_matplotlib_test.py @@ -0,0 +1,169 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for projectq.backends.circuits._drawer.py. +""" + +import pytest + +from projectq import MainEngine +from projectq.cengines import DummyEngine +from projectq.ops import CNOT, BasicGate, Command, H, Measure, Rx, Swap, X +from projectq.types import WeakQubitRef + +from . import _drawer_matplotlib as _drawer +from ._drawer_matplotlib import CircuitDrawerMatplotlib + + +class MockInputFunction: + def __init__(self, return_value=None): + self.return_value = return_value + self._orig_input_fn = __builtins__['input'] + + def _mock_input_fn(self, prompt): + print(prompt + str(self.return_value)) + return self.return_value + + def __enter__(self): + __builtins__['input'] = self._mock_input_fn + + def __exit__(self, type, value, traceback): + __builtins__['input'] = self._orig_input_fn + + +def test_drawer_measurement(): + drawer = CircuitDrawerMatplotlib(default_measure=0) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 0 + + drawer = CircuitDrawerMatplotlib(default_measure=1) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + Measure | qubit + assert int(qubit) == 1 + + drawer = CircuitDrawerMatplotlib(accept_input=True) + eng = MainEngine(drawer, []) + qubit = eng.allocate_qubit() + + with MockInputFunction(return_value='1'): + Measure | qubit + assert int(qubit) == 1 + + qb1 = WeakQubitRef(engine=eng, idx=1) + qb2 = WeakQubitRef(engine=eng, idx=2) + with pytest.raises(ValueError): + eng.backend._process(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2])) + + +class MockEngine: + def is_available(self, cmd): + self.cmd = cmd + self.called = True + return False + + +def test_drawer_isavailable(): + drawer = CircuitDrawerMatplotlib() + drawer.is_last_engine = True + + qb0 = WeakQubitRef(None, 0) + qb1 = WeakQubitRef(None, 1) + qb2 = WeakQubitRef(None, 2) + qb3 = WeakQubitRef(None, 3) + + for gate in (X, Rx(1.0)): + for qubits in (([qb0],), ([qb0, qb1],), ([qb0, qb1, qb2],)): + print(qubits) + cmd = Command(None, gate, qubits) + assert drawer.is_available(cmd) + + cmd0 = Command(None, X, ([qb0],)) + cmd1 = Command(None, Swap, ([qb0], [qb1])) + cmd2 = Command(None, Swap, ([qb0], [qb1]), [qb2]) + cmd3 = Command(None, Swap, ([qb0], [qb1]), [qb2, qb3]) + + assert drawer.is_available(cmd1) + assert drawer.is_available(cmd2) + assert drawer.is_available(cmd3) + + mock_engine = MockEngine() + mock_engine.called = False + drawer.is_last_engine = False + drawer.next_engine = mock_engine + + assert not drawer.is_available(cmd0) + assert mock_engine.called + assert mock_engine.cmd is cmd0 + + assert not drawer.is_available(cmd1) + assert mock_engine.called + assert mock_engine.cmd is cmd1 + + +def _draw_subst(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): + return qubit_lines + + +class MyGate(BasicGate): + def __init__(self, *args): + super().__init__() + self.params = args + + def __str__(self): + param_str = f'{self.params[0]}' + for param in self.params[1:]: + param_str += f',{param}' + return f"{str(self.__class__.__name__)}({param_str})" + + +def test_drawer_draw(): + old_draw = _drawer.to_draw + _drawer.to_draw = _draw_subst + + backend = DummyEngine() + + drawer = CircuitDrawerMatplotlib() + + eng = MainEngine(backend, [drawer]) + qureg = eng.allocate_qureg(3) + H | qureg[1] + H | qureg[0] + X | qureg[0] + Rx(1) | qureg[1] + CNOT | (qureg[0], qureg[1]) + Swap | (qureg[0], qureg[1]) + MyGate(1.2) | qureg[2] + MyGate(1.23456789) | qureg[2] + MyGate(1.23456789, 2.3456789) | qureg[2] + MyGate(1.23456789, 'aaaaaaaa', 'bbb', 2.34) | qureg[2] + X | qureg[0] + + qubit_lines = drawer.draw() + + assert qubit_lines == { + 0: [('H', [0], []), ('X', [0], []), None, ('Swap', [0, 1], []), ('X', [0], [])], + 1: [('H', [1], []), ('Rx(1.00)', [1], []), ('X', [1], [0]), None, None], + 2: [ + ('MyGate(1.20)', [2], []), + ('MyGate(1.23)', [2], []), + ('MyGate(1.23,2.35)', [2], []), + ('MyGate(1.23,aaaaa...,bbb,2.34)', [2], []), + None, + ], + } + + _drawer.to_draw = old_draw diff --git a/projectq/backends/_circuits/_drawer_test.py b/projectq/backends/_circuits/_drawer_test.py index 7df4bd0ee..cec9488a0 100755 --- a/projectq/backends/_circuits/_drawer_test.py +++ b/projectq/backends/_circuits/_drawer_test.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,28 +11,39 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends.circuits._drawer.py. """ import pytest +import projectq.backends._circuits._drawer as _drawer from projectq import MainEngine -from projectq.cengines import LastEngineException -from projectq.ops import (H, - X, - CNOT, - Measure) -from projectq.meta import Control +from projectq.backends._circuits._drawer import CircuitDrawer, CircuitItem +from projectq.ops import CNOT, Command, H, Measure, X +from projectq.types import WeakQubitRef -import projectq.backends._circuits._drawer as _drawer -from projectq.backends._circuits._drawer import CircuitItem, CircuitDrawer +class MockInputFunction: + def __init__(self, return_value=None): + self.return_value = return_value + self._orig_input_fn = __builtins__['input'] + + def _mock_input_fn(self, prompt): + print(prompt + str(self.return_value)) + return self.return_value + + def __enter__(self): + __builtins__['input'] = self._mock_input_fn -def test_drawer_getlatex(): + def __exit__(self, type, value, traceback): + __builtins__['input'] = self._orig_input_fn + + +@pytest.mark.parametrize("ordered", [False, True]) +def test_drawer_getlatex(ordered): old_latex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x drawer = CircuitDrawer() drawer.set_qubit_locations({0: 1, 1: 0}) @@ -46,13 +57,13 @@ def test_drawer_getlatex(): X | qureg[0] CNOT | (qureg[0], qureg[1]) - lines = drawer2.get_latex() + lines = drawer2.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 4 assert len(lines[1]) == 3 # check if it was sent on correctly: - lines = drawer.get_latex() + lines = drawer.get_latex(ordered=ordered) assert len(lines) == 2 assert len(lines[0]) == 3 assert len(lines[1]) == 4 @@ -77,12 +88,14 @@ def test_drawer_measurement(): eng = MainEngine(drawer, []) qubit = eng.allocate_qubit() - old_input = _drawer.input + with MockInputFunction(return_value='1'): + Measure | qubit + assert int(qubit) == 1 - _drawer.input = lambda x: '1' - Measure | qubit - assert int(qubit) == 1 - _drawer.input = old_input + qb1 = WeakQubitRef(engine=eng, idx=1) + qb2 = WeakQubitRef(engine=eng, idx=2) + with pytest.raises(ValueError): + eng.backend._print_cmd(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2])) def test_drawer_qubitmapping(): @@ -101,13 +114,13 @@ def test_drawer_qubitmapping(): drawer.set_qubit_locations(invalid_mapping) eng = MainEngine(drawer, []) - qubit = eng.allocate_qubit() + qubit = eng.allocate_qubit() # noqa: F841 # mapping has begun --> can't assign it anymore with pytest.raises(RuntimeError): drawer.set_qubit_locations({0: 1, 1: 0}) -class MockEngine(object): +class MockEngine: def is_available(self, cmd): self.cmd = cmd self.called = True diff --git a/projectq/backends/_circuits/_plot.py b/projectq/backends/_circuits/_plot.py new file mode 100644 index 000000000..b450309f4 --- /dev/null +++ b/projectq/backends/_circuits/_plot.py @@ -0,0 +1,643 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module provides the basic functionality required to plot a quantum circuit in a matplotlib figure. + +It is mainly used by the CircuitDrawerMatplotlib compiler engine. + +Currently, it supports all single-qubit gates, including their controlled versions to an arbitrary number of control +qubits. It also supports multi-target qubit gates under some restrictions. Namely that the target qubits must be +neighbours in the output figure (which cannot be determined durinng compilation at this time). +""" + +from copy import deepcopy + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.collections import LineCollection, PatchCollection +from matplotlib.lines import Line2D +from matplotlib.patches import Arc, Circle, Rectangle + +# Important note on units for the plot parameters. +# The following entries are in inches: +# - column_spacing +# - labels_margin +# - wire_height +# +# The following entries are in data units (matplotlib) +# - control_radius +# - gate_offset +# - mgate_width +# - not_radius +# - swap_delta +# - x_offset +# +# The rest have misc. units (as defined by matplotlib) +_DEFAULT_PLOT_PARAMS = { + 'fontsize': 14.0, + 'column_spacing': 0.5, + 'control_radius': 0.015, + 'labels_margin': 1, + 'linewidth': 1.0, + 'not_radius': 0.03, + 'gate_offset': 0.05, + 'mgate_width': 0.1, + 'swap_delta': 0.02, + 'x_offset': 0.05, + 'wire_height': 1, +} + +# ============================================================================== + + +def to_draw(qubit_lines, qubit_labels=None, drawing_order=None, **kwargs): + """ + Translate a given circuit to a matplotlib figure. + + Args: + qubit_lines (dict): list of gates for each qubit axis + qubit_labels (dict): label to print in front of the qubit wire for each qubit ID + drawing_order (dict): index of the wire for each qubit ID to be drawn. + **kwargs (dict): additional parameters are used to update the default plot parameters + + Returns: + A tuple with (figure, axes) + + Note: + Numbering of qubit wires starts at 0 at the bottom and increases vertically. + + Note: + Additional keyword arguments can be passed to this function in order to further customize the figure output by + matplotlib (default value in parentheses): + + - fontsize (14): Font size in pt + - column_spacing (.5): Vertical spacing between two + neighbouring gates (roughly in inches) + - control_radius (.015): Radius of the circle for controls + - labels_margin (1): Margin between labels and begin of + wire (roughly in inches) + - linewidth (1): Width of line + - not_radius (.03): Radius of the circle for X/NOT gates + - gate_offset (.05): Inner margins for gates with a text + representation + - mgate_width (.1): Width of the measurement gate + - swap_delta (.02): Half-size of the SWAP gate + - x_offset (.05): Absolute X-offset for drawing within the axes + - wire_height (1): Vertical spacing between two qubit + wires (roughly in inches) + """ + if qubit_labels is None: + qubit_labels = {qubit_id: r'$|0\rangle$' for qubit_id in qubit_lines} + else: + if list(qubit_labels) != list(qubit_lines): + raise RuntimeError('Qubit IDs in qubit_labels do not match qubit IDs in qubit_lines!') + + if drawing_order is None: + n_qubits = len(qubit_lines) + drawing_order = {qubit_id: n_qubits - qubit_id - 1 for qubit_id in list(qubit_lines)} + else: + if set(drawing_order) != set(qubit_lines): + raise RuntimeError("Qubit IDs in drawing_order do not match qubit IDs in qubit_lines!") + if set(drawing_order.values()) != set(range(len(drawing_order))): + raise RuntimeError(f'Indices of qubit wires in drawing_order must be between 0 and {len(drawing_order)}!') + + plot_params = deepcopy(_DEFAULT_PLOT_PARAMS) + plot_params.update(kwargs) + + n_labels = len(list(qubit_lines)) + + wire_height = plot_params['wire_height'] + # Grid in inches + wire_grid = np.arange(wire_height, (n_labels + 1) * wire_height, wire_height, dtype=float) + + fig, axes = create_figure(plot_params) + + # Grid in inches + gate_grid = calculate_gate_grid(axes, qubit_lines, plot_params) + + width = gate_grid[-1] + plot_params['column_spacing'] + height = wire_grid[-1] + wire_height + + resize_figure(fig, axes, width, height, plot_params) + + # Convert grids into data coordinates + units_per_inch = plot_params['units_per_inch'] + + gate_grid *= units_per_inch + gate_grid = gate_grid + plot_params['x_offset'] + wire_grid *= units_per_inch + plot_params['column_spacing'] *= units_per_inch + + draw_wires(axes, n_labels, gate_grid, wire_grid, plot_params) + + draw_labels(axes, qubit_labels, drawing_order, wire_grid, plot_params) + + draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, plot_params) + return fig, axes + + +# ============================================================================== +# Functions used to calculate the layout + + +def gate_width(axes, gate_str, plot_params): + """ + Calculate the width of a gate based on its string representation. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_str (str): string representation of a gate + plot_params (dict): plot parameters + + Returns: + The width of a gate on the figure (in inches) + """ + if gate_str == 'X': + return 2 * plot_params['not_radius'] / plot_params['units_per_inch'] + if gate_str == 'Swap': + return 2 * plot_params['swap_delta'] / plot_params['units_per_inch'] + + if gate_str == 'Measure': + return plot_params['mgate_width'] + + obj = axes.text( + 0, + 0, + gate_str, + visible=True, + bbox={'edgecolor': 'k', 'facecolor': 'w', 'fill': True, 'lw': 1.0}, + fontsize=14, + ) + obj.figure.canvas.draw() + width = obj.get_window_extent(obj.figure.canvas.get_renderer()).width / axes.figure.dpi + obj.remove() + return width + 2 * plot_params['gate_offset'] + + +def calculate_gate_grid(axes, qubit_lines, plot_params): + """ + Calculate an optimal grid spacing for a list of quantum gates. + + Args: + axes (matplotlib.axes.Axes): axes object + qubit_lines (dict): list of gates for each qubit axis + plot_params (dict): plot parameters + + Returns: + An array (np.ndarray) with the gate x positions. + """ + # NB: column_spacing is still in inch when this function is called + column_spacing = plot_params['column_spacing'] + data = list(qubit_lines.values()) + depth = len(data[0]) + + width_list = [ + max(gate_width(axes, line[idx][0], plot_params) if line[idx] else 0 for line in data) for idx in range(depth) + ] + + gate_grid = np.array([0] * (depth + 1), dtype=float) + + gate_grid[0] = plot_params['labels_margin'] + if depth > 0: + gate_grid[0] += width_list[0] * 0.5 + for idx in range(1, depth): + gate_grid[idx] = gate_grid[idx - 1] + column_spacing + (width_list[idx] + width_list[idx - 1]) * 0.5 + gate_grid[-1] = gate_grid[-2] + column_spacing + width_list[-1] * 0.5 + return gate_grid + + +# ============================================================================== +# Basic helper functions + + +def text(axes, gate_pos, wire_pos, textstr, plot_params): + """ + Draw a text box on the figure. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + textstr (str): text of the gate and box + plot_params (dict): plot parameters + box (bool): draw the rectangle box if box is True + """ + return axes.text( + gate_pos, + wire_pos, + textstr, + color='k', + ha='center', + va='center', + clip_on=True, + size=plot_params['fontsize'], + ) + + +# ============================================================================== + + +def create_figure(plot_params): + """ + Create a new figure as well as a new axes instance. + + Args: + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig = plt.figure(facecolor='w', edgecolor='w') + axes = plt.axes() + axes.set_axis_off() + axes.set_aspect('equal') + plot_params['units_per_inch'] = fig.dpi / axes.get_window_extent().width + return fig, axes + + +def resize_figure(fig, axes, width, height, plot_params): + """ + Resize a figure and adjust the limits of the axes instance. + + This functions makes sure that the distances in data coordinates on the screen stay constant. + + Args: + fig (matplotlib.figure.Figure): figure object + axes (matplotlib.axes.Axes): axes object + width (float): new figure width + height (float): new figure height + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + fig.set_size_inches(width, height) + + new_limits = plot_params['units_per_inch'] * np.array([width, height]) + axes.set_xlim(0, new_limits[0]) + axes.set_ylim(0, new_limits[1]) + + +def draw_gates( # pylint: disable=too-many-arguments + axes, qubit_lines, drawing_order, gate_grid, wire_grid, plot_params +): + """ + Draw the gates. + + Args: + qubit_lines (dict): list of gates for each qubit axis + drawing_order (dict): index of the wire for each qubit ID to be drawn + gate_grid (np.ndarray): x positions of the gates + wire_grid (np.ndarray): y positions of the qubit wires + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + for qubit_line in qubit_lines.values(): + for idx, data in enumerate(qubit_line): + if data is not None: + (gate_str, targets, controls) = data + targets_order = [drawing_order[tgt] for tgt in targets] + draw_gate( + axes, + gate_str, + gate_grid[idx], + [wire_grid[tgt] for tgt in targets_order], + targets_order, + [wire_grid[drawing_order[ctrl]] for ctrl in controls], + plot_params, + ) + + +def draw_gate( + axes, gate_str, gate_pos, target_wires, targets_order, control_wires, plot_params +): # pylint: disable=too-many-arguments + """ + Draw a single gate at a given location. + + Args: + axes (AxesSubplot): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + target_wires (list): y coordinates of the target qubits + targets_order (list): index of the wires corresponding to the target qubit IDs + control_wires (list): y coordinates of the control qubits + plot_params (dict): plot parameters + + Returns: + A tuple with (figure, axes) + """ + # Special cases + if gate_str == 'Z' and len(control_wires) == 1: + draw_control_z_gate(axes, gate_pos, target_wires[0], control_wires[0], plot_params) + elif gate_str == 'X': + draw_x_gate(axes, gate_pos, target_wires[0], plot_params) + elif gate_str == 'Swap': + draw_swap_gate(axes, gate_pos, target_wires[0], target_wires[1], plot_params) + elif gate_str == 'Measure': + draw_measure_gate(axes, gate_pos, target_wires[0], plot_params) + else: + if len(target_wires) == 1: + draw_generic_gate(axes, gate_pos, target_wires[0], gate_str, plot_params) + else: + if sorted(targets_order) != list(range(min(targets_order), max(targets_order) + 1)): + raise RuntimeError( + f"Multi-qubit gate with non-neighbouring qubits!\nGate: {gate_str} on wires {targets_order}" + ) + + multi_qubit_gate( + axes, + gate_str, + gate_pos, + min(target_wires), + max(target_wires), + plot_params, + ) + + if not control_wires: + return + + for control_wire in control_wires: + axes.add_patch( + Circle( + (gate_pos, control_wire), + plot_params['control_radius'], + ec='k', + fc='k', + fill=True, + lw=plot_params['linewidth'], + ) + ) + + all_wires = target_wires + control_wires + axes.add_line( + Line2D( + (gate_pos, gate_pos), + (min(all_wires), max(all_wires)), + color='k', + lw=plot_params['linewidth'], + ) + ) + + +def draw_generic_gate(axes, gate_pos, wire_pos, gate_str, plot_params): + """ + Draw a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + gate_str (str) : string representation of a gate + plot_params (dict): plot parameters + """ + obj = text(axes, gate_pos, wire_pos, gate_str, plot_params) + obj.set_zorder(7) + + factor = plot_params['units_per_inch'] / obj.figure.dpi + gate_offset = plot_params['gate_offset'] + + renderer = obj.figure.canvas.get_renderer() + width = obj.get_window_extent(renderer).width * factor + 2 * gate_offset + height = obj.get_window_extent(renderer).height * factor + 2 * gate_offset + + axes.add_patch( + Rectangle( + (gate_pos - width / 2, wire_pos - height / 2), + width, + height, + ec='k', + fc='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6, + ) + ) + + +def draw_measure_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draw a measurement gate. + + Args: + axes (AxesSubplot): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + width = plot_params['mgate_width'] + height = 0.9 * width + y_ref = wire_pos - 0.3 * height + + # Cannot use PatchCollection for the arc due to bug in matplotlib code... + arc = Arc( + (gate_pos, y_ref), + width * 0.7, + height * 0.8, + theta1=0, + theta2=180, + ec='k', + fc='w', + zorder=5, + ) + axes.add_patch(arc) + + patches = [ + Rectangle((gate_pos - width / 2, wire_pos - height / 2), width, height, fill=True), + Line2D( + (gate_pos, gate_pos + width * 0.35), + (y_ref, wire_pos + height * 0.35), + color='k', + linewidth=1, + ), + ] + + gate = PatchCollection( + patches, + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth'], + zorder=5, + ) + gate.set_label('Measure') + axes.add_collection(gate) + + +def multi_qubit_gate( # pylint: disable=too-many-arguments + axes, gate_str, gate_pos, wire_pos_min, wire_pos_max, plot_params +): + """ + Draw a multi-target qubit gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_str (str): string representation of a gate + gate_pos (float): x coordinate of the gate [data units] + wire_pos_min (float): y coordinate of the lowest qubit wire + wire_pos_max (float): y coordinate of the highest qubit wire + plot_params (dict): plot parameters + """ + gate_offset = plot_params['gate_offset'] + y_center = (wire_pos_max - wire_pos_min) / 2 + wire_pos_min + obj = axes.text( + gate_pos, + y_center, + gate_str, + color='k', + ha='center', + va='center', + size=plot_params['fontsize'], + zorder=7, + ) + height = wire_pos_max - wire_pos_min + 2 * gate_offset + inv = axes.transData.inverted() + width = inv.transform_bbox(obj.get_window_extent(obj.figure.canvas.get_renderer())).width + return axes.add_patch( + Rectangle( + (gate_pos - width / 2, wire_pos_min - gate_offset), + width, + height, + edgecolor='k', + facecolor='w', + fill=True, + lw=plot_params['linewidth'], + zorder=6, + ) + ) + + +def draw_x_gate(axes, gate_pos, wire_pos, plot_params): + """ + Draw the symbol for a X/NOT gate. + + Args: + axes (matplotlib.axes.Axes): axes object + gate_pos (float): x coordinate of the gate [data units] + wire_pos (float): y coordinate of the qubit wire [data units] + plot_params (dict): plot parameters + """ + not_radius = plot_params['not_radius'] + + gate = PatchCollection( + [ + Circle((gate_pos, wire_pos), not_radius, fill=False), + Line2D((gate_pos, gate_pos), (wire_pos - not_radius, wire_pos + not_radius)), + ], + edgecolors='k', + facecolors='w', + linewidths=plot_params['linewidth'], + ) + gate.set_label('NOT') + axes.add_collection(gate) + + +def draw_control_z_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draw the symbol for a controlled-Z gate. + + Args: + axes (matplotlib.axes.Axes): axes object + wire_pos (float): x coordinate of the gate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + gate = PatchCollection( + [ + Circle((gate_pos, wire_pos1), plot_params['control_radius'], fill=True), + Circle((gate_pos, wire_pos2), plot_params['control_radius'], fill=True), + Line2D((gate_pos, gate_pos), (wire_pos1, wire_pos2)), + ], + edgecolors='k', + facecolors='k', + linewidths=plot_params['linewidth'], + ) + gate.set_label('CZ') + axes.add_collection(gate) + + +def draw_swap_gate(axes, gate_pos, wire_pos1, wire_pos2, plot_params): + """ + Draw the symbol for a SWAP gate. + + Args: + axes (matplotlib.axes.Axes): axes object + x (float): x coordinate [data units] + y1 (float): y coordinate of the 1st qubit wire + y2 (float): y coordinate of the 2nd qubit wire + plot_params (dict): plot parameters + """ + delta = plot_params['swap_delta'] + + lines = [] + for wire_pos in (wire_pos1, wire_pos2): + lines.append([(gate_pos - delta, wire_pos - delta), (gate_pos + delta, wire_pos + delta)]) + lines.append([(gate_pos - delta, wire_pos + delta), (gate_pos + delta, wire_pos - delta)]) + lines.append([(gate_pos, wire_pos1), (gate_pos, wire_pos2)]) + + gate = LineCollection(lines, colors='k', linewidths=plot_params['linewidth']) + gate.set_label('SWAP') + axes.add_collection(gate) + + +def draw_wires(axes, n_labels, gate_grid, wire_grid, plot_params): + """ + Draw all the circuit qubit wires. + + Args: + axes (matplotlib.axes.Axes): axes object + n_labels (int): number of qubit + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit wires + plot_params (dict): plot parameters + """ + # pylint: disable=invalid-name + + lines = [] + for i in range(n_labels): + lines.append( + ( + (gate_grid[0] - plot_params['column_spacing'], wire_grid[i]), + (gate_grid[-1], wire_grid[i]), + ) + ) + all_lines = LineCollection(lines, linewidths=plot_params['linewidth'], edgecolor='k') + all_lines.set_label('qubit_wires') + axes.add_collection(all_lines) + + +def draw_labels(axes, qubit_labels, drawing_order, wire_grid, plot_params): + """ + Draw the labels at the start of each qubit wire. + + Args: + axes (matplotlib.axes.Axes): axes object + qubit_labels (list): labels of the qubit to be drawn + drawing_order (dict): Mapping between wire indices and qubit IDs + gate_grid (ndarray): array with the ref. x positions of the gates + wire_grid (ndarray): array with the ref. y positions of the qubit wires + plot_params (dict): plot parameters + """ + for qubit_id in qubit_labels: + wire_idx = drawing_order[qubit_id] + text( + axes, + plot_params['x_offset'], + wire_grid[wire_idx], + qubit_labels[qubit_id], + plot_params, + ) diff --git a/projectq/backends/_circuits/_plot_test.py b/projectq/backends/_circuits/_plot_test.py new file mode 100644 index 000000000..3e81d23d1 --- /dev/null +++ b/projectq/backends/_circuits/_plot_test.py @@ -0,0 +1,261 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + Tests for projectq.backends._circuits._plot.py. + + To generate the baseline images, + run the tests with '--mpl-generate-path=baseline' + + Then run the tests simply with '--mpl' +""" +from copy import deepcopy + +import pytest + +import projectq.backends._circuits._plot as _plot + +# ============================================================================== + + +class PseudoCanvas: + def __init__(self): + pass + + def draw(self): + pass + + def get_renderer(self): + return + + +class PseudoFigure: + def __init__(self): + self.canvas = PseudoCanvas() + self.dpi = 1 + + +class PseudoBBox: + def __init__(self, width, height): + self.width = width + self.height = height + + +class PseudoText: + def __init__(self, text): + self.text = text + self.figure = PseudoFigure() + + def get_window_extent(self, *args): + return PseudoBBox(len(self.text), 1) + + def remove(self): + pass + + +class PseudoTransform: + def __init__(self): + pass + + def inverted(self): + return self + + def transform_bbox(self, bbox): + return bbox + + +class PseudoAxes: + def __init__(self): + self.figure = PseudoFigure() + self.transData = PseudoTransform() + + def add_patch(self, x): + return x + + def text(self, x, y, text, *args, **kwargse): + return PseudoText(text) + + +# ============================================================================== + + +@pytest.fixture(scope="module") +def plot_params(): + params = deepcopy(_plot._DEFAULT_PLOT_PARAMS) + params.update([('units_per_inch', 1)]) + return params + + +@pytest.fixture +def axes(): + return PseudoAxes() + + +# ============================================================================== + + +@pytest.mark.parametrize('gate_str', ['X', 'Swap', 'Measure', 'Y', 'Rz(1.00)']) +def test_gate_width(axes, gate_str, plot_params): + width = _plot.gate_width(axes, gate_str, plot_params) + if gate_str == 'X': + assert width == 2 * plot_params['not_radius'] / plot_params['units_per_inch'] + elif gate_str == 'Swap': + assert width == 2 * plot_params['swap_delta'] / plot_params['units_per_inch'] + elif gate_str == 'Measure': + assert width == plot_params['mgate_width'] + else: + assert width == len(gate_str) + 2 * plot_params['gate_offset'] + + +def test_calculate_gate_grid(axes, plot_params): + qubit_lines = {0: [('X', [0], []), ('X', [0], []), ('X', [0], []), ('X', [0], [])]} + + gate_grid = _plot.calculate_gate_grid(axes, qubit_lines, plot_params) + assert len(gate_grid) == 5 + assert gate_grid[0] > plot_params['labels_margin'] + width = [gate_grid[i + 1] - gate_grid[i] for i in range(4)] + + # Column grid is given by: + # |---*---|---*---|---*---|---*---| + # |-- w --|-- w --|-- w --|.5w| + + column_spacing = plot_params['column_spacing'] + ref_width = _plot.gate_width(axes, 'X', plot_params) + + for w in width[:-1]: + assert ref_width + column_spacing == pytest.approx(w) + assert 0.5 * ref_width + column_spacing == pytest.approx(width[-1]) + + +def test_create_figure(plot_params): + fig, axes = _plot.create_figure(plot_params) + + +def test_draw_single_gate(axes, plot_params): + with pytest.raises(RuntimeError): + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 3], [], plot_params) + _plot.draw_gate(axes, 'MyGate', 2, [0, 0, 0], [0, 1, 2], [], plot_params) + + +def test_draw_simple(plot_params): + qubit_lines = { + 0: [ + ('X', [0], []), + ('Z', [0], []), + ('Z', [0], [1]), + ('Swap', [0, 1], []), + ('Measure', [0], []), + ], + 1: [None, None, None, None, None], + } + fig, axes = _plot.to_draw(qubit_lines) + + units_per_inch = plot_params['units_per_inch'] + not_radius = plot_params['not_radius'] + control_radius = plot_params['control_radius'] + swap_delta = plot_params['swap_delta'] + wire_height = plot_params['wire_height'] * units_per_inch + mgate_width = plot_params['mgate_width'] + + labels = [] + text_gates = [] + measure_gates = [] + for text in axes.texts: + if text.get_text() == '$|0\\rangle$': + labels.append(text) + elif text.get_text() == ' ': + measure_gates.append(text) + else: + text_gates.append(text) + + assert all(label.get_position()[0] == pytest.approx(plot_params['x_offset']) for label in labels) + assert abs(labels[1].get_position()[1] - labels[0].get_position()[1]) == pytest.approx(wire_height) + + # X gate + x_gate = [obj for obj in axes.collections if obj.get_label() == 'NOT'][0] + # find the filled circles + assert x_gate.get_paths()[0].get_extents().width == pytest.approx(2 * not_radius) + assert x_gate.get_paths()[0].get_extents().height == pytest.approx(2 * not_radius) + # find the vertical bar + x_vertical = x_gate.get_paths()[1] + assert len(x_vertical) == 2 + assert x_vertical.get_extents().width == 0.0 + assert x_vertical.get_extents().height == pytest.approx(2 * plot_params['not_radius']) + + # Z gate + assert len(text_gates) == 1 + assert text_gates[0].get_text() == 'Z' + assert text_gates[0].get_position()[1] == pytest.approx(2 * wire_height) + + # CZ gate + cz_gate = [obj for obj in axes.collections if obj.get_label() == 'CZ'][0] + # find the filled circles + for control in cz_gate.get_paths()[:-1]: + assert control.get_extents().width == pytest.approx(2 * control_radius) + assert control.get_extents().height == pytest.approx(2 * control_radius) + # find the vertical bar + cz_vertical = cz_gate.get_paths()[-1] + assert len(cz_vertical) == 2 + assert cz_vertical.get_extents().width == 0.0 + assert cz_vertical.get_extents().height == pytest.approx(wire_height) + + # Swap gate + swap_gate = [obj for obj in axes.collections if obj.get_label() == 'SWAP'][0] + # find the filled circles + for qubit in swap_gate.get_paths()[:-1]: + assert qubit.get_extents().width == pytest.approx(2 * swap_delta) + assert qubit.get_extents().height == pytest.approx(2 * swap_delta) + # find the vertical bar + swap_vertical = swap_gate.get_paths()[-1] + assert len(swap_vertical) == 2 + assert swap_vertical.get_extents().width == 0.0 + assert swap_vertical.get_extents().height == pytest.approx(wire_height) + + # Measure gate + measure_gate = [obj for obj in axes.collections if obj.get_label() == 'Measure'][0] + + assert measure_gate.get_paths()[0].get_extents().width == pytest.approx(mgate_width) + assert measure_gate.get_paths()[0].get_extents().height == pytest.approx(0.9 * mgate_width) + + +def test_draw_advanced(plot_params): + qubit_lines = {0: [('X', [0], []), ('Measure', [0], [])], 1: [None, None]} + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, qubit_labels={1: 'qb1', 2: 'qb2'}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={0: 0, 1: 2}) + + with pytest.raises(RuntimeError): + _plot.to_draw(qubit_lines, drawing_order={1: 1, 2: 0}) + + # -------------------------------------------------------------------------- + + _, axes = _plot.to_draw(qubit_lines) + for text in axes.texts: + assert text.get_text() == r'$|0\rangle$' + + # NB numbering of wire starts from bottom. + _, axes = _plot.to_draw(qubit_lines, qubit_labels={0: 'qb0', 1: 'qb1'}, drawing_order={0: 0, 1: 1}) + assert [axes.texts[qubit_id].get_text() for qubit_id in range(2)] == ['qb0', 'qb1'] + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] > positions[0][1] + + _, axes = _plot.to_draw(qubit_lines, qubit_labels={0: 'qb2', 1: 'qb3'}, drawing_order={0: 1, 1: 0}) + + assert [axes.texts[qubit_id].get_text() for qubit_id in range(2)] == ['qb2', 'qb3'] + + positions = [axes.texts[qubit_id].get_position() for qubit_id in range(2)] + assert positions[1][1] < positions[0][1] diff --git a/projectq/backends/_circuits/_to_latex.py b/projectq/backends/_circuits/_to_latex.py index 5cbe89cae..c2f870b7d 100755 --- a/projectq/backends/_circuits/_to_latex.py +++ b/projectq/backends/_circuits/_to_latex.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,44 +12,71 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module for exporting quantum circuits to LaTeX code.""" + import json -from projectq.ops import (Allocate, Deallocate, DaggeredGate, get_inverse, - Measure, SqrtSwap, Swap, X, Z) +from projectq.ops import ( + Allocate, + DaggeredGate, + Deallocate, + Measure, + SqrtSwap, + Swap, + X, + Z, + get_inverse, +) + + +def _gate_name(gate): + """ + Return the string representation of the gate. + + Tries to use gate.tex_str and, if that is not available, uses str(gate) instead. -def to_latex(circuit): + Args: + gate: Gate object of which to get the name / latex representation. + + Returns: + gate_name (string): Latex gate name. """ - Translates a given circuit to a TikZ picture in a Latex document. + try: + name = gate.tex_str() + except AttributeError: + name = str(gate) + return name - It uses a json-configuration file which (if it does not exist) is created - automatically upon running this function for the first time. The config - file can be used to determine custom gate sizes, offsets, etc. - New gate options can be added under settings['gates'], using the gate - class name string as a key. Every gate can have its own width, height, pre - offset and offset. +def to_latex(circuit, drawing_order=None, draw_gates_in_parallel=True): + """ + Translate a given circuit to a TikZ picture in a Latex document. + + It uses a json-configuration file which (if it does not exist) is created automatically upon running this function + for the first time. The config file can be used to determine custom gate sizes, offsets, etc. + + New gate options can be added under settings['gates'], using the gate class name string as a key. Every gate can + have its own width, height, pre offset and offset. Example: .. code-block:: python - settings['gates']['HGate'] = {'width': .5, 'offset': .15} + settings['gates']['HGate'] = {'width': 0.5, 'offset': 0.15} - The default settings can be acquired using the get_default_settings() - function, and written using write_settings(). + The default settings can be acquired using the get_default_settings() function, and written using write_settings(). Args: - circuit (list>): Each qubit line is a list of + circuit (list): Each qubit line is a list of CircuitItem objects, i.e., in circuit[line]. + drawing_order (list): A list of qubit lines from which + the gates to be read from + draw_gates_in_parallel (bool): If gates should (False) + or not (True) be parallel in the circuit Returns: tex_doc_str (string): Latex document string which can be compiled using, e.g., pdflatex. """ - try: - FileNotFoundError - except NameError: - FileNotFoundError = IOError # for Python2 compatibility - try: with open('settings.json') as settings_file: settings = json.load(settings_file) @@ -57,8 +84,8 @@ class name string as a key. Every gate can have its own width, height, pre settings = write_settings(get_default_settings()) text = _header(settings) - text += _body(circuit, settings) - text += _footer(settings) + text += _body(circuit, settings, drawing_order, draw_gates_in_parallel=draw_gates_in_parallel) + text += _footer() return text @@ -81,66 +108,69 @@ def get_default_settings(): Returns: settings (dict): Default circuit settings """ - settings = dict() + settings = {} settings['gate_shadow'] = True - settings['lines'] = ({'style': 'very thin', 'double_classical': True, - 'init_quantum': True, 'double_lines_sep': .04}) - settings['gates'] = ({'HGate': {'width': .5, 'offset': .3, - 'pre_offset': .1}, - 'XGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtXGate': {'width': .7, 'offset': .3, - 'pre_offset': .1}, - 'SwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'SqrtSwapGate': {'width': .35, 'height': .35, - 'offset': .1}, - 'Rx': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ry': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Rz': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'Ph': {'width': 1., 'height': .8, 'pre_offset': .2, - 'offset': .3}, - 'EntangleGate': {'width': 1.8, 'offset': .2, - 'pre_offset': .2}, - 'DeallocateQubitGate': {'height': .15, 'offset': .2, - 'width': .2, - 'pre_offset': .1}, - 'AllocateQubitGate': {'height': .15, 'width': .2, - 'offset': .1, - 'pre_offset': .1, - 'draw_id': False, - 'allocate_at_zero': False}, - 'MeasureGate': {'width': 0.75, 'offset': .2, - 'height': .5, 'pre_offset': .2} - }) - settings['control'] = {'size': .1, 'shadow': False} + settings['lines'] = { + 'style': 'very thin', + 'double_classical': True, + 'init_quantum': True, + 'double_lines_sep': 0.04, + } + settings['gates'] = { + 'HGate': {'width': 0.5, 'offset': 0.3, 'pre_offset': 0.1}, + 'XGate': {'width': 0.35, 'height': 0.35, 'offset': 0.1}, + 'SqrtXGate': {'width': 0.7, 'offset': 0.3, 'pre_offset': 0.1}, + 'SwapGate': {'width': 0.35, 'height': 0.35, 'offset': 0.1}, + 'SqrtSwapGate': {'width': 0.35, 'height': 0.35, 'offset': 0.1}, + 'Rx': {'width': 1.0, 'height': 0.8, 'pre_offset': 0.2, 'offset': 0.3}, + 'Ry': {'width': 1.0, 'height': 0.8, 'pre_offset': 0.2, 'offset': 0.3}, + 'Rz': {'width': 1.0, 'height': 0.8, 'pre_offset': 0.2, 'offset': 0.3}, + 'Ph': {'width': 1.0, 'height': 0.8, 'pre_offset': 0.2, 'offset': 0.3}, + 'EntangleGate': {'width': 1.8, 'offset': 0.2, 'pre_offset': 0.2}, + 'DeallocateQubitGate': { + 'height': 0.15, + 'offset': 0.2, + 'width': 0.2, + 'pre_offset': 0.1, + }, + 'AllocateQubitGate': { + 'height': 0.15, + 'width': 0.2, + 'offset': 0.1, + 'pre_offset': 0.1, + 'draw_id': False, + 'allocate_at_zero': False, + }, + 'MeasureGate': {'width': 0.75, 'offset': 0.2, 'height': 0.5, 'pre_offset': 0.2}, + } + settings['control'] = {'size': 0.1, 'shadow': False} return settings def _header(settings): """ - Writes the Latex header using the settings file. + Write the Latex header using the settings file. The header includes all packages and defines all tikz styles. Returns: header (string): Header of the Latex document. """ - packages = ("\\documentclass{standalone}\n\\usepackage[margin=1in]" - "{geometry}\n\\usepackage[hang,small,bf]{caption}\n" - "\\usepackage{tikz}\n" - "\\usepackage{braket}\n\\usetikzlibrary{backgrounds,shadows." - "blur,fit,decorations.pathreplacing,shapes}\n\n") - - init = ("\\begin{document}\n" - "\\begin{tikzpicture}[scale=0.8, transform shape]\n\n") - - gate_style = ("\\tikzstyle{basicshadow}=[blur shadow={shadow blur steps=8," - " shadow xshift=0.7pt, shadow yshift=-0.7pt, shadow scale=" - "1.02}]") + packages = ( + "\\documentclass{standalone}\n\\usepackage[margin=1in]" + "{geometry}\n\\usepackage[hang,small,bf]{caption}\n" + "\\usepackage{tikz}\n" + "\\usepackage{braket}\n\\usetikzlibrary{backgrounds,shadows." + "blur,fit,decorations.pathreplacing,shapes}\n\n" + ) + + init = "\\begin{document}\n\\begin{tikzpicture}[scale=0.8, transform shape]\n\n" + + gate_style = ( + "\\tikzstyle{basicshadow}=[blur shadow={shadow blur steps=8," + " shadow xshift=0.7pt, shadow yshift=-0.7pt, shadow scale=" + "1.02}]" + ) if not (settings['gate_shadow'] or settings['control']['shadow']): gate_style = "" @@ -150,45 +180,45 @@ def _header(settings): gate_style += "basicshadow" gate_style += "]\n" - gate_style += ("\\tikzstyle{operator}=[basic,minimum size=1.5em]\n" - "\\tikzstyle{phase}=[fill=black,shape=circle," + - "minimum size={}".format(settings['control']['size']) + - "cm,inner sep=0pt,outer sep=0pt,draw=black" - ) + gate_style += ( + "\\tikzstyle{{operator}}=[basic,minimum size=1.5em]\n" + f"\\tikzstyle{{phase}}=[fill=black,shape=circle,minimum size={settings['control']['size']}cm," + "inner sep=0pt,outer sep=0pt,draw=black" + ) if settings['control']['shadow']: gate_style += ",basicshadow" - gate_style += ("]\n\\tikzstyle{none}=[inner sep=0pt,outer sep=-.5pt," - "minimum height=0.5cm+1pt]\n" - "\\tikzstyle{measure}=[operator,inner sep=0pt,minimum " + - "height={}cm, minimum width={}cm]\n".format( - settings['gates']['MeasureGate']['height'], - settings['gates']['MeasureGate']['width']) + - "\\tikzstyle{xstyle}=[circle,basic,minimum height=") - x_gate_radius = min(settings['gates']['XGate']['height'], - settings['gates']['XGate']['width']) - gate_style += ("{x_rad}cm,minimum width={x_rad}cm,inner sep=-1pt," - "{linestyle}]\n" - ).format(x_rad=x_gate_radius, - linestyle=settings['lines']['style']) + gate_style += ( + "]\n\\tikzstyle{{none}}=[inner sep=0pt,outer sep=-.5pt,minimum height=0.5cm+1pt]\n" + "\\tikzstyle{{measure}}=[operator,inner sep=0pt," + f"minimum height={settings['gates']['MeasureGate']['height']}cm," + f"minimum width={settings['gates']['MeasureGate']['width']}cm]\n" + "\\tikzstyle{{xstyle}}=[circle,basic,minimum height=" + ) + x_gate_radius = min(settings['gates']['XGate']['height'], settings['gates']['XGate']['width']) + gate_style += f"{x_gate_radius}cm,minimum width={x_gate_radius}cm,inner sep=-1pt,{settings['lines']['style']}]\n" if settings['gate_shadow']: - gate_style += ("\\tikzset{\nshadowed/.style={preaction={transform " - "canvas={shift={(0.5pt,-0.5pt)}}, draw=gray, opacity=" - "0.4}},\n}\n") + gate_style += ( + "\\tikzset{\nshadowed/.style={preaction={transform " + "canvas={shift={(0.5pt,-0.5pt)}}, draw=gray, opacity=" + "0.4}},\n}\n" + ) gate_style += "\\tikzstyle{swapstyle}=[" gate_style += "inner sep=-1pt, outer sep=-1pt, minimum width=0pt]\n" - edge_style = ("\\tikzstyle{edgestyle}=[" + settings['lines']['style'] + - "]\n") + edge_style = f"\\tikzstyle{{edgestyle}}=[{settings['lines']['style']}]\n" return packages + init + gate_style + edge_style -def _body(circuit, settings): +def _body(circuit, settings, drawing_order=None, draw_gates_in_parallel=True): """ - Return the body of the Latex document, including the entire circuit in - TikZ format. + Return the body of the Latex document, including the entire circuit in TikZ format. Args: circuit (list>): Circuit to draw. + settings: Dictionary of settings to use for the TikZ image. + drawing_order: A list of circuit wires from where to read one gate command. + draw_gates_in_parallel: Are the gate/commands occupying a single time step in the circuit diagram? For example, + False means that gates can be parallel in the circuit. Returns: tex_str (string): Latex string to draw the entire circuit. @@ -196,26 +226,39 @@ def _body(circuit, settings): code = [] conv = _Circ2Tikz(settings, len(circuit)) - for line in range(len(circuit)): - code.append(conv.to_tikz(line, circuit)) + + to_where = None + if drawing_order is None: + drawing_order = list(range(len(circuit))) + else: + to_where = 1 + + for line in drawing_order: + code.append( + conv.to_tikz( + line, + circuit, + end=to_where, + draw_gates_in_parallel=draw_gates_in_parallel, + ) + ) return "".join(code) -def _footer(settings): +def _footer(): """ Return the footer of the Latex document. Returns: tex_footer_str (string): Latex document footer. """ - return "\n\n\end{tikzpicture}\n\end{document}" + return "\n\n\\end{tikzpicture}\n\\end{document}" -class _Circ2Tikz(object): +class _Circ2Tikz: # pylint: disable=too-few-public-methods """ - The Circ2Tikz class takes a circuit (list of lists of CircuitItem objects) - and turns them into Latex/TikZ code. + The Circ2Tikz class takes a circuit (list of lists of CircuitItem objects) and turns them into Latex/TikZ code. It uses the settings dictionary for gate offsets, sizes, spacing, ... """ @@ -230,28 +273,28 @@ def __init__(self, settings, num_lines): circuit. """ self.settings = settings - self.pos = [0.] * num_lines + self.pos = [0.0] * num_lines self.op_count = [0] * num_lines self.is_quantum = [settings['lines']['init_quantum']] * num_lines - def to_tikz(self, line, circuit, end=None): + def to_tikz( # pylint: disable=too-many-branches,too-many-locals,too-many-statements + self, line, circuit, end=None, draw_gates_in_parallel=True + ): """ - Generate the TikZ code for one line of the circuit up to a certain - gate. + Generate the TikZ code for one line of the circuit up to a certain gate. - It modifies the circuit to include only the gates which have not been - drawn. It automatically switches to other lines if the gates on those - lines have to be drawn earlier. + It modifies the circuit to include only the gates which have not been drawn. It automatically switches to other + lines if the gates on those lines have to be drawn earlier. Args: line (int): Line to generate the TikZ code for. circuit (list>): The circuit to draw. end (int): Gate index to stop at (for recursion). + draw_gates_in_parallel (bool): True or False for how to place gates Returns: - tikz_code (string): TikZ code representing the current qubit line - and, if it was necessary to draw other lines, those lines as - well. + tikz_code (string): TikZ code representing the current qubit line and, if it was necessary to draw other + lines, those lines as well. """ if end is None: end = len(circuit[line]) @@ -266,133 +309,110 @@ def to_tikz(self, line, circuit, end=None): all_lines = lines + ctrl_lines all_lines.remove(line) # remove current line - for l in all_lines: + for _line in all_lines: gate_idx = 0 - while not (circuit[l][gate_idx] == cmds[i]): + while not circuit[_line][gate_idx] == cmds[i]: gate_idx += 1 - tikz_code.append(self.to_tikz(l, circuit, gate_idx)) + tikz_code.append(self.to_tikz(_line, circuit, gate_idx)) + # we are taking care of gate 0 (the current one) - circuit[l] = circuit[l][1:] + circuit[_line] = circuit[_line][1:] all_lines = lines + ctrl_lines - pos = max([self.pos[l] for l in range(min(all_lines), - max(all_lines) + 1)]) - for l in range(min(all_lines), max(all_lines) + 1): - self.pos[l] = pos + self._gate_pre_offset(gate) + pos = max(self.pos[ll] for ll in range(min(all_lines), max(all_lines) + 1)) + for _line in range(min(all_lines), max(all_lines) + 1): + self.pos[_line] = pos + self._gate_pre_offset(gate) connections = "" - for l in all_lines: - connections += self._line(self.op_count[l] - 1, - self.op_count[l], line=l) + for _line in all_lines: + connections += self._line(self.op_count[_line] - 1, self.op_count[_line], line=_line) add_str = "" if gate == X: # draw NOT-gate with controls add_str = self._x_gate(lines, ctrl_lines) # and make the target qubit quantum if one of the controls is if not self.is_quantum[lines[0]]: - if sum([self.is_quantum[i] for i in ctrl_lines]) > 0: + if sum(self.is_quantum[i] for i in ctrl_lines) > 0: self.is_quantum[lines[0]] = True elif gate == Z and len(ctrl_lines) > 0: add_str = self._cz_gate(lines + ctrl_lines) elif gate == Swap: add_str = self._swap_gate(lines, ctrl_lines) elif gate == SqrtSwap: - add_str = self._sqrtswap_gate(lines, ctrl_lines, - daggered=False) + add_str = self._sqrtswap_gate(lines, ctrl_lines, daggered=False) elif gate == get_inverse(SqrtSwap): add_str = self._sqrtswap_gate(lines, ctrl_lines, daggered=True) elif gate == Measure: # draw measurement gate - for l in lines: - op = self._op(l) + for _line in lines: + op = self._op(_line) width = self._gate_width(Measure) height = self._gate_height(Measure) - shift0 = .07 * height - shift1 = .36 * height - shift2 = .1 * width - add_str += ("\n\\node[measure,edgestyle] ({op}) at ({pos}" - ",-{line}) {{}};\n\\draw[edgestyle] ([yshift=" - "-{shift1}cm,xshift={shift2}cm]{op}.west) to " - "[out=60,in=180] ([yshift={shift0}cm]{op}." - "center) to [out=0, in=120] ([yshift=-{shift1}" - "cm,xshift=-{shift2}cm]{op}.east);\n" - "\\draw[edgestyle] ([yshift=-{shift1}cm]{op}." - "center) to ([yshift=-{shift2}cm,xshift=-" - "{shift1}cm]{op}.north east);" - ).format(op=op, pos=self.pos[l], line=l, - shift0=shift0, shift1=shift1, - shift2=shift2) - self.op_count[l] += 1 - self.pos[l] += (self._gate_width(gate) + - self._gate_offset(gate)) - self.is_quantum[l] = False + shift0 = 0.07 * height + shift1 = 0.36 * height + shift2 = 0.1 * width + add_str += ( + f"\n\\node[measure,edgestyle] ({op}) at ({self.pos[_line]}" + f",-{_line}) {{}};\n\\draw[edgestyle] ([yshift=" + f"-{shift1}cm,xshift={shift2}cm]{op}.west) to " + f"[out=60,in=180] ([yshift={shift0}cm]{op}." + f"center) to [out=0, in=120] ([yshift=-{shift1}" + f"cm,xshift=-{shift2}cm]{op}.east);\n" + f"\\draw[edgestyle] ([yshift=-{shift1}cm]{op}." + f"center) to ([yshift=-{shift2}cm,xshift=-" + f"{shift1}cm]{op}.north east);" + ) + self.op_count[_line] += 1 + self.pos[_line] += self._gate_width(gate) + self._gate_offset(gate) + self.is_quantum[_line] = False elif gate == Allocate: # draw 'begin line' - add_str = "\n\\node[none] ({}) at ({},-{}) {{$\Ket{{0}}{}$}};" id_str = "" if self.settings['gates']['AllocateQubitGate']['draw_id']: - id_str = "^{{\\textcolor{{red}}{{{}}}}}".format(cmds[i].id) + id_str = f"^{{\\textcolor{{red}}{{{cmds[i].id}}}}}" xpos = self.pos[line] try: - if (self.settings['gates']['AllocateQubitGate'] - ['allocate_at_zero']): + if self.settings['gates']['AllocateQubitGate']['allocate_at_zero']: self.pos[line] -= self._gate_pre_offset(gate) xpos = self._gate_pre_offset(gate) except KeyError: pass - self.pos[line] = max(xpos + self._gate_offset(gate) + - self._gate_width(gate), self.pos[line]) - add_str = add_str.format(self._op(line), xpos, line, - id_str) + self.pos[line] = max( + xpos + self._gate_offset(gate) + self._gate_width(gate), + self.pos[line], + ) + add_str = f"\n\\node[none] ({self._op(line)}) at ({xpos},-{line}) {{$\\Ket{{0}}{id_str}$}};" self.op_count[line] += 1 self.is_quantum[line] = self.settings['lines']['init_quantum'] elif gate == Deallocate: # draw 'end of line' op = self._op(line) - add_str = "\n\\node[none] ({}) at ({},-{}) {{}};" - add_str = add_str.format(op, self.pos[line], line) - yshift = str(self._gate_height(gate)) + "cm]" - add_str += ("\n\\draw ([yshift={yshift}{op}.center) edge " - "[edgestyle] ([yshift=-{yshift}{op}.center);" - ).format(op=op, yshift=yshift) + add_str = f"\n\\node[none] ({op}) at ({self.pos[line]},-{line}) {{}};" + yshift = f"{str(self._gate_height(gate))}cm]" + add_str += f"\n\\draw ([yshift={yshift}{op}.center) edge [edgestyle] ([yshift=-{yshift}{op}.center);" self.op_count[line] += 1 - self.pos[line] += (self._gate_width(gate) + - self._gate_offset(gate)) + self.pos[line] += self._gate_width(gate) + self._gate_offset(gate) else: # regular gate must draw the lines it does not act upon # if it spans multiple qubits add_str = self._regular_gate(gate, lines, ctrl_lines) - for l in lines: - self.is_quantum[l] = True + for _line in lines: + self.is_quantum[_line] = True tikz_code.append(add_str) if not gate == Allocate: tikz_code.append(connections) + if not draw_gates_in_parallel: + for _line, _ in enumerate(self.pos): + if _line != line: + self.pos[_line] = self.pos[line] + circuit[line] = circuit[line][end:] return "".join(tikz_code) - def _gate_name(self, gate): - """ - Return the string representation of the gate. - - Tries to use gate.tex_str and, if that is not available, uses str(gate) - instead. - - Args: - gate: Gate object of which to get the name / latex representation. - - Returns: - gate_name (string): Latex gate name. - """ - try: - name = gate.tex_str() - except AttributeError: - name = str(gate) - return name - - def _sqrtswap_gate(self, lines, ctrl_lines, daggered): + def _sqrtswap_gate(self, lines, ctrl_lines, daggered): # pylint: disable=too-many-locals """ Return the TikZ code for a Square-root Swap-gate. @@ -402,7 +422,8 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): ctrl_lines (list): List of qubit lines which act as controls. daggered (bool): Show the daggered one if True. """ - assert(len(lines) == 2) # sqrt swap gate acts on 2 qubits + if len(lines) != 2: + raise RuntimeError('Sqrt SWAP gate acts on 2 qubits') delta_pos = self._gate_offset(SqrtSwap) gate_width = self._gate_width(SqrtSwap) lines.sort() @@ -410,36 +431,30 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): gate_str = "" for line in lines: op = self._op(line) - w = "{}cm".format(.5 * gate_width) - s1 = "[xshift=-{w},yshift=-{w}]{op}.center".format(w=w, op=op) - s2 = "[xshift={w},yshift={w}]{op}.center".format(w=w, op=op) - s3 = "[xshift=-{w},yshift={w}]{op}.center".format(w=w, op=op) - s4 = "[xshift={w},yshift=-{w}]{op}.center".format(w=w, op=op) + width = f"{0.5 * gate_width}cm" + blc = f"[xshift=-{width},yshift=-{width}]{op}.center" + trc = f"[xshift={width},yshift={width}]{op}.center" + tlc = f"[xshift=-{width},yshift={width}]{op}.center" + brc = f"[xshift={width},yshift=-{width}]{op}.center" swap_style = "swapstyle,edgestyle" if self.settings['gate_shadow']: swap_style += ",shadowed" - gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" - "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) - + gate_str += ( + f"\n\\node[swapstyle] ({op}) at ({self.pos[line]},-{line}) {{}};" + f"\n\\draw[{swap_style}] ({blc})--({trc});\n" + f"\\draw[{swap_style}] ({tlc})--({brc});" + ) # add a circled 1/2 - midpoint = (lines[0] + lines[1]) / 2. + midpoint = (lines[0] + lines[1]) / 2.0 pos = self.pos[lines[0]] - op_mid = "line{}_gate{}".format( - '{}-{}'.format(*lines), self.op_count[lines[0]]) - gate_str += ("\n\\node[xstyle] ({op}) at ({pos},-{line})\ - {{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};" - ).format(op=op_mid, line=midpoint, pos=pos, - dagger='^{{\dagger}}' if daggered else '') + # pylint: disable=consider-using-f-string + op_mid = f"line{'{}-{}'.format(*lines)}_gate{self.op_count[lines[0]]}" + dagger = '^{{\\dagger}}' if daggered else '' + gate_str += f"\n\\node[xstyle] ({op}) at ({pos},-{midpoint}){{\\scriptsize $\\frac{{1}}{{2}}{dagger}$}};" # add two vertical lines to connect circled 1/2 - gate_str += "\n\\draw ({}) edge[edgestyle] ({});".format( - self._op(lines[0]), op_mid) - gate_str += "\n\\draw ({}) edge[edgestyle] ({});".format( - op_mid, self._op(lines[1])) + gate_str += f"\n\\draw ({self._op(lines[0])}) edge[edgestyle] ({op_mid});" + gate_str += f"\n\\draw ({op_mid}) edge[edgestyle] ({self._op(lines[1])});" if len(ctrl_lines) > 0: for ctrl in ctrl_lines: @@ -458,7 +473,7 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): self.pos[i] = new_pos return gate_str - def _swap_gate(self, lines, ctrl_lines): + def _swap_gate(self, lines, ctrl_lines): # pylint: disable=too-many-locals """ Return the TikZ code for a Swap-gate. @@ -468,7 +483,8 @@ def _swap_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 2) # swap gate acts on 2 qubits + if len(lines) != 2: + raise RuntimeError('SWAP gate acts on 2 qubits') delta_pos = self._gate_offset(Swap) gate_width = self._gate_width(Swap) lines.sort() @@ -476,20 +492,19 @@ def _swap_gate(self, lines, ctrl_lines): gate_str = "" for line in lines: op = self._op(line) - w = "{}cm".format(.5 * gate_width) - s1 = "[xshift=-{w},yshift=-{w}]{op}.center".format(w=w, op=op) - s2 = "[xshift={w},yshift={w}]{op}.center".format(w=w, op=op) - s3 = "[xshift=-{w},yshift={w}]{op}.center".format(w=w, op=op) - s4 = "[xshift={w},yshift=-{w}]{op}.center".format(w=w, op=op) + width = f"{0.5 * gate_width}cm" + blc = f"[xshift=-{width},yshift=-{width}]{op}.center" + trc = f"[xshift={width},yshift={width}]{op}.center" + tlc = f"[xshift=-{width},yshift={width}]{op}.center" + brc = f"[xshift={width},yshift=-{width}]{op}.center" swap_style = "swapstyle,edgestyle" if self.settings['gate_shadow']: swap_style += ",shadowed" - gate_str += ("\n\\node[swapstyle] ({op}) at ({pos},-{line}) {{}};" - "\n\\draw[{swap_style}] ({s1})--({s2});\n" - "\\draw[{swap_style}] ({s3})--({s4});" - ).format(op=op, s1=s1, s2=s2, s3=s3, s4=s4, - line=line, pos=self.pos[line], - swap_style=swap_style) + gate_str += ( + f"\n\\node[swapstyle] ({op}) at ({self.pos[line]},-{line}) {{}};" + f"\n\\draw[{swap_style}] ({blc})--({trc});\n" + f"\\draw[{swap_style}] ({tlc})--({brc});" + ) gate_str += self._line(lines[0], lines[1]) if len(ctrl_lines) > 0: @@ -519,15 +534,17 @@ def _x_gate(self, lines, ctrl_lines): ctrl_lines (list): List of qubit lines which act as controls. """ - assert(len(lines) == 1) # NOT gate only acts on 1 qubit + if len(lines) != 1: + raise RuntimeError('X gate acts on 1 qubits') line = lines[0] delta_pos = self._gate_offset(X) gate_width = self._gate_width(X) op = self._op(line) - gate_str = ("\n\\node[xstyle] ({op}) at ({pos},-{line}) {{}};\n\\draw" - "[edgestyle] ({op}.north)--({op}.south);\n\\draw" - "[edgestyle] ({op}.west)--({op}.east);" - ).format(op=op, line=line, pos=self.pos[line]) + gate_str = ( + f"\n\\node[xstyle] ({op}) at ({self.pos[line]},-{line}) {{}};\n\\draw" + f"[edgestyle] ({op}.north)--({op}.south);\n\\draw" + f"[edgestyle] ({op}.west)--({op}.east);" + ) if len(ctrl_lines) > 0: for ctrl in ctrl_lines: @@ -549,7 +566,6 @@ def _cz_gate(self, lines): Args: lines (list): List of all qubits involved. """ - assert len(lines) > 1 line = lines[0] delta_pos = self._gate_offset(Z) gate_width = self._gate_width(Z) @@ -575,12 +591,12 @@ def _gate_width(self, gate): (settings['gates'][gate_class_name]['width']) """ if isinstance(gate, DaggeredGate): - gate = gate._gate + gate = gate._gate # pylint: disable=protected-access try: gates = self.settings['gates'] gate_width = gates[gate.__class__.__name__]['width'] except KeyError: - gate_width = .5 + gate_width = 0.5 return gate_width def _gate_pre_offset(self, gate): @@ -592,7 +608,7 @@ def _gate_pre_offset(self, gate): (settings['gates'][gate_class_name]['pre_offset']) """ if isinstance(gate, DaggeredGate): - gate = gate._gate + gate = gate._gate # pylint: disable=protected-access try: gates = self.settings['gates'] delta_pos = gates[gate.__class__.__name__]['pre_offset'] @@ -602,20 +618,20 @@ def _gate_pre_offset(self, gate): def _gate_offset(self, gate): """ - Return the offset to use after placing this gate and, if no pre_offset - is defined, the same offset is used in front of the gate. + Return the offset to use after placing this gate. + + If no pre_offset is defined, the same offset is used in front of the gate. Returns: - gate_offset (float): Offset. - (settings['gates'][gate_class_name]['offset']) + gate_offset (float): Offset. (settings['gates'][gate_class_name]['offset']) """ if isinstance(gate, DaggeredGate): - gate = gate._gate + gate = gate._gate # pylint: disable=protected-access try: gates = self.settings['gates'] delta_pos = gates[gate.__class__.__name__]['offset'] except KeyError: - delta_pos = .2 + delta_pos = 0.2 return delta_pos def _gate_height(self, gate): @@ -627,11 +643,11 @@ def _gate_height(self, gate): (settings['gates'][gate_class_name]['height']) """ if isinstance(gate, DaggeredGate): - gate = gate._gate + gate = gate._gate # pylint: disable=protected-access try: height = self.settings['gates'][gate.__class__.__name__]['height'] except KeyError: - height = .5 + height = 0.5 return height def _phase(self, line, pos): @@ -646,12 +662,11 @@ def _phase(self, line, pos): tex_str (string): Latex string representing a control circle at the given position. """ - phase_str = "\n\\node[phase] ({}) at ({},-{}) {{}};" - return phase_str.format(self._op(line), pos, line) + return f"\n\\node[phase] ({self._op(line)}) at ({pos},-{line}) {{}};" def _op(self, line, op=None, offset=0): """ - Returns the gate name for placing a gate on a line. + Return the gate name for placing a gate on a line. Args: line (int): Line number. @@ -663,21 +678,21 @@ def _op(self, line, op=None, offset=0): """ if op is None: op = self.op_count[line] - return "line{}_gate{}".format(line, op + offset) + return f"line{line}_gate{op + offset}" - def _line(self, p1, p2, double=False, line=None): + def _line(self, point1, point2, double=False, line=None): # pylint: disable=too-many-locals,unused-argument """ - Connects p1 and p2, where p1 and p2 are either to qubit line indices, - in which case the two most recent gates are connected, or two gate - indices, in which case line denotes the line number and the two gates + Create a line that connects two points. + + Connects point1 and point2, where point1 and point2 are either to qubit line indices, in which case the two most + recent gates are connected, or two gate indices, in which case line denotes the line number and the two gates are connected on the given line. Args: p1 (int): Index of the first object to connect. p2 (int): Index of the second object to connect. double (bool): Draws double lines if True. - line (int or None): Line index - if provided, p1 and p2 are gate - indices. + line (int or None): Line index - if provided, p1 and p2 are gate indices. Returns: tex_str (string): Latex code to draw this / these line(s). @@ -685,33 +700,30 @@ def _line(self, p1, p2, double=False, line=None): dbl_classical = self.settings['lines']['double_classical'] if line is None: - quantum = not dbl_classical or self.is_quantum[p1] - op1, op2 = self._op(p1), self._op(p2) + quantum = not dbl_classical or self.is_quantum[point1] + op1, op2 = self._op(point1), self._op(point2) loc1, loc2 = 'north', 'south' shift = "xshift={}cm" else: quantum = not dbl_classical or self.is_quantum[line] - op1, op2 = self._op(line, p1), self._op(line, p2) + op1, op2 = self._op(line, point1), self._op(line, point2) loc1, loc2 = 'west', 'east' shift = "yshift={}cm" if quantum: - return "\n\\draw ({}) edge[edgestyle] ({});".format(op1, op2) - else: - if p2 > p1: - loc1, loc2 = loc2, loc1 - edge_str = ("\n\\draw ([{shift}]{op1}.{loc1}) edge[edgestyle] " - "([{shift}]{op2}.{loc2});") - line_sep = self.settings['lines']['double_lines_sep'] - shift1 = shift.format(line_sep / 2.) - shift2 = shift.format(-line_sep / 2.) - edges_str = edge_str.format(shift=shift1, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) - edges_str += edge_str.format(shift=shift2, op1=op1, op2=op2, - loc1=loc1, loc2=loc2) - return edges_str - - def _regular_gate(self, gate, lines, ctrl_lines): + return f"\n\\draw ({op1}) edge[edgestyle] ({op2});" + + if point2 > point1: + loc1, loc2 = loc2, loc1 + edge_str = "\n\\draw ([{shift}]{op1}.{loc1}) edge[edgestyle] ([{shift}]{op2}.{loc2});" + line_sep = self.settings['lines']['double_lines_sep'] + shift1 = shift.format(line_sep / 2.0) + shift2 = shift.format(-line_sep / 2.0) + edges_str = edge_str.format(shift=shift1, op1=op1, op2=op2, loc1=loc1, loc2=loc2) + edges_str += edge_str.format(shift=shift2, op1=op1, op2=op2, loc1=loc1, loc2=loc2) + return edges_str + + def _regular_gate(self, gate, lines, ctrl_lines): # pylint: disable=too-many-locals """ Draw a regular gate. @@ -733,7 +745,7 @@ def _regular_gate(self, gate, lines, ctrl_lines): gate_width = self._gate_width(gate) gate_height = self._gate_height(gate) - name = self._gate_name(gate) + name = _gate_name(gate) lines = list(range(imin, imax + 1)) @@ -741,34 +753,30 @@ def _regular_gate(self, gate, lines, ctrl_lines): pos = self.pos[lines[0]] node_str = "\n\\node[none] ({}) at ({},-{}) {{}};" - for l in lines: - node1 = node_str.format(self._op(l), pos, l) - node2 = ("\n\\node[none,minimum height={}cm,outer sep=0] ({}) at" - " ({},-{}) {{}};" - ).format(gate_height, self._op(l, offset=1), - pos + gate_width / 2., l) - node3 = node_str.format(self._op(l, offset=2), - pos + gate_width, l) + for line in lines: + node1 = node_str.format(self._op(line), pos, line) + node2 = ( + "\n\\node[none,minimum height={gate_height}cm,outer sep=0] ({self._op(line, offset=1)}) " + f"at ({pos + gate_width / 2.0},-{line}) {{}};" + ) + node3 = node_str.format(self._op(line, offset=2), pos + gate_width, line) tex_str += node1 + node2 + node3 - if l not in gate_lines: - tex_str += self._line(self.op_count[l] - 1, self.op_count[l], - line=l) - - tex_str += ("\n\\draw[operator,edgestyle,outer sep={width}cm] ([" - "yshift={half_height}cm]{op1}) rectangle ([yshift=-" - "{half_height}cm]{op2}) node[pos=.5] {{{name}}};" - ).format(width=gate_width, op1=self._op(imin), - op2=self._op(imax, offset=2), - half_height=.5 * gate_height, - name=name) - - for l in lines: - self.pos[l] = pos + gate_width / 2. - self.op_count[l] += 1 + if line not in gate_lines: + tex_str += self._line(self.op_count[line] - 1, self.op_count[line], line=line) + + tex_str += ( + f"\n\\draw[operator,edgestyle,outer sep={gate_width}cm] ([" + f"yshift={0.5 * gate_height}cm]{self._op(imin)}) rectangle ([yshift=-" + f"{0.5 * gate_height}cm]{self._op(imax, offset=2)}) node[pos=.5] {{{name}}};" + ) + + for line in lines: + self.pos[line] = pos + gate_width / 2.0 + self.op_count[line] += 1 for ctrl in ctrl_lines: if ctrl not in lines: - tex_str += self._phase(ctrl, pos + gate_width / 2.) + tex_str += self._phase(ctrl, pos + gate_width / 2.0) connect_to = imax if abs(connect_to - ctrl) > abs(imin - ctrl): connect_to = imin @@ -776,9 +784,9 @@ def _regular_gate(self, gate, lines, ctrl_lines): self.pos[ctrl] = pos + delta_pos + gate_width self.op_count[ctrl] += 1 - for l in lines: - self.op_count[l] += 2 + for line in lines: + self.op_count[line] += 2 - for l in range(min(ctrl_lines + lines), max(ctrl_lines + lines) + 1): - self.pos[l] = pos + delta_pos + gate_width + for line in range(min(ctrl_lines + lines), max(ctrl_lines + lines) + 1): + self.pos[line] = pos + delta_pos + gate_width return tex_str diff --git a/projectq/backends/_circuits/_to_latex_test.py b/projectq/backends/_circuits/_to_latex_test.py index 8d8ff81df..3369e681a 100755 --- a/projectq/backends/_circuits/_to_latex_test.py +++ b/projectq/backends/_circuits/_to_latex_test.py @@ -11,34 +11,31 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._circuits._to_latex.py. """ -import pytest -import builtins import copy -from projectq import MainEngine -from projectq.cengines import LastEngineException -from projectq.ops import (BasicGate, - H, - X, - CNOT, - Measure, - Z, - Swap, - SqrtX, - SqrtSwap, - C, - get_inverse, - ) -from projectq.meta import Control -from projectq.backends import CircuitDrawer +import pytest -import projectq.backends._circuits._to_latex as _to_latex import projectq.backends._circuits._drawer as _drawer +import projectq.backends._circuits._to_latex as _to_latex +from projectq import MainEngine +from projectq.meta import Control +from projectq.ops import ( + CNOT, + BasicGate, + C, + H, + Measure, + SqrtSwap, + SqrtX, + Swap, + X, + Z, + get_inverse, +) def test_tolatex(): @@ -47,8 +44,8 @@ def test_tolatex(): old_footer = _to_latex._footer _to_latex._header = lambda x: "H" - _to_latex._body = lambda x, y: x - _to_latex._footer = lambda x: "F" + _to_latex._body = lambda x, settings, drawing_order, draw_gates_in_parallel: x + _to_latex._footer = lambda: "F" latex = _to_latex.to_latex("B") assert latex == "HBF" @@ -68,11 +65,15 @@ def test_default_settings(): def test_header(): - settings = {'gate_shadow': False, 'control': {'shadow': False, 'size': 0}, - 'gates': {'MeasureGate': {'height': 0, 'width': 0}, - 'XGate': {'height': 1, 'width': .5} - }, - 'lines': {'style': 'my_style'}} + settings = { + 'gate_shadow': False, + 'control': {'shadow': False, 'size': 0}, + 'gates': { + 'MeasureGate': {'height': 0, 'width': 0}, + 'XGate': {'height': 1, 'width': 0.5}, + }, + 'lines': {'style': 'my_style'}, + } header = _to_latex._header(settings) assert 'minimum' in header @@ -104,7 +105,7 @@ def test_large_gates(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -136,7 +137,7 @@ def test_body(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() @@ -173,20 +174,149 @@ def test_body(): assert code.count("swapstyle") == 36 # CZ is two phases plus 2 from CNOTs + 2 from cswap + 2 from csqrtswap assert code.count("phase") == 8 - assert code.count("{{{}}}".format(str(H))) == 2 # 2 hadamard gates - assert code.count("{$\Ket{0}") == 3 # 3 qubits allocated + assert code.count(f"{{{str(H)}}}") == 2 # 2 hadamard gates + assert code.count("{$\\Ket{0}") == 3 # 3 qubits allocated # 1 cnot, 1 not gate, 3 SqrtSwap, 1 inv(SqrtSwap) assert code.count("xstyle") == 7 assert code.count("measure") == 1 # 1 measurement - assert code.count("{{{}}}".format(str(Z))) == 1 # 1 Z gate + assert code.count(f"{{{str(Z)}}}") == 1 # 1 Z gate assert code.count("{red}") == 3 +@pytest.mark.parametrize('gate, n_qubits', ((SqrtSwap, 3), (Swap, 3), (X, 2)), ids=str) +def test_invalid_number_of_qubits(gate, n_qubits): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qureg = eng.allocate_qureg(n_qubits) + + gate | (*qureg,) + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + + with pytest.raises(RuntimeError): + _to_latex._body(circuit_lines, settings) + + +def test_body_with_drawing_order_and_gates_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order: first the 3 allocations, then the 3 Hadamard and 1 CNOT gates + order = [0, 1, 2, 0, 1, 2, 0] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, drawing_order=order, draw_gates_in_parallel=True) + + # there are three Hadamards in parallel + assert code.count("node[pos=.5] {H}") == 3 + + # line1_gate0 is initialisation + # line1_gate1 is empty + # line1_gate2 is for Hadamard on line1 + # line1_gate3 is empty + # XOR of CNOT is node[xstyle] (line1_gate4) + assert code.count("node[xstyle] (line2_gate4)") == 1 + + # and the CNOT is at position 1.4, because of the offsets + assert code.count("node[phase] (line0_gate4) at (1.4") == 1 + assert code.count("node[xstyle] (line2_gate4) at (1.4") == 1 + + +def test_body_with_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + # replicates the above order: first the 3 allocations, then the 3 Hadamard and 1 CNOT gates + order = [0, 1, 2, 0, 1, 2, 0] + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, drawing_order=order, draw_gates_in_parallel=False) + + # and the CNOT is at position 4.0, because of the offsets + # which are 0.5 * 3 * 2 (due to three Hadamards) + the initialisations + assert code.count("node[phase] (line0_gate4) at (4.0,-0)") == 1 + assert code.count("node[xstyle] (line2_gate4) at (4.0,-2)") == 1 + + +def test_body_without_drawing_order_and_gates_not_parallel(): + drawer = _drawer.CircuitDrawer() + eng = MainEngine(drawer, []) + old_tolatex = _drawer.to_latex + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x + + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + + H | qubit1 + H | qubit2 + H | qubit3 + CNOT | (qubit1, qubit3) + + del qubit1 + eng.flush() + + circuit_lines = drawer.get_latex() + _drawer.to_latex = old_tolatex + + settings = _to_latex.get_default_settings() + settings['gates']['AllocateQubitGate']['draw_id'] = True + code = _to_latex._body(circuit_lines, settings, draw_gates_in_parallel=False) + + # line1_gate1 is after the cnot line2_gate_4 + idx1 = code.find("node[xstyle] (line2_gate4)") + idx2 = code.find("node[none] (line1_gate1)") + assert idx1 < idx2 + + def test_qubit_allocations_at_zero(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x a = eng.allocate_qureg(4) @@ -219,7 +349,7 @@ def test_qubit_lines_classicalvsquantum1(): drawer = _drawer.CircuitDrawer() eng = MainEngine(drawer, []) old_tolatex = _drawer.to_latex - _drawer.to_latex = lambda x: x + _drawer.to_latex = lambda x, drawing_order, draw_gates_in_parallel: x qubit1 = eng.allocate_qubit() @@ -247,7 +377,7 @@ def test_qubit_lines_classicalvsquantum2(): H | action code = drawer.get_latex() - assert code.count("{{{}}}".format(str(H))) == 1 # 1 Hadamard + assert code.count(f"{{{str(H)}}}") == 1 # 1 Hadamard assert code.count("{$") == 4 # four allocate gates assert code.count("node[phase]") == 3 # 3 controls @@ -266,7 +396,7 @@ def test_qubit_lines_classicalvsquantum3(): H | (action1, action2) code = drawer.get_latex() - assert code.count("{{{}}}".format(str(H))) == 1 # 1 Hadamard + assert code.count(f"{{{str(H)}}}") == 1 # 1 Hadamard assert code.count("{$") == 7 # 8 allocate gates assert code.count("node[phase]") == 3 # 1 control # (other controls are within the gate -> are not drawn) diff --git a/projectq/backends/_exceptions.py b/projectq/backends/_exceptions.py new file mode 100644 index 000000000..9ebd6390a --- /dev/null +++ b/projectq/backends/_exceptions.py @@ -0,0 +1,43 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exception classes for projectq.backends.""" + + +class DeviceTooSmall(Exception): + """Raised when a device does not have enough qubits for a desired job.""" + + +class DeviceOfflineError(Exception): + """Raised when a device is required but is currently offline.""" + + +class DeviceNotHandledError(Exception): + """Exception raised if a selected device cannot handle the circuit or is not supported by ProjectQ.""" + + +class RequestTimeoutError(Exception): + """Raised if a request to the job creation API times out.""" + + +class JobSubmissionError(Exception): + """Raised when the job creation API contains an error of some kind.""" + + +class InvalidCommandError(Exception): + """Raised if the backend encounters an invalid command.""" + + +class MidCircuitMeasurementError(Exception): + """Raised when a mid-circuit measurement is detected on a qubit.""" diff --git a/projectq/backends/_ibm/__init__.py b/projectq/backends/_ibm/__init__.py index 289b40833..254142fbc 100755 --- a/projectq/backends/_ibm/__init__.py +++ b/projectq/backends/_ibm/__init__.py @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module for supporting the IBM QE platform.""" + from ._ibm import IBMBackend diff --git a/projectq/backends/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index 1bae69722..2a11aa5a7 100755 --- a/projectq/backends/_ibm/_ibm.py +++ b/projectq/backends/_ibm/_ibm.py @@ -12,41 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Back-end to run quantum program on IBM's Quantum Experience.""" +"""Back-end to run quantum program on IBM's Quantum Experience.""" +import math import random -import json from projectq.cengines import BasicEngine -from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import (NOT, - Y, - Z, - T, - Tdag, - S, - Sdag, - H, - Rx, - Ry, - Rz, - Measure, - Allocate, - Deallocate, - Barrier, - FlushGate) - -from ._ibm_http_client import send, retrieve - - -class IBMBackend(BasicEngine): +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control +from projectq.ops import ( + NOT, + Allocate, + Barrier, + Deallocate, + FlushGate, + H, + Measure, + Rx, + Ry, + Rz, +) +from projectq.types import WeakQubitRef + +from .._exceptions import InvalidCommandError +from ._ibm_http_client import retrieve, send + + +class IBMBackend(BasicEngine): # pylint: disable=too-many-instance-attributes """ - The IBM Backend class, which stores the circuit, transforms it to JSON - QASM, and sends the circuit through the IBM API. + Define the compiler engine class that handles interactions with the IBM API. + + The IBM Backend class, which stores the circuit, transforms it to JSON, and sends the circuit through the IBM API. """ - def __init__(self, use_hardware=False, num_runs=1024, verbose=False, - user=None, password=None, device='ibmqx4', - retrieve_execution=None): + + def __init__( + self, + use_hardware=False, + num_runs=1024, + verbose=False, + token='', + device='ibmq_essex', + num_retries=3000, + interval=1, + retrieve_execution=None, + ): # pylint: disable=too-many-arguments """ Initialize the Backend object. @@ -58,25 +66,31 @@ def __init__(self, use_hardware=False, num_runs=1024, verbose=False, verbose (bool): If True, statistics are printed, in addition to the measurement result being registered (at the end of the circuit). - user (string): IBM Quantum Experience user name - password (string): IBM Quantum Experience password - device (string): Device to use ('ibmqx4', or 'ibmqx5') - if use_hardware is set to True. Default is ibmqx4. + token (str): IBM quantum experience user password. + device (str): name of the IBM device to use. ibmq_essex By default + num_retries (int): Number of times to retry to obtain + results from the IBM API. (default is 3000) + interval (float, int): Number of seconds between successive + attempts to obtain results from the IBM API. + (default is 1) retrieve_execution (int): Job ID to retrieve instead of re- running the circuit (e.g., if previous run timed out). """ - BasicEngine.__init__(self) + super().__init__() + self._clear = False self._reset() if use_hardware: self.device = device else: - self.device = 'simulator' + self.device = 'ibmq_qasm_simulator' self._num_runs = num_runs self._verbose = verbose - self._user = user - self._password = password - self._probabilities = dict() + self._token = token + self._num_retries = num_retries + self._interval = interval + self._probabilities = {} self.qasm = "" + self._json = [] self._measured_ids = [] self._allocated_qubits = set() self._retrieve_execution = retrieve_execution @@ -85,30 +99,37 @@ def is_available(self, cmd): """ Return true if the command can be executed. - The IBM quantum chip can do X, Y, Z, T, Tdag, S, Sdag, - rotation gates, barriers, and CX / CNOT. + The IBM quantum chip can only do U1,U2,U3,barriers, and CX / CNOT. + Conversion implemented for Rotation gates and H gates. Args: cmd (Command): Command for which to check availability """ - g = cmd.gate - if g == NOT and get_control_count(cmd) <= 1: - return True + if has_negative_control(cmd): + return False + + gate = cmd.gate + + if get_control_count(cmd) == 1: + return gate == NOT if get_control_count(cmd) == 0: - if g in (T, Tdag, S, Sdag, H, Y, Z): - return True - if isinstance(g, (Rx, Ry, Rz)): - return True - if g in (Measure, Allocate, Deallocate, Barrier): - return True + return gate == H or isinstance(gate, (Rx, Ry, Rz)) or gate in (Measure, Allocate, Deallocate, Barrier) return False + def get_qasm(self): + """ + Return the QASM representation of the circuit sent to the backend. + + Should be called AFTER calling the ibm device. + """ + return self.qasm + def _reset(self): - """ Reset all temporary variables (after flush gate). """ + """Reset all temporary variables (after flush gate).""" self._clear = True self._measured_ids = [] - def _store(self, cmd): + def _store(self, cmd): # pylint: disable=too-many-branches,too-many-statements """ Temporarily store the command cmd. @@ -117,14 +138,17 @@ def _store(self, cmd): Args: cmd: Command to store """ + if self.main_engine.mapper is None: + raise RuntimeError('No mapper is present in the compiler engine list!') + if self._clear: - self._probabilities = dict() + self._probabilities = {} self._clear = False self.qasm = "" + self._json = [] self._allocated_qubits = set() gate = cmd.gate - if gate == Allocate: self._allocated_qubits.add(cmd.qubits[0][0].id) return @@ -133,42 +157,49 @@ def _store(self, cmd): return if gate == Measure: - assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 - qb_id = cmd.qubits[0][0].id logical_id = None - for t in cmd.tags: - if isinstance(t, LogicalQubitIDTag): - logical_id = t.logical_qubit_id + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + logical_id = tag.logical_qubit_id break - assert logical_id is not None + if logical_id is None: + raise RuntimeError('No LogicalQubitIDTag found in command!') self._measured_ids += [logical_id] elif gate == NOT and get_control_count(cmd) == 1: ctrl_pos = cmd.control_qubits[0].id qb_pos = cmd.qubits[0][0].id - self.qasm += "\ncx q[{}], q[{}];".format(ctrl_pos, qb_pos) + self.qasm += f"\ncx q[{ctrl_pos}], q[{qb_pos}];" + self._json.append({'qubits': [ctrl_pos, qb_pos], 'name': 'cx'}) elif gate == Barrier: qb_pos = [qb.id for qr in cmd.qubits for qb in qr] self.qasm += "\nbarrier " qb_str = "" for pos in qb_pos: - qb_str += "q[{}], ".format(pos) - self.qasm += qb_str[:-2] + ";" + qb_str += f"q[{pos}], " + self.qasm += f"{qb_str[:-2]};" + self._json.append({'qubits': qb_pos, 'name': 'barrier'}) elif isinstance(gate, (Rx, Ry, Rz)): - assert get_control_count(cmd) == 0 qb_pos = cmd.qubits[0][0].id - u_strs = {'Rx': 'u3({}, -pi/2, pi/2)', 'Ry': 'u3({}, 0, 0)', - 'Rz': 'u1({})'} - gate = u_strs[str(gate)[0:2]].format(gate.angle) - self.qasm += "\n{} q[{}];".format(gate, qb_pos) - else: - assert get_control_count(cmd) == 0 - if str(gate) in self._gate_names: - gate_str = self._gate_names[str(gate)] - else: - gate_str = str(gate).lower() - + u_strs = {'Rx': 'u3({}, -pi/2, pi/2)', 'Ry': 'u3({}, 0, 0)', 'Rz': 'u1({})'} + u_name = {'Rx': 'u3', 'Ry': 'u3', 'Rz': 'u1'} + u_angle = { + 'Rx': [gate.angle, -math.pi / 2, math.pi / 2], + 'Ry': [gate.angle, 0, 0], + 'Rz': [gate.angle], + } + gate_qasm = u_strs[str(gate)[0:2]].format(gate.angle) + gate_name = u_name[str(gate)[0:2]] + params = u_angle[str(gate)[0:2]] + self.qasm += f"\n{gate_qasm} q[{qb_pos}];" + self._json.append({'qubits': [qb_pos], 'name': gate_name, 'params': params}) + elif gate == H: qb_pos = cmd.qubits[0][0].id - self.qasm += "\n{} q[{}];".format(gate_str, qb_pos) + self.qasm += f"\nu2(0,pi/2) q[{qb_pos}];" + self._json.append({'qubits': [qb_pos], 'name': 'u2', 'params': [0, 3.141592653589793]}) + else: + raise InvalidCommandError( + 'Command not authorized. You should run the circuit with the appropriate ibm setup.' + ) def _logical_to_physical(self, qb_id): """ @@ -178,22 +209,23 @@ def _logical_to_physical(self, qb_id): qb_id (int): ID of the logical qubit whose position should be returned. """ - assert self.main_engine.mapper is not None mapping = self.main_engine.mapper.current_mapping if qb_id not in mapping: - raise RuntimeError("Unknown qubit id {}. Please make sure " - "eng.flush() was called and that the qubit " - "was eliminated during optimization." - .format(qb_id)) + raise RuntimeError( + f"Unknown qubit id {qb_id}. " + "Please make sure eng.flush() was called and that the qubit was eliminated during optimization." + ) return mapping[qb_id] def get_probabilities(self, qureg): """ - Return the list of basis states with corresponding probabilities. + Return the probability of the outcome `bit_string` when measuring the quantum register `qureg`. + + Return the list of basis states with corresponding probabilities. If input qureg is a subset of the register + used for the experiment, then returns the projected probabilities over the other states. - The measured bits are ordered according to the supplied quantum - register, i.e., the left-most bit in the state-string corresponds to - the first qubit in the supplied quantum register. + The measured bits are ordered according to the supplied quantum register, i.e., the left-most bit in the + state-string corresponds to the first qubit in the supplied quantum register. Warning: Only call this function after the circuit has been executed! @@ -203,99 +235,104 @@ def get_probabilities(self, qureg): qubits. Returns: - probability_dict (dict): Dictionary mapping n-bit strings to - probabilities. + probability_dict (dict): Dictionary mapping n-bit strings to probabilities. Raises: - RuntimeError: If no data is available (i.e., if the circuit has - not been executed). Or if a qubit was supplied which was not - present in the circuit (might have gotten optimized away). + RuntimeError: If no data is available (i.e., if the circuit has not been executed). Or if a qubit was + supplied which was not present in the circuit (might have gotten optimized away). """ if len(self._probabilities) == 0: raise RuntimeError("Please, run the circuit first!") - probability_dict = dict() - - for state in self._probabilities: + probability_dict = {} + for state, probability in self._probabilities.items(): mapped_state = ['0'] * len(qureg) - for i in range(len(qureg)): - mapped_state[i] = state[self._logical_to_physical(qureg[i].id)] - probability = self._probabilities[state] - probability_dict["".join(mapped_state)] = probability - + for i, val in enumerate(qureg): + mapped_state[i] = state[self._logical_to_physical(val.id)] + mapped_state = "".join(mapped_state) + if mapped_state not in probability_dict: + probability_dict[mapped_state] = probability + else: + probability_dict[mapped_state] += probability return probability_dict - def _run(self): + def _run(self): # pylint: disable=too-many-locals """ Run the circuit. - Send the circuit via the IBM API (JSON QASM) using the provided user - data / ask for username & password. + Send the circuit via a non documented IBM API (using JSON written + circuits) using the provided user data / ask for the user token. """ - if self.qasm == "": - return # finally: add measurements (no intermediate measurements are allowed) for measured_id in self._measured_ids: qb_loc = self.main_engine.mapper.current_mapping[measured_id] - self.qasm += "\nmeasure q[{}] -> c[{}];".format(qb_loc, - qb_loc) - - max_qubit_id = max(self._allocated_qubits) - qasm = ("\ninclude \"qelib1.inc\";\nqreg q[{nq}];\ncreg c[{nq}];" + - self.qasm).format(nq=max_qubit_id + 1) + self.qasm += f"\nmeasure q[{qb_loc}] -> c[{qb_loc}];" + self._json.append({'qubits': [qb_loc], 'name': 'measure', 'memory': [qb_loc]}) + # return if no operations / measurements have been performed. + if self.qasm == "": + return + max_qubit_id = max(self._allocated_qubits) + 1 info = {} - info['qasms'] = [{'qasm': qasm}] + info['json'] = self._json + info['nq'] = max_qubit_id + info['shots'] = self._num_runs - info['maxCredits'] = 5 + info['maxCredits'] = 10 info['backend'] = {'name': self.device} - info = json.dumps(info) - try: if self._retrieve_execution is None: - res = send(info, device=self.device, - user=self._user, password=self._password, - shots=self._num_runs, verbose=self._verbose) + res = send( + info, + device=self.device, + token=self._token, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) else: - res = retrieve(device=self.device, user=self._user, - password=self._password, - jobid=self._retrieve_execution) - + res = retrieve( + device=self.device, + token=self._token, + jobid=self._retrieve_execution, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) counts = res['data']['counts'] # Determine random outcome - P = random.random() - p_sum = 0. + random_outcome = random.random() + p_sum = 0.0 measured = "" for state in counts: - probability = counts[state] * 1. / self._num_runs - state = list(reversed(state)) - state = "".join(state) + probability = counts[state] * 1.0 / self._num_runs + state = f"{int(state, 0):b}" + state = state.zfill(max_qubit_id) + # states in ibmq are right-ordered, so need to reverse state string + state = state[::-1] p_sum += probability star = "" - if p_sum >= P and measured == "": + if p_sum >= random_outcome and measured == "": measured = state star = "*" self._probabilities[state] = probability if self._verbose and probability > 0: - print(str(state) + " with p = " + str(probability) + - star) - - class QB(): - def __init__(self, ID): - self.id = ID + print(f"{str(state)} with p = {probability}{star}") - # register measurement result - for ID in self._measured_ids: - location = self._logical_to_physical(ID) + # register measurement result from IBM + for qubit_id in self._measured_ids: + location = self._logical_to_physical(qubit_id) result = int(measured[location]) - self.main_engine.set_measurement_result(QB(ID), result) + self.main_engine.set_measurement_result(WeakQubitRef(self, qubit_id), result) self._reset() - except TypeError: - raise Exception("Failed to run the circuit. Aborting.") + except TypeError as err: + raise Exception("Failed to run the circuit. Aborting.") from err def receive(self, command_list): """ - Receives a command list and, for each command, stores it until - completion. + Receive a list of commands. + + Receive a command list and, for each command, stores it until completion. Upon flush, send the data to the + IBM QE API. Args: command_list: List of commands to execute @@ -306,9 +343,3 @@ def receive(self, command_list): else: self._run() self._reset() - - """ - Mapping of gate names from our gate objects to the IBM QASM representation. - """ - _gate_names = {str(Tdag): "tdg", - str(Sdag): "sdg"} diff --git a/projectq/backends/_ibm/_ibm_http_client.py b/projectq/backends/_ibm/_ibm_http_client.py index 83b054fcd..27602984b 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -12,79 +12,376 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Back-end to run quantum program on IBM QE cloud platform.""" + + # helpers to run the jsonified gate sequence on ibm quantum experience server -# api documentation is at https://qcwi-staging.mybluemix.net/explorer/ -import requests +# api documentation does not exist and has to be deduced from the qiskit code +# source at: https://github.com/Qiskit/qiskit-ibmq-provider + import getpass -import json -import sys +import signal import time +import uuid + +import requests +from requests import Session from requests.compat import urljoin +from .._exceptions import DeviceOfflineError, DeviceTooSmall + +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' + +# TODO: call to get the API version automatically +CLIENT_APPLICATION = 'ibmqprovider/0.4.4' + + +class IBMQ(Session): + """Manage a session between ProjectQ and the IBMQ web API.""" + + def __init__(self, **kwargs): + """Initialize a session with the IBM QE's APIs.""" + super().__init__(**kwargs) + self.backends = {} + self.timeout = 5.0 + + def get_list_devices(self, verbose=False): + """ + Get the list of available IBM backends with their properties. + + Args: + verbose (bool): print the returned dictionary if True + + Returns: + (dict) backends dictionary by name device, containing the qubit size 'nq', the coupling map 'coupling_map' + as well as the device version 'version' + """ + list_device_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + argument = {'allow_redirects': True, 'timeout': (self.timeout, None)} + request = super().get(urljoin(_API_URL, list_device_url), **argument) + request.raise_for_status() + r_json = request.json() + self.backends = {} + for obj in r_json: + self.backends[obj['backend_name']] = { + 'nq': obj['n_qubits'], + 'coupling_map': obj['coupling_map'], + 'version': obj['backend_version'], + } + + if verbose: + print('- List of IBMQ devices available:') + print(self.backends) + return self.backends + + def is_online(self, device): + """ + Check if the device is in the list of available IBM backends. + + Args: + device (str): name of the device to check + + Returns: + (bool) True if device is available, False otherwise + """ + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Check if the device is big enough to run the code. + + Args: + info (dict): dictionary sent by the backend containing the code to run + device (str): name of the ibm device to use + + Returns: + (tuple): (bool) True if device is big enough, False otherwise + (int) maximum number of qubit available on the device + (int) number of qubit needed for the circuit + + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed + + def authenticate(self, token=None): + """ + Authenticate with IBM's Web API. -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' + Args: + token (str): IBM quantum experience user API token. + """ + if token is None: + token = getpass.getpass(prompt="IBM QE token > ") + if len(token) == 0: + raise Exception('Error with the IBM QE token') + self.headers.update({'X-Qx-Client-Application': CLIENT_APPLICATION}) + args = { + 'data': None, + 'json': {'apiToken': token}, + 'timeout': (self.timeout, None), + } + request = super().post(_AUTH_API_URL, **args) + request.raise_for_status() + r_json = request.json() + self.params.update({'access_token': r_json['id']}) + def run(self, info, device): # pylint: disable=too-many-locals + """ + Run the quantum code to the IBMQ machine. -class DeviceOfflineError(Exception): - pass + Update since September 2020: only protocol available is what they call 'object storage' where a job request + via the POST method gets in return a url link to which send the json data. A final http validates the data + communication. + Args: + info (dict): dictionary sent by the backend containing the code to run + device (str): name of the ibm device to use -def is_online(device): - url = 'Backends/{}/queue/status'.format(device) - r = requests.get(urljoin(_api_url, url)) - return r.json()['state'] + Returns: + (tuple): (str) Execution Id + """ + # STEP1: Obtain most of the URLs for handling communication with + # quantum device + json_step1 = { + 'data': None, + 'json': { + 'backend': {'name': device}, + 'allowObjectStorage': True, + 'shareLevel': 'none', + }, + 'timeout': (self.timeout, None), + } + request = super().post( + urljoin(_API_URL, 'Network/ibm-q/Groups/open/Projects/main/Jobs'), + **json_step1, + ) + request.raise_for_status() + r_json = request.json() + upload_url = r_json['objectStorageInfo']['uploadUrl'] + execution_id = r_json['id'] + # STEP2: WE UPLOAD THE CIRCUIT DATA + n_classical_reg = info['nq'] + # hack: easier to restrict labels to measured qubits + n_qubits = n_classical_reg # self.backends[device]['nq'] + instructions = info['json'] + maxcredit = info['maxCredits'] + c_label = [["c", i] for i in range(n_classical_reg)] + q_label = [["q", i] for i in range(n_qubits)] + + # hack: the data value in the json quantum code is a string + instruction_str = str(instructions).replace('\'', '\"') + data = '{"qobj_id": "' + str(uuid.uuid4()) + '", ' + data += '"header": {"backend_name": "' + device + '", ' + data += '"backend_version": "' + self.backends[device]['version'] + '"}, ' + data += '"config": {"shots": ' + str(info['shots']) + ', ' + data += '"max_credits": ' + str(maxcredit) + ', "memory": false, ' + data += '"parameter_binds": [], "memory_slots": ' + str(n_classical_reg) + data += ', "n_qubits": ' + str(n_qubits) + '}, "schema_version": "1.2.0", ' + data += '"type": "QASM", "experiments": [{"config": ' + data += '{"n_qubits": ' + str(n_qubits) + ', ' + data += '"memory_slots": ' + str(n_classical_reg) + '}, ' + data += '"header": {"qubit_labels": ' + str(q_label).replace('\'', '\"') + ', ' + data += '"n_qubits": ' + str(n_classical_reg) + ', ' + data += '"qreg_sizes": [["q", ' + str(n_qubits) + ']], ' + data += '"clbit_labels": ' + str(c_label).replace('\'', '\"') + ', ' + data += '"memory_slots": ' + str(n_classical_reg) + ', ' + data += '"creg_sizes": [["c", ' + str(n_classical_reg) + ']], ' + data += '"name": "circuit0", "global_phase": 0}, "instructions": ' + instruction_str + '}]}' + + json_step2 = { + 'data': data, + 'params': {'access_token': None}, + 'timeout': (5.0, None), + } + request = super().put(upload_url, **json_step2) + request.raise_for_status() + + # STEP3: CONFIRM UPLOAD + json_step3 = {'data': None, 'json': None, 'timeout': (self.timeout, None)} + + upload_data_url = urljoin( + _API_URL, + 'Network/ibm-q/Groups/open/Projects/main/Jobs/' + str(execution_id) + '/jobDataUploaded', + ) + request = super().post(upload_data_url, **json_step3) + request.raise_for_status() + + return execution_id + + def get_result( + self, device, execution_id, num_retries=3000, interval=1, verbose=False + ): # pylint: disable=too-many-arguments,too-many-locals + """Get the result of an execution.""" + job_status_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs/' + execution_id + + if verbose: + print(f"Waiting for results. [Job ID: {execution_id}]") -def retrieve(device, user, password, jobid): + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): # pragma: no cover + raise Exception(f"Interrupted. The ID of your submitted job is {execution_id}.") + + try: + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + for retries in range(num_retries): + # STEP5: WAIT FOR THE JOB TO BE RUN + json_step5 = {'allow_redirects': True, 'timeout': (self.timeout, None)} + request = super().get(urljoin(_API_URL, job_status_url), **json_step5) + request.raise_for_status() + r_json = request.json() + acceptable_status = ['VALIDATING', 'VALIDATED', 'RUNNING'] + if r_json['status'] == 'COMPLETED': + # STEP6: Get the endpoint to get the result + json_step6 = { + 'allow_redirects': True, + 'timeout': (self.timeout, None), + } + request = super().get( + urljoin(_API_URL, job_status_url + '/resultDownloadUrl'), + **json_step6, + ) + request.raise_for_status() + r_json = request.json() + + # STEP7: Get the result + json_step7 = { + 'allow_redirects': True, + 'params': {'access_token': None}, + 'timeout': (self.timeout, None), + } + request = super().get(r_json['url'], **json_step7) + r_json = request.json() + result = r_json['results'][0] + + # STEP8: Confirm the data was downloaded + json_step8 = {'data': None, 'json': None, 'timeout': (5.0, None)} + request = super().post( + urljoin(_API_URL, job_status_url + '/resultDownloaded'), + **json_step8, + ) + r_json = request.json() + return result + + # Note: if stays stuck if 'Validating' mode, then sthg went + # wrong in step 3 + if r_json['status'] not in acceptable_status: + raise Exception(f"Error while running the code. Last status: {r_json['status']}.") + time.sleep(interval) + if self.is_online(device) and retries % 60 == 0: + self.get_list_devices() + if not self.is_online(device): + raise DeviceOfflineError( + f"Device went offline. The ID of your submitted job is {execution_id}." + ) + + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise Exception(f"Timeout. The ID of your submitted job is {execution_id}.") + + +def show_devices(token=None, verbose=False): """ - Retrieves a previously run job by its ID. + Access the list of available devices and their properties (ex: for setup configuration). + + Args: + token (str): IBM quantum experience user API token. + verbose (bool): If True, additional information is printed + + Returns: + (list) list of available devices and their properties + """ + ibmq_session = IBMQ() + ibmq_session.authenticate(token=token) + return ibmq_session.get_list_devices(verbose=verbose) + + +def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): # pylint: disable=too-many-arguments + """ + Retrieve a previously run job by its ID. Args: device (str): Device on which the code was run / is running. - user (str): IBM quantum experience user (e-mail) - password (str): IBM quantum experience password + token (str): IBM quantum experience user API token. jobid (str): Id of the job to retrieve + + Returns: + (dict) result form the IBMQ server """ - user_id, access_token = _authenticate(user, password) - res = _get_result(device, jobid, access_token) + ibmq_session = IBMQ() + ibmq_session.authenticate(token) + ibmq_session.get_list_devices(verbose) + res = ibmq_session.get_result(device, jobid, num_retries=num_retries, interval=interval, verbose=verbose) return res -def send(info, device='sim_trivial_2', user=None, password=None, - shots=1, verbose=False): +def send( + info, + device='ibmq_qasm_simulator', + token=None, + shots=None, + num_retries=3000, + interval=1, + verbose=False, +): # pylint: disable=too-many-arguments """ - Sends QASM through the IBM API and runs the quantum circuit. + Send QASM through the IBM API and runs the quantum circuit. Args: - info: Contains QASM representation of the circuit to run. - device (str): Either 'simulator', 'ibmqx4', or 'ibmqx5'. - user (str): IBM quantum experience user. - password (str): IBM quantum experience user password. + info(dict): Contains representation of the circuit to run. + device (str): name of the ibm device. Simulator chosen by default + token (str): IBM quantum experience user API token. shots (int): Number of runs of the same circuit to collect statistics. - verbose (bool): If True, additional information is printed, such as - measurement statistics. Otherwise, the backend simply registers - one measurement result (same behavior as the projectq Simulator). - """ - try: - # check if the device is online - if device in ['ibmqx4', 'ibmqx5']: - online = is_online(device) + verbose (bool): If True, additional information is printed, such as measurement statistics. Otherwise, the + backend simply registers one measurement result (same behavior as the projectq Simulator). - if not online: - print("The device is offline (for maintenance?). Use the " - "simulator instead or try again later.") - raise DeviceOfflineError("Device is offline.") + Returns: + (dict) result form the IBMQ server + """ + try: + ibmq_session = IBMQ() + # Shots argument deprecated, as already + if shots is not None: + info['shots'] = shots if verbose: print("- Authenticating...") - user_id, access_token = _authenticate(user, password) + if token is not None: + print('user API token: ' + token) + ibmq_session.authenticate(token) + + # check if the device is online + ibmq_session.get_list_devices(verbose) + online = ibmq_session.is_online(device) + if not online: + print("The device is offline (for maintenance?). Use the simulator instead or try again later.") + raise DeviceOfflineError("Device is offline.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = ibmq_session.can_run_experiment(info, device) + if not runnable: + print( + f"The device is too small ({qmax} qubits available) for the code " + f"requested({qneeded} qubits needed) Try to look for another device with more qubits" + ) + raise DeviceTooSmall("Device is too small.") if verbose: - print("- Running code: {}".format( - json.loads(info)['qasms'][0]['qasm'])) - execution_id = _run(info, device, user_id, access_token, shots) + print(f"- Running code: {info}") + execution_id = ibmq_session.run(info, device) if verbose: print("- Waiting for results...") - res = _get_result(device, execution_id, access_token) + res = ibmq_session.get_result( + device, + execution_id, + num_retries=num_retries, + interval=interval, + verbose=verbose, + ) if verbose: print("- Done.") return res @@ -97,78 +394,4 @@ def send(info, device='sim_trivial_2', user=None, password=None, except KeyError as err: print("- Failed to parse response:") print(err) - - -def _authenticate(email=None, password=None): - """ - :param email: - :param password: - :return: - """ - if email is None: - try: - input_fun = raw_input - except NameError: - input_fun = input - email = input_fun('IBM QE user (e-mail) > ') - if password is None: - password = getpass.getpass(prompt='IBM QE password > ') - - r = requests.post(urljoin(_api_url, 'users/login'), - data={"email": email, "password": password}) - r.raise_for_status() - - json_data = r.json() - user_id = json_data['userId'] - access_token = json_data['id'] - - return user_id, access_token - - -def _run(qasm, device, user_id, access_token, shots): - suffix = 'Jobs' - - r = requests.post(urljoin(_api_url, suffix), - data=qasm, - params={"access_token": access_token, - "deviceRunType": device, - "fromCache": "false", - "shots": shots}, - headers={"Content-Type": "application/json"}) - r.raise_for_status() - - r_json = r.json() - execution_id = r_json["id"] - return execution_id - - -def _get_result(device, execution_id, access_token, num_retries=3000, - interval=1): - suffix = 'Jobs/{execution_id}'.format(execution_id=execution_id) - status_url = urljoin(_api_url, 'Backends/{}/queue/status'.format(device)) - - print("Waiting for results. [Job ID: {}]".format(execution_id)) - - for retries in range(num_retries): - r = requests.get(urljoin(_api_url, suffix), - params={"access_token": access_token}) - r.raise_for_status() - - r_json = r.json() - if 'qasms' in r_json: - qasm = r_json['qasms'][0] - if 'result' in qasm: - return qasm['result'] - time.sleep(interval) - if device in ['ibmqx4', 'ibmqx5'] and retries % 60 == 0: - r = requests.get(status_url) - r_json = r.json() - if 'state' in r_json and not r_json['state']: - raise DeviceOfflineError("Device went offline. The ID of your " - "submitted job is {}." - .format(execution_id)) - if 'lengthQueue' in r_json: - print("Currently there are {} jobs queued for execution on {}." - .format(r_json['lengthQueue'], device)) - raise Exception("Timeout. The ID of your submitted job is {}." - .format(execution_id)) + return None diff --git a/projectq/backends/_ibm/_ibm_http_client_test.py b/projectq/backends/_ibm/_ibm_http_client_test.py index 6162fa618..655a698e3 100755 --- a/projectq/backends/_ibm/_ibm_http_client_test.py +++ b/projectq/backends/_ibm/_ibm_http_client_test.py @@ -11,10 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm_http_client._ibm.py.""" -import json import pytest import requests from requests.compat import urljoin @@ -28,24 +26,23 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' +_API_URL = 'https://api.quantum-computing.ibm.com/api/' +_AUTH_API_URL = 'https://auth.quantum-computing.ibm.com/api/users/loginWithToken' def test_send_real_device_online_verbose(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) - name = 'projectq_test' + json_qasm = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } + token = '12345' access_token = "access" user_id = 2016 - code_id = 11 - name_item = '"name":"{name}", "jsonQASM":'.format(name=name) - json_body = ''.join([name_item, json_qasm]) - json_data = ''.join(['{', json_body, '}']) shots = 1 - device = "ibmqx4" - json_data_run = ''.join(['{"qasm":', json_qasm, '}']) - execution_id = 3 + execution_id = '3' result_ready = [False] result = "my_result" request_num = [0] # To assert correct order of calls @@ -70,24 +67,76 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if (args[0] == urljoin(_api_url_status, status_url) and - (request_num[0] == 0 or request_num[0] == 3)): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url) and (request_num[0] == 1 or request_num[0] == 6): + request_num[0] += 1 + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return MockResponse( + [ + { + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + # STEP2 + elif args[1] == f"/{execution_id}/jobUploadUrl" and request_num[0] == 3: request_num[0] += 1 - return MockResponse({"state": True}, 200) - # Getting result - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and not - result_ready[0] and request_num[0] == 3): + return MockResponse({"url": "s3_url"}, 200) + # STEP5 + elif ( + args[1] + == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}", + ) + and not result_ready[0] + and request_num[0] == 5 + ): result_ready[0] = True - return MockResponse({"status": {"id": "NotDone"}}, 200) - elif (args[0] == urljoin(_api_url, - "Jobs/{execution_id}".format(execution_id=execution_id)) and - kwargs["params"]["access_token"] == access_token and - result_ready[0] and request_num[0] == 4): - print("state ok") - return MockResponse({"qasms": [{"result": result}]}, 200) + request_num[0] += 1 + return MockResponse({"status": "RUNNING"}, 200) + elif ( + args[1] + == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}", + ) + and result_ready[0] + and request_num[0] == 7 + ): + request_num[0] += 1 + return MockResponse({"status": "COMPLETED"}, 200) + # STEP6 + elif ( + args[1] + == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}/resultDownloadUrl", + ) + and request_num[0] == 8 + ): + request_num[0] += 1 + return MockResponse({"url": "result_download_url"}, 200) + # STEP7 + elif args[1] == "result_download_url" and request_num[0] == 9: + request_num[0] += 1 + return MockResponse({"results": [result]}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -107,49 +156,103 @@ def json(self): def raise_for_status(self): pass + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' # Authentication - if (args[0] == urljoin(_api_url, "users/login") and - kwargs["data"]["email"] == email and - kwargs["data"]["password"] == password and - request_num[0] == 1): + if args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token and request_num[0] == 0: request_num[0] += 1 return MockPostResponse({"userId": user_id, "id": access_token}) - # Run code - elif (args[0] == urljoin(_api_url, "Jobs") and - kwargs["data"] == json_qasm and - kwargs["params"]["access_token"] == access_token and - kwargs["params"]["deviceRunType"] == device and - kwargs["params"]["fromCache"] == "false" and - kwargs["params"]["shots"] == shots and - kwargs["headers"]["Content-Type"] == "application/json" and - request_num[0] == 2): + # STEP1 + elif args[1] == urljoin(_API_URL, jobs_url) and request_num[0] == 2: + request_num[0] += 1 + answer1 = { + 'objectStorageInfo': { + 'downloadQObjectUrlEndpoint': 'url_dld_endpoint', + 'uploadQobjectUrlEndpoint': f"/{execution_id}/jobUploadUrl", + 'uploadUrl': 'url_upld', + }, + 'id': execution_id, + } + return MockPostResponse(answer1, 200) + + # STEP4 + elif args[1] == urljoin(_API_URL, f"{jobs_url}/{execution_id}/jobDataUploaded") and request_num[0] == 4: + request_num[0] += 1 + return MockPostResponse({}, 200) + + # STEP8 + elif ( + args[1] + == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}/resultDownloaded", + ) + and request_num[0] == 10 + ): request_num[0] += 1 - return MockPostResponse({"id": execution_id}) + return MockPostResponse({}, 200) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) - # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, url=""): + self.url = url + + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + self.request = MockRequest() + self.text = "" + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # STEP3 + if args[1] == "url_upld" and request_num[0] == 3: + request_num[0] += 1 + return MockResponse({}, 200) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) # Code to test: - res = _ibm_http_client.send(json_qasm, - device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) - print(res) + res = _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) + assert res == result + json_qasm['nq'] = 40 + request_num[0] = 0 + with pytest.raises(_ibm_http_client.DeviceTooSmall): + res = _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) + + +def test_no_password_given(monkeypatch): + token = '' + json_qasm = '' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + + with pytest.raises(Exception): + _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=1, verbose=True) def test_send_real_device_offline(monkeypatch): + token = '12345' + access_token = "access" + user_id = 2016 + def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code): @@ -159,22 +262,56 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": False}, 200) - monkeypatch.setattr("requests.get", mocked_requests_get) + def raise_for_status(self): + pass + + # Accessing status of device. Return offline. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse({}, 200) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token: + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + shots = 1 - json_qasm = "my_json_qasm" - name = 'projectq_test' + token = '12345' + json_qasm = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } with pytest.raises(_ibm_http_client.DeviceOfflineError): - _ibm_http_client.send(json_qasm, - device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + _ibm_http_client.send(json_qasm, device="ibmqx4", token=token, shots=shots, verbose=True) -def test_send_that_errors_are_caught(monkeypatch): +def test_show_device(monkeypatch): + access_token = "access" + user_id = 2016 + class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data @@ -183,124 +320,193 @@ def __init__(self, json_data, status_code): def json(self): return self.json_data + def raise_for_status(self): + pass + def mocked_requests_get(*args, **kwargs): # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return MockResponse( + [ + { + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + + def mocked_requests_post(*args, **kwargs): + class MockRequest: + def __init__(self, body="", url=""): + self.body = body + self.url = url + + class MockPostResponse: + def __init__(self, json_data, text=" "): + self.json_data = json_data + self.text = text + self.request = MockRequest() + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # Authentication + if args[1] == _AUTH_API_URL and kwargs["json"]["apiToken"] == token: + return MockPostResponse({"userId": user_id, "id": access_token}) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + # Patch login data + token = '12345' + + def user_password_input(prompt): + if prompt == "IBM QE token > ": + return token + + monkeypatch.setattr("getpass.getpass", user_password_input) + assert _ibm_http_client.show_devices() == { + 'ibmqx4': { + 'coupling_map': { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + }, + 'version': '0.1.547', + 'nq': 32, + } + } + + +def test_send_that_errors_are_caught(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.HTTPError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" - name = 'projectq_test' - _ibm_http_client.send(json_qasm, - device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + json_qasm = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } + _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) + token = '' + with pytest.raises(Exception): + _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) -def test_send_that_errors_are_caught2(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - def json(self): - return self.json_data - - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) +def test_send_that_errors_are_caught2(monkeypatch): + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise requests.exceptions.RequestException - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" - name = 'projectq_test' - _ibm_http_client.send(json_qasm, - device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + json_qasm = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } + _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) def test_send_that_errors_are_caught3(monkeypatch): - def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url_status, status_url): - return MockResponse({"state": True}, 200) + class MockResponse: + def __init__(self, json_data, status_code): + pass def mocked_requests_post(*args, **kwargs): # Test that this error gets caught raise KeyError - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) # Patch login data - password = 12345 - email = "test@projectq.ch" - monkeypatch.setitem(__builtins__, "input", lambda x: email) - monkeypatch.setitem(__builtins__, "raw_input", lambda x: email) + token = '12345' def user_password_input(prompt): - if prompt == "IBM QE password > ": - return password + if prompt == "IBM QE token > ": + return token monkeypatch.setattr("getpass.getpass", user_password_input) shots = 1 - json_qasm = "my_json_qasm" - name = 'projectq_test' - _ibm_http_client.send(json_qasm, - device="ibmqx4", - user=None, password=None, - shots=shots, verbose=True) + json_qasm = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } + _ibm_http_client.send(json_qasm, device="ibmqx4", token=None, shots=shots, verbose=True) def test_timeout_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) + qasms = { + 'qasms': [{'qasm': 'my qasm'}], + 'shots': 1, + 'json': 'instructions', + 'maxCredits': 10, + 'nq': 1, + } + json_qasm = qasms tries = [0] + execution_id = '3' def mocked_requests_get(*args, **kwargs): class MockResponse: @@ -314,14 +520,47 @@ def json(self): def raise_for_status(self): pass - # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + # Accessing status of device. Return device info. + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return MockResponse( + [ + { + 'backend_name': 'ibmqx4', + 'coupling_map': connections, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + job_url = f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}" + if args[1] == urljoin(_API_URL, job_url): tries[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse({"status": "RUNNING"}, 200) + + # STEP2 + elif args[1] == f"/{execution_id}/jobUploadUrl": + return MockResponse({"url": "s3_url"}, 200) + # STEP5 + elif args[1] == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}", + ): + return MockResponse({"status": "RUNNING"}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -340,27 +579,67 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + jobs_url = 'Network/ibm-q/Groups/open/Projects/main/Jobs' + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - if args[0] == urljoin(_api_url, 'Jobs'): - return MockPostResponse({"id": "123e"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + # STEP1 + elif args[1] == urljoin(_API_URL, jobs_url): + answer1 = { + 'objectStorageInfo': { + 'downloadQObjectUrlEndpoint': 'url_dld_endpoint', + 'uploadQobjectUrlEndpoint': f"/{execution_id}/jobUploadUrl", + 'uploadUrl': 'url_upld', + }, + 'id': execution_id, + } + return MockPostResponse(answer1, 200) + + # STEP4 + elif args[1] == urljoin(_API_URL, f"{jobs_url}/{execution_id}/jobDataUploaded"): + return MockPostResponse({}, 200) + + def mocked_requests_put(*args, **kwargs): + class MockRequest: + def __init__(self, url=""): + self.url = url + + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + self.request = MockRequest() + self.text = "" + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + # STEP3 + if args[1] == "url_upld": + return MockResponse({}, 200) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.put", mocked_requests_put) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(Exception) as excinfo: - _ibm_http_client.send(json_qasm, - device="ibmqx4", - user="test", password="test", - shots=1, verbose=False) - assert "123e" in str(excinfo.value) # check that job id is in exception + _ibm_http_client.send( + json_qasm, + device="ibmqx4", + token="test", + shots=1, + num_retries=10, + verbose=False, + ) + assert execution_id in str(excinfo.value) # check that job id is in exception assert tries[0] > 0 def test_retrieve_and_device_offline_exception(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] def mocked_requests_get(*args, **kwargs): @@ -376,15 +655,39 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url) and request_num[0] < 2: - return MockResponse({"state": True, "lengthQueue": 10}, 200) - elif args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": False}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url): + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url) and request_num[0] < 2: + return MockResponse( + [ + { + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + elif args[1] == urljoin(_API_URL, status_url): # ibmqx4 gets disconnected, replaced by ibmqx5 + return MockResponse( + [ + { + 'backend_name': 'ibmqx5', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + job_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/123e" + err_url = "Network/ibm-q/Groups/open/Projects/main/Jobs/123ee" + if args[1] == urljoin(_API_URL, job_url): request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) + return MockResponse({"status": "RUNNING", 'iteration': request_num[0]}, 200) + if args[1] == urljoin(_API_URL, err_url): + request_num[0] += 1 + return MockResponse({"status": "TERMINATED", 'iteration': request_num[0]}, 400) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -403,23 +706,22 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x with pytest.raises(_ibm_http_client.DeviceOfflineError): - _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", - jobid="123e") + _ibm_http_client.retrieve(device="ibmqx4", token="test", jobid="123e", num_retries=200) + with pytest.raises(Exception): + _ibm_http_client.retrieve(device="ibmqx4", token="test", jobid="123ee", num_retries=200) def test_retrieve(monkeypatch): - qasms = {'qasms': [{'qasm': 'my qasm'}]} - json_qasm = json.dumps(qasms) request_num = [0] + execution_id = '3' def mocked_requests_get(*args, **kwargs): class MockResponse: @@ -434,16 +736,45 @@ def raise_for_status(self): pass # Accessing status of device. Return online. - status_url = 'Backends/ibmqx4/queue/status' - if args[0] == urljoin(_api_url, status_url): - return MockResponse({"state": True}, 200) - job_url = 'Jobs/{}'.format("123e") - if args[0] == urljoin(_api_url, job_url) and request_num[0] < 1: + status_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' + if args[1] == urljoin(_API_URL, status_url): + return MockResponse( + [ + { + 'backend_name': 'ibmqx4', + 'coupling_map': None, + 'backend_version': '0.1.547', + 'n_qubits': 32, + } + ], + 200, + ) + + # STEP5 + elif ( + args[1] + == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}", + ) + and request_num[0] < 1 + ): request_num[0] += 1 - return MockResponse({"noqasms": "not done"}, 200) - elif args[0] == urljoin(_api_url, job_url): - return MockResponse({"qasms": [{'qasm': 'qasm', - 'result': 'correct'}]}, 200) + return MockResponse({"status": "RUNNING"}, 200) + elif args[1] == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}", + ): + return MockResponse({"status": "COMPLETED"}, 200) + # STEP6 + elif args[1] == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}/resultDownloadUrl", + ): + return MockResponse({"url": "result_download_url"}, 200) + # STEP7 + elif args[1] == "result_download_url": + return MockResponse({"results": ['correct']}, 200) def mocked_requests_post(*args, **kwargs): class MockRequest: @@ -462,14 +793,19 @@ def json(self): def raise_for_status(self): pass - login_url = 'users/login' - if args[0] == urljoin(_api_url, login_url): + if args[1] == _AUTH_API_URL: return MockPostResponse({"userId": "1", "id": "12"}) - monkeypatch.setattr("requests.get", mocked_requests_get) - monkeypatch.setattr("requests.post", mocked_requests_post) + # STEP8 + elif args[1] == urljoin( + _API_URL, + f"Network/ibm-q/Groups/open/Projects/main/Jobs/{execution_id}/resultDownloaded", + ): + return MockPostResponse({}, 200) + + monkeypatch.setattr("requests.sessions.Session.get", mocked_requests_get) + monkeypatch.setattr("requests.sessions.Session.post", mocked_requests_post) + _ibm_http_client.time.sleep = lambda x: x - res = _ibm_http_client.retrieve(device="ibmqx4", - user="test", password="test", - jobid="123e") + res = _ibm_http_client.retrieve(device="ibmqx4", token="test", jobid=execution_id) assert res == 'correct' diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index df1652b7a..dc9e2c2c7 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -11,27 +11,39 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.backends._ibm._ibm.py.""" +import math + import pytest -import json -import projectq.setups.decompositions -from projectq import MainEngine from projectq.backends._ibm import _ibm -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DummyEngine, - DecompositionRuleSet) -from projectq.ops import (All, Allocate, Barrier, Command, Deallocate, - Entangle, Measure, NOT, Rx, Ry, Rz, S, Sdag, T, Tdag, - X, Y, Z) - -from projectq.setups.ibm import ibmqx4_connections +from projectq.cengines import BasicMapperEngine, DummyEngine, MainEngine +from projectq.meta import LogicalQubitIDTag +from projectq.ops import ( + CNOT, + NOT, + All, + Allocate, + Barrier, + Command, + Deallocate, + Entangle, + H, + Measure, + Rx, + Ry, + Rz, + S, + Sdag, + T, + Tdag, + X, + Y, + Z, +) +from projectq.setups import restrictedgateset +from projectq.types import WeakQubitRef # Insure that no HTTP request can be made in all tests in this module @@ -40,15 +52,28 @@ def no_requests(monkeypatch): monkeypatch.delattr("requests.sessions.Session.request") -_api_url = 'https://quantumexperience.ng.bluemix.net/api/' -_api_url_status = 'https://quantumexperience.ng.bluemix.net/api/' - - -@pytest.mark.parametrize("single_qubit_gate, is_available", [ - (X, True), (Y, True), (Z, True), (T, True), (Tdag, True), (S, True), - (Sdag, True), (Allocate, True), (Deallocate, True), (Measure, True), - (NOT, True), (Rx(0.5), True), (Ry(0.5), True), (Rz(0.5), True), - (Barrier, True), (Entangle, False)]) +@pytest.mark.parametrize( + "single_qubit_gate, is_available", + [ + (X, False), + (Y, False), + (Z, False), + (H, True), + (T, False), + (Tdag, False), + (S, False), + (Sdag, False), + (Allocate, True), + (Deallocate, True), + (Measure, True), + (NOT, False), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), True), + (Barrier, True), + (Entangle, False), + ], +) def test_ibm_backend_is_available(single_qubit_gate, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() @@ -57,8 +82,7 @@ def test_ibm_backend_is_available(single_qubit_gate, is_available): assert ibm_backend.is_available(cmd) == is_available -@pytest.mark.parametrize("num_ctrl_qubits, is_available", [ - (0, True), (1, True), (2, False), (3, False)]) +@pytest.mark.parametrize("num_ctrl_qubits, is_available", [(0, False), (1, True), (2, False), (3, False)]) def test_ibm_backend_is_available_control_not(num_ctrl_qubits, is_available): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit1 = eng.allocate_qubit() @@ -68,6 +92,17 @@ def test_ibm_backend_is_available_control_not(num_ctrl_qubits, is_available): assert ibm_backend.is_available(cmd) == is_available +def test_ibm_backend_is_available_negative_control(): + backend = _ibm.IBMBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + + assert backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1], control_state='0')) + + def test_ibm_backend_init(): backend = _ibm.IBMBackend(verbose=True, use_hardware=True) assert backend.qasm == "" @@ -83,14 +118,17 @@ def test_ibm_sent_error(monkeypatch): # patch send def mock_send(*args, **kwargs): raise TypeError - monkeypatch.setattr(_ibm, "send", mock_send) + monkeypatch.setattr(_ibm, "send", mock_send) backend = _ibm.IBMBackend(verbose=True) - eng = MainEngine(backend=backend, - engine_list=[IBM5QubitMapper(), - SwapAndCNOTFlipper(set())]) + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) qubit = eng.allocate_qubit() - X | qubit + Rx(math.pi) | qubit with pytest.raises(Exception): qubit[0].__del__() eng.flush() @@ -100,26 +138,95 @@ def mock_send(*args, **kwargs): eng.next_engine = dummy +def test_ibm_sent_error_2(monkeypatch): + backend = _ibm.IBMBackend(verbose=True) + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + eng = MainEngine(backend=backend, engine_list=[mapper]) + qubit = eng.allocate_qubit() + Rx(math.pi) | qubit + + with pytest.raises(Exception): + S | qubit # no setup to decompose S gate, so not accepted by the backend + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + def test_ibm_retrieve(monkeypatch): # patch send def mock_retrieve(*args, **kwargs): - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + return { + 'data': {'counts': {'0x0': 504, '0x2': 8, '0xc': 6, '0xe': 482}}, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': 4, + 'n_qubits': 32, + 'name': 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [ + ['q', 0], + ['q', 1], + ['q', 2], + ['q', 3], + ['q', 4], + ['q', 5], + ['q', 6], + ['q', 7], + ['q', 8], + ['q', 9], + ['q', 10], + ['q', 11], + ['q', 12], + ['q', 13], + ['q', 14], + ['q', 15], + ['q', 16], + ['q', 17], + ['q', 18], + ['q', 19], + ['q', 20], + ['q', 21], + ['q', 22], + ['q', 23], + ['q', 24], + ['q', 25], + ['q', 26], + ['q', 27], + ['q', 28], + ['q', 29], + ['q', 30], + ['q', 31], + ], + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16, + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005, + } + monkeypatch.setattr(_ibm, "retrieve", mock_retrieve) - backend = _ibm.IBMBackend(retrieve_execution="ab1s2") - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - connectivity = set([(1, 2), (2, 4), (0, 2), (3, 2), (4, 3), (0, 1)]) - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + backend = _ibm.IBMBackend(retrieve_execution="ab1s2", num_runs=1000) + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry, Rz, H), two_qubit_gates=(CNOT,)) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -134,43 +241,118 @@ def mock_retrieve(*args, **kwargs): # run the circuit eng.flush() prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + assert prob_dict['000'] == pytest.approx(0.504) + assert prob_dict['111'] == pytest.approx(0.482) + assert prob_dict['011'] == pytest.approx(0.006) def test_ibm_backend_functional_test(monkeypatch): - correct_info = ('{"qasms": [{"qasm": "\\ninclude \\"qelib1.inc\\";' - '\\nqreg q[3];\\ncreg c[3];\\nh q[2];\\ncx q[2], q[0];' - '\\ncx q[2], q[1];\\ntdg q[2];\\nsdg q[2];' - '\\nbarrier q[2], q[0], q[1];' - '\\nu3(0.2, -pi/2, pi/2) q[2];\\nmeasure q[2] -> ' - 'c[2];\\nmeasure q[0] -> c[0];\\nmeasure q[1] -> c[1];"}]' - ', "shots": 1024, "maxCredits": 5, "backend": {"name": ' - '"simulator"}}') + correct_info = { + 'json': [ + {'qubits': [1], 'name': 'u2', 'params': [0, 3.141592653589793]}, + {'qubits': [1, 2], 'name': 'cx'}, + {'qubits': [1, 3], 'name': 'cx'}, + {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, + {'qubits': [1], 'name': 'u1', 'params': [11.780972450962]}, + {'qubits': [1], 'name': 'u3', 'params': [6.28318530718, 0, 0]}, + {'qubits': [1], 'name': 'u1', 'params': [10.995574287564]}, + {'qubits': [1, 2, 3], 'name': 'barrier'}, + { + 'qubits': [1], + 'name': 'u3', + 'params': [0.2, -1.5707963267948966, 1.5707963267948966], + }, + {'qubits': [1], 'name': 'measure', 'memory': [1]}, + {'qubits': [2], 'name': 'measure', 'memory': [2]}, + {'qubits': [3], 'name': 'measure', 'memory': [3]}, + ], + 'nq': 4, + 'shots': 1000, + 'maxCredits': 10, + 'backend': {'name': 'ibmq_qasm_simulator'}, + } def mock_send(*args, **kwargs): - assert json.loads(args[0]) == json.loads(correct_info) - return {'date': '2017-01-19T14:28:47.622Z', - 'data': {'time': 14.429004907608032, 'counts': {'00111': 396, - '00101': 27, - '00000': 601}, - 'qasm': ('...')}} + assert args[0] == correct_info + return { + 'data': {'counts': {'0x0': 504, '0x2': 8, '0xc': 6, '0xe': 482}}, + 'header': { + 'clbit_labels': [['c', 0], ['c', 1], ['c', 2], ['c', 3]], + 'creg_sizes': [['c', 4]], + 'memory_slots': 4, + 'n_qubits': 32, + 'name': 'circuit0', + 'qreg_sizes': [['q', 32]], + 'qubit_labels': [ + ['q', 0], + ['q', 1], + ['q', 2], + ['q', 3], + ['q', 4], + ['q', 5], + ['q', 6], + ['q', 7], + ['q', 8], + ['q', 9], + ['q', 10], + ['q', 11], + ['q', 12], + ['q', 13], + ['q', 14], + ['q', 15], + ['q', 16], + ['q', 17], + ['q', 18], + ['q', 19], + ['q', 20], + ['q', 21], + ['q', 22], + ['q', 23], + ['q', 24], + ['q', 25], + ['q', 26], + ['q', 27], + ['q', 28], + ['q', 29], + ['q', 30], + ['q', 31], + ], + }, + 'metadata': { + 'measure_sampling': True, + 'method': 'statevector', + 'parallel_shots': 1, + 'parallel_state_update': 16, + }, + 'seed_simulator': 465435780, + 'shots': 1000, + 'status': 'DONE', + 'success': True, + 'time_taken': 0.0045786460000000005, + } + monkeypatch.setattr(_ibm, "send", mock_send) - backend = _ibm.IBMBackend(verbose=True) + backend = _ibm.IBMBackend(verbose=True, num_runs=1000) + import sys + # no circuit has been executed -> raises exception with pytest.raises(RuntimeError): backend.get_probabilities([]) - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - - engine_list = [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] - eng = MainEngine(backend=backend, engine_list=engine_list) + mapper = BasicMapperEngine() + res = {} + for i in range(4): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + setup = restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry, Rz, H), two_qubit_gates=(CNOT,), other_gates=(Barrier,) + ) + setup.extend(ibm_setup) + eng = MainEngine(backend=backend, engine_list=setup) + # 4 qubits circuit is run, but first is unused to test ability for + # get_probability to return the correct values for a subset of the total + # register unused_qubit = eng.allocate_qubit() qureg = eng.allocate_qureg(3) # entangle the qureg @@ -184,9 +366,40 @@ def mock_send(*args, **kwargs): All(Measure) | qureg # run the circuit eng.flush() - prob_dict = eng.backend.get_probabilities([qureg[0], qureg[2], qureg[1]]) - assert prob_dict['111'] == pytest.approx(0.38671875) - assert prob_dict['101'] == pytest.approx(0.0263671875) + prob_dict = eng.backend.get_probabilities([qureg[2], qureg[1]]) + assert prob_dict['00'] == pytest.approx(0.512) + assert prob_dict['11'] == pytest.approx(0.488) + result = "\nu2(0,pi/2) q[1];\ncx q[1], q[2];\ncx q[1], q[3];" + if sys.version_info.major == 3: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972450962) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.995574287564) q[1];" + else: + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(11.780972451) q[1];" + result += "\nu3(6.28318530718, 0, 0) q[1];\nu1(10.9955742876) q[1];" + result += "\nbarrier q[1], q[2], q[3];" + result += "\nu3(0.2, -pi/2, pi/2) q[1];\nmeasure q[1] -> c[1];" + result += "\nmeasure q[2] -> c[2];\nmeasure q[3] -> c[3];" + + assert eng.backend.get_qasm() == result with pytest.raises(RuntimeError): eng.backend.get_probabilities(eng.allocate_qubit()) + + +def test_ibm_errors(): + backend = _ibm.IBMBackend(verbose=True, num_runs=1000) + mapper = BasicMapperEngine() + mapper.current_mapping = {0: 0} + eng = MainEngine(backend=backend, engine_list=[mapper]) + + qb0 = WeakQubitRef(engine=None, idx=0) + + # No LogicalQubitIDTag + with pytest.raises(RuntimeError): + eng.backend._store(Command(engine=eng, gate=Measure, qubits=([qb0],))) + + eng = MainEngine(backend=backend, engine_list=[]) + + # No mapper + with pytest.raises(RuntimeError): + eng.backend._store(Command(engine=eng, gate=Measure, qubits=([qb0],), tags=(LogicalQubitIDTag(1),))) diff --git a/projectq/backends/_ionq/__init__.py b/projectq/backends/_ionq/__init__.py new file mode 100644 index 000000000..8d132eaf8 --- /dev/null +++ b/projectq/backends/_ionq/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ProjectQ module for supporting the IonQ platform.""" + +from ._ionq import IonQBackend + +__all__ = ['IonQBackend'] diff --git a/projectq/backends/_ionq/_ionq.py b/projectq/backends/_ionq/_ionq.py new file mode 100644 index 000000000..4c59369c7 --- /dev/null +++ b/projectq/backends/_ionq/_ionq.py @@ -0,0 +1,370 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Back-end to run quantum programs using IonQ hardware.""" + +import random + +from projectq.cengines import BasicEngine +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control +from projectq.ops import ( + Allocate, + Barrier, + DaggeredGate, + Deallocate, + FlushGate, + HGate, + Measure, + R, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + Sdag, + SGate, + SqrtXGate, + SwapGate, + Tdag, + TGate, + XGate, + YGate, + ZGate, +) +from projectq.types import WeakQubitRef + +from .._exceptions import InvalidCommandError, MidCircuitMeasurementError +from .._utils import _rearrange_result +from . import _ionq_http_client as http_client + +GATE_MAP = { + XGate: 'x', + YGate: 'y', + ZGate: 'z', + HGate: 'h', + Rx: 'rx', + Ry: 'ry', + Rz: 'rz', + SGate: 's', + TGate: 't', + SqrtXGate: 'v', + Rxx: 'xx', + Ryy: 'yy', + Rzz: 'zz', + SwapGate: 'swap', +} +SUPPORTED_GATES = tuple(GATE_MAP.keys()) + + +class IonQBackend(BasicEngine): # pylint: disable=too-many-instance-attributes + """Backend for building circuits and submitting them to the IonQ API.""" + + def __init__( + self, + use_hardware=False, + num_runs=100, + verbose=False, + token=None, + device='ionq_simulator', + num_retries=3000, + interval=1, + retrieve_execution=None, + ): # pylint: disable=too-many-arguments + """ + Initialize an IonQBackend object. + + Args: + use_hardware (bool, optional): Whether or not to use real IonQ hardware or just a simulator. If False, the + ionq_simulator is used regardless of the value of ``device``. Defaults to False. + num_runs (int, optional): Number of times to run circuits. Defaults to 100. + verbose (bool, optional): If True, print statistics after job results have been collected. Defaults to + False. + token (str, optional): An IonQ API token. Defaults to None. + device (str, optional): Device to run jobs on. Supported devices are ``'ionq_qpu'`` or + ``'ionq_simulator'``. Defaults to ``'ionq_simulator'``. + num_retries (int, optional): Number of times to retry fetching a job after it has been submitted. Defaults + to 3000. + interval (int, optional): Number of seconds to wait in between result fetch retries. Defaults to 1. + retrieve_execution (str, optional): An IonQ API Job ID. If provided, a job with this ID will be + fetched. Defaults to None. + """ + super().__init__() + self.device = device if use_hardware else 'ionq_simulator' + self._num_runs = num_runs + self._verbose = verbose + self._token = token + self._num_retries = num_retries + self._interval = interval + self._circuit = [] + self._measured_ids = [] + self._probabilities = {} + self._retrieve_execution = retrieve_execution + self._clear = True + + def is_available(self, cmd): + """ + Test if this backend is available to process the provided command. + + Args: + cmd (Command): A command to process. + + Returns: + bool: If this backend can process the command. + """ + gate = cmd.gate + + # Metagates. + if gate in (Measure, Allocate, Deallocate, Barrier): + return True + + if has_negative_control(cmd): + return False + + # CNOT gates. + # NOTE: IonQ supports up to 7 control qubits + num_ctrl_qubits = get_control_count(cmd) + if 0 < num_ctrl_qubits <= 7: + return isinstance(gate, (XGate,)) + + # Gates without control bits. + if num_ctrl_qubits == 0: + supported = isinstance(gate, SUPPORTED_GATES) + supported_transpose = gate in (Sdag, Tdag) + return supported or supported_transpose + return False + + def _reset(self): + """ + Reset this backend. + + Note: + This sets ``_clear = True``, which will trigger state cleanup on the next call to ``_store``. + """ + # Lastly, reset internal state for measured IDs and circuit body. + self._circuit = [] + self._clear = True + + def _store(self, cmd): + """Interpret the ProjectQ command as a circuit instruction and store it. + + Args: + cmd (Command): A command to process. + + Raises: + InvalidCommandError: If the command can not be interpreted. + MidCircuitMeasurementError: If this command would result in a + mid-circuit qubit measurement. + """ + if self._clear: + self._measured_ids = [] + self._probabilities = {} + self._clear = False + + # No-op/Meta gates. + # NOTE: self.main_engine.mapper takes care qubit allocation/mapping. + gate = cmd.gate + if gate in (Allocate, Deallocate, Barrier): + return + + # Create a measurement. + if gate == Measure: + logical_id = cmd.qubits[0][0].id + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + logical_id = tag.logical_qubit_id + break + # Add the qubit id + self._measured_ids.append(logical_id) + return + + # Process the Command's gate type: + gate_type = type(gate) + gate_name = GATE_MAP.get(gate_type) + # Daggered gates get special treatment. + if isinstance(gate, DaggeredGate): + gate_name = f"{GATE_MAP[type(gate._gate)]}i" # pylint: disable=protected-access + + # Unable to determine a gate mapping here, so raise out. + if gate_name is None: + raise InvalidCommandError(f"Invalid command: {str(cmd)}") + + # Now make sure there are no existing measurements on qubits involved + # in this operation. + targets = [qb.id for qureg in cmd.qubits for qb in qureg] + controls = [qb.id for qb in cmd.control_qubits] + if len(self._measured_ids) > 0: + # Check any qubits we are trying to operate on. + gate_qubits = set(targets) | set(controls) + + # If any of them have already been measured... + already_measured = gate_qubits & set(self._measured_ids) + + # Boom! + if len(already_measured) > 0: + err = ( + 'Mid-circuit measurement is not supported. ' + f'The following qubits have already been measured: {list(already_measured)}.' + ) + raise MidCircuitMeasurementError(err) + + # Initialize the gate dict: + gate_dict = { + 'gate': gate_name, + 'targets': targets, + } + + # Check if we have a rotation + if isinstance(gate, (R, Rx, Ry, Rz, Rxx, Ryy, Rzz)): + gate_dict['rotation'] = gate.angle + + # Set controls + if len(controls) > 0: + gate_dict['controls'] = controls + + self._circuit.append(gate_dict) + + def get_probability(self, state, qureg): + """Shortcut to get a specific state's probability. + + Args: + state (str): A state in bit-string format. + qureg (Qureg): A ProjectQ Qureg object. + + Returns: + float: The probability for the provided state. + """ + if len(state) != len(qureg): + raise ValueError('Desired state and register must be the same length!') + + probs = self.get_probabilities(qureg) + return probs[state] + + def get_probabilities(self, qureg): + """ + Given the provided qubit register, determine the probability of each possible outcome. + + Note: + This method should only be called *after* a circuit has been run and its results are available. + + Args: + qureg (Qureg): A ProjectQ Qureg object. + + Returns: + dict: A dict mapping of states -> probability. + """ + if len(self._probabilities) == 0: + raise RuntimeError("Please, run the circuit first!") + + probability_dict = {} + for state, probability in self._probabilities.items(): + mapped_state = ['0'] * len(qureg) + for i, qubit in enumerate(qureg): + try: + meas_idx = self._measured_ids.index(qubit.id) + except ValueError: + continue + mapped_state[i] = state[meas_idx] + mapped_state = "".join(mapped_state) + probability_dict[mapped_state] = probability_dict.get(mapped_state, 0) + probability + return probability_dict + + def _run(self): # pylint: disable=too-many-locals + """Run the circuit this object has built during engine execution.""" + # Nothing to do with an empty circuit. + if len(self._circuit) == 0: + return + + if self._retrieve_execution is None: + qubit_mapping = self.main_engine.mapper.current_mapping + measured_ids = self._measured_ids[:] + info = { + 'circuit': self._circuit, + 'nq': len(qubit_mapping.keys()), + 'shots': self._num_runs, + 'meas_mapped': [qubit_mapping[qubit_id] for qubit_id in measured_ids], + 'meas_qubit_ids': measured_ids, + } + res = http_client.send( + info, + device=self.device, + token=self._token, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + if res is None: + raise RuntimeError('Failed to submit job to the server!') + else: + res = http_client.retrieve( + device=self.device, + token=self._token, + jobid=self._retrieve_execution, + num_retries=self._num_retries, + interval=self._interval, + verbose=self._verbose, + ) + if res is None: + raise RuntimeError(f"Failed to retrieve job with id: '{self._retrieve_execution}'!") + self._measured_ids = measured_ids = res['meas_qubit_ids'] + + # Determine random outcome from probable states. + random_outcome = random.random() + p_sum = 0.0 + measured = "" + star = "" + num_measured = len(measured_ids) + probable_outcomes = res['output_probs'] + states = probable_outcomes.keys() + self._probabilities = {} + for idx, state_int in enumerate(states): + state = _rearrange_result(int(state_int), num_measured) + probability = probable_outcomes[state_int] + p_sum += probability + if p_sum >= random_outcome and measured == "" or (idx == len(states) - 1): + measured = state + star = "*" + self._probabilities[state] = probability + if self._verbose and probability > 0: # pragma: no cover + print(f"{state} with p = {probability}{star}") + + # Register measurement results + for idx, qubit_id in enumerate(measured_ids): + result = int(measured[idx]) + qubit_ref = WeakQubitRef(self.main_engine, qubit_id) + self.main_engine.set_measurement_result(qubit_ref, result) + + def receive(self, command_list): + """Receive a command list from the ProjectQ engine pipeline. + + If a given command is a "flush" operation, the pending circuit will be + submitted to IonQ's API for processing. + + Args: + command_list (list[Command]): A list of ProjectQ Command objects. + """ + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + self._store(cmd) + else: + # After that, the circuit is ready to be submitted. + try: + self._run() + finally: + # Make sure we always reset engine state so as not to leave + # anything dirty atexit. + self._reset() + + +__all__ = ['IonQBackend'] diff --git a/projectq/backends/_ionq/_ionq_http_client.py b/projectq/backends/_ionq/_ionq_http_client.py new file mode 100644 index 000000000..72e16555f --- /dev/null +++ b/projectq/backends/_ionq/_ionq_http_client.py @@ -0,0 +1,392 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""HTTP Client for the IonQ API.""" + +import getpass +import json +import signal +import time + +import requests +from requests import Session +from requests.compat import urljoin + +from .._exceptions import ( + DeviceOfflineError, + DeviceTooSmall, + JobSubmissionError, + RequestTimeoutError, +) + +_API_URL = 'https://api.ionq.co/v0.2/' +_JOB_API_URL = urljoin(_API_URL, 'jobs/') + + +class IonQ(Session): + """A requests.Session based HTTP client for the IonQ API.""" + + def __init__(self, verbose=False): + """Initialize an session with IonQ's APIs.""" + super().__init__() + self.backends = {} + self.timeout = 5.0 + self.token = None + self._verbose = verbose + + def update_devices_list(self): + """Update the list of devices this backend can support.""" + self.authenticate(self.token) + req = super().get(urljoin(_API_URL, 'backends')) + req.raise_for_status() + r_json = req.json() + # Legacy backends, kept for backward compatibility. + self.backends = { + 'ionq_simulator': { + 'nq': 29, + 'target': 'simulator', + }, + 'ionq_qpu': { + 'nq': 11, + 'target': 'qpu', + }, + } + for backend in r_json: + self.backends[backend["backend"]] = {"nq": backend["qubits"], "target": backend["backend"]} + if self._verbose: # pragma: no cover + print('- List of IonQ devices available:') + print(self.backends) + + def is_online(self, device): + """ + Check if a given device is online. + + Args: + device (str): An IonQ device name. + + Returns: + bool: True if device is online, else False. + """ + return device in self.backends + + def can_run_experiment(self, info, device): + """ + Determine whether or not the desired device has enough allocatable qubits to run something. + + This returns a three-element tuple with whether or not the experiment + can be run, the max number of qubits possible, and the number of qubits + needed to run this experiment. + + Args: + info (dict): A dict containing number of shots, qubits, and + a circuit. + device (str): An IonQ device name. + Returns: + tuple(bool, int, int): Whether the operation can be run, max + number of qubits the device supports, and number of qubits + required for the experiment. + """ + nb_qubit_max = self.backends[device]['nq'] + nb_qubit_needed = info['nq'] + return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed + + def authenticate(self, token=None): + """Set an Authorization header for this session. + + If no token is provided, an prompt will appear to ask for one. + + Args: + token (str): IonQ user API token. + """ + if token is None: + token = getpass.getpass(prompt='IonQ apiKey > ') + if not token: + raise RuntimeError('An authentication token is required!') + self.headers.update({'Authorization': f'apiKey {token}'}) + self.token = token + + def run(self, info, device): + """Run a circuit from ``info`` on the specified ``device``. + + Args: + info (dict): A dict containing number of shots, qubits, and + a circuit. + device (str): An IonQ device name. + + Raises: + JobSubmissionError: If the job creation response from IonQ's API + had a failure result. + + Returns: + str: The ID of a newly submitted Job. + """ + argument = { + 'target': self.backends[device]['target'], + 'metadata': { + 'sdk': 'ProjectQ', + 'meas_qubit_ids': json.dumps(info['meas_qubit_ids']), + }, + 'shots': info['shots'], + 'registers': {'meas_mapped': info['meas_mapped']}, + 'lang': 'json', + 'body': { + 'qubits': info['nq'], + 'circuit': info['circuit'], + }, + } + + # _API_URL[:-1] strips the trailing slash. + # TODO: Add comprehensive error parsing for non-200 responses. + req = super().post(_JOB_API_URL[:-1], json=argument) + req.raise_for_status() + + # Process the response. + r_json = req.json() + status = r_json['status'] + + # Return the job id. + if status == 'ready': + return r_json['id'] + + # Otherwise, extract any provided failure info and raise an exception. + failure = r_json.get('failure') or { + 'code': 'UnknownError', + 'error': 'An unknown error occurred!', + } + raise JobSubmissionError(f"{failure['code']}: {failure['error']} (status={status})") + + def get_result(self, device, execution_id, num_retries=3000, interval=1): + """ + Given a backend and ID, fetch the results for this job's execution. + + The return dictionary should have at least: + + * ``nq`` (int): Number of qubits for this job. + * ``output_probs`` (dict): Map of integer states to probability values. + + Args: + device (str): The device used to run this job. + execution_id (str): An IonQ Job ID. + num_retries (int, optional): Number of times to retry the fetch + before raising a timeout error. Defaults to 3000. + interval (int, optional): Number of seconds to wait between retries. + Defaults to 1. + + Raises: + Exception: If the process receives a kill signal before completion. + Exception: If the job is in an unknown processing state. + DeviceOfflineError: If the provided device is not online. + RequestTimeoutError: If we were unable to retrieve the job results + after ``num_retries`` attempts. + + Returns: + dict: A dict of job data for an engine to consume. + """ + if self._verbose: # pragma: no cover + print(f"Waiting for results. [Job ID: {execution_id}]") + + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def _handle_sigint_during_get_result(*_): # pragma: no cover + raise Exception(f"Interrupted. The ID of your submitted job is {execution_id}.") + + signal.signal(signal.SIGINT, _handle_sigint_during_get_result) + + try: + for retries in range(num_retries): + req = super().get(urljoin(_JOB_API_URL, execution_id)) + req.raise_for_status() + r_json = req.json() + status = r_json['status'] + + # Check if job is completed. + if status == 'completed': + meas_mapped = r_json['registers']['meas_mapped'] + meas_qubit_ids = json.loads(r_json['metadata']['meas_qubit_ids']) + output_probs = r_json['data']['registers']['meas_mapped'] + return { + 'nq': r_json['qubits'], + 'output_probs': output_probs, + 'meas_mapped': meas_mapped, + 'meas_qubit_ids': meas_qubit_ids, + } + + # Otherwise, make sure it is in a known healthy state. + if status not in ('ready', 'running', 'submitted'): + # TODO: Add comprehensive API error processing here. + raise Exception(f"Error while running the code: {status}.") + + # Sleep, then check availability before trying again. + time.sleep(interval) + if self.is_online(device) and retries % 60 == 0: + self.update_devices_list() + if not self.is_online(device): # pragma: no cover + raise DeviceOfflineError( + f"Device went offline. The ID of your submitted job is {execution_id}." + ) + finally: + if original_sigint_handler is not None: + signal.signal(signal.SIGINT, original_sigint_handler) + + raise RequestTimeoutError(f"Timeout. The ID of your submitted job is {execution_id}.") + + def show_devices(self): + """Show the currently available device list for the IonQ provider. + + Returns: + list: list of available devices and their properties. + """ + self.update_devices_list() + return self.backends + + +def retrieve( + device, + token, + jobid, + num_retries=3000, + interval=1, + verbose=False, +): # pylint: disable=too-many-arguments + """Retrieve an already submitted IonQ job. + + Args: + device (str): The name of an IonQ device. + token (str): An IonQ API token. + jobid (str): An IonQ Job ID. + num_retries (int, optional): Number of times to retry while the job is + not finished. Defaults to 3000. + interval (int, optional): Sleep interval between retries, in seconds. + Defaults to 1. + verbose (bool, optional): Whether to print verbose output. + Defaults to False. + + Returns: + dict: A dict with job submission results. + """ + ionq_session = IonQ(verbose=verbose) + ionq_session.authenticate(token) + ionq_session.update_devices_list() + res = ionq_session.get_result( + device, + jobid, + num_retries=num_retries, + interval=interval, + ) + return res + + +def send( + info, + device='ionq_simulator', + token=None, + num_retries=100, + interval=1, + verbose=False, +): # pylint: disable=too-many-arguments,too-many-locals + """Submit a job to the IonQ API. + + The ``info`` dict should have at least the following keys:: + + * nq (int): Number of qubits this job will need. + * shots (dict): The number of shots to use for this job. + * meas_mapped (list): A list of qubits to measure. + * circuit (list): A list of JSON-serializable IonQ gate representations. + + Args: + info (dict): A dictionary with + device (str, optional): The IonQ device to run this on. Defaults to 'ionq_simulator'. + token (str, optional): An IonQ API token. Defaults to None. + num_retries (int, optional): Number of times to retry while the job is + not finished. Defaults to 100. + interval (int, optional): Sleep interval between retries, in seconds. + Defaults to 1. + verbose (bool, optional): Whether to print verbose output. + Defaults to False. + + Raises: + DeviceOfflineError: If the desired device is not available for job + processing. + DeviceTooSmall: If the job has a higher qubit requirement than the + device supports. + + Returns: + dict: An intermediate dict representation of an IonQ job result. + """ + try: + ionq_session = IonQ(verbose=verbose) + + if verbose: # pragma: no cover + print("- Authenticating...") + if verbose and token is not None: # pragma: no cover + print(f"user API token: {token}") + ionq_session.authenticate(token) + + # check if the device is online + ionq_session.update_devices_list() + online = ionq_session.is_online(device) + + # useless for the moment + if not online: # pragma: no cover + print("The device is offline (for maintenance?). Use the simulator instead or try again later.") + raise DeviceOfflineError("Device is offline.") + + # check if the device has enough qubit to run the code + runnable, qmax, qneeded = ionq_session.can_run_experiment(info, device) + if not runnable: + print( + f'The device is too small ({qmax} qubits available) for the code requested({qneeded} qubits needed).', + 'Try to look for another device with more qubits', + ) + raise DeviceTooSmall("Device is too small.") + if verbose: # pragma: no cover + print(f"- Running code: {info}") + execution_id = ionq_session.run(info, device) + if verbose: # pragma: no cover + print("- Waiting for results...") + res = ionq_session.get_result( + device, + execution_id, + num_retries=num_retries, + interval=interval, + ) + if verbose: # pragma: no cover + print("- Done.") + return res + except requests.exceptions.HTTPError as err: + # Re-raise auth errors, as literally nothing else will work. + if err.response is not None: + status_code = err.response.status_code + if status_code in (401, 403): + raise err + + # Try to parse client errors + if status_code == 400: + err_json = err.response.json() + raise JobSubmissionError(f"{err_json['error']}: {err_json['message']}") from err + + # Else, just print: + print("- There was an error running your code:") + print(err) + except requests.exceptions.RequestException as err: + print("- Looks like something is wrong with server:") + print(err) + return None + + +__all__ = [ + 'send', + 'retrieve', + 'IonQ', +] diff --git a/projectq/backends/_ionq/_ionq_http_client_test.py b/projectq/backends/_ionq/_ionq_http_client_test.py new file mode 100644 index 000000000..d1df8fb68 --- /dev/null +++ b/projectq/backends/_ionq/_ionq_http_client_test.py @@ -0,0 +1,621 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.backends._ionq._ionq_http_client.py.""" + +from unittest import mock + +import pytest +import requests + +from projectq.backends._exceptions import JobSubmissionError, RequestTimeoutError +from projectq.backends._ionq import _ionq_http_client + + +# Insure that no HTTP request can be made in all tests in this module +@pytest.fixture(autouse=True) +def no_requests(monkeypatch): + monkeypatch.delattr('requests.sessions.Session.request') + + +def test_authenticate(): + ionq_session = _ionq_http_client.IonQ() + ionq_session.authenticate('NotNone') + assert 'Authorization' in ionq_session.headers + assert ionq_session.token == 'NotNone' + assert ionq_session.headers['Authorization'] == 'apiKey NotNone' + + +def test_authenticate_prompt_requires_token(monkeypatch): + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return '' + + monkeypatch.setattr('getpass.getpass', user_password_input) + ionq_session = _ionq_http_client.IonQ() + with pytest.raises(RuntimeError) as excinfo: + ionq_session.authenticate() + assert str(excinfo.value) == 'An authentication token is required!' + + +def test_is_online(monkeypatch): + def mock_get(_self, path, *args, **kwargs): + assert 'https://api.ionq.co/v0.2/backends' == path + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock( + return_value=[ + { + "backend": "qpu.s11", + "status": "available", + "qubits": 11, + "average_queue_time": 3253287, + "last_updated": 1647863473555, + "characterization_url": "/characterizations/48ccd423-2913-45e0-a669-e0f676abeb82", + }, + { + "backend": "simulator", + "status": "available", + "qubits": 19, + "average_queue_time": 1499, + "last_updated": 1627065490042, + }, + ], + ) + return mock_response + + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + ionq_session = _ionq_http_client.IonQ() + ionq_session.authenticate('not none') + ionq_session.update_devices_list() + assert ionq_session.is_online('ionq_simulator') + assert ionq_session.is_online('ionq_qpu') + assert ionq_session.is_online('qpu.s11') + assert not ionq_session.is_online('ionq_unknown') + + +def test_show_devices(monkeypatch): + def mock_get(_self, path, *args, **kwargs): + assert 'https://api.ionq.co/v0.2/backends' == path + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock( + return_value=[ + { + "backend": "qpu.s11", + "status": "available", + "qubits": 11, + "average_queue_time": 3253287, + "last_updated": 1647863473555, + "characterization_url": "/characterizations/48ccd423-2913-45e0-a669-e0f676abeb82", + }, + { + "backend": "simulator", + "status": "available", + "qubits": 19, + "average_queue_time": 1499, + "last_updated": 1627065490042, + }, + ], + ) + return mock_response + + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + ionq_session = _ionq_http_client.IonQ() + ionq_session.authenticate('not none') + device_list = ionq_session.show_devices() + assert isinstance(device_list, dict) + assert len(device_list) == 4 + for info in device_list.values(): + assert 'nq' in info + assert 'target' in info + + +def test_send_too_many_qubits(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 3, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + info = { + 'nq': 4, + 'shots': 1, + 'meas_mapped': [2, 3], + 'circuit': [ + {'gate': 'x', 'targets': [0]}, + {'gate': 'x', 'targets': [1]}, + {'controls': [0], 'gate': 'cnot', 'targets': [2]}, + {'controls': [1], 'gate': 'cnot', 'targets': [2]}, + {'controls': [0, 1], 'gate': 'cnot', 'targets': [3]}, + ], + } + with pytest.raises(_ionq_http_client.DeviceTooSmall): + _ionq_http_client.send( + info, + device='dummy', + token='NotNone', + verbose=True, + ) + + +def test_send_real_device_online_verbose(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + # What the IonQ JSON API request should look like. + expected_request = { + 'target': 'dummy', + 'metadata': {'sdk': 'ProjectQ', 'meas_qubit_ids': '[2, 3]'}, + 'shots': 1, + 'registers': {'meas_mapped': [2, 3]}, + 'lang': 'json', + 'body': { + 'qubits': 4, + 'circuit': [ + {'gate': 'x', 'targets': [0]}, + {'gate': 'x', 'targets': [1]}, + {'controls': [0], 'gate': 'cnot', 'targets': [2]}, + {'controls': [1], 'gate': 'cnot', 'targets': [2]}, + {'controls': [0, 1], 'gate': 'cnot', 'targets': [3]}, + ], + }, + } + + def mock_post(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs' + assert 'json' in kwargs + assert expected_request == kwargs['json'] + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json = mock.MagicMock( + return_value={ + 'id': 'new-job-id', + 'status': 'ready', + } + ) + return mock_response + + def mock_get(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs/new-job-id' + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock( + return_value={ + 'id': 'new-job-id', + 'status': 'completed', + 'qubits': 4, + 'metadata': {'meas_qubit_ids': '[2, 3]'}, + 'registers': {'meas_mapped': [2, 3]}, + 'data': { + 'registers': {'meas_mapped': {'2': 1}}, + }, + } + ) + return mock_response + + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 4, + 'shots': 1, + 'meas_mapped': [2, 3], + 'meas_qubit_ids': [2, 3], + 'circuit': [ + {'gate': 'x', 'targets': [0]}, + {'gate': 'x', 'targets': [1]}, + {'controls': [0], 'gate': 'cnot', 'targets': [2]}, + {'controls': [1], 'gate': 'cnot', 'targets': [2]}, + {'controls': [0, 1], 'gate': 'cnot', 'targets': [3]}, + ], + } + expected = { + 'nq': 4, + 'output_probs': {'2': 1}, + 'meas_mapped': [2, 3], + 'meas_qubit_ids': [2, 3], + } + actual = _ionq_http_client.send(info, device='dummy') + assert expected == actual + + +@pytest.mark.parametrize( + 'error_type', + [ + requests.exceptions.HTTPError, + requests.exceptions.RequestException, + ], +) +def test_send_requests_errors_are_caught(monkeypatch, error_type): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + mock_post = mock.MagicMock(side_effect=error_type()) + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + _ionq_http_client.send(info, device='dummy') + mock_post.assert_called_once() + + +def test_send_auth_errors_reraise(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + + mock_response = mock.MagicMock() + mock_response.status_code = 401 + auth_error = requests.exceptions.HTTPError(response=mock_response) + mock_post = mock.MagicMock(side_effect=auth_error) + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + with pytest.raises(requests.exceptions.HTTPError) as excinfo: + _ionq_http_client.send(info, device='dummy') + mock_post.assert_called_once() + assert auth_error is excinfo.value + + +def test_send_bad_requests_reraise(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + + mock_response = mock.MagicMock() + mock_response.status_code = 400 + mock_response.json = mock.MagicMock( + return_value={ + 'error': 'Bad Request', + 'message': 'Invalid request body', + } + ) + auth_error = requests.exceptions.HTTPError(response=mock_response) + mock_post = mock.MagicMock(side_effect=auth_error) + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + with pytest.raises(JobSubmissionError) as excinfo: + _ionq_http_client.send(info, device='dummy') + mock_post.assert_called_once() + assert str(excinfo.value) == "Bad Request: Invalid request body" + + +def test_send_auth_token_required(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + + mock_post = mock.MagicMock(side_effect=Exception()) + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return None + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + with pytest.raises(RuntimeError) as excinfo: + _ionq_http_client.send(info, device='dummy') + mock_post.assert_not_called() + assert 'An authentication token is required!' == str(excinfo.value) + + +@pytest.mark.parametrize( + "expected_err, err_data", + [ + ( + "UnknownError: An unknown error occurred! (status=unknown)", + {'status': 'unknown'}, + ), + ( + 'APIError: Something failed! (status=failed)', + { + 'status': 'failed', + 'failure': { + 'error': 'Something failed!', + 'code': 'APIError', + }, + }, + ), + ], +) +def test_send_api_errors_are_raised(monkeypatch, expected_err, err_data): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + + def mock_post(_self, path, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs' + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock(return_value=err_data) + return mock_response + + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + with pytest.raises(JobSubmissionError) as excinfo: + _ionq_http_client.send(info, device='dummy') + + assert expected_err == str(excinfo.value) + + +def test_timeout_exception(monkeypatch): + # Patch the method to give back dummy devices + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + + def mock_post(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs' + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock( + return_value={ + 'id': 'new-job-id', + 'status': 'ready', + } + ) + return mock_response + + def mock_get(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs/new-job-id' + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock( + return_value={ + 'id': 'new-job-id', + 'status': 'running', + } + ) + return mock_response + + monkeypatch.setattr('requests.sessions.Session.post', mock_post) + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Called once per loop in _get_result while the job is not ready. + mock_sleep = mock.MagicMock() + monkeypatch.setattr(_ionq_http_client.time, 'sleep', mock_sleep) + + # RequestTimeoutErrors are not caught, and so will raise out. + with pytest.raises(RequestTimeoutError) as excinfo: + info = { + 'nq': 1, + 'shots': 1, + 'meas_mapped': [], + 'meas_qubit_ids': [], + 'circuit': [], + } + _ionq_http_client.send(info, device='dummy', num_retries=1) + mock_sleep.assert_called_once() + assert 'Timeout. The ID of your submitted job is new-job-id.' == str(excinfo.value) + + +@pytest.mark.parametrize('token', [None, 'NotNone']) +def test_retrieve(monkeypatch, token): + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + request_num = [0] + + def mock_get(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs/old-job-id' + json_response = { + 'id': 'old-job-id', + 'status': 'running', + } + if request_num[0] > 1: + json_response = { + 'id': 'old-job-id', + 'status': 'completed', + 'qubits': 4, + 'registers': {'meas_mapped': [2, 3]}, + 'metadata': {'meas_qubit_ids': '[2, 3]'}, + 'data': { + 'registers': {'meas_mapped': {'2': 1}}, + }, + } + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock(return_value=json_response) + request_num[0] += 1 + return mock_response + + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + expected = { + 'nq': 4, + 'output_probs': {'2': 1}, + 'meas_qubit_ids': [2, 3], + 'meas_mapped': [2, 3], + } + + # Code to test: + # Called once per loop in _get_result while the job is not ready. + mock_sleep = mock.MagicMock() + monkeypatch.setattr(_ionq_http_client.time, 'sleep', mock_sleep) + result = _ionq_http_client.retrieve('dummy', token, 'old-job-id') + assert expected == result + # We only sleep twice. + assert 2 == mock_sleep.call_count + + +def test_retrieve_that_errors_are_caught(monkeypatch): + def _dummy_update(_self): + _self.backends = {'dummy': {'nq': 4, 'target': 'dummy'}} + + monkeypatch.setattr( + _ionq_http_client.IonQ, + 'update_devices_list', + _dummy_update.__get__(None, _ionq_http_client.IonQ), + ) + request_num = [0] + + def mock_get(_self, path, *args, **kwargs): + assert path == 'https://api.ionq.co/v0.2/jobs/old-job-id' + json_response = { + 'id': 'old-job-id', + 'status': 'running', + } + if request_num[0] > 0: + json_response = { + 'id': 'old-job-id', + 'status': 'failed', + 'failure': { + 'code': 'ErrorCode', + 'error': 'A descriptive error message.', + }, + } + mock_response = mock.MagicMock() + mock_response.json = mock.MagicMock(return_value=json_response) + request_num[0] += 1 + return mock_response + + monkeypatch.setattr('requests.sessions.Session.get', mock_get) + + def user_password_input(prompt): + if prompt == 'IonQ apiKey > ': + return 'NotNone' + + monkeypatch.setattr('getpass.getpass', user_password_input) + + # Code to test: + mock_sleep = mock.MagicMock() + monkeypatch.setattr(_ionq_http_client.time, 'sleep', mock_sleep) + with pytest.raises(Exception): + _ionq_http_client.retrieve('dummy', 'NotNone', 'old-job-id') + mock_sleep.assert_called_once() diff --git a/projectq/backends/_ionq/_ionq_mapper.py b/projectq/backends/_ionq/_ionq_mapper.py new file mode 100644 index 000000000..e77b92c17 --- /dev/null +++ b/projectq/backends/_ionq/_ionq_mapper.py @@ -0,0 +1,93 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Mapper that has a maximum number of allocatable qubits.""" + +from projectq.cengines import BasicMapperEngine +from projectq.meta import LogicalQubitIDTag +from projectq.ops import AllocateQubitGate, Command, DeallocateQubitGate, FlushGate +from projectq.types import WeakQubitRef + + +class BoundedQubitMapper(BasicMapperEngine): + """Map logical qubits to a fixed number of hardware qubits.""" + + def __init__(self, max_qubits): + """Initialize a BoundedQubitMapper object.""" + super().__init__() + self._qubit_idx = 0 + self.max_qubits = max_qubits + + def _reset(self): + # Reset the mapping index. + self._qubit_idx = 0 + + def _process_cmd(self, cmd): + current_mapping = self.current_mapping + if current_mapping is None: + current_mapping = {} + + if isinstance(cmd.gate, AllocateQubitGate): + qubit_id = cmd.qubits[0][0].id + if qubit_id in current_mapping: + raise RuntimeError(f"Qubit with id {qubit_id} has already been allocated!") + + if self._qubit_idx >= self.max_qubits: + raise RuntimeError(f"Cannot allocate more than {self.max_qubits} qubits!") + + new_id = self._qubit_idx + self._qubit_idx += 1 + current_mapping[qubit_id] = new_id + qb = WeakQubitRef(engine=self, idx=new_id) + new_cmd = Command( + engine=self, + gate=AllocateQubitGate(), + qubits=([qb],), + tags=[LogicalQubitIDTag(qubit_id)], + ) + self.current_mapping = current_mapping + self.send([new_cmd]) + elif isinstance(cmd.gate, DeallocateQubitGate): + qubit_id = cmd.qubits[0][0].id + if qubit_id not in current_mapping: + raise RuntimeError("Cannot deallocate a qubit that is not already allocated!") + qb = WeakQubitRef(engine=self, idx=current_mapping[qubit_id]) + new_cmd = Command( + engine=self, + gate=DeallocateQubitGate(), + qubits=([qb],), + tags=[LogicalQubitIDTag(qubit_id)], + ) + current_mapping.pop(qubit_id) + self.current_mapping = current_mapping + self.send([new_cmd]) + else: + self._send_cmd_with_mapped_ids(cmd) + + def receive(self, command_list): + """ + Receive a list of commands. + + Args: + command_list (list): List of commands to receive. + """ + for cmd in command_list: + if isinstance(cmd.gate, FlushGate): + self._reset() + self.send([cmd]) + else: + self._process_cmd(cmd) + + +__all__ = ['BoundedQubitMapper'] diff --git a/projectq/backends/_ionq/_ionq_mapper_test.py b/projectq/backends/_ionq/_ionq_mapper_test.py new file mode 100644 index 000000000..634f7d14a --- /dev/null +++ b/projectq/backends/_ionq/_ionq_mapper_test.py @@ -0,0 +1,127 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from projectq.backends import Simulator +from projectq.backends._ionq._ionq_mapper import BoundedQubitMapper +from projectq.cengines import DummyEngine, MainEngine +from projectq.meta import LogicalQubitIDTag +from projectq.ops import AllocateQubitGate, Command, DeallocateQubitGate +from projectq.types import WeakQubitRef + + +def test_cannot_allocate_past_max(): + mapper = BoundedQubitMapper(1) + engine = MainEngine( + DummyEngine(), + engine_list=[mapper], + verbose=True, + ) + engine.allocate_qubit() + with pytest.raises(RuntimeError) as excinfo: + engine.allocate_qubit() + + assert str(excinfo.value) == "Cannot allocate more than 1 qubits!" + + # Avoid double error reporting + mapper.current_mapping = {0: 0, 1: 1} + + +def test_cannot_reallocate_same_qubit(): + engine = MainEngine( + Simulator(), + engine_list=[BoundedQubitMapper(1)], + verbose=True, + ) + qureg = engine.allocate_qubit() + qubit = qureg[0] + qubit_id = qubit.id + with pytest.raises(RuntimeError) as excinfo: + allocate_cmd = Command( + engine=engine, + gate=AllocateQubitGate(), + qubits=([WeakQubitRef(engine=engine, idx=qubit_id)],), + tags=[LogicalQubitIDTag(qubit_id)], + ) + engine.send([allocate_cmd]) + + assert str(excinfo.value) == "Qubit with id 0 has already been allocated!" + + +def test_cannot_deallocate_unknown_qubit(): + engine = MainEngine( + Simulator(), + engine_list=[BoundedQubitMapper(1)], + verbose=True, + ) + qureg = engine.allocate_qubit() + with pytest.raises(RuntimeError) as excinfo: + deallocate_cmd = Command( + engine=engine, + gate=DeallocateQubitGate(), + qubits=([WeakQubitRef(engine=engine, idx=1)],), + tags=[LogicalQubitIDTag(1)], + ) + engine.send([deallocate_cmd]) + assert str(excinfo.value) == "Cannot deallocate a qubit that is not already allocated!" + + # but we can still deallocate an already allocated one + engine.deallocate_qubit(qureg[0]) + del qureg + del engine + + +def test_cannot_deallocate_same_qubit(): + mapper = BoundedQubitMapper(1) + engine = MainEngine( + Simulator(), + engine_list=[mapper], + verbose=True, + ) + qureg = engine.allocate_qubit() + qubit_id = qureg[0].id + engine.deallocate_qubit(qureg[0]) + + with pytest.raises(RuntimeError) as excinfo: + deallocate_cmd = Command( + engine=engine, + gate=DeallocateQubitGate(), + qubits=([WeakQubitRef(engine=engine, idx=qubit_id)],), + tags=[LogicalQubitIDTag(qubit_id)], + ) + engine.send([deallocate_cmd]) + + assert str(excinfo.value) == "Cannot deallocate a qubit that is not already allocated!" + + +def test_flush_deallocates_all_qubits(): + mapper = BoundedQubitMapper(10) + engine = MainEngine( + Simulator(), + engine_list=[mapper], + verbose=True, + ) + # needed to prevent GC from removing qubit refs + qureg = engine.allocate_qureg(10) + assert len(mapper.current_mapping.keys()) == 10 + assert len(engine.active_qubits) == 10 + engine.flush() + # Should still be around after flush + assert len(engine.active_qubits) == 10 + assert len(mapper.current_mapping.keys()) == 10 + + # GC will clean things up + del qureg + assert len(engine.active_qubits) == 0 + assert len(mapper.current_mapping.keys()) == 0 diff --git a/projectq/backends/_ionq/_ionq_test.py b/projectq/backends/_ionq/_ionq_test.py new file mode 100644 index 000000000..5a846b033 --- /dev/null +++ b/projectq/backends/_ionq/_ionq_test.py @@ -0,0 +1,535 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq.backends._ionq._ionq.py.""" + +import math +from unittest import mock + +import pytest + +from projectq import MainEngine +from projectq.backends._exceptions import ( + InvalidCommandError, + MidCircuitMeasurementError, +) +from projectq.backends._ionq import _ionq, _ionq_http_client +from projectq.cengines import DummyEngine +from projectq.ops import ( + CNOT, + All, + Allocate, + Barrier, + Command, + Deallocate, + Entangle, + H, + Measure, + Ph, + R, + Rx, + Rxx, + Ry, + Rz, + S, + Sdag, + SqrtX, + T, + Tdag, + Toffoli, + X, + Y, + Z, +) +from projectq.types import WeakQubitRef + +from ._ionq_mapper import BoundedQubitMapper + + +@pytest.fixture(scope='function') +def mapper_factory(): + def _factory(n=4): + return BoundedQubitMapper(n) + + return _factory + + +# Prevent any requests from making it out. +@pytest.fixture(autouse=True) +def no_requests(monkeypatch): + monkeypatch.delattr("requests.sessions.Session.request") + + +@pytest.mark.parametrize( + "single_qubit_gate, is_available", + [ + (X, True), + (Y, True), + (Z, True), + (H, True), + (T, True), + (Tdag, True), + (S, True), + (Sdag, True), + (Allocate, True), + (Deallocate, True), + (SqrtX, True), + (Measure, True), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), True), + (R(0.5), False), + (Barrier, True), + (Entangle, False), + ], +) +def test_ionq_backend_is_available(single_qubit_gate, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + ionq_backend = _ionq.IonQBackend() + cmd = Command(eng, single_qubit_gate, (qubit1,)) + assert ionq_backend.is_available(cmd) is is_available + + +# IonQ supports up to 7 control qubits. +@pytest.mark.parametrize( + "num_ctrl_qubits, is_available", + [ + (0, True), + (1, True), + (2, True), + (3, True), + (4, True), + (5, True), + (6, True), + (7, True), + (8, False), + ], +) +def test_ionq_backend_is_available_control_not(num_ctrl_qubits, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(num_ctrl_qubits) + ionq_backend = _ionq.IonQBackend() + cmd = Command(eng, X, (qubit1,), controls=qureg) + assert ionq_backend.is_available(cmd) is is_available + + +def test_ionq_backend_is_available_negative_control(): + backend = _ionq.IonQBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) + + +def test_ionq_backend_init(): + """Test initialized backend has an empty circuit""" + backend = _ionq.IonQBackend(verbose=True, use_hardware=True) + assert hasattr(backend, '_circuit') + circuit = getattr(backend, '_circuit') + assert isinstance(circuit, list) + assert len(circuit) == 0 + + +def test_ionq_empty_circuit(): + """Test that empty circuits are still flushable.""" + backend = _ionq.IonQBackend(verbose=True) + eng = MainEngine(backend=backend) + eng.flush() + + +def test_ionq_no_circuit_executed(): + """Test that one can't retrieve probabilities if no circuit was run.""" + backend = _ionq.IonQBackend(verbose=True) + eng = MainEngine(backend=backend) + # no circuit has been executed -> raises exception + with pytest.raises(RuntimeError): + backend.get_probabilities([]) + eng.flush() + + +def test_ionq_get_probability(monkeypatch, mapper_factory): + """Test a shortcut for getting a specific state's probability""" + + def mock_retrieve(*args, **kwargs): + return { + 'nq': 3, + 'shots': 10, + 'output_probs': {'3': 0.4, '0': 0.6}, + 'meas_mapped': [0, 1], + 'meas_qubit_ids': [1, 2], + } + + monkeypatch.setattr(_ionq_http_client, "retrieve", mock_retrieve) + backend = _ionq.IonQBackend( + retrieve_execution="a3877d18-314f-46c9-86e7-316bc4dbe968", + verbose=True, + ) + eng = MainEngine(backend=backend, engine_list=[mapper_factory()]) + + unused_qubit = eng.allocate_qubit() # noqa: F841 + qureg = eng.allocate_qureg(2) + # entangle the qureg + Ry(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Ry(math.pi / 2) | qureg[0] + Rxx(math.pi / 2) | (qureg[0], qureg[1]) + Rx(7 * math.pi / 2) | qureg[0] + Ry(7 * math.pi / 2) | qureg[0] + Rx(7 * math.pi / 2) | qureg[1] + + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + assert eng.backend.get_probability('11', qureg) == pytest.approx(0.4) + assert eng.backend.get_probability('00', qureg) == pytest.approx(0.6) + + with pytest.raises(ValueError) as excinfo: + eng.backend.get_probability('111', qureg) + assert str(excinfo.value) == 'Desired state and register must be the same length!' + + +def test_ionq_get_probabilities(monkeypatch, mapper_factory): + """Test a shortcut for getting a specific state's probability""" + + def mock_retrieve(*args, **kwargs): + return { + 'nq': 3, + 'shots': 10, + 'output_probs': {'1': 0.4, '0': 0.6}, + 'meas_mapped': [1], + 'meas_qubit_ids': [1], + } + + monkeypatch.setattr(_ionq_http_client, "retrieve", mock_retrieve) + backend = _ionq.IonQBackend( + retrieve_execution="a3877d18-314f-46c9-86e7-316bc4dbe968", + verbose=True, + ) + eng = MainEngine(backend=backend, engine_list=[mapper_factory()]) + qureg = eng.allocate_qureg(2) + q0, q1 = qureg + H | q0 + CNOT | (q0, q1) + Measure | q1 + # run the circuit + eng.flush() + assert eng.backend.get_probability('01', qureg) == pytest.approx(0.4) + assert eng.backend.get_probability('00', qureg) == pytest.approx(0.6) + assert eng.backend.get_probability('1', [qureg[1]]) == pytest.approx(0.4) + assert eng.backend.get_probability('0', [qureg[1]]) == pytest.approx(0.6) + + +def test_ionq_invalid_command(): + """Test that this backend raises out with invalid commands.""" + + # Ph gate is not a valid gate + qb = WeakQubitRef(None, 1) + cmd = Command(None, gate=Ph(math.pi), qubits=[(qb,)]) + backend = _ionq.IonQBackend(verbose=True) + with pytest.raises(InvalidCommandError): + backend.receive([cmd]) + + +def test_ionq_sent_error(monkeypatch, mapper_factory): + """Test that errors on "send" will raise back out.""" + # patch send + type_error = TypeError() + mock_send = mock.MagicMock(side_effect=type_error) + monkeypatch.setattr(_ionq_http_client, "send", mock_send) + + backend = _ionq.IonQBackend() + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory()], + verbose=True, + ) + qubit = eng.allocate_qubit() + Rx(0.5) | qubit + with pytest.raises(Exception) as excinfo: + qubit[0].__del__() + eng.flush() + + # verbose=True on the engine re-raises errors instead of compacting them. + assert type_error is excinfo.value + + # atexit sends another FlushGate, therefore we remove the backend: + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + +def test_ionq_send_nonetype_response_error(monkeypatch, mapper_factory): + """Test that no return value from "send" will raise a runtime error.""" + # patch send + mock_send = mock.MagicMock(return_value=None) + monkeypatch.setattr(_ionq_http_client, "send", mock_send) + + backend = _ionq.IonQBackend() + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory()], + verbose=True, + ) + qubit = eng.allocate_qubit() + Rx(0.5) | qubit + with pytest.raises(RuntimeError) as excinfo: + eng.flush() + + # verbose=True on the engine re-raises errors instead of compacting them. + assert str(excinfo.value) == "Failed to submit job to the server!" + + # atexit sends another FlushGate, therefore we remove the backend: + dummy = DummyEngine() + dummy.is_last_engine = True + eng.next_engine = dummy + + +def test_ionq_retrieve(monkeypatch, mapper_factory): + """Test that initializing a backend with a jobid will fetch that job's results to use as its own""" + + def mock_retrieve(*args, **kwargs): + return { + 'nq': 3, + 'shots': 10, + 'output_probs': {'3': 0.4, '0': 0.6}, + 'meas_mapped': [0, 1], + 'meas_qubit_ids': [1, 2], + } + + monkeypatch.setattr(_ionq_http_client, "retrieve", mock_retrieve) + backend = _ionq.IonQBackend( + retrieve_execution="a3877d18-314f-46c9-86e7-316bc4dbe968", + verbose=True, + ) + eng = MainEngine(backend=backend, engine_list=[mapper_factory()]) + + unused_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + # entangle the qureg + Ry(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Ry(math.pi / 2) | qureg[0] + Rxx(math.pi / 2) | (qureg[0], qureg[1]) + Rx(7 * math.pi / 2) | qureg[0] + Ry(7 * math.pi / 2) | qureg[0] + Rx(7 * math.pi / 2) | qureg[1] + del unused_qubit + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[1]]) + assert prob_dict['11'] == pytest.approx(0.4) + assert prob_dict['00'] == pytest.approx(0.6) + + # Unknown qubit + invalid_qubit = [WeakQubitRef(eng, 10)] + probs = eng.backend.get_probabilities(invalid_qubit) + assert {'0': 1} == probs + + +def test_ionq_retrieve_nonetype_response_error(monkeypatch, mapper_factory): + """Test that initializing a backend with a jobid will fetch that job's results to use as its own""" + + def mock_retrieve(*args, **kwargs): + return None + + monkeypatch.setattr(_ionq_http_client, "retrieve", mock_retrieve) + backend = _ionq.IonQBackend( + retrieve_execution="a3877d18-314f-46c9-86e7-316bc4dbe968", + verbose=True, + ) + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory()], + verbose=True, + ) + + unused_qubit = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + # entangle the qureg + Ry(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Rx(math.pi / 2) | qureg[0] + Ry(math.pi / 2) | qureg[0] + Rxx(math.pi / 2) | (qureg[0], qureg[1]) + Rx(7 * math.pi / 2) | qureg[0] + Ry(7 * math.pi / 2) | qureg[0] + Rx(7 * math.pi / 2) | qureg[1] + del unused_qubit + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + with pytest.raises(RuntimeError) as excinfo: + eng.flush() + + exc = excinfo.value + expected_err = "Failed to retrieve job with id: 'a3877d18-314f-46c9-86e7-316bc4dbe968'!" + assert str(exc) == expected_err + + +def test_ionq_backend_functional_test(monkeypatch, mapper_factory): + """Test that the backend can handle a valid circuit with valid results.""" + expected = { + 'nq': 3, + 'shots': 10, + 'meas_mapped': [1, 2], + 'meas_qubit_ids': [1, 2], + 'circuit': [ + {'gate': 'ry', 'rotation': 0.5, 'targets': [1]}, + {'gate': 'rx', 'rotation': 0.5, 'targets': [1]}, + {'gate': 'rx', 'rotation': 0.5, 'targets': [1]}, + {'gate': 'ry', 'rotation': 0.5, 'targets': [1]}, + {'gate': 'xx', 'rotation': 0.5, 'targets': [1, 2]}, + {'gate': 'rx', 'rotation': 3.5, 'targets': [1]}, + {'gate': 'ry', 'rotation': 3.5, 'targets': [1]}, + {'gate': 'rx', 'rotation': 3.5, 'targets': [2]}, + ], + } + + def mock_send(*args, **kwargs): + assert args[0] == expected + return { + 'nq': 3, + 'shots': 10, + 'output_probs': {'3': 0.4, '0': 0.6}, + 'meas_mapped': [1, 2], + 'meas_qubit_ids': [1, 2], + } + + monkeypatch.setattr(_ionq_http_client, "send", mock_send) + backend = _ionq.IonQBackend(verbose=True, num_runs=10) + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory()], + verbose=True, + ) + unused_qubit = eng.allocate_qubit() # noqa: F841 + qureg = eng.allocate_qureg(2) + + # entangle the qureg + Ry(0.5) | qureg[0] + Rx(0.5) | qureg[0] + Rx(0.5) | qureg[0] + Ry(0.5) | qureg[0] + Rxx(0.5) | (qureg[0], qureg[1]) + Rx(3.5) | qureg[0] + Ry(3.5) | qureg[0] + Rx(3.5) | qureg[1] + All(Barrier) | qureg + # measure; should be all-0 or all-1 + All(Measure) | qureg + # run the circuit + eng.flush() + prob_dict = eng.backend.get_probabilities([qureg[0], qureg[1]]) + assert prob_dict['11'] == pytest.approx(0.4) + assert prob_dict['00'] == pytest.approx(0.6) + + +def test_ionq_backend_functional_aliases_test(monkeypatch, mapper_factory): + """Test that sub-classed or aliased gates are handled correctly.""" + # using alias gates, for coverage + expected = { + 'nq': 4, + 'shots': 10, + 'meas_mapped': [2, 3], + 'meas_qubit_ids': [2, 3], + 'circuit': [ + {'gate': 'x', 'targets': [0]}, + {'gate': 'x', 'targets': [1]}, + {'controls': [0], 'gate': 'x', 'targets': [2]}, + {'controls': [1], 'gate': 'x', 'targets': [2]}, + {'controls': [0, 1], 'gate': 'x', 'targets': [3]}, + {'gate': 's', 'targets': [2]}, + {'gate': 'si', 'targets': [3]}, + ], + } + + def mock_send(*args, **kwargs): + assert args[0] == expected + return { + 'nq': 4, + 'shots': 10, + 'output_probs': {'1': 0.9}, + 'meas_mapped': [2, 3], + } + + monkeypatch.setattr(_ionq_http_client, "send", mock_send) + backend = _ionq.IonQBackend(verbose=True, num_runs=10) + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory(9)], + verbose=True, + ) + # Do some stuff with a circuit. Get weird with it. + circuit = eng.allocate_qureg(4) + qubit1, qubit2, qubit3, qubit4 = circuit + All(X) | [qubit1, qubit2] + CNOT | (qubit1, qubit3) + CNOT | (qubit2, qubit3) + Toffoli | (qubit1, qubit2, qubit4) + Barrier | circuit + S | qubit3 + Sdag | qubit4 + All(Measure) | [qubit3, qubit4] + + # run the circuit + eng.flush() + prob_dict = eng.backend.get_probabilities([qubit3, qubit4]) + assert prob_dict['10'] == pytest.approx(0.9) + + +def test_ionq_no_midcircuit_measurement(monkeypatch, mapper_factory): + """Test that attempts to measure mid-circuit raise exceptions.""" + + def mock_send(*args, **kwargs): + return { + 'nq': 1, + 'shots': 10, + 'output_probs': {'0': 0.4, '1': 0.6}, + } + + monkeypatch.setattr(_ionq_http_client, "send", mock_send) + + # Create a backend to use with an engine. + backend = _ionq.IonQBackend(verbose=True, num_runs=10) + eng = MainEngine( + backend=backend, + engine_list=[mapper_factory()], + verbose=True, + ) + qubit = eng.allocate_qubit() + X | qubit + Measure | qubit + with pytest.raises(MidCircuitMeasurementError): + X | qubit + + # atexit sends another FlushGate, therefore we remove the backend: + dummy = DummyEngine() + dummy.is_last_engine = True + eng.active_qubits = [] + eng.next_engine = dummy diff --git a/projectq/backends/_printer.py b/projectq/backends/_printer.py index 7f00c677d..50c20eb5c 100755 --- a/projectq/backends/_printer.py +++ b/projectq/backends/_printer.py @@ -12,56 +12,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains a compiler engine which prints commands to stdout prior to sending -them on to the next engines (see CommandPrinter). -""" -import sys +"""Contains a compiler engine which prints commands to stdout prior to sending them on to the next engines.""" -from builtins import input +import sys from projectq.cengines import BasicEngine, LastEngineException +from projectq.meta import LogicalQubitIDTag, get_control_count from projectq.ops import FlushGate, Measure -from projectq.meta import get_control_count, LogicalQubitIDTag from projectq.types import WeakQubitRef class CommandPrinter(BasicEngine): """ - CommandPrinter is a compiler engine which prints commands to stdout prior - to sending them on to the next compiler engine. + Compiler engine that prints command to the standard output. + + CommandPrinter is a compiler engine which prints commands to stdout prior to sending them on to the next compiler + engine. """ - def __init__(self, accept_input=True, default_measure=False, - in_place=False): + + def __init__(self, accept_input=True, default_measure=False, in_place=False): """ Initialize a CommandPrinter. Args: - accept_input (bool): If accept_input is true, the printer queries - the user to input measurement results if the CommandPrinter is - the last engine. Otherwise, all measurements yield - default_measure. - default_measure (bool): Default measurement result (if - accept_input is False). - in_place (bool): If in_place is true, all output is written on the - same line of the terminal. + accept_input (bool): If accept_input is true, the printer queries the user to input measurement results if + the CommandPrinter is the last engine. Otherwise, all measurements yield default_measure. + default_measure (bool): Default measurement result (if accept_input is False). + in_place (bool): If in_place is true, all output is written on the same line of the terminal. """ - BasicEngine.__init__(self) + super().__init__() self._accept_input = accept_input self._default_measure = default_measure self._in_place = in_place def is_available(self, cmd): """ - Specialized implementation of is_available: Returns True if the - CommandPrinter is the last engine (since it can print any command). + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: Returns True if the CommandPrinter is the last engine (since it + can print any command). Args: - cmd (Command): Command of which to check availability (all - Commands can be printed). + cmd (Command): Command of which to check availability (all Commands can be printed). Returns: - availability (bool): True, unless the next engine cannot handle - the Command (if there is a next engine). + availability (bool): True, unless the next engine cannot handle the Command (if there is a next engine). """ try: return BasicEngine.is_available(self, cmd) @@ -70,45 +64,48 @@ def is_available(self, cmd): def _print_cmd(self, cmd): """ - Print a command or, if the command is a measurement instruction and - the CommandPrinter is the last engine in the engine pipeline: Query - the user for the measurement result (if accept_input = True) / Set - the result to 0 (if it's False). + Print a command. + + Print a command or, if the command is a measurement instruction and the CommandPrinter is the last engine in + the engine pipeline: Query the user for the measurement result (if accept_input = True) / Set the result to 0 + (if it's False). Args: cmd (Command): Command to print. """ if self.is_last_engine and cmd.gate == Measure: - assert(get_control_count(cmd) == 0) + if get_control_count(cmd) != 0: + raise ValueError('Cannot have control qubits with a measurement gate!') + print(cmd) for qureg in cmd.qubits: for qubit in qureg: if self._accept_input: - m = None - while m != '0' and m != '1' and m != 1 and m != 0: - prompt = ("Input measurement result (0 or 1) for" - " qubit " + str(qubit) + ": ") - m = input(prompt) + meas = None + while meas not in ('0', '1', 1, 0): + prompt = f"Input measurement result (0 or 1) for qubit {str(qubit)}: " + meas = input(prompt) else: - m = self._default_measure - m = int(m) + meas = self._default_measure + meas = int(meas) # Check there was a mapper and redirect result logical_id_tag = None for tag in cmd.tags: if isinstance(tag, LogicalQubitIDTag): logical_id_tag = tag if logical_id_tag is not None: - qubit = WeakQubitRef(qubit.engine, - logical_id_tag.logical_qubit_id) - self.main_engine.set_measurement_result(qubit, m) + qubit = WeakQubitRef(qubit.engine, logical_id_tag.logical_qubit_id) + self.main_engine.set_measurement_result(qubit, meas) else: - if self._in_place: - sys.stdout.write("\0\r\t\x1b[K" + str(cmd) + "\r") + if self._in_place: # pragma: no cover + sys.stdout.write(f'\x00\r\t\x1b[K{str(cmd)}\r') else: print(cmd) def receive(self, command_list): """ + Receive a list of commands. + Receive a list of commands from the previous engine, print the commands, and then send them on to the next engine. diff --git a/projectq/backends/_printer_test.py b/projectq/backends/_printer_test.py index 4c76425e2..658fb78c7 100755 --- a/projectq/backends/_printer_test.py +++ b/projectq/backends/_printer_test.py @@ -11,23 +11,21 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._printer.py. """ +import io + import pytest from projectq import MainEngine -from projectq.cengines import (DummyEngine, - InstructionFilter, - NotYetMeasuredError) +from projectq.backends import _printer +from projectq.cengines import DummyEngine, InstructionFilter, NotYetMeasuredError from projectq.meta import LogicalQubitIDTag -from projectq.ops import Allocate, Command, H, Measure, NOT, T +from projectq.ops import NOT, Allocate, Command, H, Measure, T from projectq.types import WeakQubitRef -from projectq.backends import _printer - def test_command_printer_is_available(): inline_cmd_printer = _printer.CommandPrinter() @@ -35,9 +33,9 @@ def test_command_printer_is_available(): def available_cmd(self, cmd): return cmd.gate == H + filter = InstructionFilter(available_cmd) - eng = MainEngine(backend=cmd_printer, - engine_list=[inline_cmd_printer, filter]) + eng = MainEngine(backend=cmd_printer, engine_list=[inline_cmd_printer, filter]) qubit = eng.allocate_qubit() cmd0 = Command(eng, H, (qubit,)) cmd1 = Command(eng, T, (qubit,)) @@ -50,17 +48,31 @@ def available_cmd(self, cmd): def test_command_printer_accept_input(monkeypatch): cmd_printer = _printer.CommandPrinter() eng = MainEngine(backend=cmd_printer, engine_list=[DummyEngine()]) - monkeypatch.setattr(_printer, "input", lambda x: 1) + + number_input = io.StringIO('1\n') + monkeypatch.setattr('sys.stdin', number_input) qubit = eng.allocate_qubit() Measure | qubit assert int(qubit) == 1 - monkeypatch.setattr(_printer, "input", lambda x: 0) + + number_input = io.StringIO('0\n') + monkeypatch.setattr('sys.stdin', number_input) qubit = eng.allocate_qubit() NOT | qubit Measure | qubit assert int(qubit) == 0 +def test_command_printer_measure_no_control(): + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + printer = _printer.CommandPrinter() + printer.is_last_engine = True + with pytest.raises(ValueError): + printer._print_cmd(Command(engine=None, gate=Measure, qubits=([qb1],), controls=[qb2])) + + def test_command_printer_no_input_default_measure(): cmd_printer = _printer.CommandPrinter(accept_input=False) eng = MainEngine(backend=cmd_printer, engine_list=[DummyEngine()]) @@ -75,8 +87,13 @@ def test_command_printer_measure_mapped_qubit(): qb1 = WeakQubitRef(engine=eng, idx=1) qb2 = WeakQubitRef(engine=eng, idx=2) cmd0 = Command(engine=eng, gate=Allocate, qubits=([qb1],)) - cmd1 = Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[], - tags=[LogicalQubitIDTag(2)]) + cmd1 = Command( + engine=eng, + gate=Measure, + qubits=([qb1],), + controls=[], + tags=[LogicalQubitIDTag(2)], + ) with pytest.raises(NotYetMeasuredError): int(qb1) with pytest.raises(NotYetMeasuredError): diff --git a/projectq/backends/_resource.py b/projectq/backends/_resource.py index 7d9b56117..fa73d74e6 100755 --- a/projectq/backends/_resource.py +++ b/projectq/backends/_resource.py @@ -11,63 +11,60 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Contains a compiler engine which counts the number of calls for each type of -gate used in a circuit, in addition to the max. number of active qubits. +Contain a compiler engine to calculate resource count used by a quantum circuit. + +A resource counter compiler engine counts the number of calls for each type of gate used in a circuit, in addition to +the max. number of active qubits. """ from projectq.cengines import BasicEngine, LastEngineException -from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import FlushGate, Deallocate, Allocate, Measure +from projectq.meta import LogicalQubitIDTag, get_control_count +from projectq.ops import Allocate, Deallocate, FlushGate, Measure from projectq.types import WeakQubitRef class ResourceCounter(BasicEngine): """ - ResourceCounter is a compiler engine which counts the number of gates and - max. number of active qubits. + ResourceCounter is a compiler engine which counts the number of gates and max. number of active qubits. Attributes: - gate_counts (dict): Dictionary of gate counts. - The keys are tuples of the form (cmd.gate, ctrl_cnt), where + gate_counts (dict): Dictionary of gate counts. The keys are tuples of the form (cmd.gate, ctrl_cnt), where ctrl_cnt is the number of control qubits. - gate_class_counts (dict): Dictionary of gate class counts. - The keys are tuples of the form (cmd.gate.__class__, ctrl_cnt), - where ctrl_cnt is the number of control qubits. - max_width (int): Maximal width (=max. number of active qubits at any - given point). + gate_class_counts (dict): Dictionary of gate class counts. The keys are tuples of the form + (cmd.gate.__class__, ctrl_cnt), where ctrl_cnt is the number of control qubits. + max_width (int): Maximal width (=max. number of active qubits at any given point). Properties: - depth_of_dag (int): It is the longest path in the directed - acyclic graph (DAG) of the program. + depth_of_dag (int): It is the longest path in the directed acyclic graph (DAG) of the program. """ + def __init__(self): """ Initialize a resource counter engine. Sets all statistics to zero. """ - BasicEngine.__init__(self) + super().__init__() self.gate_counts = {} self.gate_class_counts = {} self._active_qubits = 0 self.max_width = 0 # key: qubit id, depth of this qubit - self._depth_of_qubit = dict() + self._depth_of_qubit = {} self._previous_max_depth = 0 def is_available(self, cmd): """ - Specialized implementation of is_available: Returns True if the - ResourceCounter is the last engine (since it can count any command). + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: Returns True if the ResourceCounter is the last engine (since it + can count any command). Args: - cmd (Command): Command for which to check availability (all - Commands can be counted). + cmd (Command): Command for which to check availability (all Commands can be counted). Returns: - availability (bool): True, unless the next engine cannot handle - the Command (if there is a next engine). + availability (bool): True, unless the next engine cannot handle the Command (if there is a next engine). """ try: return BasicEngine.is_available(self, cmd) @@ -76,16 +73,14 @@ def is_available(self, cmd): @property def depth_of_dag(self): + """Return the depth of the DAG.""" if self._depth_of_qubit: current_max = max(self._depth_of_qubit.values()) return max(current_max, self._previous_max_depth) - else: - return self._previous_max_depth + return self._previous_max_depth - def _add_cmd(self, cmd): - """ - Add a gate to the count. - """ + def _add_cmd(self, cmd): # pylint: disable=too-many-branches + """Add a gate to the count.""" if cmd.gate == Allocate: self._active_qubits += 1 self._depth_of_qubit[cmd.qubits[0][0].id] = 0 @@ -104,8 +99,7 @@ def _add_cmd(self, cmd): if isinstance(tag, LogicalQubitIDTag): logical_id_tag = tag if logical_id_tag is not None: - qubit = WeakQubitRef(qubit.engine, - logical_id_tag.logical_qubit_id) + qubit = WeakQubitRef(qubit.engine, logical_id_tag.logical_qubit_id) self.main_engine.set_measurement_result(qubit, 0) else: qubit_ids = set() @@ -142,9 +136,8 @@ def __str__(self): Return the string representation of this ResourceCounter. Returns: - A summary (string) of resources used, including gates, number of - calls, and max. number of qubits that were active at the same - time. + A summary (string) of resources used, including gates, number of calls, and max. number of qubits that + were active at the same time. """ if len(self.gate_counts) > 0: gate_class_list = [] @@ -159,23 +152,26 @@ def __str__(self): gate_name = ctrl_cnt * "C" + str(gate) gate_list.append(gate_name + " : " + str(num)) - return ("Gate class counts:\n " + - "\n ".join(list(sorted(gate_class_list))) + - "\n\nGate counts:\n " + - "\n ".join(list(sorted(gate_list))) + - "\n\nMax. width (number of qubits) : " + - str(self.max_width) + ".") + return ( + "Gate class counts:\n " + + "\n ".join(sorted(gate_class_list)) + + "\n\nGate counts:\n " + + "\n ".join(sorted(gate_list)) + + "\n\nMax. width (number of qubits) : " + + str(self.max_width) + + "." + ) return "(No quantum resources used)" def receive(self, command_list): """ - Receive a list of commands from the previous engine, increases the - counters of the received commands, and then send them on to the next - engine. + Receive a list of commands. + + Receive a list of commands from the previous engine, increases the counters of the received commands, and then + send them on to the next engine. Args: - command_list (list): List of commands to receive (and - count). + command_list (list): List of commands to receive (and count). """ for cmd in command_list: if not cmd.gate == FlushGate(): diff --git a/projectq/backends/_resource_test.py b/projectq/backends/_resource_test.py index 664b687ad..426d2916e 100755 --- a/projectq/backends/_resource_test.py +++ b/projectq/backends/_resource_test.py @@ -11,22 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._resource.py. """ import pytest +from projectq.backends import ResourceCounter from projectq.cengines import DummyEngine, MainEngine, NotYetMeasuredError from projectq.meta import LogicalQubitIDTag -from projectq.ops import All, Allocate, CNOT, Command, H, Measure, QFT, Rz, X +from projectq.ops import CNOT, QFT, All, Allocate, Command, H, Measure, Rz, Rzz, X from projectq.types import WeakQubitRef -from projectq.backends import ResourceCounter - -class MockEngine(object): +class MockEngine: def is_available(self, cmd): return False @@ -46,8 +44,13 @@ def test_resource_counter_measurement(): qb1 = WeakQubitRef(engine=eng, idx=1) qb2 = WeakQubitRef(engine=eng, idx=2) cmd0 = Command(engine=eng, gate=Allocate, qubits=([qb1],)) - cmd1 = Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[], - tags=[LogicalQubitIDTag(2)]) + cmd1 = Command( + engine=eng, + gate=Measure, + qubits=([qb1],), + controls=[], + tags=[LogicalQubitIDTag(2)], + ) with pytest.raises(NotYetMeasuredError): int(qb1) with pytest.raises(NotYetMeasuredError): @@ -74,6 +77,7 @@ def test_resource_counter(): CNOT | (qubit1, qubit3) Rz(0.1) | qubit1 Rz(0.3) | qubit1 + Rzz(0.5) | qubit1 All(Measure) | qubit1 + qubit3 @@ -81,7 +85,7 @@ def test_resource_counter(): int(qubit1) assert resource_counter.max_width == 2 - assert resource_counter.depth_of_dag == 5 + assert resource_counter.depth_of_dag == 6 str_repr = str(resource_counter) assert str_repr.count(" HGate : 1") == 1 diff --git a/projectq/backends/_sim/__init__.py b/projectq/backends/_sim/__init__.py index d225b59e0..8898be48f 100755 --- a/projectq/backends/_sim/__init__.py +++ b/projectq/backends/_sim/__init__.py @@ -12,5 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._simulator import Simulator +"""ProjectQ module dedicated to simulation.""" + from ._classical_simulator import ClassicalSimulator +from ._simulator import Simulator diff --git a/projectq/backends/_sim/_classical_simulator.py b/projectq/backends/_sim/_classical_simulator.py index 821c01de9..f873d1096 100755 --- a/projectq/backends/_sim/_classical_simulator.py +++ b/projectq/backends/_sim/_classical_simulator.py @@ -12,18 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -A simulator that only permits classical operations, for faster/easier testing. -""" +"""A simulator that only permits classical operations, for faster/easier testing.""" from projectq.cengines import BasicEngine from projectq.meta import LogicalQubitIDTag -from projectq.ops import (XGate, - BasicMathGate, - Measure, - FlushGate, - Allocate, - Deallocate) +from projectq.ops import Allocate, BasicMathGate, Deallocate, FlushGate, Measure, XGate from projectq.types import WeakQubitRef @@ -31,18 +24,19 @@ class ClassicalSimulator(BasicEngine): """ A simple introspective simulator that only permits classical operations. - Allows allocation, deallocation, measuring (no-op), flushing (no-op), - controls, NOTs, and any BasicMathGate. Supports reading/writing directly - from/to bits and registers of bits. + Allows allocation, deallocation, measuring (no-op), flushing (no-op), controls, NOTs, and any + BasicMathGate. Supports reading/writing directly from/to bits and registers of bits. """ + def __init__(self): - BasicEngine.__init__(self) + """Initialize a ClassicalSimulator object.""" + super().__init__() self._state = 0 self._bit_positions = {} def _convert_logical_to_mapped_qubit(self, qubit): """ - Converts a qubit from a logical to a mapped qubit if there is a mapper. + Convert a qubit from a logical to a mapped qubit if there is a mapper. Args: qubit (projectq.types.Qubit): Logical quantum bit @@ -50,22 +44,17 @@ def _convert_logical_to_mapped_qubit(self, qubit): mapper = self.main_engine.mapper if mapper is not None: if qubit.id not in mapper.current_mapping: - raise RuntimeError("Unknown qubit id. " - "Please make sure you have called " - "eng.flush().") - return WeakQubitRef(qubit.engine, - mapper.current_mapping[qubit.id]) - else: - return qubit + raise RuntimeError("Unknown qubit id. Please make sure you have called eng.flush().") + return WeakQubitRef(qubit.engine, mapper.current_mapping[qubit.id]) + return qubit def read_bit(self, qubit): """ - Reads a bit. + Read a bit. Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. Args: qubit (projectq.types.Qubit): The bit to read. @@ -77,18 +66,20 @@ def read_bit(self, qubit): return self._read_mapped_bit(qubit) def _read_mapped_bit(self, mapped_qubit): - """ Internal use only. Does not change logical to mapped qubits.""" - p = self._bit_positions[mapped_qubit.id] - return (self._state >> p) & 1 + """ + Read a mapped bit value. + + For internal use only. Does not change logical to mapped qubits. + """ + return (self._state >> self._bit_positions[mapped_qubit.id]) & 1 def write_bit(self, qubit, value): """ Resets/sets a bit to the given value. Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. Args: qubit (projectq.types.Qubit): The bit to write. @@ -98,38 +89,39 @@ def write_bit(self, qubit, value): self._write_mapped_bit(qubit, value) def _write_mapped_bit(self, mapped_qubit, value): - """ Internal use only. Does not change logical to mapped qubits.""" - p = self._bit_positions[mapped_qubit.id] + """ + Write a mapped bit value. + + For internal use only. Does not change logical to mapped qubits. + """ + pos = self._bit_positions[mapped_qubit.id] if value: - self._state |= 1 << p + self._state |= 1 << pos else: - self._state &= ~(1 << p) + self._state &= ~(1 << pos) def _mask(self, qureg): """ - Returns a mask, to compare against the state, with bits from the - register set to 1 and other bits set to 0. + Return a mask, to compare against the state, with bits from the register set to 1 and other bits set to 0. Args: - qureg (projectq.types.Qureg): - The bits whose positions should be set. + qureg (projectq.types.Qureg): The bits whose positions should be set. Returns: int: The mask. """ - t = 0 - for q in qureg: - t |= 1 << self._bit_positions[q.id] - return t + mask = 0 + for qb in qureg: + mask |= 1 << self._bit_positions[qb.id] + return mask def read_register(self, qureg): """ - Reads a group of bits as a little-endian integer. + Read a group of bits as a little-endian integer. Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. Args: qureg (projectq.types.Qureg): @@ -144,24 +136,26 @@ def read_register(self, qureg): return self._read_mapped_register(new_qureg) def _read_mapped_register(self, mapped_qureg): - """ Internal use only. Does not change logical to mapped qubits.""" - t = 0 - for i in range(len(mapped_qureg)): - t |= self._read_mapped_bit(mapped_qureg[i]) << i - return t + """ + Read a value to some mapped quantum register. + + For internal use only. Does not change logical to mapped qubits. + """ + mask = 0 + for i, qubit in enumerate(mapped_qureg): + mask |= self._read_mapped_bit(qubit) << i + return mask def write_register(self, qureg, value): """ - Sets a group of bits to store a little-endian integer value. + Set a group of bits to store a little-endian integer value. Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. Args: - qureg (projectq.types.Qureg): - The bits to write, in little-endian order. + qureg (projectq.types.Qureg): The bits to write, in little-endian order. value (int): The integer value to store. Must fit in the register. """ new_qureg = [] @@ -170,44 +164,52 @@ def write_register(self, qureg, value): self._write_mapped_register(new_qureg, value) def _write_mapped_register(self, mapped_qureg, value): - """ Internal use only. Does not change logical to mapped qubits.""" + """ + Write a value to some mapped quantum register. + + For internal use only. Does not change logical to mapped qubits. + """ if value < 0 or value >= 1 << len(mapped_qureg): raise ValueError("Value won't fit in register.") - for i in range(len(mapped_qureg)): - self._write_mapped_bit(mapped_qureg[i], (value >> i) & 1) + for i, mapped_qubit in enumerate(mapped_qureg): + self._write_mapped_bit(mapped_qubit, (value >> i) & 1) def is_available(self, cmd): - return (cmd.gate == Measure or - cmd.gate == Allocate or - cmd.gate == Deallocate or - isinstance(cmd.gate, BasicMathGate) or - isinstance(cmd.gate, FlushGate) or - isinstance(cmd.gate, XGate)) + """Test whether a Command is supported by a compiler engine.""" + return ( + cmd.gate == Measure + or cmd.gate == Allocate + or cmd.gate == Deallocate + or isinstance(cmd.gate, (BasicMathGate, FlushGate, XGate)) + ) def receive(self, command_list): + """ + Receive a list of commands. + + This implementation simply forwards all commands to the next engine. + """ for cmd in command_list: self._handle(cmd) if not self.is_last_engine: self.send(command_list) - def _handle(self, cmd): + def _handle(self, cmd): # pylint: disable=too-many-branches,too-many-locals if isinstance(cmd.gate, FlushGate): return if cmd.gate == Measure: - for qr in cmd.qubits: - for qb in qr: + for qureg in cmd.qubits: + for qubit in qureg: # Check if a mapper assigned a different logical id logical_id_tag = None for tag in cmd.tags: if isinstance(tag, LogicalQubitIDTag): logical_id_tag = tag - log_qb = qb + log_qb = qubit if logical_id_tag is not None: - log_qb = WeakQubitRef(qb.engine, - logical_id_tag.logical_qubit_id) - self.main_engine.set_measurement_result( - log_qb, self._read_mapped_bit(qb)) + log_qb = WeakQubitRef(qubit.engine, logical_id_tag.logical_qubit_id) + self.main_engine.set_measurement_result(log_qb, self._read_mapped_bit(qubit)) return if cmd.gate == Allocate: @@ -219,22 +221,20 @@ def _handle(self, cmd): old_id = cmd.qubits[0][0].id pos = self._bit_positions[old_id] low = (1 << pos) - 1 + self._state = (self._state & low) | ((self._state >> 1) & ~low) - self._bit_positions = { - k: b - (0 if b < pos else 1) - for k, b in self._bit_positions.items() - } + self._bit_positions = {k: b - (0 if b < pos else 1) for k, b in self._bit_positions.items() if k != old_id} return controls_mask = self._mask(cmd.control_qubits) meets_controls = self._state & controls_mask == controls_mask if isinstance(cmd.gate, XGate): - assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 + if not (len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1): + raise ValueError('The XGate only accepts one qubit!') target = cmd.qubits[0][0] if meets_controls: - self._write_mapped_bit(target, - not self._read_mapped_bit(target)) + self._write_mapped_bit(target, not self._read_mapped_bit(target)) return if isinstance(cmd.gate, BasicMathGate): @@ -242,8 +242,7 @@ def _handle(self, cmd): ins = [self._read_mapped_register(reg) for reg in cmd.qubits] outs = cmd.gate.get_math_function(cmd.qubits)(ins) for reg, out in zip(cmd.qubits, outs): - self._write_mapped_register(reg, - out & ((1 << len(reg)) - 1)) + self._write_mapped_register(reg, out & ((1 << len(reg)) - 1)) return raise ValueError("Only support alloc/dealloc/measure/not/math ops.") diff --git a/projectq/backends/_sim/_classical_simulator_test.py b/projectq/backends/_sim/_classical_simulator_test.py index afcd266b9..67db700ec 100755 --- a/projectq/backends/_sim/_classical_simulator_test.py +++ b/projectq/backends/_sim/_classical_simulator_test.py @@ -15,17 +15,20 @@ import pytest from projectq import MainEngine -from projectq.ops import (All, Allocate, BasicMathGate, C, Command, Deallocate, - FlushGate, Measure, NOT, X, Y) -from projectq.cengines import (AutoReplacer, BasicMapperEngine, - DecompositionRuleSet, DummyEngine) -from ._simulator_test import mapper +from projectq.cengines import ( + AutoReplacer, + BasicMapperEngine, + DecompositionRuleSet, + DummyEngine, +) +from projectq.ops import NOT, All, BasicMathGate, C, Measure, X, Y from projectq.types import WeakQubitRef from ._classical_simulator import ClassicalSimulator +from ._simulator_test import mapper # noqa: F401 -def test_simulator_read_write(mapper): +def test_simulator_read_write(mapper): # noqa: F811 engine_list = [] if mapper is not None: engine_list.append(mapper) @@ -53,7 +56,7 @@ def test_simulator_read_write(mapper): assert sim.read_bit(b[0]) == 1 -def test_simulator_triangle_increment_cycle(mapper): +def test_simulator_triangle_increment_cycle(mapper): # noqa: F811 engine_list = [] if mapper is not None: engine_list.append(mapper) @@ -68,7 +71,7 @@ def test_simulator_triangle_increment_cycle(mapper): assert sim.read_register(a) == 0 -def test_simulator_bit_repositioning(mapper): +def test_simulator_bit_repositioning(mapper): # noqa: F811 engine_list = [] if mapper is not None: engine_list.append(mapper) @@ -82,18 +85,20 @@ def test_simulator_bit_repositioning(mapper): sim.write_register(c, 33) for q in b: eng.deallocate_qubit(q) + # Make sure that the qubit are marked as deleted + assert q.id == -1 assert sim.read_register(a) == 9 assert sim.read_register(c) == 33 -def test_simulator_arithmetic(mapper): +def test_simulator_arithmetic(mapper): # noqa: F811 class Offset(BasicMathGate): def __init__(self, amount): - BasicMathGate.__init__(self, lambda x: (x+amount,)) + super().__init__(lambda x: (x + amount,)) class Sub(BasicMathGate): def __init__(self): - BasicMathGate.__init__(self, lambda x, y: (x, y-x)) + super().__init__(lambda x, y: (x, y - x)) engine_list = [] if mapper is not None: @@ -136,7 +141,7 @@ def __init__(self): assert int(b[i]) == ((24 >> i) & 1) -def test_write_register_value_error_exception(mapper): +def test_write_register_value_error_exception(mapper): # noqa: F811 engine_list = [] if mapper is not None: engine_list.append(mapper) @@ -150,6 +155,15 @@ def test_write_register_value_error_exception(mapper): sim.write_register(a, 8) +def test_x_gate_invalid(): + sim = ClassicalSimulator() + eng = MainEngine(sim, [AutoReplacer(DecompositionRuleSet())]) + a = eng.allocate_qureg(2) + + with pytest.raises(ValueError): + X | a + + def test_available_gates(): sim = ClassicalSimulator() eng = MainEngine(sim, [AutoReplacer(DecompositionRuleSet())]) @@ -180,7 +194,7 @@ def test_wrong_gate(): def test_runtime_error(): sim = ClassicalSimulator() - mapper = BasicMapperEngine() + mapper = BasicMapperEngine() # noqa: F811 mapper.current_mapping = {} eng = MainEngine(sim, [mapper]) with pytest.raises(RuntimeError): diff --git a/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp b/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp index 02e0ead2b..7719f2d06 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp @@ -117,4 +117,3 @@ class aligned_allocator #if __cplusplus < 201103L #undef noexcept #endif - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp index 3ca031ab2..793a116fb 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp @@ -60,4 +60,3 @@ void kernel(V &psi, unsigned id0, M const& m, std::size_t ctrlmask) } } } - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp index b355acd32..e1a2c9a9b 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp @@ -69,4 +69,3 @@ void kernel(V &psi, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask } } } - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp index 7f20db0d4..2aac0f8a8 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp @@ -88,4 +88,3 @@ void kernel(V &psi, unsigned id2, unsigned id1, unsigned id0, M const& m, std::s } } } - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp index 9ff66eca3..5523a556c 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp @@ -129,4 +129,3 @@ void kernel(V &psi, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M co } } } - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp index 6fc6cf751..9cf781fa0 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp @@ -254,4 +254,3 @@ void kernel(V &psi, unsigned id4, unsigned id3, unsigned id2, unsigned id1, unsi } } } - diff --git a/projectq/backends/_sim/_cppkernels/intrin/kernels.hpp b/projectq/backends/_sim/_cppkernels/intrin/kernels.hpp index e59c94168..f592142da 100755 --- a/projectq/backends/_sim/_cppkernels/intrin/kernels.hpp +++ b/projectq/backends/_sim/_cppkernels/intrin/kernels.hpp @@ -32,4 +32,3 @@ #include "kernel3.hpp" #include "kernel4.hpp" #include "kernel5.hpp" - diff --git a/projectq/backends/_sim/_cppkernels/nointrin/kernel1.hpp b/projectq/backends/_sim/_cppkernels/nointrin/kernel1.hpp index bf3bf5a40..e1cd9e660 100755 --- a/projectq/backends/_sim/_cppkernels/nointrin/kernel1.hpp +++ b/projectq/backends/_sim/_cppkernels/nointrin/kernel1.hpp @@ -51,4 +51,3 @@ void kernel(V &psi, unsigned id0, M const& m, std::size_t ctrlmask) } } } - diff --git a/projectq/backends/_sim/_cppkernels/nointrin/kernel2.hpp b/projectq/backends/_sim/_cppkernels/nointrin/kernel2.hpp index 98809d97c..879fa8572 100755 --- a/projectq/backends/_sim/_cppkernels/nointrin/kernel2.hpp +++ b/projectq/backends/_sim/_cppkernels/nointrin/kernel2.hpp @@ -60,4 +60,3 @@ void kernel(V &psi, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask } } } - diff --git a/projectq/backends/_sim/_cppkernels/nointrin/kernel3.hpp b/projectq/backends/_sim/_cppkernels/nointrin/kernel3.hpp index 8d79f55fc..05b68afef 100755 --- a/projectq/backends/_sim/_cppkernels/nointrin/kernel3.hpp +++ b/projectq/backends/_sim/_cppkernels/nointrin/kernel3.hpp @@ -85,4 +85,3 @@ void kernel(V &psi, unsigned id2, unsigned id1, unsigned id0, M const& m, std::s } } } - diff --git a/projectq/backends/_sim/_cppkernels/nointrin/kernel4.hpp b/projectq/backends/_sim/_cppkernels/nointrin/kernel4.hpp index 9e0e9ee51..b12424a7c 100755 --- a/projectq/backends/_sim/_cppkernels/nointrin/kernel4.hpp +++ b/projectq/backends/_sim/_cppkernels/nointrin/kernel4.hpp @@ -150,4 +150,3 @@ void kernel(V &psi, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M co } } } - diff --git a/projectq/backends/_sim/_cppkernels/nointrin/kernel5.hpp b/projectq/backends/_sim/_cppkernels/nointrin/kernel5.hpp index 9480eaa65..a3e47f10f 100755 --- a/projectq/backends/_sim/_cppkernels/nointrin/kernel5.hpp +++ b/projectq/backends/_sim/_cppkernels/nointrin/kernel5.hpp @@ -371,4 +371,3 @@ void kernel(V &psi, unsigned id4, unsigned id3, unsigned id2, unsigned id1, unsi } } } - diff --git a/projectq/backends/_sim/_cppkernels/simulator.hpp b/projectq/backends/_sim/_cppkernels/simulator.hpp index c4e611eba..1a84723f7 100755 --- a/projectq/backends/_sim/_cppkernels/simulator.hpp +++ b/projectq/backends/_sim/_cppkernels/simulator.hpp @@ -38,7 +38,7 @@ class Simulator{ public: using calc_type = double; using complex_type = std::complex; - using StateVector = std::vector>; + using StateVector = std::vector>; using Map = std::map; using RndEngine = std::mt19937; using Term = std::vector>; @@ -55,11 +55,18 @@ class Simulator{ void allocate_qubit(unsigned id){ if (map_.count(id) == 0){ map_[id] = N_++; - auto newvec = StateVector(1UL << N_); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid large memory allocations + if( tmpBuff1_.capacity() >= (1UL << N_) ) + std::swap(newvec, tmpBuff1_); + newvec.resize(1UL << N_); +#pragma omp parallel for schedule(static) for (std::size_t i = 0; i < newvec.size(); ++i) newvec[i] = (i < vec_.size())?vec_[i]:0.; - vec_ = std::move(newvec); + std::swap(vec_, newvec); + // recycle large memory + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); } else throw(std::runtime_error( @@ -113,12 +120,18 @@ class Simulator{ } } else{ - StateVector newvec((1UL << (N_-1))); - #pragma omp parallel for schedule(static) + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= (1UL << (N_-1)) ) + std::swap(tmpBuff1_, newvec); + newvec.resize((1UL << (N_-1))); + #pragma omp parallel for schedule(static) if(0) for (std::size_t i = 0; i < vec_.size(); i += 2*delta) std::copy_n(&vec_[i + static_cast(value)*delta], delta, &newvec[i/2]); - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) + std::swap(tmpBuff1_, tmpBuff2_); for (auto& p : map_){ if (p.second > pos) @@ -189,8 +202,8 @@ class Simulator{ } template - void apply_controlled_gate(M const& m, std::vector ids, - std::vector ctrl){ + void apply_controlled_gate(M const& m, const std::vector& ids, + const std::vector& ctrl){ auto fused_gates = fused_gates_; fused_gates.insert(m, ids, ctrl); @@ -209,8 +222,8 @@ class Simulator{ } template - void emulate_math(F const& f, QuReg quregs, std::vector ctrl, - unsigned num_threads=1){ + void emulate_math(F const& f, QuReg quregs, const std::vector& ctrl, + bool parallelize = false){ run(); auto ctrlmask = get_control_mask(ctrl); @@ -218,37 +231,76 @@ class Simulator{ for (unsigned j = 0; j < quregs[i].size(); ++j) quregs[i][j] = map_[quregs[i][j]]; - StateVector newvec(vec_.size(), 0.); - std::vector res(quregs.size()); - - #pragma omp parallel for schedule(static) firstprivate(res) num_threads(num_threads) - for (std::size_t i = 0; i < vec_.size(); ++i){ - if ((ctrlmask&i) == ctrlmask){ - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - res[qr_i] = 0; - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) - res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; - } - f(res); - auto new_i = i; - for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ - for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ - if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) - new_i ^= (1UL << quregs[qr_i][qb_i]); - } - } - newvec[new_i] += vec_[i]; - } - else - newvec[i] += vec_[i]; + StateVector newvec; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(newvec, tmpBuff1_); + newvec.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); i++) + newvec[i] = 0; + +//#pragma omp parallel reduction(+:newvec[:newvec.size()]) if(parallelize) // requires OpenMP 4.5 + { + std::vector res(quregs.size()); + //#pragma omp for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + if ((ctrlmask&i) == ctrlmask){ + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + res[qr_i] = 0; + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) + res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; + } + f(res); + auto new_i = i; + for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ + for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ + if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) + new_i ^= (1UL << quregs[qr_i][qb_i]); + } + } + newvec[new_i] += vec_[i]; + } + else + newvec[i] += vec_[i]; + } } - vec_ = std::move(newvec); + std::swap(vec_, newvec); + std::swap(tmpBuff1_, newvec); + } + + // faster version without calling python + template + inline void emulate_math_addConstant(int a, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a](std::vector &res){for(auto& x: res) x = x + a;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_addConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x + a) % N;}, quregs, ctrl, true); + } + + // faster version without calling python + template + inline void emulate_math_multiplyByConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) + { + emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x * a) % N;}, quregs, ctrl, true); } calc_type get_expectation_value(TermsDict const& td, std::vector const& ids){ run(); calc_type expectation = 0.; - auto current_state = vec_; + + StateVector current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, current_state); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i) + current_state[i] = vec_[i]; + for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -260,17 +312,29 @@ class Simulator{ auto const a2 = std::real(vec_[i]); auto const b2 = std::imag(vec_[i]); delta += a1 * a2 - b1 * b2; + // reset vec_ + vec_[i] = current_state[i]; } expectation += coefficient * delta; - vec_ = current_state; } + std::swap(current_state, tmpBuff1_); return expectation; } void apply_qubit_operator(ComplexTermsDict const& td, std::vector const& ids){ run(); - auto new_state = StateVector(vec_.size(), 0.); - auto current_state = vec_; + StateVector new_state, current_state; // avoid costly memory reallocations + if( tmpBuff1_.capacity() >= vec_.size() ) + std::swap(tmpBuff1_, new_state); + if( tmpBuff2_.capacity() >= vec_.size() ) + std::swap(tmpBuff2_, current_state); + new_state.resize(vec_.size()); + current_state.resize(vec_.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < vec_.size(); ++i){ + new_state[i] = 0; + current_state[i] = vec_[i]; + } for (auto const& term : td){ auto const& coefficient = term.second; apply_term(term.first, ids, {}); @@ -280,7 +344,9 @@ class Simulator{ vec_[i] = current_state[i]; } } - vec_ = std::move(new_state); + std::swap(vec_, new_state); + std::swap(tmpBuff1_, new_state); + std::swap(tmpBuff2_, current_state); } calc_type get_probability(std::vector const& bit_string, @@ -389,7 +455,8 @@ class Simulator{ void collapse_wavefunction(std::vector const& ids, std::vector const& values){ run(); - assert(ids.size() == values.size()); + if (ids.size() != values.size()) + throw(std::length_error("collapse_wavefunction(): ids and values size mismatch")); if (!check_ids(ids)) throw(std::runtime_error("collapse_wavefunction(): Unknown qubit id(s) provided. Try calling eng.flush() before invoking this function.")); std::size_t mask = 0, val = 0; @@ -452,6 +519,8 @@ class Simulator{ #pragma omp parallel kernel(vec_, ids[4], ids[3], ids[2], ids[1], ids[0], m, ctrlmask); break; + default: + throw std::invalid_argument("Gates with more than 5 qubits are not supported!"); } fused_gates_ = Fusion(); @@ -500,6 +569,12 @@ class Simulator{ unsigned fusion_qubits_min_, fusion_qubits_max_; RndEngine rnd_eng_; std::function rng_; + + // large array buffers to avoid costly reallocations + static StateVector tmpBuff1_, tmpBuff2_; }; +Simulator::StateVector Simulator::tmpBuff1_; +Simulator::StateVector Simulator::tmpBuff2_; + #endif diff --git a/projectq/backends/_sim/_cppsim.cpp b/projectq/backends/_sim/_cppsim.cpp index 74498d4e2..2402812bf 100755 --- a/projectq/backends/_sim/_cppsim.cpp +++ b/projectq/backends/_sim/_cppsim.cpp @@ -34,13 +34,14 @@ template void emulate_math_wrapper(Simulator &sim, py::function const& pyfunc, QR const& qr, std::vector const& ctrls){ auto f = [&](std::vector& x) { pybind11::gil_scoped_acquire acquire; - x = std::move(pyfunc(x).cast>()); + x = pyfunc(x).cast>(); }; pybind11::gil_scoped_release release; sim.emulate_math(f, qr, ctrls); } -PYBIND11_PLUGIN(_cppsim) { - py::module m("_cppsim", "_cppsim"); + +PYBIND11_MODULE(_cppsim, m) +{ py::class_(m, "Simulator") .def(py::init()) .def("allocate_qubit", &Simulator::allocate_qubit) @@ -50,6 +51,9 @@ PYBIND11_PLUGIN(_cppsim) { .def("measure_qubits", &Simulator::measure_qubits_return) .def("apply_controlled_gate", &Simulator::apply_controlled_gate) .def("emulate_math", &emulate_math_wrapper) + .def("emulate_math_addConstant", &Simulator::emulate_math_addConstant) + .def("emulate_math_addConstantModN", &Simulator::emulate_math_addConstantModN) + .def("emulate_math_multiplyByConstantModN", &Simulator::emulate_math_multiplyByConstantModN) .def("get_expectation_value", &Simulator::get_expectation_value) .def("apply_qubit_operator", &Simulator::apply_qubit_operator) .def("emulate_time_evolution", &Simulator::emulate_time_evolution) @@ -60,5 +64,4 @@ PYBIND11_PLUGIN(_cppsim) { .def("run", &Simulator::run) .def("cheat", &Simulator::cheat) ; - return m.ptr(); } diff --git a/projectq/backends/_sim/_pysim.py b/projectq/backends/_sim/_pysim.py index 4faf811f6..f6fb58fd5 100755 --- a/projectq/backends/_sim/_pysim.py +++ b/projectq/backends/_sim/_pysim.py @@ -13,58 +13,59 @@ # limitations under the License. """ -Contains a (slow) Python simulator. +A (slow) Python simulator. Please compile the c++ simulator for large-scale simulations. """ +import os import random + import numpy as _np +_USE_REFCHECK = True +if 'CI' in os.environ: # pragma: no cover + _USE_REFCHECK = False + -class Simulator(object): +class Simulator: """ Python implementation of a quantum computer simulator. - This Simulator can be used as a backup if compiling the c++ simulator is - not an option (for some reason). It has the same features but is much - slower, so please consider building the c++ version for larger experiments. + This Simulator can be used as a backup if compiling the c++ simulator is not an option (for some reason). It has the + same features but is much slower, so please consider building the c++ version for larger experiments. """ - def __init__(self, rnd_seed, *args, **kwargs): + + def __init__(self, rnd_seed, *args, **kwargs): # pylint: disable=unused-argument """ Initialize the simulator. Args: rnd_seed (int): Seed to initialize the random number generator. - args: Dummy argument to allow an interface identical to the c++ - simulator. + args: Dummy argument to allow an interface identical to the c++ simulator. kwargs: Same as args. """ random.seed(rnd_seed) self._state = _np.ones(1, dtype=_np.complex128) - self._map = dict() + self._map = {} self._num_qubits = 0 print("(Note: This is the (slow) Python simulator.)") def cheat(self): """ - Return the qubit index to bit location map and the corresponding state - vector. + Return the qubit index to bit location map and the corresponding state vector. - This function can be used to measure expectation values more - efficiently (emulation). + This function can be used to measure expectation values more efficiently (emulation). Returns: - A tuple where the first entry is a dictionary mapping qubit indices - to bit-locations and the second entry is the corresponding state - vector + A tuple where the first entry is a dictionary mapping qubit indices to bit-locations and the second entry is + the corresponding state vector """ return (self._map, self._state) def measure_qubits(self, ids): """ - Measure the qubits with IDs ids and return a list of measurement - outcomes (True/False). + Measure the qubits with IDs ids and return a list of measurement outcomes (True/False). Args: ids (list): List of qubit IDs to measure. @@ -72,10 +73,10 @@ def measure_qubits(self, ids): Returns: List of measurement results (containing either True or False). """ - P = random.random() - val = 0. + random_outcome = random.random() + val = 0.0 i_picked = 0 - while val < P and i_picked < len(self._state): + while val < random_outcome and i_picked < len(self._state): val += _np.abs(self._state[i_picked]) ** 2 i_picked += 1 @@ -86,90 +87,86 @@ def measure_qubits(self, ids): mask = 0 val = 0 - for i in range(len(pos)): - res[i] = (((i_picked >> pos[i]) & 1) == 1) - mask |= (1 << pos[i]) - val |= ((res[i] & 1) << pos[i]) + for i, _pos in enumerate(pos): + res[i] = ((i_picked >> _pos) & 1) == 1 + mask |= 1 << _pos + val |= (res[i] & 1) << _pos - nrm = 0. - for i in range(len(self._state)): + nrm = 0.0 + for i, _state in enumerate(self._state): if (mask & i) != val: - self._state[i] = 0. + self._state[i] = 0.0 else: - nrm += _np.abs(self._state[i]) ** 2 + nrm += _np.abs(_state) ** 2 - self._state *= 1. / _np.sqrt(nrm) + self._state *= 1.0 / _np.sqrt(nrm) return res - def allocate_qubit(self, ID): + def allocate_qubit(self, qubit_id): """ Allocate a qubit. Args: - ID (int): ID of the qubit which is being allocated. + qubit_id (int): ID of the qubit which is being allocated. """ - self._map[ID] = self._num_qubits + self._map[qubit_id] = self._num_qubits self._num_qubits += 1 - self._state.resize(1 << self._num_qubits) + self._state.resize(1 << self._num_qubits, refcheck=_USE_REFCHECK) - def get_classical_value(self, ID, tol=1.e-10): + def get_classical_value(self, qubit_id, tol=1.0e-10): """ - Return the classical value of a classical bit (i.e., a qubit which has - been measured / uncomputed). + Return the classical value of a classical bit (i.e., a qubit which has been measured / uncomputed). Args: - ID (int): ID of the qubit of which to get the classical value. - tol (float): Tolerance for numerical errors when determining - whether the qubit is indeed classical. + qubit_it (int): ID of the qubit of which to get the classical value. + tol (float): Tolerance for numerical errors when determining whether the qubit is indeed classical. Raises: - RuntimeError: If the qubit is in a superposition, i.e., has not - been measured / uncomputed. + RuntimeError: If the qubit is in a superposition, i.e., has not been measured / uncomputed. """ - pos = self._map[ID] - up = down = False + pos = self._map[qubit_id] + state_up = state_down = False for i in range(0, len(self._state), (1 << (pos + 1))): for j in range(0, (1 << pos)): if _np.abs(self._state[i + j]) > tol: - up = True + state_up = True if _np.abs(self._state[i + j + (1 << pos)]) > tol: - down = True - if up and down: - raise RuntimeError("Qubit has not been measured / " - "uncomputed. Cannot access its " - "classical value and/or deallocate a " - "qubit in superposition!") - return down - - def deallocate_qubit(self, ID): + state_down = True + if state_up and state_down: + raise RuntimeError( + "Qubit has not been measured / " + "uncomputed. Cannot access its " + "classical value and/or deallocate a " + "qubit in superposition!" + ) + return state_down + + def deallocate_qubit(self, qubit_id): """ Deallocate a qubit (if it has been measured / uncomputed). Args: - ID (int): ID of the qubit to deallocate. + qubit_id (int): ID of the qubit to deallocate. Raises: - RuntimeError: If the qubit is in a superposition, i.e., has not - been measured / uncomputed. + RuntimeError: If the qubit is in a superposition, i.e., has not been measured / uncomputed. """ - pos = self._map[ID] + pos = self._map[qubit_id] - cv = self.get_classical_value(ID) + classical_value = self.get_classical_value(qubit_id) - newstate = _np.zeros((1 << (self._num_qubits - 1)), - dtype=_np.complex128) + newstate = _np.zeros((1 << (self._num_qubits - 1)), dtype=_np.complex128) k = 0 - for i in range((1 << pos) * int(cv), len(self._state), - (1 << (pos + 1))): - newstate[k:k + (1 << pos)] = self._state[i:i + (1 << pos)] - k += (1 << pos) + for i in range((1 << pos) * int(classical_value), len(self._state), (1 << (pos + 1))): + newstate[k : k + (1 << pos)] = self._state[i : i + (1 << pos)] # noqa: E203 + k += 1 << pos - newmap = dict() + newmap = {} for key, value in self._map.items(): if value > pos: newmap[key] = value - 1 - elif key != ID: + elif key != qubit_id: newmap[key] = value self._map = newmap self._state = newstate @@ -185,18 +182,17 @@ def _get_control_mask(self, ctrlids): mask = 0 for ctrlid in ctrlids: ctrlpos = self._map[ctrlid] - mask |= (1 << ctrlpos) + mask |= 1 << ctrlpos return mask - def emulate_math(self, f, qubit_ids, ctrlqubit_ids): + def emulate_math(self, func, qubit_ids, ctrlqubit_ids): # pylint: disable=too-many-locals """ Emulate a math function (e.g., BasicMathGate). Args: - f (function): Function executing the operation to emulate. - qubit_ids (list>): List of lists of qubit IDs to which - the gate is being applied. Every gate is applied to a tuple of - quantum registers, which corresponds to this 'list of lists'. + func (function): Function executing the operation to emulate. + qubit_ids (list>): List of lists of qubit IDs to which the gate is being applied. Every gate is + applied to a tuple of quantum registers, which corresponds to this 'list of lists'. ctrlqubit_ids (list): List of control qubit ids. """ mask = self._get_control_mask(ctrlqubit_ids) @@ -208,24 +204,22 @@ def emulate_math(self, f, qubit_ids, ctrlqubit_ids): qb_locs[-1].append(self._map[qubit_id]) newstate = _np.zeros_like(self._state) - for i in range(0, len(self._state)): + for i, state in enumerate(self._state): if (mask & i) == mask: arg_list = [0] * len(qb_locs) - for qr_i in range(len(qb_locs)): - for qb_i in range(len(qb_locs[qr_i])): - arg_list[qr_i] |= (((i >> qb_locs[qr_i][qb_i]) & 1) << - qb_i) + for qr_i, qr_loc in enumerate(qb_locs): + for qb_i, qb_loc in enumerate(qr_loc): + arg_list[qr_i] |= ((i >> qb_loc) & 1) << qb_i - res = f(arg_list) + res = func(arg_list) new_i = i - for qr_i in range(len(qb_locs)): - for qb_i in range(len(qb_locs[qr_i])): - if not (((new_i >> qb_locs[qr_i][qb_i]) & 1) == - ((res[qr_i] >> qb_i) & 1)): - new_i ^= (1 << qb_locs[qr_i][qb_i]) - newstate[new_i] = self._state[i] + for qr_i, qr_loc in enumerate(qb_locs): + for qb_i, qb_loc in enumerate(qr_loc): + if not ((new_i >> qb_loc) & 1) == ((res[qr_i] >> qb_i) & 1): + new_i ^= 1 << qb_loc + newstate[new_i] = state else: - newstate[i] = self._state[i] + newstate[i] = state self._state = newstate @@ -240,9 +234,9 @@ def get_expectation_value(self, terms_dict, ids): Returns: Expectation value """ - expectation = 0. + expectation = 0.0 current_state = _np.copy(self._state) - for (term, coefficient) in terms_dict: + for term, coefficient in terms_dict: self._apply_term(term, ids) delta = coefficient * _np.vdot(current_state, self._state).real expectation += delta @@ -259,7 +253,7 @@ def apply_qubit_operator(self, terms_dict, ids): """ new_state = _np.zeros_like(self._state) current_state = _np.copy(self._state) - for (term, coefficient) in terms_dict: + for term, coefficient in terms_dict: self._apply_term(term, ids) self._state *= coefficient new_state += self._state @@ -268,8 +262,7 @@ def apply_qubit_operator(self, terms_dict, ids): def get_probability(self, bit_string, ids): """ - Return the probability of the outcome `bit_string` when measuring - the qubits given by the list of ids. + Return the probability of the outcome `bit_string` when measuring the qubits given by the list of ids. Args: bit_string (list[bool|int]): Measurement outcome. @@ -281,134 +274,122 @@ def get_probability(self, bit_string, ids): Raises: RuntimeError if an unknown qubit id was provided. """ - for i in range(len(ids)): - if ids[i] not in self._map: - raise RuntimeError("get_probability(): Unknown qubit id. " - "Please make sure you have called " - "eng.flush().") + for qubit_id in ids: + if qubit_id not in self._map: + raise RuntimeError("get_probability(): Unknown qubit id. Please make sure you have called eng.flush().") mask = 0 bit_str = 0 - for i in range(len(ids)): - mask |= (1 << self._map[ids[i]]) - bit_str |= (bit_string[i] << self._map[ids[i]]) - probability = 0. - for i in range(len(self._state)): + for i, qubit_id in enumerate(ids): + mask |= 1 << self._map[qubit_id] + bit_str |= bit_string[i] << self._map[qubit_id] + probability = 0.0 + for i, state in enumerate(self._state): if (i & mask) == bit_str: - e = self._state[i] - probability += e.real**2 + e.imag**2 + probability += state.real**2 + state.imag**2 return probability def get_amplitude(self, bit_string, ids): """ Return the probability amplitude of the supplied `bit_string`. + The ordering is given by the list of qubit ids. Args: bit_string (list[bool|int]): Computational basis state - ids (list[int]): List of qubit ids determining the - ordering. Must contain all allocated qubits. + ids (list[int]): List of qubit ids determining the ordering. Must contain all allocated qubits. Returns: Probability amplitude of the provided bit string. Raises: - RuntimeError if the second argument is not a permutation of all - allocated qubits. + RuntimeError if the second argument is not a permutation of all allocated qubits. """ if not set(ids) == set(self._map): - raise RuntimeError("The second argument to get_amplitude() must" - " be a permutation of all allocated qubits. " - "Please make sure you have called " - "eng.flush().") + raise RuntimeError( + "The second argument to get_amplitude() must be a permutation of all allocated qubits. " + "Please make sure you have called eng.flush()." + ) index = 0 - for i in range(len(ids)): - index |= (bit_string[i] << self._map[ids[i]]) + for i, qubit_id in enumerate(ids): + index |= bit_string[i] << self._map[qubit_id] return self._state[index] - def emulate_time_evolution(self, terms_dict, time, ids, ctrlids): + def emulate_time_evolution(self, terms_dict, time, ids, ctrlids): # pylint: disable=too-many-locals """ - Applies exp(-i*time*H) to the wave function, i.e., evolves under - the Hamiltonian H for a given time. The terms in the Hamiltonian - are not required to commute. + Apply exp(-i*time*H) to the wave function, i.e., evolves under the Hamiltonian H for a given time. + + The terms in the Hamiltonian are not required to commute. - This function computes the action of the matrix exponential using - ideas from Al-Mohy and Higham, 2011. + This function computes the action of the matrix exponential using ideas from Al-Mohy and Higham, 2011. TODO: Implement better estimates for s. Args: - terms_dict (dict): Operator dictionary (see QubitOperator.terms) - defining the Hamiltonian. + terms_dict (dict): Operator dictionary (see QubitOperator.terms) defining the Hamiltonian. time (scalar): Time to evolve for ids (list): A list of qubit IDs to which to apply the evolution. ctrlids (list): A list of control qubit IDs. """ - # Determine the (normalized) trace, which is nonzero only for identity - # terms: - tr = sum([c for (t, c) in terms_dict if len(t) == 0]) + # Determine the (normalized) trace, which is nonzero only for identity terms: + trace = sum(c for (t, c) in terms_dict if len(t) == 0) terms_dict = [(t, c) for (t, c) in terms_dict if len(t) > 0] - op_nrm = abs(time) * sum([abs(c) for (_, c) in terms_dict]) + op_nrm = abs(time) * sum(abs(c) for (_, c) in terms_dict) # rescale the operator by s: - s = int(op_nrm + 1.) - correction = _np.exp(-1j * time * tr / float(s)) + scale = int(op_nrm + 1.0) + correction = _np.exp(-1j * time * trace / float(scale)) output_state = _np.copy(self._state) mask = self._get_control_mask(ctrlids) - for i in range(s): + for _ in range(scale): j = 0 - nrm_change = 1. - while nrm_change > 1.e-12: - coeff = (-time * 1j) / float(s * (j + 1)) + nrm_change = 1.0 + while nrm_change > 1.0e-12: + coeff = (-time * 1j) / float(scale * (j + 1)) current_state = _np.copy(self._state) update = 0j - for t, c in terms_dict: - self._apply_term(t, ids) - self._state *= c + for term, tcoeff in terms_dict: + self._apply_term(term, ids) + self._state *= tcoeff update += self._state self._state = _np.copy(current_state) update *= coeff self._state = update - for i in range(len(update)): - if (i & mask) == mask: - output_state[i] += update[i] + for k, _update in enumerate(update): + if (k & mask) == mask: + output_state[k] += _update nrm_change = _np.linalg.norm(update) j += 1 - for i in range(len(update)): - if (i & mask) == mask: - output_state[i] *= correction + for k in range(len(update)): + if (k & mask) == mask: + output_state[k] *= correction self._state = _np.copy(output_state) - def apply_controlled_gate(self, m, ids, ctrlids): + def apply_controlled_gate(self, matrix, ids, ctrlids): """ - Applies the k-qubit gate matrix m to the qubits with indices ids, - using ctrlids as control qubits. + Apply the k-qubit gate matrix m to the qubits with indices ids, using ctrlids as control qubits. Args: - m (list[list]): 2^k x 2^k complex matrix describing the k-qubit - gate. - ids (list): A list containing the qubit IDs to which to apply the - gate. - ctrlids (list): A list of control qubit IDs (i.e., the gate is - only applied where these qubits are 1). + matrix (list[list]): 2^k x 2^k complex matrix describing the k-qubit gate. + ids (list): A list containing the qubit IDs to which to apply the gate. + ctrlids (list): A list of control qubit IDs (i.e., the gate is only applied where these qubits are 1). """ mask = self._get_control_mask(ctrlids) - if len(m) == 2: + if len(matrix) == 2: pos = self._map[ids[0]] - self._single_qubit_gate(m, pos, mask) + self._single_qubit_gate(matrix, pos, mask) else: pos = [self._map[ID] for ID in ids] - self._multi_qubit_gate(m, pos, mask) + self._multi_qubit_gate(matrix, pos, mask) - def _single_qubit_gate(self, m, pos, mask): + def _single_qubit_gate(self, matrix, pos, mask): """ - Applies the single qubit gate matrix m to the qubit at position `pos` - using `mask` to identify control qubits. + Apply the single qubit gate matrix m to the qubit at position `pos` using `mask` to identify control qubits. Args: - m (list[list]): 2x2 complex matrix describing the single-qubit - gate. + matrix (list[list]): 2x2 complex matrix describing the single-qubit gate. pos (int): Bit-position of the qubit. mask (int): Bit-mask where set bits indicate control qubits. """ - def kernel(u, d, m): + + def kernel(u, d, m): # pylint: disable=invalid-name return u * m[0][0] + d * m[0][1], u * m[1][0] + d * m[1][1] for i in range(0, len(self._state), (1 << (pos + 1))): @@ -416,43 +397,38 @@ def kernel(u, d, m): if ((i + j) & mask) == mask: id1 = i + j id2 = id1 + (1 << pos) - self._state[id1], self._state[id2] = kernel( - self._state[id1], - self._state[id2], - m) + self._state[id1], self._state[id2] = kernel(self._state[id1], self._state[id2], matrix) - def _multi_qubit_gate(self, m, pos, mask): + def _multi_qubit_gate(self, matrix, pos, mask): # pylint: disable=too-many-locals """ - Applies the k-qubit gate matrix m to the qubits at `pos` - using `mask` to identify control qubits. + Apply the k-qubit gate matrix m to the qubits at `pos` using `mask` to identify control qubits. Args: - m (list[list]): 2^k x 2^k complex matrix describing the k-qubit - gate. + matrix (list[list]): 2^k x 2^k complex matrix describing the k-qubit gate. pos (list[int]): List of bit-positions of the qubits. mask (int): Bit-mask where set bits indicate control qubits. """ # follows the description in https://arxiv.org/abs/1704.01127 inactive = [p for p in range(len(self._map)) if p not in pos] - matrix = _np.matrix(m) + matrix = _np.matrix(matrix) subvec = _np.zeros(1 << len(pos), dtype=complex) subvec_idx = [0] * len(subvec) - for c in range(1 << len(inactive)): + for k in range(1 << len(inactive)): # determine base index (state of inactive qubits) base = 0 - for i in range(len(inactive)): - base |= ((c >> i) & 1) << inactive[i] + for i, _inactive in enumerate(inactive): + base |= ((k >> i) & 1) << _inactive # check the control mask if mask != (base & mask): continue # now gather all elements involved in mat-vec mul - for x in range(len(subvec_idx)): + for j in range(len(subvec_idx)): # pylint: disable=consider-using-enumerate offset = 0 - for i in range(len(pos)): - offset |= ((x >> i) & 1) << pos[i] - subvec_idx[x] = base | offset - subvec[x] = self._state[subvec_idx[x]] + for i, _pos in enumerate(pos): + offset |= ((j >> i) & 1) << _pos + subvec_idx[j] = base | offset + subvec[j] = self._state[subvec_idx[j]] # perform mat-vec mul self._state[subvec_idx] = matrix.dot(subvec) @@ -461,19 +437,20 @@ def set_wavefunction(self, wavefunction, ordering): Set wavefunction and qubit ordering. Args: - wavefunction (list[complex]): Array of complex amplitudes - describing the wavefunction (must be normalized). - ordering (list): List of ids describing the new ordering of qubits - (i.e., the ordering of the provided wavefunction). + wavefunction (list[complex]): Array of complex amplitudes describing the wavefunction (must be normalized). + ordering (list): List of ids describing the new ordering of qubits (i.e., the ordering of the provided + wavefunction). """ # wavefunction contains 2^n values for n qubits - assert len(wavefunction) == (1 << len(ordering)) + if len(wavefunction) != (1 << len(ordering)): # pragma: no cover + raise ValueError('The wavefunction must contain 2^n elements!') + # all qubits must have been allocated before - if (not all([Id in self._map for Id in ordering]) or - len(self._map) != len(ordering)): - raise RuntimeError("set_wavefunction(): Invalid mapping provided." - " Please make sure all qubits have been " - "allocated previously (call eng.flush()).") + if not all(qubit_id in self._map for qubit_id in ordering) or len(self._map) != len(ordering): + raise RuntimeError( + "set_wavefunction(): Invalid mapping provided. Please make sure all qubits have been " + "allocated previously (call eng.flush())." + ) self._state = _np.array(wavefunction, dtype=_np.complex128) self._map = {ordering[i]: i for i in range(len(ordering))} @@ -484,47 +461,49 @@ def collapse_wavefunction(self, ids, values): Args: ids (list[int]): Qubit IDs to collapse. - values (list[bool]): Measurement outcome for each of the qubit IDs - in `ids`. + values (list[bool]): Measurement outcome for each of the qubit IDs in `ids`. + Raises: - RuntimeError: If probability of outcome is ~0 or unknown qubits - are provided. + RuntimeError: If probability of outcome is ~0 or unknown qubits are provided. """ - assert len(ids) == len(values) + if len(ids) != len(values): + raise ValueError('The number of ids and values do not match!') # all qubits must have been allocated before - if not all([Id in self._map for Id in ids]): - raise RuntimeError("collapse_wavefunction(): Unknown qubit id(s)" - " provided. Try calling eng.flush() before " - "invoking this function.") + if not all(Id in self._map for Id in ids): + raise RuntimeError( + "collapse_wavefunction(): Unknown qubit id(s) provided. Try calling eng.flush() before " + "invoking this function." + ) mask = 0 val = 0 - for i in range(len(ids)): - pos = self._map[ids[i]] - mask |= (1 << pos) - val |= (int(values[i]) << pos) - nrm = 0. - for i in range(len(self._state)): + for i, qubit_id in enumerate(ids): + pos = self._map[qubit_id] + mask |= 1 << pos + val |= int(values[i]) << pos + nrm = 0.0 + for i, state in enumerate(self._state): if (mask & i) == val: - nrm += _np.abs(self._state[i]) ** 2 - if nrm < 1.e-12: - raise RuntimeError("collapse_wavefunction(): Invalid collapse! " - "Probability is ~0.") - inv_nrm = 1. / _np.sqrt(nrm) - for i in range(len(self._state)): + nrm += _np.abs(state) ** 2 + if nrm < 1.0e-12: + raise RuntimeError("collapse_wavefunction(): Invalid collapse! Probability is ~0.") + inv_nrm = 1.0 / _np.sqrt(nrm) + for i in range(len(self._state)): # pylint: disable=consider-using-enumerate if (mask & i) != val: - self._state[i] = 0. + self._state[i] = 0.0 else: self._state[i] *= inv_nrm def run(self): """ - Dummy function to implement the same interface as the c++ simulator. + Provide a dummy implementation for running a quantum circuit. + + Only defined to provide the same interface as the c++ simulator. """ - pass - def _apply_term(self, term, ids, ctrlids=[]): + def _apply_term(self, term, ids, ctrlids=None): """ - Applies a QubitOperator term to the state vector. + Apply a QubitOperator term to the state vector. + (Helper function for time evolution & expectation) Args: @@ -532,11 +511,12 @@ def _apply_term(self, term, ids, ctrlids=[]): ids (list[int]): Term index to Qubit ID mapping ctrlids (list[int]): Control qubit IDs """ - X = [[0., 1.], [1., 0.]] - Y = [[0., -1j], [1j, 0.]] - Z = [[1., 0.], [0., -1.]] + X = [[0.0, 1.0], [1.0, 0.0]] + Y = [[0.0, -1j], [1j, 0.0]] + Z = [[1.0, 0.0], [0.0, -1.0]] gates = [X, Y, Z] + if not ctrlids: + ctrlids = [] for local_op in term: qb_id = ids[local_op[0]] - self.apply_controlled_gate(gates[ord(local_op[1]) - ord('X')], - [qb_id], ctrlids) + self.apply_controlled_gate(gates[ord(local_op[1]) - ord('X')], [qb_id], ctrlids) diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 883cc5cfe..f3365fbcc 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,111 +13,111 @@ # limitations under the License. """ -Contains the projectq interface to a C++-based simulator, which has to be -built first. If the c++ simulator is not exported to python, a (slow) python +The ProjectQ interface to a C++-based simulator. + +The C++ simulator has to be built first. If the C++ simulator is not exported to python, a (slow) python implementation is used as an alternative. """ import math import random + from projectq.cengines import BasicEngine -from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import (NOT, - H, - R, - Measure, - FlushGate, - Allocate, - Deallocate, - BasicMathGate, - TimeEvolution) +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control +from projectq.ops import ( + Allocate, + BasicMathGate, + Deallocate, + FlushGate, + Measure, + TimeEvolution, +) from projectq.types import WeakQubitRef +FALLBACK_TO_PYSIM = False try: from ._cppsim import Simulator as SimulatorBackend -except ImportError: +except ImportError: # pragma: no cover from ._pysim import Simulator as SimulatorBackend + FALLBACK_TO_PYSIM = True + class Simulator(BasicEngine): """ - Simulator is a compiler engine which simulates a quantum computer using - C++-based kernels. + Simulator is a compiler engine which simulates a quantum computer using C++-based kernels. - OpenMP is enabled and the number of threads can be controlled using the - OMP_NUM_THREADS environment variable, i.e. + OpenMP is enabled and the number of threads can be controlled using the OMP_NUM_THREADS environment variable, i.e. .. code-block:: bash export OMP_NUM_THREADS=4 # use 4 threads export OMP_PROC_BIND=spread # bind threads to processors by spreading """ + def __init__(self, gate_fusion=False, rnd_seed=None): """ - Construct the C++/Python-simulator object and initialize it with a - random seed. + Construct the C++/Python-simulator object and initialize it with a random seed. Args: - gate_fusion (bool): If True, gates are cached and only executed - once a certain gate-size has been reached (only has an effect - for the c++ simulator). - rnd_seed (int): Random seed (uses random.randint(0, 4294967295) by - default). - - Example of gate_fusion: Instead of applying a Hadamard gate to 5 - qubits, the simulator calculates the kronecker product of the 1-qubit - gate matrices and then applies one 5-qubit gate. This increases - operational intensity and keeps the simulator from having to iterate - through the state vector multiple times. Depending on the system (and, - especially, number of threads), this may or may not be beneficial. + gate_fusion (bool): If True, gates are cached and only executed once a certain gate-size has been reached + (only has an effect for the c++ simulator). + rnd_seed (int): Random seed (uses random.randint(0, 4294967295) by default). + + Example of gate_fusion: Instead of applying a Hadamard gate to 5 qubits, the simulator calculates the + kronecker product of the 1-qubit gate matrices and then applies one 5-qubit gate. This increases operational + intensity and keeps the simulator from having to iterate through the state vector multiple times. Depending on + the system (and, especially, number of threads), this may or may not be beneficial. Note: - If the C++ Simulator extension was not built or cannot be found, - the Simulator defaults to a Python implementation of the kernels. - While this is much slower, it is still good enough to run basic - quantum algorithms. - - If you need to run large simulations, check out the tutorial in - the docs which gives futher hints on how to build the C++ - extension. + If the C++ Simulator extension was not built or cannot be found, the Simulator defaults to a Python + implementation of the kernels. While this is much slower, it is still good enough to run basic quantum + algorithms. + + If you need to run large simulations, check out the tutorial in the docs which gives further hints on how + to build the C++ extension. """ if rnd_seed is None: rnd_seed = random.randint(0, 4294967295) - BasicEngine.__init__(self) + super().__init__() self._simulator = SimulatorBackend(rnd_seed) self._gate_fusion = gate_fusion def is_available(self, cmd): """ - Specialized implementation of is_available: The simulator can deal - with all arbitrarily-controlled gates which provide a - gate-matrix (via gate.matrix) and acts on 5 or less qubits (not - counting the control qubits). + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: The simulator can deal with all arbitrarily-controlled gates which + provide a gate-matrix (via gate.matrix) and acts on 5 or less qubits (not counting the control qubits). Args: - cmd (Command): Command for which to check availability (single- - qubit gate, arbitrary controls) + cmd (Command): Command for which to check availability (single- qubit gate, arbitrary controls) Returns: True if it can be simulated and False otherwise. """ - if (cmd.gate == Measure or cmd.gate == Allocate or - cmd.gate == Deallocate or - isinstance(cmd.gate, BasicMathGate) or - isinstance(cmd.gate, TimeEvolution)): + if has_negative_control(cmd): + return False + + if ( + cmd.gate == Measure + or cmd.gate == Allocate + or cmd.gate == Deallocate + or isinstance(cmd.gate, (BasicMathGate, TimeEvolution)) + ): return True try: - m = cmd.gate.matrix + matrix = cmd.gate.matrix # Allow up to 5-qubit gates - if len(m) > 2 ** 5: + if len(matrix) > 2**5: return False return True - except: + except AttributeError: return False def _convert_logical_to_mapped_qureg(self, qureg): """ - Converts a qureg from logical to mapped qubits if there is a mapper. + Convert a qureg from logical to mapped qubits if there is a mapper. Args: qureg (list[Qubit],Qureg): Logical quantum bits @@ -127,18 +127,16 @@ def _convert_logical_to_mapped_qureg(self, qureg): mapped_qureg = [] for qubit in qureg: if qubit.id not in mapper.current_mapping: - raise RuntimeError("Unknown qubit id. " - "Please make sure you have called " - "eng.flush().") - new_qubit = WeakQubitRef(qubit.engine, - mapper.current_mapping[qubit.id]) + raise RuntimeError("Unknown qubit id. Please make sure you have called eng.flush().") + new_qubit = WeakQubitRef(qubit.engine, mapper.current_mapping[qubit.id]) mapped_qureg.append(new_qubit) return mapped_qureg - else: - return qureg + return qureg def get_expectation_value(self, qubit_operator, qureg): """ + Return the expectation value of a qubit operator. + Get the expectation value of qubit_operator w.r.t. the current wave function represented by the supplied quantum register. @@ -150,74 +148,59 @@ def get_expectation_value(self, qubit_operator, qureg): Expectation value Note: - Make sure all previous commands (especially allocations) have - passed through the compilation chain (call main_engine.flush() to - make sure). + Make sure all previous commands (especially allocations) have passed through the compilation chain (call + main_engine.flush() to make sure). Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. Raises: - Exception: If `qubit_operator` acts on more qubits than present in - the `qureg` argument. + Exception: If `qubit_operator` acts on more qubits than present in the `qureg` argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) num_qubits = len(qureg) for term, _ in qubit_operator.terms.items(): if not term == () and term[-1][0] >= num_qubits: - raise Exception("qubit_operator acts on more qubits than " - "contained in the qureg.") - operator = [(list(term), coeff) for (term, coeff) - in qubit_operator.terms.items()] - return self._simulator.get_expectation_value(operator, - [qb.id for qb in qureg]) + raise Exception("qubit_operator acts on more qubits than contained in the qureg.") + operator = [(list(term), coeff) for (term, coeff) in qubit_operator.terms.items()] + return self._simulator.get_expectation_value(operator, [qb.id for qb in qureg]) def apply_qubit_operator(self, qubit_operator, qureg): """ - Apply a (possibly non-unitary) qubit_operator to the current wave - function represented by the supplied quantum register. + Apply a (possibly non-unitary) qubit_operator to the current wave function represented by a quantum register. Args: qubit_operator (projectq.ops.QubitOperator): Operator to apply. - qureg (list[Qubit],Qureg): Quantum bits to which to apply the - operator. + qureg (list[Qubit],Qureg): Quantum bits to which to apply the operator. Raises: Exception: If `qubit_operator` acts on more qubits than present in the `qureg` argument. Warning: - This function allows applying non-unitary gates and it will not - re-normalize the wave function! It is for numerical experiments - only and should not be used for other purposes. + This function allows applying non-unitary gates and it will not re-normalize the wave function! It is for + numerical experiments only and should not be used for other purposes. Note: - Make sure all previous commands (especially allocations) have - passed through the compilation chain (call main_engine.flush() to - make sure). + Make sure all previous commands (especially allocations) have passed through the compilation chain (call + main_engine.flush() to make sure). Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) num_qubits = len(qureg) for term, _ in qubit_operator.terms.items(): if not term == () and term[-1][0] >= num_qubits: - raise Exception("qubit_operator acts on more qubits than " - "contained in the qureg.") - operator = [(list(term), coeff) for (term, coeff) - in qubit_operator.terms.items()] - return self._simulator.apply_qubit_operator(operator, - [qb.id for qb in qureg]) + raise Exception("qubit_operator acts on more qubits than contained in the qureg.") + operator = [(list(term), coeff) for (term, coeff) in qubit_operator.terms.items()] + return self._simulator.apply_qubit_operator(operator, [qb.id for qb in qureg]) def get_probability(self, bit_string, qureg): """ - Return the probability of the outcome `bit_string` when measuring - the quantum register `qureg`. + Return the probability of the outcome `bit_string` when measuring the quantum register `qureg`. Args: bit_string (list[bool|int]|string[0|1]): Measurement outcome. @@ -227,48 +210,42 @@ def get_probability(self, bit_string, qureg): Probability of measuring the provided bit string. Note: - Make sure all previous commands (especially allocations) have - passed through the compilation chain (call main_engine.flush() to - make sure). + Make sure all previous commands (especially allocations) have passed through the compilation chain (call + main_engine.flush() to make sure). Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) bit_string = [bool(int(b)) for b in bit_string] - return self._simulator.get_probability(bit_string, - [qb.id for qb in qureg]) + return self._simulator.get_probability(bit_string, [qb.id for qb in qureg]) def get_amplitude(self, bit_string, qureg): """ Return the probability amplitude of the supplied `bit_string`. + The ordering is given by the quantum register `qureg`, which must contain all allocated qubits. Args: bit_string (list[bool|int]|string[0|1]): Computational basis state - qureg (Qureg|list[Qubit]): Quantum register determining the - ordering. Must contain all allocated qubits. + qureg (Qureg|list[Qubit]): Quantum register determining the ordering. Must contain all allocated qubits. Returns: Probability amplitude of the provided bit string. Note: - Make sure all previous commands (especially allocations) have - passed through the compilation chain (call main_engine.flush() to - make sure). + Make sure all previous commands (especially allocations) have passed through the compilation chain (call + main_engine.flush() to make sure). Note: - If there is a mapper present in the compiler, this function - automatically converts from logical qubits to mapped qubits for - the qureg argument. + If there is a mapper present in the compiler, this function automatically converts from logical qubits to + mapped qubits for the qureg argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) bit_string = [bool(int(b)) for b in bit_string] - return self._simulator.get_amplitude(bit_string, - [qb.id for qb in qureg]) + return self._simulator.get_amplitude(bit_string, [qb.id for qb in qureg]) def set_wavefunction(self, wavefunction, qureg): """ @@ -294,8 +271,7 @@ def set_wavefunction(self, wavefunction, qureg): the qureg argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) - self._simulator.set_wavefunction(wavefunction, - [qb.id for qb in qureg]) + self._simulator.set_wavefunction(wavefunction, [qb.id for qb in qureg]) def collapse_wavefunction(self, qureg, values): """ @@ -303,8 +279,8 @@ def collapse_wavefunction(self, qureg, values): Args: qureg (Qureg|list[Qubit]): Qubits to collapse. - values (list[bool]): Measurement outcome for each of the qubits - in `qureg`. + values (list[bool|int]|string[0|1]): Measurement outcome for each + of the qubits in `qureg`. Raises: RuntimeError: If an outcome has probability (approximately) 0 or @@ -320,9 +296,7 @@ def collapse_wavefunction(self, qureg, values): the qureg argument. """ qureg = self._convert_logical_to_mapped_qureg(qureg) - return self._simulator.collapse_wavefunction([qb.id for qb in qureg], - [bool(v) for v in - values]) + return self._simulator.collapse_wavefunction([qb.id for qb in qureg], [bool(int(v)) for v in values]) def cheat(self): """ @@ -347,87 +321,113 @@ def cheat(self): """ return self._simulator.cheat() - def _handle(self, cmd): + def _handle(self, cmd): # pylint: disable=too-many-branches,too-many-locals,too-many-statements """ - Handle all commands, i.e., call the member functions of the C++- - simulator object corresponding to measurement, allocation/ + Handle all commands. + + i.e., call the member functions of the C++- simulator object corresponding to measurement, allocation/ deallocation, and (controlled) single-qubit gate. Args: cmd (Command): Command to handle. Raises: - Exception: If a non-single-qubit gate needs to be processed - (which should never happen due to is_available). + Exception: If a non-single-qubit gate needs to be processed (which should never happen due to + is_available). """ if cmd.gate == Measure: - assert(get_control_count(cmd) == 0) + if get_control_count(cmd) != 0: + raise ValueError('Cannot have control qubits with a measurement gate!') ids = [qb.id for qr in cmd.qubits for qb in qr] out = self._simulator.measure_qubits(ids) i = 0 - for qr in cmd.qubits: - for qb in qr: + for qureg in cmd.qubits: + for qb in qureg: # Check if a mapper assigned a different logical id logical_id_tag = None for tag in cmd.tags: if isinstance(tag, LogicalQubitIDTag): logical_id_tag = tag if logical_id_tag is not None: - qb = WeakQubitRef(qb.engine, - logical_id_tag.logical_qubit_id) + qb = WeakQubitRef(qb.engine, logical_id_tag.logical_qubit_id) self.main_engine.set_measurement_result(qb, out[i]) i += 1 elif cmd.gate == Allocate: - ID = cmd.qubits[0][0].id - self._simulator.allocate_qubit(ID) + qubit_id = cmd.qubits[0][0].id + self._simulator.allocate_qubit(qubit_id) elif cmd.gate == Deallocate: - ID = cmd.qubits[0][0].id - self._simulator.deallocate_qubit(ID) + qubit_id = cmd.qubits[0][0].id + self._simulator.deallocate_qubit(qubit_id) elif isinstance(cmd.gate, BasicMathGate): + # improve performance by using C++ code for some commomn gates + from projectq.libs.math import ( # pylint: disable=import-outside-toplevel + AddConstant, + AddConstantModN, + MultiplyByConstantModN, + ) + qubitids = [] - for qr in cmd.qubits: + for qureg in cmd.qubits: qubitids.append([]) - for qb in qr: + for qb in qureg: qubitids[-1].append(qb.id) - math_fun = cmd.gate.get_math_function(cmd.qubits) - self._simulator.emulate_math(math_fun, qubitids, - [qb.id for qb in cmd.control_qubits]) + if FALLBACK_TO_PYSIM: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, [qb.id for qb in cmd.control_qubits]) + else: + # individual code for different standard gates to make it faster! + if isinstance(cmd.gate, AddConstant): + self._simulator.emulate_math_addConstant(cmd.gate.a, qubitids, [qb.id for qb in cmd.control_qubits]) + elif isinstance(cmd.gate, AddConstantModN): + self._simulator.emulate_math_addConstantModN( + cmd.gate.a, + cmd.gate.N, + qubitids, + [qb.id for qb in cmd.control_qubits], + ) + elif isinstance(cmd.gate, MultiplyByConstantModN): + self._simulator.emulate_math_multiplyByConstantModN( + cmd.gate.a, + cmd.gate.N, + qubitids, + [qb.id for qb in cmd.control_qubits], + ) + else: + math_fun = cmd.gate.get_math_function(cmd.qubits) + self._simulator.emulate_math(math_fun, qubitids, [qb.id for qb in cmd.control_qubits]) elif isinstance(cmd.gate, TimeEvolution): - op = [(list(term), coeff) for (term, coeff) - in cmd.gate.hamiltonian.terms.items()] - t = cmd.gate.time + op = [(list(term), coeff) for (term, coeff) in cmd.gate.hamiltonian.terms.items()] + time = cmd.gate.time qubitids = [qb.id for qb in cmd.qubits[0]] ctrlids = [qb.id for qb in cmd.control_qubits] - self._simulator.emulate_time_evolution(op, t, qubitids, ctrlids) - elif len(cmd.gate.matrix) <= 2 ** 5: + self._simulator.emulate_time_evolution(op, time, qubitids, ctrlids) + elif len(cmd.gate.matrix) <= 2**5: matrix = cmd.gate.matrix - ids = [qb.id for qr in cmd.qubits for qb in qr] + ids = [qb.id for qureg in cmd.qubits for qb in qureg] if not 2 ** len(ids) == len(cmd.gate.matrix): - raise Exception("Simulator: Error applying {} gate: " - "{}-qubit gate applied to {} qubits.".format( - str(cmd.gate), - int(math.log(len(cmd.gate.matrix), 2)), - len(ids))) - self._simulator.apply_controlled_gate(matrix.tolist(), - ids, - [qb.id for qb in - cmd.control_qubits]) + raise Exception( + f"Simulator: Error applying {str(cmd.gate)} gate: {int(math.log(len(cmd.gate.matrix), 2))}-qubit" + f" gate applied to {len(ids)} qubits." + ) + self._simulator.apply_controlled_gate(matrix.tolist(), ids, [qb.id for qb in cmd.control_qubits]) + if not self._gate_fusion: self._simulator.run() else: - raise Exception("This simulator only supports controlled k-qubit" - " gates with k < 6!\nPlease add an auto-replacer" - " engine to your list of compiler engines.") + raise Exception( + "This simulator only supports controlled k-qubit gates with k < 6!\nPlease add an auto-replacer" + " engine to your list of compiler engines." + ) def receive(self, command_list): """ - Receive a list of commands from the previous engine and handle them - (simulate them classically) prior to sending them on to the next - engine. + Receive a list of commands. + + Receive a list of commands from the previous engine and handle them (simulate them classically) prior to + sending them on to the next engine. Args: - command_list (list): List of commands to execute on the - simulator. + command_list (list): List of commands to execute on the simulator. """ for cmd in command_list: if not cmd.gate == FlushGate(): diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 19c46eb15..1b4b6b7bf 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for projectq.backends._sim._simulator.py, using both the Python and the C++ simulator as backends. @@ -19,34 +18,58 @@ import copy import math +import random + import numpy import pytest -import random import scipy import scipy.sparse import scipy.sparse.linalg from projectq import MainEngine -from projectq.cengines import (BasicEngine, BasicMapperEngine, DummyEngine, - LocalOptimizer, NotYetMeasuredError) -from projectq.ops import (All, Allocate, BasicGate, BasicMathGate, CNOT, - Command, H, Measure, QubitOperator, Rx, Ry, Rz, S, - TimeEvolution, Toffoli, X, Y, Z) +from projectq.backends import Simulator +from projectq.cengines import ( + BasicMapperEngine, + DummyEngine, + LocalOptimizer, + NotYetMeasuredError, +) from projectq.meta import Control, Dagger, LogicalQubitIDTag +from projectq.ops import ( + CNOT, + All, + Allocate, + BasicGate, + BasicMathGate, + Command, + H, + MatrixGate, + Measure, + QubitOperator, + Rx, + Ry, + Rz, + S, + TimeEvolution, + Toffoli, + X, + Y, + Z, +) from projectq.types import WeakQubitRef -from projectq.backends import Simulator - def test_is_cpp_simulator_present(): import projectq.backends._sim._cppsim + assert projectq.backends._sim._cppsim def get_available_simulators(): result = ["py_simulator"] try: - import projectq.backends._sim._cppsim as _ + import projectq.backends._sim._cppsim # noqa: F401 + result.append("cpp_simulator") except ImportError: # The C++ simulator was either not installed or is misconfigured. Skip. @@ -58,11 +81,13 @@ def get_available_simulators(): def sim(request): if request.param == "cpp_simulator": from projectq.backends._sim._cppsim import Simulator as CppSim + sim = Simulator(gate_fusion=True) sim._simulator = CppSim(1) return sim if request.param == "py_simulator": from projectq.backends._sim._pysim import Simulator as PySim + sim = Simulator() sim._simulator = PySim(1) return sim @@ -77,8 +102,8 @@ def mapper(request): class TrivialMapper(BasicMapperEngine): def __init__(self): - BasicEngine.__init__(self) - self.current_mapping = dict() + super().__init__() + self.current_mapping = {} def receive(self, command_list): for cmd in command_list: @@ -97,37 +122,32 @@ def receive(self, command_list): return None -class Mock1QubitGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 +class Mock1QubitGate(MatrixGate): + def __init__(self): + super().__init__() + self.cnt = 0 - @property - def matrix(self): - self.cnt += 1 - return [[0, 1], [1, 0]] + @property + def matrix(self): + self.cnt += 1 + return [[0, 1], [1, 0]] -class Mock6QubitGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 +class Mock6QubitGate(MatrixGate): + def __init__(self): + super().__init__() + self.cnt = 0 - @property - def matrix(self): - self.cnt += 1 - return numpy.eye(2 ** 6) + @property + def matrix(self): + self.cnt += 1 + return numpy.eye(2**6) class MockNoMatrixGate(BasicGate): - def __init__(self): - BasicGate.__init__(self) - self.cnt = 0 - - @property - def matrix(self): - self.cnt += 1 - raise AttributeError + def __init__(self): + super().__init__() + self.cnt = 0 def test_simulator_is_available(sim): @@ -155,7 +175,21 @@ def test_simulator_is_available(sim): new_cmd.gate = MockNoMatrixGate() assert not sim.is_available(new_cmd) - assert new_cmd.gate.cnt == 1 + assert new_cmd.gate.cnt == 0 + + +def test_simulator_is_available_negative_control(sim): + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2])) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) def test_simulator_cheat(sim): @@ -174,7 +208,7 @@ def test_simulator_cheat(sim): assert len(sim.cheat()[0]) == 1 assert sim.cheat()[0][0] == 0 assert len(sim.cheat()[1]) == 2 - assert 1. == pytest.approx(abs(sim.cheat()[1][0])) + assert 1.0 == pytest.approx(abs(sim.cheat()[1][0])) qubit[0].__del__() # should be empty: @@ -193,9 +227,14 @@ def test_simulator_functional_measurement(sim): All(Measure) | qubits - bit_value_sum = sum([int(qubit) for qubit in qubits]) + bit_value_sum = sum(int(qubit) for qubit in qubits) assert bit_value_sum == 0 or bit_value_sum == 5 + qb1 = WeakQubitRef(engine=eng, idx=qubits[0].id) + qb2 = WeakQubitRef(engine=eng, idx=qubits[1].id) + with pytest.raises(ValueError): + eng.backend._handle(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2])) + def test_simulator_measure_mapped_qubit(sim): eng = MainEngine(sim, []) @@ -203,8 +242,13 @@ def test_simulator_measure_mapped_qubit(sim): qb2 = WeakQubitRef(engine=eng, idx=2) cmd0 = Command(engine=eng, gate=Allocate, qubits=([qb1],)) cmd1 = Command(engine=eng, gate=X, qubits=([qb1],)) - cmd2 = Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[], - tags=[LogicalQubitIDTag(2)]) + cmd2 = Command( + engine=eng, + gate=Measure, + qubits=([qb1],), + controls=[], + tags=[LogicalQubitIDTag(2)], + ) with pytest.raises(NotYetMeasuredError): int(qb1) with pytest.raises(NotYetMeasuredError): @@ -218,7 +262,7 @@ def test_simulator_measure_mapped_qubit(sim): class Plus2Gate(BasicMathGate): def __init__(self): - BasicMathGate.__init__(self, lambda x: (x+2,)) + super().__init__(lambda x: (x + 2,)) def test_simulator_emulation(sim): @@ -230,12 +274,12 @@ def test_simulator_emulation(sim): with Control(eng, qubit3): Plus2Gate() | (qubit1 + qubit2) - assert 1. == pytest.approx(sim.cheat()[1][0]) + assert 1.0 == pytest.approx(sim.cheat()[1][0]) X | qubit3 with Control(eng, qubit3): Plus2Gate() | (qubit1 + qubit2) - assert 1. == pytest.approx(sim.cheat()[1][6]) + assert 1.0 == pytest.approx(sim.cheat()[1][6]) All(Measure) | (qubit1 + qubit2 + qubit3) @@ -267,12 +311,12 @@ def matrix(self): with Control(eng, qubit): with Dagger(eng): KQubitGate() | qureg - assert sim.get_amplitude('0' * 5, qubit + qureg) == pytest.approx(1.) + assert sim.get_amplitude('0' * 5, qubit + qureg) == pytest.approx(1.0) class LargerGate(BasicGate): @property def matrix(self): - return numpy.eye(2 ** 6) + return numpy.eye(2**6) with pytest.raises(Exception): LargerGate() | (qureg + qubit) @@ -308,8 +352,7 @@ def test_simulator_probability(sim, mapper): eng.flush() bits = [0, 0, 1, 0, 1, 0] for i in range(6): - assert (eng.backend.get_probability(bits[:i], qubits[:i]) == - pytest.approx(0.5**i)) + assert eng.backend.get_probability(bits[:i], qubits[:i]) == pytest.approx(0.5**i) extra_qubit = eng.allocate_qubit() with pytest.raises(RuntimeError): eng.backend.get_probability([0], extra_qubit) @@ -321,12 +364,9 @@ def test_simulator_probability(sim, mapper): Ry(2 * math.acos(math.sqrt(0.4))) | qubits[2] eng.flush() assert eng.backend.get_probability([0], [qubits[2]]) == pytest.approx(0.4) - assert (eng.backend.get_probability([0, 0], qubits[:3:2]) == - pytest.approx(0.12)) - assert (eng.backend.get_probability([0, 1], qubits[:3:2]) == - pytest.approx(0.18)) - assert (eng.backend.get_probability([1, 0], qubits[:3:2]) == - pytest.approx(0.28)) + assert eng.backend.get_probability([0, 0], qubits[:3:2]) == pytest.approx(0.12) + assert eng.backend.get_probability([0, 1], qubits[:3:2]) == pytest.approx(0.18) + assert eng.backend.get_probability([1, 0], qubits[:3:2]) == pytest.approx(0.28) All(Measure) | qubits @@ -340,11 +380,11 @@ def test_simulator_amplitude(sim, mapper): All(H) | qubits eng.flush() bits = [0, 0, 1, 0, 1, 0] - assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(1. / 8.) + assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(1.0 / 8.0) bits = [0, 0, 0, 0, 1, 0] - assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1. / 8.) + assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1.0 / 8.0) bits = [0, 1, 1, 0, 1, 0] - assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1. / 8.) + assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(-1.0 / 8.0) All(H) | qubits All(X) | qubits Ry(2 * math.acos(0.3)) | qubits[0] @@ -352,8 +392,7 @@ def test_simulator_amplitude(sim, mapper): bits = [0] * 6 assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(0.3) bits[0] = 1 - assert (eng.backend.get_amplitude(bits, qubits) == - pytest.approx(math.sqrt(0.91))) + assert eng.backend.get_amplitude(bits, qubits) == pytest.approx(math.sqrt(0.91)) All(Measure) | qubits # raises if not all qubits are in the list: with pytest.raises(RuntimeError): @@ -361,7 +400,7 @@ def test_simulator_amplitude(sim, mapper): # doesn't just check for length: with pytest.raises(RuntimeError): eng.backend.get_amplitude(bits, qubits[:-1] + [qubits[0]]) - extra_qubit = eng.allocate_qubit() + extra_qubit = eng.allocate_qubit() # noqa: F841 eng.flush() # there is a new qubit now! with pytest.raises(RuntimeError): @@ -376,42 +415,44 @@ def test_simulator_expectation(sim, mapper): qureg = eng.allocate_qureg(3) op0 = QubitOperator('Z0') expectation = sim.get_expectation_value(op0, qureg) - assert 1. == pytest.approx(expectation) + assert 1.0 == pytest.approx(expectation) X | qureg[0] expectation = sim.get_expectation_value(op0, qureg) - assert -1. == pytest.approx(expectation) + assert -1.0 == pytest.approx(expectation) H | qureg[0] op1 = QubitOperator('X0') expectation = sim.get_expectation_value(op1, qureg) - assert -1. == pytest.approx(expectation) + assert -1.0 == pytest.approx(expectation) Z | qureg[0] expectation = sim.get_expectation_value(op1, qureg) - assert 1. == pytest.approx(expectation) + assert 1.0 == pytest.approx(expectation) X | qureg[0] S | qureg[0] Z | qureg[0] X | qureg[0] op2 = QubitOperator('Y0') expectation = sim.get_expectation_value(op2, qureg) - assert 1. == pytest.approx(expectation) + assert 1.0 == pytest.approx(expectation) Z | qureg[0] expectation = sim.get_expectation_value(op2, qureg) - assert -1. == pytest.approx(expectation) + assert -1.0 == pytest.approx(expectation) op_sum = QubitOperator('Y0 X1 Z2') + QubitOperator('X1') H | qureg[1] X | qureg[2] expectation = sim.get_expectation_value(op_sum, qureg) - assert 2. == pytest.approx(expectation) + assert 2.0 == pytest.approx(expectation) op_sum = QubitOperator('Y0 X1 Z2') + QubitOperator('X1') X | qureg[2] expectation = sim.get_expectation_value(op_sum, qureg) - assert 0. == pytest.approx(expectation) + assert 0.0 == pytest.approx(expectation) - op_id = .4 * QubitOperator(()) + op_id = 0.4 * QubitOperator(()) expectation = sim.get_expectation_value(op_id, qureg) - assert .4 == pytest.approx(expectation) + assert 0.4 == pytest.approx(expectation) + + All(Measure) | qureg def test_simulator_expectation_exception(sim): @@ -444,27 +485,30 @@ def test_simulator_applyqubitoperator(sim, mapper): engine_list = [] if mapper is not None: engine_list.append(mapper) - eng = MainEngine(sim, engine_list=engine_list) + eng = MainEngine(sim, engine_list=engine_list, verbose=True) qureg = eng.allocate_qureg(3) op = QubitOperator('X0 Y1 Z2') sim.apply_qubit_operator(op, qureg) X | qureg[0] Y | qureg[1] Z | qureg[2] - assert sim.get_amplitude('000', qureg) == pytest.approx(1.) + assert sim.get_amplitude('000', qureg) == pytest.approx(1.0) H | qureg[0] - op_H = 1. / math.sqrt(2.) * (QubitOperator('X0') + QubitOperator('Z0')) + op_H = 1.0 / math.sqrt(2.0) * (QubitOperator('X0') + QubitOperator('Z0')) sim.apply_qubit_operator(op_H, [qureg[0]]) - assert sim.get_amplitude('000', qureg) == pytest.approx(1.) + assert sim.get_amplitude('000', qureg) == pytest.approx(1.0) op_Proj0 = 0.5 * (QubitOperator('') + QubitOperator('Z0')) op_Proj1 = 0.5 * (QubitOperator('') - QubitOperator('Z0')) H | qureg[0] sim.apply_qubit_operator(op_Proj0, [qureg[0]]) - assert sim.get_amplitude('000', qureg) == pytest.approx(1. / math.sqrt(2.)) + assert sim.get_amplitude('000', qureg) == pytest.approx(1.0 / math.sqrt(2.0)) sim.apply_qubit_operator(op_Proj1, [qureg[0]]) - assert sim.get_amplitude('000', qureg) == pytest.approx(0.) + assert sim.get_amplitude('000', qureg) == pytest.approx(0.0) + + # TODO: this is suspicious... + eng.backend.set_wavefunction([1, 0, 0, 0, 0, 0, 0, 0], qureg) def test_simulator_time_evolution(sim): @@ -491,6 +535,7 @@ def test_simulator_time_evolution(sim): eng.flush() qbit_to_bit_map, final_wavefunction = copy.deepcopy(eng.backend.cheat()) All(Measure) | qureg + ctrl_qubit + # Check manually: def build_matrix(list_single_matrices): @@ -498,18 +543,18 @@ def build_matrix(list_single_matrices): for i in range(1, len(list_single_matrices)): res = scipy.sparse.kron(res, list_single_matrices[i]) return res + id_sp = scipy.sparse.identity(2, format="csr", dtype=complex) - x_sp = scipy.sparse.csr_matrix([[0., 1.], [1., 0.]], dtype=complex) - y_sp = scipy.sparse.csr_matrix([[0., -1.j], [1.j, 0.]], dtype=complex) - z_sp = scipy.sparse.csr_matrix([[1., 0.], [0., -1.]], dtype=complex) + x_sp = scipy.sparse.csr_matrix([[0.0, 1.0], [1.0, 0.0]], dtype=complex) + y_sp = scipy.sparse.csr_matrix([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) + z_sp = scipy.sparse.csr_matrix([[1.0, 0.0], [0.0, -1.0]], dtype=complex) gates = [x_sp, y_sp, z_sp] res_matrix = 0 for t, c in op.terms.items(): matrix = [id_sp] * N for idx, gate in t: - matrix[qbit_to_bit_map[qureg[idx].id]] = gates[ord(gate) - - ord('X')] + matrix[qbit_to_bit_map[qureg[idx].id]] = gates[ord(gate) - ord('X')] matrix.reverse() res_matrix += build_matrix(matrix) * c res_matrix *= -1j * time_to_evolve @@ -519,11 +564,10 @@ def build_matrix(list_single_matrices): res = scipy.sparse.linalg.expm_multiply(res_matrix, init_wavefunction) half = int(len(final_wavefunction) / 2) - hadamard_f = 1. / math.sqrt(2.) + hadamard_f = 1.0 / math.sqrt(2.0) # check evolution and control assert numpy.allclose(hadamard_f * res, final_wavefunction[half:]) - assert numpy.allclose(final_wavefunction[:half], hadamard_f * - init_wavefunction) + assert numpy.allclose(final_wavefunction[:half], hadamard_f * init_wavefunction) def test_simulator_set_wavefunction(sim, mapper): @@ -532,23 +576,23 @@ def test_simulator_set_wavefunction(sim, mapper): engine_list.append(mapper) eng = MainEngine(sim, engine_list=engine_list) qubits = eng.allocate_qureg(2) - wf = [0., 0., math.sqrt(0.2), math.sqrt(0.8)] + wf = [0.0, 0.0, math.sqrt(0.2), math.sqrt(0.8)] with pytest.raises(RuntimeError): eng.backend.set_wavefunction(wf, qubits) eng.flush() eng.backend.set_wavefunction(wf, qubits) - assert pytest.approx(eng.backend.get_probability('1', [qubits[0]])) == .8 - assert pytest.approx(eng.backend.get_probability('01', qubits)) == .2 - assert pytest.approx(eng.backend.get_probability('1', [qubits[1]])) == 1. + assert pytest.approx(eng.backend.get_probability('1', [qubits[0]])) == 0.8 + assert pytest.approx(eng.backend.get_probability('01', qubits)) == 0.2 + assert pytest.approx(eng.backend.get_probability('1', [qubits[1]])) == 1.0 All(Measure) | qubits def test_simulator_set_wavefunction_always_complex(sim): - """ Checks that wavefunction is always complex """ + """Checks that wavefunction is always complex""" eng = MainEngine(sim) qubit = eng.allocate_qubit() eng.flush() - wf = [1., 0] + wf = [1.0, 0] eng.backend.set_wavefunction(wf, qubit) Y | qubit eng.flush() @@ -565,24 +609,28 @@ def test_simulator_collapse_wavefunction(sim, mapper): with pytest.raises(RuntimeError): eng.backend.collapse_wavefunction(qubits, [0] * 4) eng.flush() + + # mismatch in length: raises + with pytest.raises(ValueError): + eng.backend.collapse_wavefunction(qubits, [0] * 5) eng.backend.collapse_wavefunction(qubits, [0] * 4) - assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == 1. + assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == 1.0 All(H) | qubits[1:] eng.flush() - assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == .125 + assert pytest.approx(eng.backend.get_probability([0] * 4, qubits)) == 0.125 # impossible outcome: raises with pytest.raises(RuntimeError): eng.backend.collapse_wavefunction(qubits, [1] + [0] * 3) eng.backend.collapse_wavefunction(qubits[:-1], [0, 1, 0]) probability = eng.backend.get_probability([0, 1, 0, 1], qubits) - assert probability == pytest.approx(.5) - eng.backend.set_wavefunction([1.] + [0.] * 15, qubits) + assert probability == pytest.approx(0.5) + eng.backend.set_wavefunction([1.0] + [0.0] * 15, qubits) H | qubits[0] CNOT | (qubits[0], qubits[1]) eng.flush() eng.backend.collapse_wavefunction([qubits[0]], [1]) probability = eng.backend.get_probability([1, 1], qubits[0:2]) - assert probability == pytest.approx(1.) + assert probability == pytest.approx(1.0) def test_simulator_no_uncompute_exception(sim): @@ -595,7 +643,7 @@ def test_simulator_no_uncompute_exception(sim): assert qubit[0].id == -1 -class MockSimulatorBackend(object): +class MockSimulatorBackend: def __init__(self): self.run_cnt = 0 @@ -637,10 +685,10 @@ def test_simulator_functional_entangle(sim): CNOT | (qubits[0], qb) # check the state vector: - assert .5 == pytest.approx(abs(sim.cheat()[1][0])**2) - assert .5 == pytest.approx(abs(sim.cheat()[1][31])**2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][0]) ** 2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][31]) ** 2) for i in range(1, 31): - assert 0. == pytest.approx(abs(sim.cheat()[1][i])) + assert 0.0 == pytest.approx(abs(sim.cheat()[1][i])) # unentangle all except the first 2 for qb in qubits[2:]: @@ -651,10 +699,10 @@ def test_simulator_functional_entangle(sim): Toffoli | (qubits[0], qubits[1], qb) # check the state vector: - assert .5 == pytest.approx(abs(sim.cheat()[1][0])**2) - assert .5 == pytest.approx(abs(sim.cheat()[1][31])**2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][0]) ** 2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][31]) ** 2) for i in range(1, 31): - assert 0. == pytest.approx(abs(sim.cheat()[1][i])) + assert 0.0 == pytest.approx(abs(sim.cheat()[1][i])) # uncompute using multi-controlled NOTs with Control(eng, qubits[0:-1]): @@ -667,9 +715,9 @@ def test_simulator_functional_entangle(sim): H | qubits[0] # check the state vector: - assert 1. == pytest.approx(abs(sim.cheat()[1][0])**2) + assert 1.0 == pytest.approx(abs(sim.cheat()[1][0]) ** 2) for i in range(1, 32): - assert 0. == pytest.approx(abs(sim.cheat()[1][i])) + assert 0.0 == pytest.approx(abs(sim.cheat()[1][i])) All(Measure) | qubits @@ -684,7 +732,55 @@ def receive(command_list): eng = MainEngine(sim, [mapper]) qubit0 = eng.allocate_qubit() qubit1 = eng.allocate_qubit() - mapper.current_mapping = {qubit0[0].id: qubit1[0].id, - qubit1[0].id: qubit0[0].id} - assert (sim._convert_logical_to_mapped_qureg(qubit0 + qubit1) == - qubit1 + qubit0) + mapper.current_mapping = {qubit0[0].id: qubit1[0].id, qubit1[0].id: qubit0[0].id} + assert sim._convert_logical_to_mapped_qureg(qubit0 + qubit1) == qubit1 + qubit0 + + +def test_simulator_constant_math_emulation(): + if "cpp_simulator" not in get_available_simulators(): + pytest.skip("No C++ simulator") + return + + results = [[[1, 1, 0, 0, 0]], [[0, 1, 0, 0, 0]], [[0, 1, 1, 1, 0]]] + + import projectq.backends._sim._simulator as _sim + from projectq.backends._sim._cppsim import Simulator as CppSim + from projectq.backends._sim._pysim import Simulator as PySim + from projectq.libs.math import AddConstant, AddConstantModN, MultiplyByConstantModN + + def gate_filter(eng, cmd): + g = cmd.gate + if isinstance(g, BasicMathGate): + return False + return eng.next_engine.is_available(cmd) + + def run_simulation(sim): + eng = MainEngine(sim, []) + quint = eng.allocate_qureg(5) + AddConstant(3) | quint + All(Measure) | quint + eng.flush() + results[0].append([int(qb) for qb in quint]) + + AddConstantModN(4, 5) | quint + All(Measure) | quint + eng.flush() + results[1].append([int(qb) for qb in quint]) + + MultiplyByConstantModN(15, 16) | quint + All(Measure) | quint + eng.flush() + results[2].append([int(qb) for qb in quint]) + + cppsim = Simulator(gate_fusion=False) + cppsim._simulator = CppSim(1) + run_simulation(cppsim) + + _sim.FALLBACK_TO_PYSIM = True + pysim = Simulator() + pysim._simulator = PySim(1) + + for result in results: + ref = result[0] + for res in result[1:]: + assert ref == res diff --git a/projectq/backends/_sim/_simulator_test_fixtures.py b/projectq/backends/_sim/_simulator_test_fixtures.py new file mode 100644 index 000000000..e04f9084d --- /dev/null +++ b/projectq/backends/_sim/_simulator_test_fixtures.py @@ -0,0 +1,44 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from projectq.cengines import BasicMapperEngine + + +@pytest.fixture(params=["mapper", "no_mapper"]) +def mapper(request): + """Add a mapper which changes qubit ids by adding 1.""" + if request.param == "mapper": + + class TrivialMapper(BasicMapperEngine): + def __init__(self): + super().__init__() + self.current_mapping = {} + + def receive(self, command_list): + for cmd in command_list: + for qureg in cmd.all_qubits: + for qubit in qureg: + if qubit.id == -1: + continue + elif qubit.id not in self.current_mapping: + previous_map = self.current_mapping + previous_map[qubit.id] = qubit.id + 1 + self.current_mapping = previous_map + self._send_cmd_with_mapped_ids(cmd) + + return TrivialMapper() + if request.param == "no_mapper": + return None diff --git a/projectq/backends/_unitary.py b/projectq/backends/_unitary.py new file mode 100644 index 000000000..a8941b1e3 --- /dev/null +++ b/projectq/backends/_unitary.py @@ -0,0 +1,285 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Contain a backend that saves the unitary of a quantum circuit.""" + +import itertools +import math +import random +import warnings +from copy import deepcopy + +import numpy as np + +from projectq.cengines import BasicEngine +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control +from projectq.ops import AllocateQubitGate, DeallocateQubitGate, FlushGate, MeasureGate +from projectq.types import WeakQubitRef + + +def _qidmask(target_ids, control_ids, n_qubits): + """ + Calculate index masks. + + Args: + target_ids (list): list of target qubit indices + control_ids (list): list of control qubit indices + control_state (list): list of states for the control qubits (0 or 1) + n_qubits (int): number of qubits + """ + mask_list = [] + perms = np.array([x[::-1] for x in itertools.product("01", repeat=n_qubits)]).astype(int) + all_ids = np.array(range(n_qubits)) + irel_ids = np.delete(all_ids, control_ids + target_ids) + + if len(control_ids) > 0: + cmask = np.where(np.all(perms[:, control_ids] == [1] * len(control_ids), axis=1)) + else: + cmask = np.array(range(perms.shape[0])) + + if len(irel_ids) > 0: + irel_perms = np.array([x[::-1] for x in itertools.product("01", repeat=len(irel_ids))]).astype(int) + for i in range(2 ** len(irel_ids)): + irel_mask = np.where(np.all(perms[:, irel_ids] == irel_perms[i], axis=1)) + common = np.intersect1d(irel_mask, cmask) + if len(common) > 0: + mask_list.append(common) + else: + irel_mask = np.array(range(perms.shape[0])) + mask_list.append(np.intersect1d(irel_mask, cmask)) + return mask_list + + +class UnitarySimulator(BasicEngine): + """ + Simulator engine aimed at calculating the unitary transformation that represents the current quantum circuit. + + Attributes: + unitary (np.ndarray): Current unitary representing the quantum circuit being processed so far. + history (list): List of previous quantum circuit unitaries. + + Note: + The current implementation of this backend resets the unitary after the first gate that is neither a qubit + deallocation nor a measurement occurs after one of those two aforementioned gates. + + The old unitary call be accessed at anytime after such a situation occurs via the `history` property. + + .. code-block:: python + + eng = MainEngine(backend=UnitarySimulator(), engine_list=[]) + qureg = eng.allocate_qureg(3) + All(X) | qureg + + eng.flush() + All(Measure) | qureg + eng.deallocate_qubit(qureg[1]) + + X | qureg[0] # WARNING: appending gate after measurements or deallocations resets the unitary + """ + + def __init__(self): + """Initialize a UnitarySimulator object.""" + super().__init__() + self._qubit_map = {} + self._unitary = [1] + self._num_qubits = 0 + self._is_valid = True + self._is_flushed = False + self._state = [1] + self._history = [] + + @property + def unitary(self): + """ + Access the last unitary matrix directly. + + Returns: + A numpy array which is the unitary matrix of the circuit. + """ + return deepcopy(self._unitary) + + @property + def history(self): + """ + Access all previous unitary matrices. + + The current unitary matrix is appended to this list once a gate is received after either a measurement or a + qubit deallocation has occurred. + + Returns: + A list where the elements are all previous unitary matrices representing the circuit, separated by + measurement/deallocate gates. + """ + return deepcopy(self._history) + + def is_available(self, cmd): + """ + Test whether a Command is supported by a compiler engine. + + Specialized implementation of is_available: The unitary simulator can deal with all arbitrarily-controlled gates + which provide a gate-matrix (via gate.matrix). + + Args: + cmd (Command): Command for which to check availability (single- qubit gate, arbitrary controls) + + Returns: + True if it can be simulated and False otherwise. + """ + if has_negative_control(cmd): + return False + + if isinstance(cmd.gate, (AllocateQubitGate, DeallocateQubitGate, MeasureGate)): + return True + + try: + gate_mat = cmd.gate.matrix + if len(gate_mat) > 2**6: + warnings.warn(f"Potentially large matrix gate encountered! ({math.log2(len(gate_mat))} qubits)") + return True + except AttributeError: + return False + + def receive(self, command_list): + """ + Receive a list of commands. + + Receive a list of commands from the previous engine and handle them: + * update the unitary of the quantum circuit + * update the internal quantum state if a measurement or a qubit deallocation occurs + + prior to sending them on to the next engine. + + Args: + command_list (list): List of commands to execute on the simulator. + """ + for cmd in command_list: + self._handle(cmd) + + if not self.is_last_engine: + self.send(command_list) + + def _flush(self): + """Flush the simulator state.""" + if not self._is_flushed: + self._is_flushed = True + self._state = self._unitary @ self._state + + def _handle(self, cmd): + """ + Handle all commands. + + Args: + cmd (Command): Command to handle. + + Raises: + RuntimeError: If a measurement is performed before flush gate. + """ + if isinstance(cmd.gate, AllocateQubitGate): + self._qubit_map[cmd.qubits[0][0].id] = self._num_qubits + self._num_qubits += 1 + self._unitary = np.kron(np.identity(2), self._unitary) + self._state.extend([0] * len(self._state)) + + elif isinstance(cmd.gate, DeallocateQubitGate): + pos = self._qubit_map[cmd.qubits[0][0].id] + self._qubit_map = {key: value - 1 if value > pos else value for key, value in self._qubit_map.items()} + self._num_qubits -= 1 + self._is_valid = False + + elif isinstance(cmd.gate, MeasureGate): + self._is_valid = False + + if not self._is_flushed: + raise RuntimeError( + 'Please make sure all previous gates are flushed before measurement so the state gets updated' + ) + + if get_control_count(cmd) != 0: + raise ValueError('Cannot have control qubits with a measurement gate!') + + all_qubits = [qb for qr in cmd.qubits for qb in qr] + measurements = self.measure_qubits([qb.id for qb in all_qubits]) + + for qb, res in zip(all_qubits, measurements): + # Check if a mapper assigned a different logical id + for tag in cmd.tags: + if isinstance(tag, LogicalQubitIDTag): + qb = WeakQubitRef(qb.engine, tag.logical_qubit_id) + break + self.main_engine.set_measurement_result(qb, res) + + elif isinstance(cmd.gate, FlushGate): + self._flush() + else: + if not self._is_valid: + self._flush() + + warnings.warn( + "Processing of other gates after a qubit deallocation or measurement will reset the unitary," + "previous unitary can be accessed in history" + ) + self._history.append(self._unitary) + self._unitary = np.identity(2**self._num_qubits, dtype=complex) + self._state = np.array([1] + ([0] * (2**self._num_qubits - 1)), dtype=complex) + self._is_valid = True + + self._is_flushed = False + mask_list = _qidmask( + [self._qubit_map[qb.id] for qr in cmd.qubits for qb in qr], + [self._qubit_map[qb.id] for qb in cmd.control_qubits], + self._num_qubits, + ) + for mask in mask_list: + cache = np.identity(2**self._num_qubits, dtype=complex) + cache[np.ix_(mask, mask)] = cmd.gate.matrix + self._unitary = cache @ self._unitary + + def measure_qubits(self, ids): + """ + Measure the qubits with IDs ids and return a list of measurement outcomes (True/False). + + Args: + ids (list): List of qubit IDs to measure. + + Returns: + List of measurement results (containing either True or False). + """ + random_outcome = random.random() + val = 0.0 + i_picked = 0 + while val < random_outcome and i_picked < len(self._state): + val += np.abs(self._state[i_picked]) ** 2 + i_picked += 1 + + i_picked -= 1 + + pos = [self._qubit_map[ID] for ID in ids] + res = [False] * len(pos) + + mask = 0 + val = 0 + for i, _pos in enumerate(pos): + res[i] = ((i_picked >> _pos) & 1) == 1 + mask |= 1 << _pos + val |= (res[i] & 1) << _pos + + nrm = 0.0 + for i, _state in enumerate(self._state): + if (mask & i) != val: + self._state[i] = 0.0 + else: + nrm += np.abs(_state) ** 2 + + self._state *= 1.0 / np.sqrt(nrm) + return res diff --git a/projectq/backends/_unitary_test.py b/projectq/backends/_unitary_test.py new file mode 100644 index 000000000..4053bd413 --- /dev/null +++ b/projectq/backends/_unitary_test.py @@ -0,0 +1,292 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Contains the tests for the UnitarySimulator +""" + +import itertools + +import numpy as np +import pytest +from scipy.stats import unitary_group + +from projectq.cengines import DummyEngine, MainEngine, NotYetMeasuredError +from projectq.meta import Control, LogicalQubitIDTag +from projectq.ops import ( + CNOT, + All, + Allocate, + BasicGate, + Command, + Deallocate, + H, + MatrixGate, + Measure, + Rx, + Rxx, + X, + Y, +) +from projectq.types import WeakQubitRef + +from ._unitary import UnitarySimulator + + +def test_unitary_is_available(): + sim = UnitarySimulator() + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + qb3 = WeakQubitRef(engine=None, idx=2) + qb4 = WeakQubitRef(engine=None, idx=2) + qb5 = WeakQubitRef(engine=None, idx=2) + qb6 = WeakQubitRef(engine=None, idx=2) + + assert sim.is_available(Command(None, Allocate, qubits=([qb0],))) + assert sim.is_available(Command(None, Deallocate, qubits=([qb0],))) + assert sim.is_available(Command(None, Measure, qubits=([qb0],))) + assert sim.is_available(Command(None, X, qubits=([qb0],))) + assert sim.is_available(Command(None, Rx(1.2), qubits=([qb0],))) + assert sim.is_available(Command(None, Rxx(1.2), qubits=([qb0, qb1],))) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + + assert not sim.is_available(Command(None, BasicGate(), qubits=([qb0],))) + assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + with pytest.warns(UserWarning): + assert sim.is_available( + Command( + None, + MatrixGate(np.identity(2**7)), + qubits=([qb0, qb1, qb2, qb3, qb4, qb5, qb6],), + ) + ) + + +def test_unitary_warnings(): + eng = MainEngine(backend=DummyEngine(save_commands=True), engine_list=[UnitarySimulator()]) + qubit = eng.allocate_qubit() + X | qubit + + with pytest.raises(RuntimeError): + Measure | qubit + + +def test_unitary_not_last_engine(): + eng = MainEngine(backend=DummyEngine(save_commands=True), engine_list=[UnitarySimulator()]) + qubit = eng.allocate_qubit() + X | qubit + eng.flush() + Measure | qubit + assert len(eng.backend.received_commands) == 4 + + +def test_unitary_flush_does_not_invalidate(): + eng = MainEngine(backend=UnitarySimulator(), engine_list=[]) + qureg = eng.allocate_qureg(2) + + X | qureg[0] + eng.flush() + + Y | qureg[1] + eng.flush() + + # Make sure that calling flush() multiple time is ok (before measurements) + eng.flush() + eng.flush() + + # Nothing should be added to the history here since no measurements or qubit deallocation happened + assert not eng.backend.history + assert np.allclose(eng.backend.unitary, np.kron(Y.matrix, X.matrix)) + + All(Measure) | qureg + + # Make sure that calling flush() multiple time is ok (after measurement) + eng.flush() + eng.flush() + + # Nothing should be added to the history here since no gate since measurements or qubit deallocation happened + assert not eng.backend.history + assert np.allclose(eng.backend.unitary, np.kron(Y.matrix, X.matrix)) + + +def test_unitary_after_deallocation_or_measurement(): + eng = MainEngine(backend=UnitarySimulator(), engine_list=[]) + qubit = eng.allocate_qubit() + X | qubit + + assert not eng.backend.history + + eng.flush() + Measure | qubit + + # FlushGate and MeasureGate do not append to the history + assert not eng.backend.history + assert np.allclose(eng.backend.unitary, X.matrix) + + with pytest.warns(UserWarning): + Y | qubit + + # YGate after FlushGate and MeasureGate does not append current unitary (identity) to the history + assert len(eng.backend.history) == 1 + assert np.allclose(eng.backend.unitary, Y.matrix) # Reset of unitary when applying Y above + assert np.allclose(eng.backend.history[0], X.matrix) + + # Still ok + eng.flush() + Measure | qubit + + # FlushGate and MeasureGate do not append to the history + assert len(eng.backend.history) == 1 + assert np.allclose(eng.backend.unitary, Y.matrix) + assert np.allclose(eng.backend.history[0], X.matrix) + + # Make sure that the new gate will trigger appending to the history and modify the current unitary + with pytest.warns(UserWarning): + Rx(1) | qubit + assert len(eng.backend.history) == 2 + assert np.allclose(eng.backend.unitary, Rx(1).matrix) + assert np.allclose(eng.backend.history[0], X.matrix) + assert np.allclose(eng.backend.history[1], Y.matrix) + + # -------------------------------------------------------------------------- + + eng = MainEngine(backend=UnitarySimulator(), engine_list=[]) + qureg = eng.allocate_qureg(2) + All(X) | qureg + + XX_matrix = np.kron(X.matrix, X.matrix) + assert not eng.backend.history + assert np.allclose(eng.backend.unitary, XX_matrix) + + eng.deallocate_qubit(qureg[0]) + + assert not eng.backend.history + + with pytest.warns(UserWarning): + Y | qureg[1] + + # An internal call to flush() happens automatically since the X + # gate occurs as the simulator is in an invalid state (after qubit + # deallocation) + assert len(eng.backend.history) == 1 + assert np.allclose(eng.backend.history[0], XX_matrix) + assert np.allclose(eng.backend.unitary, Y.matrix) + + # Still ok + eng.flush() + Measure | qureg[1] + + # Nothing should have changed + assert len(eng.backend.history) == 1 + assert np.allclose(eng.backend.history[0], XX_matrix) + assert np.allclose(eng.backend.unitary, Y.matrix) + + +def test_unitary_simulator(): + def create_random_unitary(n): + return unitary_group.rvs(2**n) + + mat1 = create_random_unitary(1) + mat2 = create_random_unitary(2) + mat3 = create_random_unitary(3) + mat4 = create_random_unitary(1) + + n_qubits = 3 + + def apply_gates(eng, qureg): + MatrixGate(mat1) | qureg[0] + MatrixGate(mat2) | qureg[1:] + MatrixGate(mat3) | qureg + + with Control(eng, qureg[1]): + MatrixGate(mat2) | (qureg[0], qureg[2]) + MatrixGate(mat4) | qureg[0] + + with Control(eng, qureg[1], ctrl_state='0'): + MatrixGate(mat1) | qureg[0] + with Control(eng, qureg[2], ctrl_state='0'): + MatrixGate(mat1) | qureg[0] + + for basis_state in [list(x[::-1]) for x in itertools.product([0, 1], repeat=2**n_qubits)][1:]: + ref_eng = MainEngine(engine_list=[], verbose=True) + ref_qureg = ref_eng.allocate_qureg(n_qubits) + ref_eng.backend.set_wavefunction(basis_state, ref_qureg) + apply_gates(ref_eng, ref_qureg) + + test_eng = MainEngine(backend=UnitarySimulator(), engine_list=[], verbose=True) + test_qureg = test_eng.allocate_qureg(n_qubits) + + assert np.allclose(test_eng.backend.unitary, np.identity(2**n_qubits)) + + apply_gates(test_eng, test_qureg) + + qubit_map, ref_state = ref_eng.backend.cheat() + assert qubit_map == {i: i for i in range(n_qubits)} + + test_state = test_eng.backend.unitary @ np.array(basis_state) + + assert np.allclose(ref_eng.backend.cheat()[1], test_state) + + ref_eng.flush() + test_eng.flush() + All(Measure) | ref_qureg + All(Measure) | test_qureg + + +def test_unitary_functional_measurement(): + eng = MainEngine(UnitarySimulator()) + qubits = eng.allocate_qureg(5) + # entangle all qubits: + H | qubits[0] + for qb in qubits[1:]: + CNOT | (qubits[0], qb) + eng.flush() + All(Measure) | qubits + + bit_value_sum = sum(int(qubit) for qubit in qubits) + assert bit_value_sum == 0 or bit_value_sum == 5 + + qb1 = WeakQubitRef(engine=eng, idx=qubits[0].id) + qb2 = WeakQubitRef(engine=eng, idx=qubits[1].id) + with pytest.raises(ValueError): + eng.backend._handle(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2])) + + +def test_unitary_measure_mapped_qubit(): + eng = MainEngine(UnitarySimulator()) + qb1 = WeakQubitRef(engine=eng, idx=1) + qb2 = WeakQubitRef(engine=eng, idx=2) + cmd0 = Command(engine=eng, gate=Allocate, qubits=([qb1],)) + cmd1 = Command(engine=eng, gate=X, qubits=([qb1],)) + cmd2 = Command( + engine=eng, + gate=Measure, + qubits=([qb1],), + controls=[], + tags=[LogicalQubitIDTag(2)], + ) + with pytest.raises(NotYetMeasuredError): + int(qb1) + with pytest.raises(NotYetMeasuredError): + int(qb2) + + eng.send([cmd0, cmd1]) + eng.flush() + eng.send([cmd2]) + with pytest.raises(NotYetMeasuredError): + int(qb1) + assert int(qb2) == 1 diff --git a/projectq/backends/_utils.py b/projectq/backends/_utils.py new file mode 100644 index 000000000..2c33068ae --- /dev/null +++ b/projectq/backends/_utils.py @@ -0,0 +1,28 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module containing some utility functions.""" + + +def _rearrange_result(input_result, length): + """Turn ``input_result`` from an integer into a bit-string. + + Args: + input_result (int): An integer representation of qubit states. + length (int): The total number of bits (for padding, if needed). + + Returns: + str: A bit-string representation of ``input_result``. + """ + return f'{input_result:0{length}b}'[::-1] diff --git a/projectq/backends/_utils_test.py b/projectq/backends/_utils_test.py new file mode 100644 index 000000000..f2dddcd9b --- /dev/null +++ b/projectq/backends/_utils_test.py @@ -0,0 +1,35 @@ +# Copyright 2022 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for projectq._utils.py.""" + +import pytest + +from ._utils import _rearrange_result + + +@pytest.mark.parametrize( + "input_result, length, expected_result", + [ + (5, 3, '101'), + (5, 4, '1010'), + (5, 5, '10100'), + (16, 5, '00001'), + (16, 6, '000010'), + (63, 6, '111111'), + (63, 7, '1111110'), + ], +) +def test_rearrange_result(input_result, length, expected_result): + assert expected_result == _rearrange_result(input_result, length) diff --git a/projectq/cengines/__init__.py b/projectq/cengines/__init__.py index 966159e78..bb487c748 100755 --- a/projectq/cengines/__init__.py +++ b/projectq/cengines/__init__.py @@ -12,23 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._basics import (BasicEngine, - LastEngineException, - ForwarderEngine) -from ._cmdmodifier import CommandModifier -from ._basicmapper import BasicMapperEngine +"""ProjectQ module containing all compiler engines.""" + +from contextlib import contextmanager + +from ._basics import BasicEngine, ForwarderEngine, LastEngineException # isort:skip +from ._cmdmodifier import CommandModifier # isort:skip +from ._basicmapper import BasicMapperEngine # isort:skip + +# isort: split + from ._ibm5qubitmapper import IBM5QubitMapper -from ._swapandcnotflipper import SwapAndCNOTFlipper from ._linearmapper import LinearMapper, return_swap_depth +from ._main import MainEngine, NotYetMeasuredError, UnsupportedEngineError from ._manualmapper import ManualMapper -from ._main import (MainEngine, - NotYetMeasuredError, - UnsupportedEngineError) from ._optimize import LocalOptimizer -from ._replacer import (AutoReplacer, - InstructionFilter, - DecompositionRuleSet, - DecompositionRule) +from ._replacer import ( + AutoReplacer, + DecompositionRule, + DecompositionRuleSet, + InstructionFilter, +) +from ._swapandcnotflipper import SwapAndCNOTFlipper from ._tagremover import TagRemover from ._testengine import CompareEngine, DummyEngine from ._twodmapper import GridMapper + + +@contextmanager +def flushing(engine): + """ + Context manager to flush the given engine at the end of the 'with' context block. + + Example: + with flushing(MainEngine()) as eng: + qubit = eng.allocate_qubit() + ... + + Calling 'eng.flush()' is no longer needed because the engine will be flushed at the + end of the 'with' block even if an exception has been raised within that block. + """ + try: + yield engine + finally: + engine.flush() diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 4d4cef177..45f477d30 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -11,41 +11,43 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines the parent class from which all mappers should be derived. +The parent class from which all mappers should be derived. -There is only one engine currently allowed to be derived from -BasicMapperEngine. This allows the simulator to automatically translate -logical qubit ids to mapped ids. +There is only one engine currently allowed to be derived from BasicMapperEngine. This allows the simulator to +automatically translate logical qubit ids to mapped ids. """ from copy import deepcopy -from projectq.cengines import BasicEngine, CommandModifier -from projectq.meta import drop_engine_after, insert_engine, LogicalQubitIDTag +from projectq.meta import LogicalQubitIDTag, drop_engine_after, insert_engine from projectq.ops import MeasureGate +from ._basics import BasicEngine +from ._cmdmodifier import CommandModifier + class BasicMapperEngine(BasicEngine): """ Parent class for all Mappers. Attributes: - self.current_mapping (dict): Keys are the logical qubit ids and values - are the mapped qubit ids. + self.current_mapping (dict): Keys are the logical qubit ids and values are the mapped qubit ids. """ def __init__(self): - BasicEngine.__init__(self) + """Initialize a BasicMapperEngine object.""" + super().__init__() self._current_mapping = None @property def current_mapping(self): + """Access the current mapping.""" return deepcopy(self._current_mapping) @current_mapping.setter def current_mapping(self, current_mapping): + """Set the current mapping.""" self._current_mapping = current_mapping def _send_cmd_with_mapped_ids(self, cmd): @@ -67,12 +69,9 @@ def _send_cmd_with_mapped_ids(self, cmd): for qubit in control_qubits: qubit.id = self.current_mapping[qubit.id] if isinstance(new_cmd.gate, MeasureGate): - assert len(new_cmd.qubits) == 1 and len(new_cmd.qubits[0]) == 1 - # Add LogicalQubitIDTag to MeasureGate def add_logical_id(command, old_tags=deepcopy(cmd.tags)): - command.tags = (old_tags + - [LogicalQubitIDTag(cmd.qubits[0][0].id)]) + command.tags = old_tags + [LogicalQubitIDTag(cmd.qubits[0][0].id)] return command tagger_eng = CommandModifier(add_logical_id) @@ -81,3 +80,13 @@ def add_logical_id(command, old_tags=deepcopy(cmd.tags)): drop_engine_after(self) else: self.send([new_cmd]) + + def receive(self, command_list): + """ + Receive a list of commands. + + This implementation simply forwards all commands to the next compiler engine while adjusting the qubit IDs of + measurement gates. + """ + for cmd in command_list: + self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_basicmapper_test.py b/projectq/cengines/_basicmapper_test.py index 9a15bf2e1..2d319702d 100644 --- a/projectq/cengines/_basicmapper_test.py +++ b/projectq/cengines/_basicmapper_test.py @@ -11,19 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._basicmapper.py.""" -from copy import deepcopy - -from projectq.cengines import DummyEngine +from projectq.cengines import DummyEngine, _basicmapper from projectq.meta import LogicalQubitIDTag -from projectq.ops import (Allocate, BasicGate, Command, Deallocate, FlushGate, - Measure) +from projectq.ops import Allocate, BasicGate, Command, Deallocate, FlushGate, Measure from projectq.types import WeakQubitRef -from projectq.cengines import _basicmapper - def test_basic_mapper_engine_send_cmd_with_mapped_ids(): mapper = _basicmapper.BasicMapperEngine() @@ -36,14 +30,16 @@ def test_basic_mapper_engine_send_cmd_with_mapped_ids(): qb1 = WeakQubitRef(engine=None, idx=1) qb2 = WeakQubitRef(engine=None, idx=2) qb3 = WeakQubitRef(engine=None, idx=3) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) - cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb1],), controls=[], - tags=[]) - cmd2 = Command(engine=None, gate=Measure, qubits=([qb2],), controls=[], - tags=["SomeTag"]) - cmd3 = Command(engine=None, gate=BasicGate(), qubits=([qb0, qb1], [qb2]), - controls=[qb3], tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) + cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb1],), controls=[], tags=[]) + cmd2 = Command(engine=None, gate=Measure, qubits=([qb2],), controls=[], tags=["SomeTag"]) + cmd3 = Command( + engine=None, + gate=BasicGate(), + qubits=([qb0, qb1], [qb2]), + controls=[qb3], + tags=[], + ) cmd4 = Command(None, FlushGate(), ([WeakQubitRef(None, -1)],)) mapper._send_cmd_with_mapped_ids(cmd0) mapper._send_cmd_with_mapped_ids(cmd1) diff --git a/projectq/cengines/_basics.py b/projectq/cengines/_basics.py index 1851d7259..8737d36d9 100755 --- a/projectq/cengines/_basics.py +++ b/projectq/cengines/_basics.py @@ -12,50 +12,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -from projectq.ops import Allocate, Deallocate -from projectq.types import Qubit, Qureg -from projectq.ops import Command -import projectq.cengines +"""Module containing the basic definition of a compiler engine.""" + +from projectq.ops import Allocate, Command, Deallocate +from projectq.types import Qubit, Qureg, WeakQubitRef class LastEngineException(Exception): """ - Exception thrown when the last engine tries to access the next one. - (Next engine does not exist) + Exception thrown when the last engine tries to access the next one. (Next engine does not exist). - The default implementation of isAvailable simply asks the next engine - whether the command is available. An engine which legally may be the last - engine, this behavior needs to be adapted (see BasicEngine.isAvailable). + The default implementation of isAvailable simply asks the next engine whether the command is available. An engine + which legally may be the last engine, this behavior needs to be adapted (see BasicEngine.isAvailable). """ + def __init__(self, engine): - Exception.__init__(self, ("\nERROR: Sending to next engine failed. " - "{} as last engine?\nIf this is legal, " - "please override 'isAvailable' to adapt its" - " behavior." - ).format(engine.__class__.__name__)) + """Initialize the exception.""" + super().__init__( + ( + f"\nERROR: Sending to next engine failed. {engine.__class__.__name__} as last engine?" + "\nIf this is legal, please override 'isAvailable' to adapt its behavior." + ), + ) -class BasicEngine(object): +class BasicEngine: """ Basic compiler engine: All compiler engines are derived from this class. - It provides basic functionality such as qubit allocation/deallocation and - functions that provide information about the engine's position (e.g., next - engine). - This information is provided by the MainEngine, which initializes all - further engines. + It provides basic functionality such as qubit allocation/deallocation and functions that provide information about + the engine's position (e.g., next engine). + + This information is provided by the MainEngine, which initializes all further engines. Attributes: next_engine (BasicEngine): Next compiler engine (or the back-end). main_engine (MainEngine): Reference to the main compiler engine. is_last_engine (bool): True for the last engine, which is the back-end. """ + def __init__(self): """ Initialize the basic engine. - Initializes local variables such as _next_engine, _main_engine, etc. to - None. + Initializes local variables such as _next_engine, _main_engine, etc. to None. """ self.main_engine = None self.next_engine = None @@ -63,9 +63,10 @@ def __init__(self): def is_available(self, cmd): """ - Default implementation of is_available: - Ask the next engine whether a command is available, i.e., - whether it can be executed by the next engine(s). + Test whether a Command is supported by a compiler engine. + + Default implementation of is_available: Ask the next engine whether a command is available, i.e., whether it can + be executed by the next engine(s). Args: cmd (Command): Command for which to check availability. @@ -74,31 +75,24 @@ def is_available(self, cmd): True if the command can be executed. Raises: - LastEngineException: If is_last_engine is True but is_available - is not implemented. + LastEngineException: If is_last_engine is True but is_available is not implemented. """ if not self.is_last_engine: return self.next_engine.is_available(cmd) - else: - raise LastEngineException(self) + raise LastEngineException(self) def allocate_qubit(self, dirty=False): """ - Return a new qubit as a list containing 1 qubit object (quantum - register of size 1). - - Allocates a new qubit by getting a (new) qubit id from the MainEngine, - creating the qubit object, and then sending an AllocateQubit command - down the pipeline. If dirty=True, the fresh qubit can be replaced by - a pre-allocated one (in an unknown, dirty, initial state). Dirty qubits - must be returned to their initial states before they are deallocated / - freed. - - All allocated qubits are added to the MainEngine's set of active - qubits as weak references. This allows proper clean-up at the end of - the Python program (using atexit), deallocating all qubits which are - still alive. Qubit ids of dirty qubits are registered in MainEngine's - dirty_qubits set. + Return a new qubit as a list containing 1 qubit object (quantum register of size 1). + + Allocates a new qubit by getting a (new) qubit id from the MainEngine, creating the qubit object, and then + sending an AllocateQubit command down the pipeline. If dirty=True, the fresh qubit can be replaced by a + pre-allocated one (in an unknown, dirty, initial state). Dirty qubits must be returned to their initial states + before they are deallocated / freed. + + All allocated qubits are added to the MainEngine's set of active qubits as weak references. This allows proper + clean-up at the end of the Python program (using atexit), deallocating all qubits which are still alive. Qubit + ids of dirty qubits are registered in MainEngine's dirty_qubits set. Args: dirty (bool): If True, indicates that the allocated qubit may be @@ -111,7 +105,10 @@ def allocate_qubit(self, dirty=False): qb = Qureg([Qubit(self, new_id)]) cmd = Command(self, Allocate, (qb,)) if dirty: - from projectq.meta import DirtyQubitTag + from projectq.meta import ( # pylint: disable=import-outside-toplevel + DirtyQubitTag, + ) + if self.is_meta_tag_supported(DirtyQubitTag): cmd.tags += [DirtyQubitTag()] self.main_engine.dirty_qubits.add(qb[0].id) @@ -119,23 +116,22 @@ def allocate_qubit(self, dirty=False): self.send([cmd]) return qb - def allocate_qureg(self, n): + def allocate_qureg(self, n_qubits): """ - Allocate n qubits and return them as a quantum register, which is a - list of qubit objects. + Allocate n qubits and return them as a quantum register, which is a list of qubit objects. Args: n (int): Number of qubits to allocate Returns: Qureg of length n, a list of n newly allocated qubits. """ - return Qureg([self.allocate_qubit()[0] for _ in range(n)]) + return Qureg([self.allocate_qubit()[0] for _ in range(n_qubits)]) def deallocate_qubit(self, qubit): """ - Deallocate a qubit (and sends the deallocation command down the - pipeline). If the qubit was allocated as a dirty qubit, add - DirtyQubitTag() to Deallocate command. + Deallocate a qubit (and sends the deallocation command down the pipeline). + + If the qubit was allocated as a dirty qubit, add DirtyQubitTag() to Deallocate command. Args: qubit (BasicQubit): Qubit to deallocate. @@ -145,74 +141,79 @@ def deallocate_qubit(self, qubit): if qubit.id == -1: raise ValueError("Already deallocated.") - from projectq.meta import DirtyQubitTag + from projectq.meta import ( # pylint: disable=import-outside-toplevel + DirtyQubitTag, + ) + is_dirty = qubit.id in self.main_engine.dirty_qubits - self.send([Command(self, - Deallocate, - (Qureg([qubit]),), - tags=[DirtyQubitTag()] if is_dirty else [])]) + self.send( + [ + Command( + self, + Deallocate, + ([WeakQubitRef(engine=qubit.engine, idx=qubit.id)],), + tags=[DirtyQubitTag()] if is_dirty else [], + ) + ] + ) + # Mark qubit as deallocated + qubit.id = -1 def is_meta_tag_supported(self, meta_tag): """ - Check if there is a compiler engine handling the meta tag + Check if there is a compiler engine handling the meta tag. Args: - engine: First engine to check (then iteratively calls - getNextEngine) + engine: First engine to check (then iteratively calls getNextEngine) meta_tag: Meta tag class for which to check support Returns: - supported (bool): True if one of the further compiler engines is a - meta tag handler, i.e., engine.is_meta_tag_handler(meta_tag) - returns True. + supported (bool): True if one of the further compiler engines is a meta tag handler, i.e., + engine.is_meta_tag_handler(meta_tag) returns True. """ engine = self - try: - while True: - try: - if engine.is_meta_tag_handler(meta_tag): - return True - except AttributeError: - pass - engine = engine.next_engine - except: - return False + while engine is not None: + try: + if engine.is_meta_tag_handler(meta_tag): + return True + except AttributeError: + pass + engine = engine.next_engine + return False def send(self, command_list): - """ - Forward the list of commands to the next engine in the pipeline. - """ + """Forward the list of commands to the next engine in the pipeline.""" self.next_engine.receive(command_list) class ForwarderEngine(BasicEngine): """ - A ForwarderEngine is a trivial engine which forwards all commands to the - next engine. + A ForwarderEngine is a trivial engine which forwards all commands to the next engine. - It is mainly used as a substitute for the MainEngine at lower levels such - that meta operations still work (e.g., with Compute). + It is mainly used as a substitute for the MainEngine at lower levels such that meta operations still work (e.g., + with Compute). """ + def __init__(self, engine, cmd_mod_fun=None): """ Initialize a ForwarderEngine. Args: engine (BasicEngine): Engine to forward all commands to. - cmd_mod_fun (function): Function which is called before sending a - command. Each command cmd is replaced by the command it - returns when getting called with cmd. + cmd_mod_fun (function): Function which is called before sending a command. Each command cmd is replaced by + the command it returns when getting called with cmd. """ - BasicEngine.__init__(self) + super().__init__() self.main_engine = engine.main_engine self.next_engine = engine if cmd_mod_fun is None: + def cmd_mod_fun(cmd): return cmd self._cmd_mod_fun = cmd_mod_fun def receive(self, command_list): - """ Forward all commands to the next engine. """ + """Forward all commands to the next engine.""" new_command_list = [self._cmd_mod_fun(cmd) for cmd in command_list] self.send(new_command_list) diff --git a/projectq/cengines/_basics_test.py b/projectq/cengines/_basics_test.py index 2984e631b..b521abf28 100755 --- a/projectq/cengines/_basics_test.py +++ b/projectq/cengines/_basics_test.py @@ -11,26 +11,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._basics.py.""" import types + import pytest -# try: -# import mock -# except ImportError: -# from unittest import mock from projectq import MainEngine -from projectq.types import Qubit -from projectq.cengines import DummyEngine, InstructionFilter +from projectq.cengines import DummyEngine, InstructionFilter, _basics from projectq.meta import DirtyQubitTag -from projectq.ops import (AllocateQubitGate, - DeallocateQubitGate, - H, FastForwardingGate, - ClassicalInstructionGate) +from projectq.ops import ( + AllocateQubitGate, + ClassicalInstructionGate, + DeallocateQubitGate, + FastForwardingGate, + H, +) +from projectq.types import Qubit -from projectq.cengines import _basics +# try: +# import mock +# except ImportError: +# from unittest import mock def test_basic_engine_init(): @@ -64,13 +66,13 @@ def test_basic_engine_allocate_and_deallocate_qubit_and_qureg(): # any allocate or deallocate gates cmd_sent_by_main_engine = [] - def receive(self, cmd_list): cmd_sent_by_main_engine.append(cmd_list) + def receive(self, cmd_list): + cmd_sent_by_main_engine.append(cmd_list) eng.receive = types.MethodType(receive, eng) # Create test engines: saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[eng, DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[eng, DummyEngine()]) # Allocate and deallocate qubits qubit = eng.allocate_qubit() # Try to allocate dirty qubit but it should give a non dirty qubit @@ -80,8 +82,7 @@ def receive(self, cmd_list): cmd_sent_by_main_engine.append(cmd_list) def allow_dirty_qubits(self, meta_tag): return meta_tag == DirtyQubitTag - saving_backend.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, - saving_backend) + saving_backend.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, saving_backend) dirty_qubit = eng.allocate_qubit(dirty=True) qureg = eng.allocate_qureg(2) # Test qubit allocation @@ -107,8 +108,18 @@ def allow_dirty_qubits(self, meta_tag): assert tmp_qubit in main_engine.active_qubits assert id(tmp_qubit.engine) == id(eng) # Test uniqueness of ids - assert len(set([qubit[0].id, not_dirty_qubit[0].id, dirty_qubit[0].id, - qureg[0].id, qureg[1].id])) == 5 + assert ( + len( + { + qubit[0].id, + not_dirty_qubit[0].id, + dirty_qubit[0].id, + qureg[0].id, + qureg[1].id, + } + ) + == 5 + ) # Test allocate gates were sent assert len(cmd_sent_by_main_engine) == 0 assert len(saving_backend.received_commands) == 5 @@ -137,9 +148,11 @@ def test_deallocate_qubit_exception(): def test_basic_engine_is_meta_tag_supported(): eng = _basics.BasicEngine() + # BasicEngine needs receive function to function so let's add it: - def receive(self, cmd_list): self.send(cmd_list) + def receive(self, cmd_list): + self.send(cmd_list) eng.receive = types.MethodType(receive, eng) backend = DummyEngine() @@ -152,10 +165,8 @@ def allow_dirty_qubits(self, meta_tag): return True return False - engine2.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, - engine2) - main_engine = MainEngine(backend=backend, - engine_list=[engine0, engine1, engine2]) + engine2.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, engine2) + main_engine = MainEngine(backend=backend, engine_list=[engine0, engine1, engine2]) assert not main_engine.is_meta_tag_supported("NotSupported") assert main_engine.is_meta_tag_supported(DirtyQubitTag) @@ -163,8 +174,7 @@ def allow_dirty_qubits(self, meta_tag): def test_forwarder_engine(): backend = DummyEngine(save_commands=True) engine0 = DummyEngine() - main_engine = MainEngine(backend=backend, - engine_list=[engine0]) + main_engine = MainEngine(backend=backend, engine_list=[engine0]) def cmd_mod_fun(cmd): cmd.tags = "NewTag" @@ -180,8 +190,7 @@ def cmd_mod_fun(cmd): received_commands = [] # Remove Allocate and Deallocate gates for cmd in backend.received_commands: - if not (isinstance(cmd.gate, FastForwardingGate) or - isinstance(cmd.gate, ClassicalInstructionGate)): + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): received_commands.append(cmd) for cmd in received_commands: print(cmd) diff --git a/projectq/cengines/_cmdmodifier.py b/projectq/cengines/_cmdmodifier.py index 0a1df34c6..1f8769304 100755 --- a/projectq/cengines/_cmdmodifier.py +++ b/projectq/cengines/_cmdmodifier.py @@ -11,47 +11,52 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Contains a CommandModifier engine, which can be used to, e.g., modify the tags -of all commands which pass by (see the AutoReplacer for an example). +A CommandModifier engine that can be used to apply a user-defined transformation to all incoming commands. + +A CommandModifier engine can be used to, e.g., modify the tags of all commands which pass by (see the +AutoReplacer for an example). """ -from projectq.cengines import BasicEngine + +from ._basics import BasicEngine class CommandModifier(BasicEngine): """ - CommandModifier is a compiler engine which applies a function to all - incoming commands, sending on the resulting command instead of the - original one. + Compiler engine applying a user-defined transformation to all incoming commands. + + CommandModifier is a compiler engine which applies a function to all incoming commands, sending on the resulting + command instead of the original one. """ + def __init__(self, cmd_mod_fun): """ Initialize the CommandModifier. Args: - cmd_mod_fun (function): Function which, given a command cmd, - returns the command it should send instead. + cmd_mod_fun (function): Function which, given a command cmd, returns the command it should send instead. Example: .. code-block:: python def cmd_mod_fun(cmd): cmd.tags += [MyOwnTag()] + + compiler_engine = CommandModifier(cmd_mod_fun) ... """ - BasicEngine.__init__(self) + super().__init__() self._cmd_mod_fun = cmd_mod_fun def receive(self, command_list): """ - Receive a list of commands from the previous engine, modify all - commands, and send them on to the next engine. + Receive a list of commands. + + Receive a list of commands from the previous engine, modify all commands, and send them on to the next engine. Args: - command_list (list): List of commands to receive and then - (after modification) send on. + command_list (list): List of commands to receive and then (after modification) send on. """ new_command_list = [self._cmd_mod_fun(cmd) for cmd in command_list] self.send(new_command_list) diff --git a/projectq/cengines/_cmdmodifier_test.py b/projectq/cengines/_cmdmodifier_test.py index afc7e16a2..06fd5ae82 100755 --- a/projectq/cengines/_cmdmodifier_test.py +++ b/projectq/cengines/_cmdmodifier_test.py @@ -11,14 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._cmdmodifier.py.""" from projectq import MainEngine -from projectq.cengines import DummyEngine -from projectq.ops import H, FastForwardingGate, ClassicalInstructionGate - -from projectq.cengines import _cmdmodifier +from projectq.cengines import DummyEngine, _cmdmodifier +from projectq.ops import ClassicalInstructionGate, FastForwardingGate, H def test_command_modifier(): @@ -35,8 +32,7 @@ def cmd_mod_fun(cmd): received_commands = [] # Remove Allocate and Deallocate gates for cmd in backend.received_commands: - if not (isinstance(cmd.gate, FastForwardingGate) or - isinstance(cmd.gate, ClassicalInstructionGate)): + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): received_commands.append(cmd) for cmd in received_commands: print(cmd) diff --git a/projectq/cengines/_ibm5qubitmapper.py b/projectq/cengines/_ibm5qubitmapper.py index 7a2659a30..bb7549ea6 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -12,17 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains a compiler engine to map to the 5-qubit IBM chip -""" -from copy import deepcopy +"""Contains a compiler engine to map to the 5-qubit IBM chip.""" import itertools -from projectq.cengines import BasicMapperEngine -from projectq.ops import FlushGate, NOT, Allocate -from projectq.meta import get_control_count from projectq.backends import IBMBackend +from projectq.meta import get_control_count +from projectq.ops import NOT, Allocate, FlushGate + +from ._basicmapper import BasicMapperEngine class IBM5QubitMapper(BasicMapperEngine): @@ -35,25 +33,43 @@ class IBM5QubitMapper(BasicMapperEngine): The mapper has to be run once on the entire circuit. Warning: - If the provided circuit cannot be mapped to the hardware layout - without performing Swaps, the mapping procedure - **raises an Exception**. + If the provided circuit cannot be mapped to the hardware layout without performing Swaps, the mapping + procedure **raises an Exception**. """ - def __init__(self): + def __init__(self, connections=None): """ Initialize an IBM 5-qubit mapper compiler engine. Resets the mapping. """ - BasicMapperEngine.__init__(self) - self.current_mapping = dict() + super().__init__() + self.current_mapping = {} self._reset() + self._cmds = [] + self._interactions = {} + + if connections is None: + # general connectivity easier for testing functions + self.connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + else: + self.connections = connections def is_available(self, cmd): """ - Check if the IBM backend can perform the Command cmd and return True - if so. + Check if the IBM backend can perform the Command cmd and return True if so. Args: cmd (Command): The command to check @@ -61,82 +77,64 @@ def is_available(self, cmd): return IBMBackend().is_available(cmd) def _reset(self): - """ - Reset the mapping parameters so the next circuit can be mapped. - """ + """Reset the mapping parameters so the next circuit can be mapped.""" self._cmds = [] - self._interactions = dict() - - def _is_cnot(self, cmd): - """ - Check if the command corresponds to a CNOT (controlled NOT gate). - - Args: - cmd (Command): Command to check whether it is a controlled NOT - gate. - """ - return (isinstance(cmd.gate, NOT.__class__) and - get_control_count(cmd) == 1) + self._interactions = {} def _determine_cost(self, mapping): """ - Determines the cost of the circuit with the given mapping. + Determine the cost of the circuit with the given mapping. Args: - mapping (dict): Dictionary with key, value pairs where keys are - logical qubit ids and the corresponding value is the physical - location on the IBM Q chip. + mapping (dict): Dictionary with key, value pairs where keys are logical qubit ids and the corresponding + value is the physical location on the IBM Q chip. Returns: - Cost measure taking into account CNOT directionality or None - if the circuit cannot be executed given the mapping. + Cost measure taking into account CNOT directionality or None if the circuit cannot be executed given the + mapping. """ - from projectq.setups.ibm import ibmqx4_connections as connections cost = 0 - for tpl in self._interactions: + for tpl, interaction in self._interactions.items(): ctrl_id = tpl[0] target_id = tpl[1] ctrl_pos = mapping[ctrl_id] target_pos = mapping[target_id] - if not (ctrl_pos, target_pos) in connections: - if (target_pos, ctrl_pos) in connections: - cost += self._interactions[tpl] + if not (ctrl_pos, target_pos) in self.connections: + if (target_pos, ctrl_pos) in self.connections: + cost += interaction else: return None return cost def _run(self): """ - Runs all stored gates. + Run all stored gates. Raises: Exception: - If the mapping to the IBM backend cannot be performed or if - the mapping was already determined but more CNOTs get sent - down the pipeline. - """ - if (len(self.current_mapping) > 0 and - max(self.current_mapping.values()) > 4): - raise RuntimeError("Too many qubits allocated. The IBM Q " - "device supports at most 5 qubits and no " - "intermediate measurements / " - "reallocations.") + If the mapping to the IBM backend cannot be performed or if the mapping was already determined but + more CNOTs get sent down the pipeline. + """ + if len(self.current_mapping) > 0 and max(self.current_mapping.values()) > 4: + raise RuntimeError( + "Too many qubits allocated. The IBM Q " + "device supports at most 5 qubits and no " + "intermediate measurements / " + "reallocations." + ) if len(self._interactions) > 0: - logical_ids = [qbid for qbid in self.current_mapping] + logical_ids = list(self.current_mapping) best_mapping = self.current_mapping best_cost = None - for physical_ids in itertools.permutations(list(range(5)), - len(logical_ids)): - mapping = {logical_ids[i]: physical_ids[i] - for i in range(len(logical_ids))} + for physical_ids in itertools.permutations(list(range(5)), len(logical_ids)): + mapping = {logical_ids[i]: physical_ids[i] for i in range(len(logical_ids))} new_cost = self._determine_cost(mapping) if new_cost is not None: if best_cost is None or new_cost < best_cost: best_cost = new_cost best_mapping = mapping if best_cost is None: - raise RuntimeError("Circuit cannot be mapped without using " - "Swaps. Mapping failed.") - self._interactions = dict() + raise RuntimeError("Circuit cannot be mapped without using Swaps. Mapping failed.") + self._interactions = {} self.current_mapping = best_mapping for cmd in self._cmds: @@ -153,7 +151,7 @@ def _store(self, cmd): """ if not cmd.gate == FlushGate(): target = cmd.qubits[0][0].id - if self._is_cnot(cmd): + if _is_cnot(cmd): # CNOT encountered ctrl = cmd.control_qubits[0].id if not (ctrl, target) in self._interactions: @@ -169,21 +167,32 @@ def _store(self, cmd): def receive(self, command_list): """ - Receives a command list and, for each command, stores it until - completion. + Receive a list of commands. + + Receive a command list and, for each command, stores it until completion. Args: command_list (list of Command objects): list of commands to receive. Raises: - Exception: If mapping the CNOT gates to 1 qubit would require - Swaps. The current version only supports remapping of CNOT - gates without performing any Swaps due to the large costs - associated with Swapping given the CNOT constraints. + Exception: If mapping the CNOT gates to 1 qubit would require Swaps. The current version only supports + remapping of CNOT gates without performing any Swaps due to the large costs associated with Swapping + given the CNOT constraints. """ for cmd in command_list: self._store(cmd) if isinstance(cmd.gate, FlushGate): self._run() self._reset() + + +def _is_cnot(cmd): + """ + Check if the command corresponds to a CNOT (controlled NOT gate). + + Args: + cmd (Command): Command to check whether it is a controlled NOT + gate. + """ + return isinstance(cmd.gate, NOT.__class__) and get_control_count(cmd) == 1 diff --git a/projectq/cengines/_ibm5qubitmapper_test.py b/projectq/cengines/_ibm5qubitmapper_test.py index 5c4c4c4da..f0b3285b4 100755 --- a/projectq/cengines/_ibm5qubitmapper_test.py +++ b/projectq/cengines/_ibm5qubitmapper_test.py @@ -11,32 +11,33 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._ibm5qubitmapper.py.""" import pytest from projectq import MainEngine -from projectq.cengines import DummyEngine -from projectq.ops import H, CNOT, X, Measure, All - -from projectq.cengines import _ibm5qubitmapper, SwapAndCNOTFlipper from projectq.backends import IBMBackend +from projectq.cengines import DummyEngine, SwapAndCNOTFlipper, _ibm5qubitmapper +from projectq.ops import CNOT, All, H def test_ibm5qubitmapper_is_available(monkeypatch): # Test that IBM5QubitMapper calls IBMBackend if gate is available. def mock_send(*args, **kwargs): return "Yes" + monkeypatch.setattr(_ibm5qubitmapper.IBMBackend, "is_available", mock_send) mapper = _ibm5qubitmapper.IBM5QubitMapper() assert mapper.is_available("TestCommand") == "Yes" def test_ibm5qubitmapper_invalid_circuit(): + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper(connections=connectivity)], + ) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -51,9 +52,12 @@ def test_ibm5qubitmapper_invalid_circuit(): def test_ibm5qubitmapper_valid_circuit1(): + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper(connections=connectivity)], + ) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -70,9 +74,12 @@ def test_ibm5qubitmapper_valid_circuit1(): def test_ibm5qubitmapper_valid_circuit2(): + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=backend, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper(connections=connectivity)], + ) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -89,6 +96,7 @@ def test_ibm5qubitmapper_valid_circuit2(): def test_ibm5qubitmapper_valid_circuit2_ibmqx4(): + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} backend = DummyEngine(save_commands=True) class FakeIBMBackend(IBMBackend): @@ -99,8 +107,10 @@ class FakeIBMBackend(IBMBackend): fake.is_available = backend.is_available backend.is_last_engine = True - eng = MainEngine(backend=fake, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper()]) + eng = MainEngine( + backend=fake, + engine_list=[_ibm5qubitmapper.IBM5QubitMapper(connections=connectivity)], + ) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() @@ -118,14 +128,18 @@ class FakeIBMBackend(IBMBackend): def test_ibm5qubitmapper_optimizeifpossible(): backend = DummyEngine(save_commands=True) - connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) - qb0 = eng.allocate_qubit() + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(connections=connectivity), + SwapAndCNOTFlipper(connectivity), + ], + ) + qb0 = eng.allocate_qubit() # noqa: F841 qb1 = eng.allocate_qubit() qb2 = eng.allocate_qubit() - qb3 = eng.allocate_qubit() + qb3 = eng.allocate_qubit() # noqa: F841 CNOT | (qb1, qb2) CNOT | (qb2, qb1) CNOT | (qb1, qb2) @@ -156,10 +170,14 @@ def test_ibm5qubitmapper_optimizeifpossible(): def test_ibm5qubitmapper_toomanyqubits(): backend = DummyEngine(save_commands=True) - connectivity = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) - eng = MainEngine(backend=backend, - engine_list=[_ibm5qubitmapper.IBM5QubitMapper(), - SwapAndCNOTFlipper(connectivity)]) + connectivity = {(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)} + eng = MainEngine( + backend=backend, + engine_list=[ + _ibm5qubitmapper.IBM5QubitMapper(), + SwapAndCNOTFlipper(connectivity), + ], + ) qubits = eng.allocate_qureg(6) All(H) | qubits CNOT | (qubits[0], qubits[1]) diff --git a/projectq/cengines/_linearmapper.py b/projectq/cengines/_linearmapper.py index 85ab72b3b..0e39bbf56 100644 --- a/projectq/cengines/_linearmapper.py +++ b/projectq/cengines/_linearmapper.py @@ -11,41 +11,43 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Mapper for a quantum circuit to a linear chain of qubits. -Input: Quantum circuit with 1 and 2 qubit gates on n qubits. Gates are assumed - to be applied in parallel if they act on disjoint qubit(s) and any pair - of qubits can perform a 2 qubit gate (all-to-all connectivity) -Output: Quantum circuit in which qubits are placed in 1-D chain in which only - nearest neighbour qubits can perform a 2 qubit gate. The mapper uses - Swap gates in order to move qubits next to each other. +Input: Quantum circuit with 1 and 2 qubit gates on n qubits. Gates are assumed to be applied in parallel if they act + on disjoint qubit(s) and any pair of qubits can perform a 2 qubit gate (all-to-all connectivity) +Output: Quantum circuit in which qubits are placed in 1-D chain in which only nearest neighbour qubits can perform a 2 + qubit gate. The mapper uses Swap gates in order to move qubits next to each other. """ -from collections import deque from copy import deepcopy -from projectq.cengines import BasicMapperEngine from projectq.meta import LogicalQubitIDTag -from projectq.ops import (Allocate, AllocateQubitGate, Deallocate, - DeallocateQubitGate, Command, FlushGate, - MeasureGate, Swap) +from projectq.ops import ( + Allocate, + AllocateQubitGate, + Command, + Deallocate, + DeallocateQubitGate, + FlushGate, + Swap, +) from projectq.types import WeakQubitRef +from ._basicmapper import BasicMapperEngine + def return_swap_depth(swaps): """ - Returns the circuit depth to execute these swaps. + Return the circuit depth to execute these swaps. Args: - swaps(list of tuples): Each tuple contains two integers representing - the two IDs of the qubits involved in the + swaps(list of tuples): Each tuple contains two integers representing the two IDs of the qubits involved in the Swap operation Returns: Circuit depth to execute these swaps. """ - depth_of_qubits = dict() + depth_of_qubits = {} for qb0_id, qb1_id in swaps: if qb0_id not in depth_of_qubits: depth_of_qubits[qb0_id] = 0 @@ -57,30 +59,27 @@ def return_swap_depth(swaps): return max(list(depth_of_qubits.values()) + [0]) -class LinearMapper(BasicMapperEngine): +class LinearMapper(BasicMapperEngine): # pylint: disable=too-many-instance-attributes """ - Maps a quantum circuit to a linear chain of nearest neighbour interactions. + Map a quantum circuit to a linear chain of nearest neighbour interactions. - Maps a quantum circuit to a linear chain of qubits with nearest neighbour - interactions using Swap gates. It supports open or cyclic boundary - conditions. + Maps a quantum circuit to a linear chain of qubits with nearest neighbour interactions using Swap gates. It + supports open or cyclic boundary conditions. Attributes: - current_mapping: Stores the mapping: key is logical qubit id, value - is mapped qubit id from 0,...,self.num_qubits + current_mapping: Stores the mapping: key is logical qubit id, value is mapped qubit id from + 0,...,self.num_qubits cyclic (Bool): If chain is cyclic or not storage (int): Number of gate it caches before mapping. num_mappings (int): Number of times the mapper changed the mapping - depth_of_swaps (dict): Key are circuit depth of swaps, value is the - number of such mappings which have been + depth_of_swaps (dict): Key are circuit depth of swaps, value is the number of such mappings which have been applied - num_of_swaps_per_mapping (dict): Key are the number of swaps per - mapping, value is the number of such - mappings which have been applied + num_of_swaps_per_mapping (dict): Key are the number of swaps per mapping, value is the number of such mappings + which have been applied Note: - 1) Gates are cached and only mapped from time to time. A - FastForwarding gate doesn't empty the cache, only a FlushGate does. + 1) Gates are cached and only mapped from time to time. A FastForwarding gate doesn't empty the cache, only a + FlushGate does. 2) Only 1 and two qubit gates allowed. 3) Does not optimize for dirty qubits. """ @@ -94,61 +93,46 @@ def __init__(self, num_qubits, cyclic=False, storage=1000): cyclic(bool): If 1D chain is a cycle. Default is False. storage(int): Number of gates to temporarily store, default is 1000 """ - BasicMapperEngine.__init__(self) + super().__init__() self.num_qubits = num_qubits self.cyclic = cyclic self.storage = storage # Storing commands - self._stored_commands = list() + self._stored_commands = [] # Logical qubit ids for which the Allocate gate has already been # processed and sent to the next engine but which are not yet # deallocated: self._currently_allocated_ids = set() # Statistics: self.num_mappings = 0 - self.depth_of_swaps = dict() - self.num_of_swaps_per_mapping = dict() + self.depth_of_swaps = {} + self.num_of_swaps_per_mapping = {} def is_available(self, cmd): - """ - Only allows 1 or two qubit gates. - """ + """Only allows 1 or two qubit gates.""" num_qubits = 0 for qureg in cmd.all_qubits: num_qubits += len(qureg) - if num_qubits <= 2: - return True - else: - return False + return num_qubits <= 2 @staticmethod - def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, - stored_commands, current_mapping): + def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, stored_commands, current_mapping): """ - Builds a mapping of qubits to a linear chain. + Build a mapping of qubits to a linear chain. - It goes through stored_commands and tries to find a - mapping to apply these gates on a first come first served basis. - More compilicated scheme could try to optimize to apply as many gates - as possible between the Swaps. + It goes through stored_commands and tries to find a mapping to apply these gates on a first come first served + basis. More complicated scheme could try to optimize to apply as many gates as possible between the Swaps. Args: num_qubits(int): Total number of qubits in the linear chain cyclic(bool): If linear chain is a cycle. - currently_allocated_ids(set of int): Logical qubit ids for which - the Allocate gate has already - been processed and sent to - the next engine but which are - not yet deallocated and hence - need to be included in the - new mapping. - stored_commands(list of Command objects): Future commands which - should be applied next. - current_mapping: A current mapping as a dict. key is logical qubit - id, value is placement id. If there are different - possible maps, this current mapping is used to - minimize the swaps to go to the new mapping by a - heuristic. + currently_allocated_ids(set of int): Logical qubit ids for which the Allocate gate has already been + processed and sent to the next engine but which are not yet + deallocated and hence need to be included in the new mapping. + stored_commands(list of Command objects): Future commands which should be applied next. + current_mapping: A current mapping as a dict. key is logical qubit id, value is placement id. If there are + different possible maps, this current mapping is used to minimize the swaps to go to the + new mapping by a heuristic. Returns: A new mapping as a dict. key is logical qubit id, value is placement id @@ -159,17 +143,16 @@ def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, allocated_qubits = deepcopy(currently_allocated_ids) active_qubits = deepcopy(currently_allocated_ids) # Segments contains a list of segments. A segment is a list of - # neighouring qubit ids + # neighbouring qubit ids segments = [] # neighbour_ids only used to speedup the lookup process if qubits # are already connected. key: qubit_id, value: set of neighbour ids - neighbour_ids = dict() + neighbour_ids = {} for qubit_id in active_qubits: neighbour_ids[qubit_id] = set() for cmd in stored_commands: - if (len(allocated_qubits) == num_qubits and - len(active_qubits) == 0): + if len(allocated_qubits) == num_qubits and len(active_qubits) == 0: break qubit_ids = [] @@ -178,10 +161,9 @@ def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, qubit_ids.append(qubit.id) if len(qubit_ids) > 2 or len(qubit_ids) == 0: - raise Exception("Invalid command (number of qubits): " + - str(cmd)) + raise Exception(f"Invalid command (number of qubits): {str(cmd)}") - elif isinstance(cmd.gate, AllocateQubitGate): + if isinstance(cmd.gate, AllocateQubitGate): qubit_id = cmd.qubits[0][0].id if len(allocated_qubits) < num_qubits: allocated_qubits.add(qubit_id) @@ -208,39 +190,41 @@ def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, qubit1=qubit_ids[1], active_qubits=active_qubits, segments=segments, - neighbour_ids=neighbour_ids) + neighbour_ids=neighbour_ids, + ) return LinearMapper._return_new_mapping_from_segments( num_qubits=num_qubits, segments=segments, allocated_qubits=allocated_qubits, - current_mapping=current_mapping) + current_mapping=current_mapping, + ) @staticmethod - def _process_two_qubit_gate(num_qubits, cyclic, qubit0, qubit1, - active_qubits, segments, neighbour_ids): + def _process_two_qubit_gate( # pylint: disable=too-many-arguments,too-many-branches,too-many-statements + num_qubits, cyclic, qubit0, qubit1, active_qubits, segments, neighbour_ids + ): """ - Processes a two qubit gate. + Process a two qubit gate. - It either removes the two qubits from active_qubits if the gate is not - possible or updates the segements such that the gate is possible. + It either removes the two qubits from active_qubits if the gate is not possible or updates the segments such + that the gate is possible. Args: num_qubits (int): Total number of qubits in the chain cyclic (bool): If linear chain is a cycle qubit0 (int): qubit.id of one of the qubits qubit1 (int): qubit.id of the other qubit - active_qubits (set): contains all qubit ids which for which gates - can be applied in this cycle before the swaps - segments: List of segments. A segment is a list of neighbouring - qubits. + active_qubits (set): contains all qubit ids which for which gates can be applied in this cycle before the + swaps + segments: List of segments. A segment is a list of neighbouring qubits. neighbour_ids (dict): Key: qubit.id Value: qubit.id of neighbours """ # already connected if qubit1 in neighbour_ids and qubit0 in neighbour_ids[qubit1]: return # at least one qubit is not an active qubit: - elif qubit0 not in active_qubits or qubit1 not in active_qubits: + if qubit0 not in active_qubits or qubit1 not in active_qubits: active_qubits.discard(qubit0) active_qubits.discard(qubit1) # at least one qubit is in the inside of a segment: @@ -248,7 +232,7 @@ def _process_two_qubit_gate(num_qubits, cyclic, qubit0, qubit1, active_qubits.discard(qubit0) active_qubits.discard(qubit1) # qubits are both active and either not yet in a segment or at - # the end of segement: + # the end of segment: else: segment_index_qb0 = None qb0_is_left_end = None @@ -304,21 +288,17 @@ def _process_two_qubit_gate(num_qubits, cyclic, qubit0, qubit1, # both qubits are at the end of different segments -> combine them else: if not qb0_is_left_end and qb1_is_left_end: - segments[segment_index_qb0].extend( - segments[segment_index_qb1]) + segments[segment_index_qb0].extend(segments[segment_index_qb1]) segments.pop(segment_index_qb1) elif not qb0_is_left_end and not qb1_is_left_end: - segments[segment_index_qb0].extend( - reversed(segments[segment_index_qb1])) + segments[segment_index_qb0].extend(reversed(segments[segment_index_qb1])) segments.pop(segment_index_qb1) elif qb0_is_left_end and qb1_is_left_end: segments[segment_index_qb0].reverse() - segments[segment_index_qb0].extend( - segments[segment_index_qb1]) + segments[segment_index_qb0].extend(segments[segment_index_qb1]) segments.pop(segment_index_qb1) else: - segments[segment_index_qb1].extend( - segments[segment_index_qb0]) + segments[segment_index_qb1].extend(segments[segment_index_qb0]) segments.pop(segment_index_qb0) # Add new neighbour ids and make sure to check cyclic neighbour_ids[qubit0].add(qubit1) @@ -329,31 +309,25 @@ def _process_two_qubit_gate(num_qubits, cyclic, qubit0, qubit1, return @staticmethod - def _return_new_mapping_from_segments(num_qubits, segments, - allocated_qubits, current_mapping): + def _return_new_mapping_from_segments( # pylint: disable=too-many-locals,too-many-branches + num_qubits, segments, allocated_qubits, current_mapping + ): """ - Combines the individual segments into a new mapping. + Combine the individual segments into a new mapping. - It tries to minimize the number of swaps to go from the old mapping - in self.current_mapping to the new mapping which it returns. The - strategy is to map a segment to the same region where most of the - qubits are already. Note that this is not a global optimal strategy - but helps if currently the qubits can be divided into independent - groups without interactions between the groups. + It tries to minimize the number of swaps to go from the old mapping in self.current_mapping to the new mapping + which it returns. The strategy is to map a segment to the same region where most of the qubits are + already. Note that this is not a global optimal strategy but helps if currently the qubits can be divided into + independent groups without interactions between the groups. Args: num_qubits (int): Total number of qubits in the linear chain - segments: List of segments. A segment is a list of qubit ids which - should be nearest neighbour in the new map. - Individual qubits are in allocated_qubits but not in - any segment - allocated_qubits: A set of all qubit ids which need to be present - in the new map - current_mapping: A current mapping as a dict. key is logical qubit - id, value is placement id. If there are different - possible maps, this current mapping is used to - minimize the swaps to go to the new mapping by a - heuristic. + segments: List of segments. A segment is a list of qubit ids which should be nearest neighbour in the new + map. Individual qubits are in allocated_qubits but not in any segment + allocated_qubits: A set of all qubit ids which need to be present in the new map + current_mapping: A current mapping as a dict. key is logical qubit id, value is placement id. If there are + different possible maps, this current mapping is used to minimize the swaps to go to the + new mapping by a heuristic. Returns: A new mapping as a dict. key is logical qubit id, value is placement id @@ -390,29 +364,33 @@ def _return_new_mapping_from_segments(num_qubits, segments, segment_ids = set(segment) segment_ids.discard(None) - overlap = len(previous_chain_ids.intersection( - segment_ids)) + previous_chain[idx0:idx1].count(None) + overlap = len(previous_chain_ids.intersection(segment_ids)) + previous_chain[idx0:idx1].count(None) if overlap == 0: overlap_fraction = 0 elif overlap == len(segment): overlap_fraction = 1 else: overlap_fraction = overlap / float(len(segment)) - if ((overlap_fraction == 1 and padding < best_padding) or - overlap_fraction > highest_overlap_fraction or - highest_overlap_fraction == 0): + if ( + (overlap_fraction == 1 and padding < best_padding) + or overlap_fraction > highest_overlap_fraction + or highest_overlap_fraction == 0 + ): best_segment = segment best_padding = padding highest_overlap_fraction = overlap_fraction # Add best segment and padding to new_chain - new_chain[current_position_to_fill+best_padding: - current_position_to_fill+best_padding + - len(best_segment)] = best_segment + new_chain[ + current_position_to_fill + + best_padding : current_position_to_fill # noqa: E203 + + best_padding + + len(best_segment) + ] = best_segment remaining_segments.remove(best_segment) current_position_to_fill += best_padding + len(best_segment) num_unused_qubits -= best_padding # Create mapping - new_mapping = dict() + new_mapping = {} for pos, logical_id in enumerate(new_chain): if logical_id is not None: new_mapping[logical_id] = pos @@ -420,59 +398,57 @@ def _return_new_mapping_from_segments(num_qubits, segments, def _odd_even_transposition_sort_swaps(self, old_mapping, new_mapping): """ - Returns the swap operation for an odd-even transposition sort. + Return the swap operation for an odd-even transposition sort. See https://en.wikipedia.org/wiki/Odd-even_sort for more info. Args: - old_mapping: dict: keys are logical ids and values are mapped - qubit ids - new_mapping: dict: keys are logical ids and values are mapped - qubit ids + old_mapping: dict: keys are logical ids and values are mapped qubit ids + new_mapping: dict: keys are logical ids and values are mapped qubit ids Returns: - List of tuples. Each tuple is a swap operation which needs to be - applied. Tuple contains the two MappedQubit ids for the Swap. + List of tuples. Each tuple is a swap operation which needs to be applied. Tuple contains the two + MappedQubit ids for the Swap. """ final_positions = [None] * self.num_qubits # move qubits which are in both mappings for logical_id in old_mapping: if logical_id in new_mapping: - final_positions[old_mapping[logical_id]] = new_mapping[ - logical_id] + final_positions[old_mapping[logical_id]] = new_mapping[logical_id] # exchange all remaining None with the not yet used mapped ids used_mapped_ids = set(final_positions) used_mapped_ids.discard(None) all_ids = set(range(self.num_qubits)) not_used_mapped_ids = list(all_ids.difference(used_mapped_ids)) not_used_mapped_ids = sorted(not_used_mapped_ids, reverse=True) - for i in range(len(final_positions)): - if final_positions[i] is None: + for i, pos in enumerate(final_positions): + if pos is None: final_positions[i] = not_used_mapped_ids.pop() - assert len(not_used_mapped_ids) == 0 + if len(not_used_mapped_ids) > 0: # pragma: no cover + raise RuntimeError('Internal compiler error: len(not_used_mapped_ids) > 0') # Start sorting: swap_operations = [] finished_sorting = False while not finished_sorting: finished_sorting = True - for i in range(1, len(final_positions)-1, 2): - if final_positions[i] > final_positions[i+1]: - swap_operations.append((i, i+1)) + for i in range(1, len(final_positions) - 1, 2): + if final_positions[i] > final_positions[i + 1]: + swap_operations.append((i, i + 1)) tmp = final_positions[i] - final_positions[i] = final_positions[i+1] - final_positions[i+1] = tmp + final_positions[i] = final_positions[i + 1] + final_positions[i + 1] = tmp finished_sorting = False - for i in range(0, len(final_positions)-1, 2): - if final_positions[i] > final_positions[i+1]: - swap_operations.append((i, i+1)) + for i in range(0, len(final_positions) - 1, 2): + if final_positions[i] > final_positions[i + 1]: + swap_operations.append((i, i + 1)) tmp = final_positions[i] - final_positions[i] = final_positions[i+1] - final_positions[i+1] = tmp + final_positions[i] = final_positions[i + 1] + final_positions[i + 1] = tmp finished_sorting = False return swap_operations - def _send_possible_commands(self): + def _send_possible_commands(self): # pylint: disable=too-many-branches """ - Sends the stored commands possible without changing the mapping. + Send the stored commands possible without changing the mapping. Note: self.current_mapping must exist already """ @@ -481,35 +457,32 @@ def _send_possible_commands(self): active_ids.add(logical_id) new_stored_commands = [] - for i in range(len(self._stored_commands)): - cmd = self._stored_commands[i] + for i, cmd in enumerate(self._stored_commands): if len(active_ids) == 0: new_stored_commands += self._stored_commands[i:] break if isinstance(cmd.gate, AllocateQubitGate): if cmd.qubits[0][0].id in self.current_mapping: self._currently_allocated_ids.add(cmd.qubits[0][0].id) - qb = WeakQubitRef( - engine=self, - idx=self.current_mapping[cmd.qubits[0][0].id]) + qb = WeakQubitRef(engine=self, idx=self.current_mapping[cmd.qubits[0][0].id]) new_cmd = Command( engine=self, gate=AllocateQubitGate(), qubits=([qb],), - tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)], + ) self.send([new_cmd]) else: new_stored_commands.append(cmd) elif isinstance(cmd.gate, DeallocateQubitGate): if cmd.qubits[0][0].id in active_ids: - qb = WeakQubitRef( - engine=self, - idx=self.current_mapping[cmd.qubits[0][0].id]) + qb = WeakQubitRef(engine=self, idx=self.current_mapping[cmd.qubits[0][0].id]) new_cmd = Command( engine=self, gate=DeallocateQubitGate(), qubits=([qb],), - tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)], + ) self._currently_allocated_ids.remove(cmd.qubits[0][0].id) active_ids.remove(cmd.qubits[0][0].id) self._current_mapping.pop(cmd.qubits[0][0].id) @@ -528,9 +501,9 @@ def _send_possible_commands(self): # Check that mapped ids are nearest neighbour if len(mapped_ids) == 2: mapped_ids = list(mapped_ids) - diff = abs(mapped_ids[0]-mapped_ids[1]) + diff = abs(mapped_ids[0] - mapped_ids[1]) if self.cyclic: - if diff != 1 and diff != self.num_qubits-1: + if diff not in (1, self.num_qubits - 1): send_gate = False else: if diff != 1: @@ -544,47 +517,45 @@ def _send_possible_commands(self): new_stored_commands.append(cmd) self._stored_commands = new_stored_commands - def _run(self): + def _run(self): # pylint: disable=too-many-locals,too-many-branches """ - Creates a new mapping and executes possible gates. + Create a new mapping and executes possible gates. - It first allocates all 0, ..., self.num_qubits-1 mapped qubit ids, if - they are not already used because we might need them all for the - swaps. Then it creates a new map, swaps all the qubits to the new map, - executes all possible gates, and finally deallocates mapped qubit ids - which don't store any information. + It first allocates all 0, ..., self.num_qubits-1 mapped qubit ids, if they are not already used because we + might need them all for the swaps. Then it creates a new map, swaps all the qubits to the new map, executes + all possible gates, and finally deallocates mapped qubit ids which don't store any information. """ num_of_stored_commands_before = len(self._stored_commands) if not self.current_mapping: - self.current_mapping = dict() + self.current_mapping = {} else: self._send_possible_commands() if len(self._stored_commands) == 0: return - new_mapping = self.return_new_mapping(self.num_qubits, - self.cyclic, - self._currently_allocated_ids, - self._stored_commands, - self.current_mapping) - swaps = self._odd_even_transposition_sort_swaps( - old_mapping=self.current_mapping, new_mapping=new_mapping) + new_mapping = self.return_new_mapping( + self.num_qubits, + self.cyclic, + self._currently_allocated_ids, + self._stored_commands, + self.current_mapping, + ) + swaps = self._odd_even_transposition_sort_swaps(old_mapping=self.current_mapping, new_mapping=new_mapping) if swaps: # first mapping requires no swaps # Allocate all mapped qubit ids (which are not already allocated, # i.e., contained in self._currently_allocated_ids) mapped_ids_used = set() for logical_id in self._currently_allocated_ids: mapped_ids_used.add(self.current_mapping[logical_id]) - not_allocated_ids = set(range(self.num_qubits)).difference( - mapped_ids_used) + not_allocated_ids = set(range(self.num_qubits)).difference(mapped_ids_used) for mapped_id in not_allocated_ids: qb = WeakQubitRef(engine=self, idx=mapped_id) cmd = Command(engine=self, gate=Allocate, qubits=([qb],)) self.send([cmd]) # Send swap operations to arrive at new_mapping: for qubit_id0, qubit_id1 in swaps: - q0 = WeakQubitRef(engine=self, idx=qubit_id0) - q1 = WeakQubitRef(engine=self, idx=qubit_id1) - cmd = Command(engine=self, gate=Swap, qubits=([q0], [q1])) + qb0 = WeakQubitRef(engine=self, idx=qubit_id0) + qb1 = WeakQubitRef(engine=self, idx=qubit_id1) + cmd = Command(engine=self, gate=Swap, qubits=([qb0], [qb1])) self.send([cmd]) # Register statistics: self.num_mappings += 1 @@ -602,12 +573,10 @@ def _run(self): mapped_ids_used = set() for logical_id in self._currently_allocated_ids: mapped_ids_used.add(new_mapping[logical_id]) - not_needed_anymore = set(range(self.num_qubits)).difference( - mapped_ids_used) + not_needed_anymore = set(range(self.num_qubits)).difference(mapped_ids_used) for mapped_id in not_needed_anymore: qb = WeakQubitRef(engine=self, idx=mapped_id) - cmd = Command(engine=self, gate=Deallocate, - qubits=([qb],)) + cmd = Command(engine=self, gate=Deallocate, qubits=([qb],)) self.send([cmd]) # Change to new map: self.current_mapping = new_mapping @@ -615,23 +584,24 @@ def _run(self): self._send_possible_commands() # Check that mapper actually made progress if len(self._stored_commands) == num_of_stored_commands_before: - raise RuntimeError("Mapper is potentially in an infinite loop. " - "It is likely that the algorithm requires " - "too many qubits. Increase the number of " - "qubits for this mapper.") + raise RuntimeError( + "Mapper is potentially in an infinite loop. It is likely that the algorithm requires too many" + "qubits. Increase the number of qubits for this mapper." + ) def receive(self, command_list): """ - Receives a command list and, for each command, stores it until - we do a mapping (FlushGate or Cache of stored commands is full). + Receive a list of commands. + + Receive a command list and, for each command, stores it until we do a mapping (FlushGate or Cache of stored + commands is full). Args: - command_list (list of Command objects): list of commands to - receive. + command_list (list of Command objects): list of commands to receive. """ for cmd in command_list: if isinstance(cmd.gate, FlushGate): - while(len(self._stored_commands)): + while self._stored_commands: self._run() self.send([cmd]) else: diff --git a/projectq/cengines/_linearmapper_test.py b/projectq/cengines/_linearmapper_test.py index dd76da617..5a5657adf 100644 --- a/projectq/cengines/_linearmapper_test.py +++ b/projectq/cengines/_linearmapper_test.py @@ -11,20 +11,26 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._linearmapper.py.""" from copy import deepcopy import pytest from projectq.cengines import DummyEngine +from projectq.cengines import _linearmapper as lm from projectq.meta import LogicalQubitIDTag -from projectq.ops import (Allocate, BasicGate, CNOT, Command, Deallocate, - FlushGate, Measure, QFT, X) +from projectq.ops import ( + CNOT, + QFT, + Allocate, + BasicGate, + Command, + Deallocate, + FlushGate, + X, +) from projectq.types import WeakQubitRef -from projectq.cengines import _linearmapper as lm - def test_return_swap_depth(): swaps = [] @@ -63,7 +69,8 @@ def test_return_new_mapping_too_many_qubits(): cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) + current_mapping=mapper.current_mapping, + ) cmd1 = Command(None, BasicGate(), qubits=([],)) mapper._stored_commands = [cmd1] with pytest.raises(Exception): @@ -72,14 +79,15 @@ def test_return_new_mapping_too_many_qubits(): cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) + current_mapping=mapper.current_mapping, + ) def test_return_new_mapping_allocate_qubits(): mapper = lm.LinearMapper(num_qubits=2, cyclic=False) qb0 = WeakQubitRef(engine=None, idx=0) qb1 = WeakQubitRef(engine=None, idx=1) - mapper._currently_allocated_ids = set([4]) + mapper._currently_allocated_ids = {4} cmd0 = Command(None, Allocate, ([qb0],)) cmd1 = Command(None, Allocate, ([qb1],)) mapper._stored_commands = [cmd0, cmd1] @@ -88,8 +96,9 @@ def test_return_new_mapping_allocate_qubits(): cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) - assert mapper._currently_allocated_ids == set([4]) + current_mapping=mapper.current_mapping, + ) + assert mapper._currently_allocated_ids == {4} assert mapper._stored_commands == [cmd0, cmd1] assert len(new_mapping) == 2 assert 4 in new_mapping and 0 in new_mapping @@ -98,7 +107,7 @@ def test_return_new_mapping_allocate_qubits(): def test_return_new_mapping_allocate_only_once(): mapper = lm.LinearMapper(num_qubits=1, cyclic=False) qb0 = WeakQubitRef(engine=None, idx=0) - qb1 = WeakQubitRef(engine=None, idx=1) + qb1 = WeakQubitRef(engine=None, idx=1) # noqa: F841 mapper._currently_allocated_ids = set() cmd0 = Command(None, Allocate, ([qb0],)) cmd1 = Command(None, Deallocate, ([qb0],)) @@ -106,12 +115,13 @@ def test_return_new_mapping_allocate_only_once(): # This would otherwise trigger an error (test by num_qubits=2) cmd2 = None mapper._stored_commands = [cmd0, cmd1, cmd2] - new_mapping = mapper.return_new_mapping( + mapper.return_new_mapping( num_qubits=mapper.num_qubits, cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) + current_mapping=mapper.current_mapping, + ) def test_return_new_mapping_possible_map(): @@ -131,9 +141,9 @@ def test_return_new_mapping_possible_map(): cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) - assert (new_mapping == {0: 2, 1: 1, 2: 0} or - new_mapping == {0: 0, 1: 1, 2: 2}) + current_mapping=mapper.current_mapping, + ) + assert new_mapping == {0: 2, 1: 1, 2: 0} or new_mapping == {0: 0, 1: 1, 2: 2} def test_return_new_mapping_previous_error(): @@ -148,48 +158,53 @@ def test_return_new_mapping_previous_error(): cmd3 = Command(None, Allocate, ([qb3],)) cmd4 = Command(None, CNOT, qubits=([qb2],), controls=[qb3]) mapper._stored_commands = [cmd0, cmd1, cmd2, cmd3, cmd4] - new_mapping = mapper.return_new_mapping( + mapper.return_new_mapping( num_qubits=mapper.num_qubits, cyclic=mapper.cyclic, currently_allocated_ids=mapper._currently_allocated_ids, stored_commands=mapper._stored_commands, - current_mapping=mapper.current_mapping) + current_mapping=mapper.current_mapping, + ) def test_process_two_qubit_gate_not_in_segments_test0(): mapper = lm.LinearMapper(num_qubits=5, cyclic=False) segments = [[0, 1]] - active_qubits = set([0, 1, 4, 6]) - neighbour_ids = {0: set([1]), 1: set([0]), 4: set(), 6: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=4, - qubit1=6, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 4, 6} + neighbour_ids = {0: {1}, 1: {0}, 4: set(), 6: set()} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=4, + qubit1=6, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert len(segments) == 2 assert segments[0] == [0, 1] assert segments[1] == [4, 6] - assert neighbour_ids[4] == set([6]) - assert neighbour_ids[6] == set([4]) - assert active_qubits == set([0, 1, 4, 6]) + assert neighbour_ids[4] == {6} + assert neighbour_ids[6] == {4} + assert active_qubits == {0, 1, 4, 6} def test_process_two_qubit_gate_not_in_segments_test1(): mapper = lm.LinearMapper(num_qubits=5, cyclic=False) segments = [] - active_qubits = set([4, 6]) + active_qubits = {4, 6} neighbour_ids = {4: set(), 6: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=5, - qubit1=6, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=5, + qubit1=6, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert len(segments) == 0 - assert active_qubits == set([4]) + assert active_qubits == {4} @pytest.mark.parametrize("qb0, qb1", [(1, 2), (2, 1)]) @@ -197,19 +212,21 @@ def test_process_two_qubit_gate_one_qb_free_one_qb_in_segment(qb0, qb1): # add on the right to segment mapper = lm.LinearMapper(num_qubits=3, cyclic=False) segments = [[0, 1]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: {1}, 1: {0}, 2: set()} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([0, 1, 2]) - assert neighbour_ids[1] == set([0, 2]) - assert neighbour_ids[2] == set([1]) + assert active_qubits == {0, 1, 2} + assert neighbour_ids[1] == {0, 2} + assert neighbour_ids[2] == {1} @pytest.mark.parametrize("qb0, qb1", [(0, 1), (1, 0)]) @@ -217,38 +234,42 @@ def test_process_two_qubit_gate_one_qb_free_one_qb_in_segment2(qb0, qb1): # add on the left to segment mapper = lm.LinearMapper(num_qubits=3, cyclic=False) segments = [[1, 2]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([]), 1: set([2]), 2: set([1])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: set(), 1: {2}, 2: {1}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([0, 1, 2]) - assert neighbour_ids[1] == set([0, 2]) - assert neighbour_ids[0] == set([1]) + assert active_qubits == {0, 1, 2} + assert neighbour_ids[1] == {0, 2} + assert neighbour_ids[0] == {1} @pytest.mark.parametrize("qb0, qb1", [(1, 2), (2, 1)]) def test_process_two_qubit_gate_one_qb_free_one_qb_in_segment_cycle(qb0, qb1): mapper = lm.LinearMapper(num_qubits=3, cyclic=True) segments = [[0, 1]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: {1}, 1: {0}, 2: set()} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([0, 1, 2]) - assert neighbour_ids[1] == set([0, 2]) - assert neighbour_ids[2] == set([1, 0]) + assert active_qubits == {0, 1, 2} + assert neighbour_ids[1] == {0, 2} + assert neighbour_ids[2] == {1, 0} @pytest.mark.parametrize("qb0, qb1", [(1, 2), (2, 1)]) @@ -256,104 +277,128 @@ def test_process_two_qubit_gate_one_qb_free_one_qb_in_seg_cycle2(qb0, qb1): # not yet long enough segment for cycle mapper = lm.LinearMapper(num_qubits=4, cyclic=True) segments = [[0, 1]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: {1}, 1: {0}, 2: set()} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([0, 1, 2]) - assert neighbour_ids[1] == set([0, 2]) - assert neighbour_ids[2] == set([1]) + assert active_qubits == {0, 1, 2} + assert neighbour_ids[1] == {0, 2} + assert neighbour_ids[2] == {1} def test_process_two_qubit_gate_one_qubit_in_middle_of_segment(): mapper = lm.LinearMapper(num_qubits=5, cyclic=False) segments = [] - active_qubits = set([0, 1, 2, 3]) - neighbour_ids = {0: set([1]), 1: set([0, 2]), 2: set([1]), 3: set()} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=1, - qubit1=3, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2, 3} + neighbour_ids = {0: {1}, 1: {0, 2}, 2: {1}, 3: set()} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=1, + qubit1=3, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert len(segments) == 0 - assert active_qubits == set([0, 2]) + assert active_qubits == {0, 2} def test_process_two_qubit_gate_both_in_same_segment(): mapper = lm.LinearMapper(num_qubits=3, cyclic=False) segments = [[0, 1, 2]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([1]), 1: set([0, 2]), 2: set([1])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=0, - qubit1=2, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: {1}, 1: {0, 2}, 2: {1}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=0, + qubit1=2, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([1]) + assert active_qubits == {1} def test_process_two_qubit_gate_already_connected(): mapper = lm.LinearMapper(num_qubits=3, cyclic=False) segments = [[0, 1, 2]] - active_qubits = set([0, 1, 2]) - neighbour_ids = {0: set([1]), 1: set([0, 2]), 2: set([1])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=0, - qubit1=1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2} + neighbour_ids = {0: {1}, 1: {0, 2}, 2: {1}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=0, + qubit1=1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [[0, 1, 2]] - assert active_qubits == set([0, 1, 2]) + assert active_qubits == {0, 1, 2} -@pytest.mark.parametrize("qb0, qb1, result_seg", [ - (0, 2, [1, 0, 2, 3]), (0, 3, [2, 3, 0, 1]), (1, 2, [0, 1, 2, 3]), - (1, 3, [0, 1, 3, 2])]) +@pytest.mark.parametrize( + "qb0, qb1, result_seg", + [ + (0, 2, [1, 0, 2, 3]), + (0, 3, [2, 3, 0, 1]), + (1, 2, [0, 1, 2, 3]), + (1, 3, [0, 1, 3, 2]), + ], +) def test_process_two_qubit_gate_combine_segments(qb0, qb1, result_seg): mapper = lm.LinearMapper(num_qubits=4, cyclic=False) segments = [[0, 1], [2, 3]] - active_qubits = set([0, 1, 2, 3, 4]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set([3]), 3: set([2])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2, 3, 4} + neighbour_ids = {0: {1}, 1: {0}, 2: {3}, 3: {2}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [result_seg] or segments == [reversed(result_seg)] assert qb1 in neighbour_ids[qb0] assert qb0 in neighbour_ids[qb1] -@pytest.mark.parametrize("qb0, qb1, result_seg", [ - (0, 2, [1, 0, 2, 3]), (0, 3, [2, 3, 0, 1]), (1, 2, [0, 1, 2, 3]), - (1, 3, [0, 1, 3, 2])]) +@pytest.mark.parametrize( + "qb0, qb1, result_seg", + [ + (0, 2, [1, 0, 2, 3]), + (0, 3, [2, 3, 0, 1]), + (1, 2, [0, 1, 2, 3]), + (1, 3, [0, 1, 3, 2]), + ], +) def test_process_two_qubit_gate_combine_segments_cycle(qb0, qb1, result_seg): mapper = lm.LinearMapper(num_qubits=4, cyclic=True) segments = [[0, 1], [2, 3]] - active_qubits = set([0, 1, 2, 3, 4]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set([3]), 3: set([2])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2, 3, 4} + neighbour_ids = {0: {1}, 1: {0}, 2: {3}, 3: {2}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [result_seg] or segments == [reversed(result_seg)] assert qb1 in neighbour_ids[qb0] assert qb0 in neighbour_ids[qb1] @@ -361,22 +406,30 @@ def test_process_two_qubit_gate_combine_segments_cycle(qb0, qb1, result_seg): assert result_seg[-1] in neighbour_ids[result_seg[0]] -@pytest.mark.parametrize("qb0, qb1, result_seg", [ - (0, 2, [1, 0, 2, 3]), (0, 3, [2, 3, 0, 1]), (1, 2, [0, 1, 2, 3]), - (1, 3, [0, 1, 3, 2])]) +@pytest.mark.parametrize( + "qb0, qb1, result_seg", + [ + (0, 2, [1, 0, 2, 3]), + (0, 3, [2, 3, 0, 1]), + (1, 2, [0, 1, 2, 3]), + (1, 3, [0, 1, 3, 2]), + ], +) def test_process_two_qubit_gate_combine_segments_cycle2(qb0, qb1, result_seg): # Not long enough segment for cyclic mapper = lm.LinearMapper(num_qubits=5, cyclic=True) segments = [[0, 1], [2, 3]] - active_qubits = set([0, 1, 2, 3, 4]) - neighbour_ids = {0: set([1]), 1: set([0]), 2: set([3]), 3: set([2])} - mapper._process_two_qubit_gate(num_qubits=mapper.num_qubits, - cyclic=mapper.cyclic, - qubit0=qb0, - qubit1=qb1, - active_qubits=active_qubits, - segments=segments, - neighbour_ids=neighbour_ids) + active_qubits = {0, 1, 2, 3, 4} + neighbour_ids = {0: {1}, 1: {0}, 2: {3}, 3: {2}} + mapper._process_two_qubit_gate( + num_qubits=mapper.num_qubits, + cyclic=mapper.cyclic, + qubit0=qb0, + qubit1=qb1, + active_qubits=active_qubits, + segments=segments, + neighbour_ids=neighbour_ids, + ) assert segments == [result_seg] or segments == [reversed(result_seg)] assert qb1 in neighbour_ids[qb0] assert qb0 in neighbour_ids[qb1] @@ -385,15 +438,17 @@ def test_process_two_qubit_gate_combine_segments_cycle2(qb0, qb1, result_seg): @pytest.mark.parametrize( - "segments, current_chain, correct_chain, allocated_qubits", [ + "segments, current_chain, correct_chain, allocated_qubits", + [ ([[0, 2, 4]], [0, 1, 2, 3, 4], [0, 2, 4, 3, 1], [0, 1, 2, 3, 4]), ([[0, 2, 4]], [0, 1, 2, 3, 4], [0, 2, 4, 3, None], [0, 2, 3, 4]), ([[1, 2], [3, 0]], [0, 1, 2, 3, 4], [None, 1, 2, 3, 0], [0, 1, 2, 3]), - ([[1, 2], [3, 0]], [0, 1, 2, 3, 4], [1, 2, 3, 0, 4], [0, 1, 2, 3, 4])]) -def test_return_new_mapping_from_segments(segments, current_chain, - correct_chain, allocated_qubits): + ([[1, 2], [3, 0]], [0, 1, 2, 3, 4], [1, 2, 3, 0, 4], [0, 1, 2, 3, 4]), + ], +) +def test_return_new_mapping_from_segments(segments, current_chain, correct_chain, allocated_qubits): mapper = lm.LinearMapper(num_qubits=5, cyclic=False) - current_mapping = dict() + current_mapping = {} for pos, logical_id in enumerate(current_chain): current_mapping[logical_id] = pos mapper.current_mapping = current_mapping @@ -401,24 +456,28 @@ def test_return_new_mapping_from_segments(segments, current_chain, num_qubits=mapper.num_qubits, segments=segments, allocated_qubits=allocated_qubits, - current_mapping=mapper.current_mapping) - correct_mapping = dict() + current_mapping=mapper.current_mapping, + ) + correct_mapping = {} for pos, logical_id in enumerate(correct_chain): if logical_id is not None: correct_mapping[logical_id] = pos assert correct_mapping == new_mapping -@pytest.mark.parametrize("old_chain, new_chain", [ - ([0, 1, 2, 3, 4], [4, 3, 2, 1, 0]), - ([2, 0, 14, 44, 12], [14, 12, 44, 0, 2]), - ([2, None, 14, 44, 12], [14, 1, 44, 0, 2]), - ([2, None, 14, 44, 12], [14, None, 44, 0, 2]) - ]) +@pytest.mark.parametrize( + "old_chain, new_chain", + [ + ([0, 1, 2, 3, 4], [4, 3, 2, 1, 0]), + ([2, 0, 14, 44, 12], [14, 12, 44, 0, 2]), + ([2, None, 14, 44, 12], [14, 1, 44, 0, 2]), + ([2, None, 14, 44, 12], [14, None, 44, 0, 2]), + ], +) def test_odd_even_transposition_sort_swaps(old_chain, new_chain): mapper = lm.LinearMapper(num_qubits=5, cyclic=False) - old_map = dict() - new_map = dict() + old_map = {} + new_map = {} for pos, logical_id in enumerate(old_chain): if logical_id is not None: old_map[logical_id] = pos @@ -447,12 +506,11 @@ def test_send_possible_commands_allocate(): backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) mapper._stored_commands = [cmd0] - mapper._currently_allocated_ids = set([10]) + mapper._currently_allocated_ids = {10} # not in mapping: - mapper.current_mapping = dict() + mapper.current_mapping = {} assert len(backend.received_commands) == 0 mapper._send_possible_commands() assert len(backend.received_commands) == 0 @@ -465,7 +523,7 @@ def test_send_possible_commands_allocate(): assert backend.received_commands[0].gate == Allocate assert backend.received_commands[0].qubits[0][0].id == 3 assert backend.received_commands[0].tags == [LogicalQubitIDTag(0)] - assert mapper._currently_allocated_ids == set([10, 0]) + assert mapper._currently_allocated_ids == {10, 0} def test_send_possible_commands_deallocate(): @@ -474,11 +532,10 @@ def test_send_possible_commands_deallocate(): backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) - cmd0 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], tags=[]) mapper._stored_commands = [cmd0] - mapper.current_mapping = dict() - mapper._currently_allocated_ids = set([10]) + mapper.current_mapping = {} + mapper._currently_allocated_ids = {10} # not yet allocated: mapper._send_possible_commands() assert len(backend.received_commands) == 0 @@ -492,8 +549,8 @@ def test_send_possible_commands_deallocate(): assert backend.received_commands[0].qubits[0][0].id == 3 assert backend.received_commands[0].tags == [LogicalQubitIDTag(0)] assert len(mapper._stored_commands) == 0 - assert mapper.current_mapping == dict() - assert mapper._currently_allocated_ids == set([10]) + assert mapper.current_mapping == {} + assert mapper._currently_allocated_ids == {10} def test_send_possible_commands_keep_remaining_gates(): @@ -503,12 +560,9 @@ def test_send_possible_commands_keep_remaining_gates(): mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) qb1 = WeakQubitRef(engine=None, idx=1) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) - cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], - tags=[]) - cmd2 = Command(engine=None, gate=Allocate, qubits=([qb1],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) + cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], tags=[]) + cmd2 = Command(engine=None, gate=Allocate, qubits=([qb1],), controls=[], tags=[]) mapper._stored_commands = [cmd0, cmd1, cmd2] mapper.current_mapping = {0: 0} @@ -525,7 +579,7 @@ def test_send_possible_commands_not_cyclic(): qb1 = WeakQubitRef(engine=None, idx=1) qb2 = WeakQubitRef(engine=None, idx=2) qb3 = WeakQubitRef(engine=None, idx=3) - mapper._currently_allocated_ids = set([0, 1, 2, 3]) + mapper._currently_allocated_ids = {0, 1, 2, 3} cmd0 = Command(None, CNOT, qubits=([qb0],), controls=[qb2]) cmd1 = Command(None, CNOT, qubits=([qb1],), controls=[qb2]) cmd2 = Command(None, CNOT, qubits=([qb1],), controls=[qb3]) @@ -535,9 +589,7 @@ def test_send_possible_commands_not_cyclic(): mapper.current_mapping = {0: 0, 2: 1, 3: 2, 1: 3} mapper._send_possible_commands() assert len(backend.received_commands) == 2 - assert (backend.received_commands[0] == Command(None, CNOT, - qubits=([qb0],), - controls=[qb1])) + assert backend.received_commands[0] == Command(None, CNOT, qubits=([qb0],), controls=[qb1]) assert backend.received_commands[1] == Command(None, X, qubits=([qb0],)) # Following chain 0 <-> 2 <-> 1 <-> 3 mapper.current_mapping = {0: 0, 2: 1, 3: 3, 1: 2} @@ -555,7 +607,7 @@ def test_send_possible_commands_cyclic(): qb1 = WeakQubitRef(engine=None, idx=1) qb2 = WeakQubitRef(engine=None, idx=2) qb3 = WeakQubitRef(engine=None, idx=3) - mapper._currently_allocated_ids = set([0, 1, 2, 3]) + mapper._currently_allocated_ids = {0, 1, 2, 3} cmd0 = Command(None, CNOT, qubits=([qb0],), controls=[qb1]) cmd1 = Command(None, CNOT, qubits=([qb1],), controls=[qb2]) cmd2 = Command(None, CNOT, qubits=([qb1],), controls=[qb3]) @@ -565,9 +617,7 @@ def test_send_possible_commands_cyclic(): mapper.current_mapping = {0: 0, 2: 1, 3: 2, 1: 3} mapper._send_possible_commands() assert len(backend.received_commands) == 2 - assert (backend.received_commands[0] == Command(None, CNOT, - qubits=([qb0],), - controls=[qb3])) + assert backend.received_commands[0] == Command(None, CNOT, qubits=([qb0],), controls=[qb3]) assert backend.received_commands[1] == Command(None, X, qubits=([qb0],)) # Following chain 0 <-> 2 <-> 1 <-> 3 mapper.current_mapping = {0: 0, 2: 1, 3: 3, 1: 2} @@ -597,13 +647,15 @@ def test_run_and_receive(): mapper.receive([cmd_flush]) assert mapper._stored_commands == [] assert len(backend.received_commands) == 7 - assert mapper._currently_allocated_ids == set([0, 2]) - assert (mapper.current_mapping == {0: 2, 2: 0} or - mapper.current_mapping == {0: 0, 2: 2}) + assert mapper._currently_allocated_ids == {0, 2} + assert mapper.current_mapping == {0: 2, 2: 0} or mapper.current_mapping == { + 0: 0, + 2: 2, + } cmd6 = Command(None, X, qubits=([qb0],), controls=[qb2]) mapper.storage = 1 mapper.receive([cmd6]) - assert mapper._currently_allocated_ids == set([0, 2]) + assert mapper._currently_allocated_ids == {0, 2} assert mapper._stored_commands == [] assert len(mapper.current_mapping) == 2 assert 0 in mapper.current_mapping @@ -611,11 +663,12 @@ def test_run_and_receive(): assert len(backend.received_commands) == 11 for cmd in backend.received_commands: print(cmd) - assert (backend.received_commands[-1] == Command(None, X, - qubits=([WeakQubitRef(engine=None, - idx=mapper.current_mapping[qb0.id])],), - controls=[WeakQubitRef(engine=None, - idx=mapper.current_mapping[qb2.id])])) + assert backend.received_commands[-1] == Command( + None, + X, + qubits=([WeakQubitRef(engine=None, idx=mapper.current_mapping[qb0.id])],), + controls=[WeakQubitRef(engine=None, idx=mapper.current_mapping[qb2.id])], + ) assert mapper.num_mappings == 1 @@ -679,7 +732,8 @@ def test_send_possible_cmds_before_new_mapping(): backend.is_last_engine = True mapper.next_engine = backend - def dont_call_mapping(): raise Exception + def dont_call_mapping(): + raise Exception mapper._return_new_mapping = dont_call_mapping mapper.current_mapping = {0: 1} @@ -710,6 +764,5 @@ def test_correct_stats(): cmd8 = Command(None, X, qubits=([qb1],), controls=[qb2]) qb_flush = WeakQubitRef(engine=None, idx=-1) cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush],)) - mapper.receive([cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7, cmd8, - cmd_flush]) + mapper.receive([cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7, cmd8, cmd_flush]) assert mapper.num_mappings == 2 diff --git a/projectq/cengines/_main.py b/projectq/cengines/_main.py index f9bc0dbfe..87ec1b3d3 100755 --- a/projectq/cengines/_main.py +++ b/projectq/cengines/_main.py @@ -12,38 +12,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains the main engine of every compiler engine pipeline, called MainEngine. -""" +"""The main engine of every compiler engine pipeline, called MainEngine.""" import atexit import sys import traceback import weakref -import projectq -from projectq.cengines import BasicEngine, BasicMapperEngine +from projectq.backends import Simulator from projectq.ops import Command, FlushGate from projectq.types import WeakQubitRef -from projectq.backends import Simulator + +from ._basicmapper import BasicMapperEngine +from ._basics import BasicEngine class NotYetMeasuredError(Exception): - pass + """Exception raised when trying to access the measurement value of a qubit that has not yet been measured.""" class UnsupportedEngineError(Exception): - pass + """Exception raised when a non-supported compiler engine is encountered.""" -class MainEngine(BasicEngine): +class _ErrorEngine: # pylint: disable=too-few-public-methods + """ + Fake compiler engine class. + + Fake compiler engine class only used to ensure gracious failure when an exception occurs in the MainEngine + constructor. """ - The MainEngine class provides all functionality of the main compiler - engine. - It initializes all further compiler engines (calls, e.g., - .next_engine=...) and keeps track of measurement results and active - qubits (and their IDs). + def receive(self, command_list): # pylint: disable=unused-argument + """No-op.""" + + +_N_ENGINES_THRESHOLD = 100 + + +class MainEngine(BasicEngine): # pylint: disable=too-many-instance-attributes + """ + The MainEngine class provides all functionality of the main compiler engine. + + It initializes all further compiler engines (calls, e.g., .next_engine=...) and keeps track of measurement results + and active qubits (and their IDs). Attributes: next_engine (BasicEngine): Next compiler engine (or the back-end). @@ -52,20 +64,23 @@ class MainEngine(BasicEngine): dirty_qubits (Set): Containing all dirty qubit ids backend (BasicEngine): Access the back-end. mapper (BasicMapperEngine): Access to the mapper if there is one. - + n_engines (int): Current number of compiler engines in the engine list + n_engines_max (int): Maximum number of compiler engines allowed in the engine list. Defaults to 100. """ - def __init__(self, backend=None, engine_list=None, verbose=False): + + def __init__( # pylint: disable=too-many-statements,too-many-branches + self, backend=None, engine_list=None, verbose=False + ): """ Initialize the main compiler engine and all compiler engines. - Sets 'next_engine'- and 'main_engine'-attributes of all compiler - engines and adds the back-end as the last engine. + Sets 'next_engine'- and 'main_engine'-attributes of all compiler engines and adds the back-end as the last + engine. Args: backend (BasicEngine): Backend to send the compiled circuit to. - engine_list (list): List of engines / backends to use - as compiler engines. Note: The engine list must not contain - multiple mappers (instances of BasicMapperEngine). + engine_list (list): List of engines / backends to use as compiler engines. Note: The engine + list must not contain multiple mappers (instances of BasicMapperEngine). Default: projectq.setups.default.get_engine_list() verbose (bool): Either print full or compact error messages. Default: False (i.e. compact error messages). @@ -74,7 +89,8 @@ def __init__(self, backend=None, engine_list=None, verbose=False): .. code-block:: python from projectq import MainEngine - eng = MainEngine() # uses default engine_list and the Simulator + + eng = MainEngine() # uses default engine_list and the Simulator Instead of the default `engine_list` one can use, e.g., one of the IBM setups which defines a custom `engine_list` useful for one of the IBM @@ -85,6 +101,7 @@ def __init__(self, backend=None, engine_list=None, verbose=False): import projectq.setups.ibm as ibm_setup from projectq import MainEngine + eng = MainEngine(engine_list=ibm_setup.get_engine_list()) # eng uses the default Simulator backend @@ -93,31 +110,45 @@ def __init__(self, backend=None, engine_list=None, verbose=False): Example: .. code-block:: python - from projectq.cengines import (TagRemover, AutoReplacer, - LocalOptimizer, - DecompositionRuleSet) + from projectq.cengines import ( + TagRemover, + AutoReplacer, + LocalOptimizer, + DecompositionRuleSet, + ) from projectq.backends import Simulator from projectq import MainEngine + rule_set = DecompositionRuleSet() - engines = [AutoReplacer(rule_set), TagRemover(), - LocalOptimizer(3)] + engines = [AutoReplacer(rule_set), TagRemover(), LocalOptimizer(3)] eng = MainEngine(Simulator(), engines) """ - BasicEngine.__init__(self) + super().__init__() + self.active_qubits = weakref.WeakSet() + self._measurements = {} + self.dirty_qubits = set() + self.verbose = verbose + self.main_engine = self + self.n_engines_max = _N_ENGINES_THRESHOLD if backend is None: backend = Simulator() else: # Test that backend is BasicEngine object if not isinstance(backend, BasicEngine): + self.next_engine = _ErrorEngine() raise UnsupportedEngineError( "\nYou supplied a backend which is not supported,\n" "i.e. not an instance of BasicEngine.\n" "Did you forget the brackets to create an instance?\n" "E.g. MainEngine(backend=Simulator) instead of \n" - " MainEngine(backend=Simulator())") + " MainEngine(backend=Simulator())" + ) + self.backend = backend + # default engine_list is projectq.setups.default.get_engine_list() if engine_list is None: - import projectq.setups.default + import projectq.setups.default # pylint: disable=import-outside-toplevel + engine_list = projectq.setups.default.get_engine_list() self.mapper = None @@ -125,32 +156,39 @@ def __init__(self, backend=None, engine_list=None, verbose=False): # Test that engine list elements are all BasicEngine objects for current_eng in engine_list: if not isinstance(current_eng, BasicEngine): + self.next_engine = _ErrorEngine() raise UnsupportedEngineError( "\nYou supplied an unsupported engine in engine_list," "\ni.e. not an instance of BasicEngine.\n" "Did you forget the brackets to create an instance?\n" - "E.g. MainEngine(engine_list=[AutoReplacer]) instead " - "of\n MainEngine(engine_list=[AutoReplacer()])") + "E.g. MainEngine(engine_list=[AutoReplacer]) instead of\n" + " MainEngine(engine_list=[AutoReplacer()])" + ) if isinstance(current_eng, BasicMapperEngine): if self.mapper is None: self.mapper = current_eng else: - raise UnsupportedEngineError( - "More than one mapper engine is not supported.") + self.next_engine = _ErrorEngine() + raise UnsupportedEngineError("More than one mapper engine is not supported.") else: - raise UnsupportedEngineError( - "The provided list of engines is not a list!") + self.next_engine = _ErrorEngine() + raise UnsupportedEngineError("The provided list of engines is not a list!") engine_list = engine_list + [backend] - self.backend = backend # Test that user did not supply twice the same engine instance - num_different_engines = len(set([id(item) for item in engine_list])) + num_different_engines = len({id(item) for item in engine_list}) if len(engine_list) != num_different_engines: + self.next_engine = _ErrorEngine() raise UnsupportedEngineError( "\nError:\n You supplied twice the same engine as backend" " or item in engine_list. This doesn't work. Create two \n" " separate instances of a compiler engine if it is needed\n" - " twice.\n") + " twice.\n" + ) + + self.n_engines = len(engine_list) + if self.n_engines > self.n_engines_max: + raise ValueError('Too many compiler engines added to the MainEngine!') self._qubit_idx = int(0) for i in range(len(engine_list) - 1): @@ -159,11 +197,6 @@ def __init__(self, backend=None, engine_list=None, verbose=False): engine_list[-1].main_engine = self engine_list[-1].is_last_engine = True self.next_engine = engine_list[0] - self.main_engine = self - self.active_qubits = weakref.WeakSet() - self._measurements = dict() - self.dirty_qubits = set() - self.verbose = verbose # In order to terminate an example code without eng.flush def atexit_function(weakref_main_eng): @@ -171,9 +204,8 @@ def atexit_function(weakref_main_eng): if eng is not None: if not hasattr(sys, "last_type"): eng.flush(deallocate_qubits=True) - # An exception causes the termination, don't send a flush and - # make sure no qubits send deallocation gates anymore as this - # might trigger additional exceptions + # An exception causes the termination, don't send a flush and make sure no qubits send deallocation + # gates anymore as this might trigger additional exceptions else: for qubit in eng.active_qubits: qubit.id = -1 @@ -186,37 +218,33 @@ def __del__(self): """ Destroy the main engine. - Flushes the entire circuit down the pipeline, clearing all temporary - buffers (in, e.g., optimizers). + Flushes the entire circuit down the pipeline, clearing all temporary buffers (in, e.g., optimizers). """ if not hasattr(sys, "last_type"): self.flush(deallocate_qubits=True) try: atexit.unregister(self._delfun) # only available in Python3 - except AttributeError: + except AttributeError: # pragma: no cover pass def set_measurement_result(self, qubit, value): """ - Register a measurement result + Register a measurement result. - The engine being responsible for measurement results needs to register - these results with the master engine such that they are available when - the user calls an int() or bool() conversion operator on a measured - qubit. + The engine being responsible for measurement results needs to register these results with the master engine + such that they are available when the user calls an int() or bool() conversion operator on a measured qubit. Args: - qubit (BasicQubit): Qubit for which to register the measurement - result. - value (bool): Boolean value of the measurement outcome - (True / False = 1 / 0 respectively). + qubit (BasicQubit): Qubit for which to register the measurement result. + value (bool): Boolean value of the measurement outcome (True / False = 1 / 0 respectively). """ self._measurements[qubit.id] = bool(value) def get_measurement_result(self, qubit): """ - Return the classical value of a measured qubit, given that an engine - registered this result previously (see setMeasurementResult). + Return the classical value of a measured qubit, given that an engine registered this result previously. + + See also setMeasurementResult. Args: qubit (BasicQubit): Qubit of which to get the measurement result. @@ -226,34 +254,32 @@ def get_measurement_result(self, qubit): from projectq.ops import H, Measure from projectq import MainEngine + eng = MainEngine() - qubit = eng.allocate_qubit() # quantum register of size 1 + qubit = eng.allocate_qubit() # quantum register of size 1 H | qubit Measure | qubit eng.get_measurement_result(qubit[0]) == int(qubit) """ if qubit.id in self._measurements: return self._measurements[qubit.id] - else: - raise NotYetMeasuredError( - "\nError: Can't access measurement result for " - "qubit #" + str(qubit.id) + ". The problem may " - "be:\n\t1. Your " - "code lacks a measurement statement\n\t" - "2. You have not yet called engine.flush() to " - "force execution of your code\n\t3. The " - "underlying backend failed to register " - "the measurement result\n") + raise NotYetMeasuredError( + "\nError: Can't access measurement result for qubit #" + str(qubit.id) + ". The problem may be:\n\t" + "1. Your code lacks a measurement statement\n\t" + "2. You have not yet called engine.flush() to force execution of your code\n\t" + "3. The " + "underlying backend failed to register the measurement result\n" + ) def get_new_qubit_id(self): """ - Returns a unique qubit id to be used for the next qubit allocation. + Return a unique qubit id to be used for the next qubit allocation. Returns: new_qubit_id (int): New unique qubit id. """ self._qubit_idx += 1 - return (self._qubit_idx - 1) + return self._qubit_idx - 1 def receive(self, command_list): """ @@ -273,32 +299,28 @@ def send(self, command_list): """ try: self.next_engine.receive(command_list) - except: + except Exception as err: # pylint: disable=broad-except if self.verbose: raise - else: - exc_type, exc_value, exc_traceback = sys.exc_info() - # try: - last_line = traceback.format_exc().splitlines() - compact_exception = exc_type(str(exc_value) + - '\n raised in:\n' + - repr(last_line[-3]) + - "\n" + repr(last_line[-2])) - compact_exception.__cause__ = None - raise compact_exception # use verbose=True for more info + exc_type, exc_value, _ = sys.exc_info() + # try: + last_line = traceback.format_exc().splitlines() + compact_exception = exc_type( + str(exc_value) + '\n raised in:\n' + repr(last_line[-3]) + "\n" + repr(last_line[-2]) + ) + compact_exception.__cause__ = None + raise compact_exception from err # use verbose=True for more info def flush(self, deallocate_qubits=False): """ - Flush the entire circuit down the pipeline, clearing potential buffers - (of, e.g., optimizers). + Flush the entire circuit down the pipeline, clearing potential buffers (of, e.g., optimizers). Args: - deallocate_qubits (bool): If True, deallocates all qubits that are - still alive (invalidating references to them by setting their - id to -1). + deallocate_qubits (bool): If True, deallocates all qubits that are still alive (invalidating references to + them by setting their id to -1). """ if deallocate_qubits: - while len(self.active_qubits): - qb = self.active_qubits.pop() - qb.__del__() + while [qb for qb in self.active_qubits if qb is not None]: + qb = self.active_qubits.pop() # noqa: F841 + qb.__del__() # pylint: disable=unnecessary-dunder-call self.receive([Command(self, FlushGate(), ([WeakQubitRef(self, -1)],))]) diff --git a/projectq/cengines/_main_test.py b/projectq/cengines/_main_test.py index a79a1a57f..665e8815f 100755 --- a/projectq/cengines/_main_test.py +++ b/projectq/cengines/_main_test.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,20 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._main.py.""" import sys import weakref import pytest -import projectq.setups.default -from projectq.cengines import DummyEngine, BasicMapperEngine, LocalOptimizer from projectq.backends import Simulator -from projectq.ops import (AllocateQubitGate, DeallocateQubitGate, FlushGate, - H, X) - -from projectq.cengines import _main +from projectq.cengines import BasicMapperEngine, DummyEngine, LocalOptimizer, _main +from projectq.ops import AllocateQubitGate, DeallocateQubitGate, FlushGate, H def test_main_engine_init(): @@ -50,14 +45,14 @@ def test_main_engine_init(): def test_main_engine_init_failure(): with pytest.raises(_main.UnsupportedEngineError): - eng = _main.MainEngine(backend=DummyEngine) + _main.MainEngine(backend=DummyEngine) with pytest.raises(_main.UnsupportedEngineError): - eng = _main.MainEngine(engine_list=DummyEngine) + _main.MainEngine(engine_list=DummyEngine) with pytest.raises(_main.UnsupportedEngineError): - eng = _main.MainEngine(engine_list=[DummyEngine(), DummyEngine]) + _main.MainEngine(engine_list=[DummyEngine(), DummyEngine]) with pytest.raises(_main.UnsupportedEngineError): engine = DummyEngine() - eng = _main.MainEngine(backend=engine, engine_list=[engine]) + _main.MainEngine(backend=engine, engine_list=[engine]) def test_main_engine_init_defaults(): @@ -69,13 +64,25 @@ def test_main_engine_init_defaults(): current_engine = current_engine.next_engine assert isinstance(eng_list[-1].next_engine, Simulator) import projectq.setups.default + default_engines = projectq.setups.default.get_engine_list() for engine, expected in zip(eng_list, default_engines): - assert type(engine) == type(expected) + assert type(engine) is type(expected) -def test_main_engine_init_mapper(): +def test_main_engine_too_many_compiler_engines(): + old = _main._N_ENGINES_THRESHOLD + _main._N_ENGINES_THRESHOLD = 3 + + _main.MainEngine(backend=DummyEngine(), engine_list=[DummyEngine(), DummyEngine()]) + + with pytest.raises(ValueError): + _main.MainEngine(backend=DummyEngine(), engine_list=[DummyEngine(), DummyEngine(), DummyEngine()]) + _main._N_ENGINES_THRESHOLD = old + + +def test_main_engine_init_mapper(): class LinearMapper(BasicMapperEngine): pass @@ -89,7 +96,7 @@ class LinearMapper(BasicMapperEngine): assert eng2.mapper == mapper2 engine_list3 = [mapper1, mapper2] with pytest.raises(_main.UnsupportedEngineError): - eng3 = _main.MainEngine(engine_list=engine_list3) + _main.MainEngine(engine_list=engine_list3) def test_main_engine_del(): @@ -97,7 +104,7 @@ def test_main_engine_del(): sys.last_type = None del sys.last_type # need engine which caches commands to test that del calls flush - caching_engine = LocalOptimizer(m=5) + caching_engine = LocalOptimizer(cache_size=5) backend = DummyEngine(save_commands=True) eng = _main.MainEngine(backend=backend, engine_list=[caching_engine]) qubit = eng.allocate_qubit() @@ -152,7 +159,7 @@ def test_main_engine_atexit_no_error(): del sys.last_type backend = DummyEngine(save_commands=True) eng = _main.MainEngine(backend=backend, engine_list=[]) - qb = eng.allocate_qubit() + qb = eng.allocate_qubit() # noqa: F841 eng._delfun(weakref.ref(eng)) assert len(backend.received_commands) == 3 assert backend.received_commands[0].gate == AllocateQubitGate() @@ -164,7 +171,7 @@ def test_main_engine_atexit_with_error(): sys.last_type = "Something" backend = DummyEngine(save_commands=True) eng = _main.MainEngine(backend=backend, engine_list=[]) - qb = eng.allocate_qubit() + qb = eng.allocate_qubit() # noqa: F841 eng._delfun(weakref.ref(eng)) assert len(backend.received_commands) == 1 assert backend.received_commands[0].gate == AllocateQubitGate() @@ -174,10 +181,16 @@ def test_exceptions_are_forwarded(): class ErrorEngine(DummyEngine): def receive(self, command_list): raise TypeError + eng = _main.MainEngine(backend=ErrorEngine(), engine_list=[]) with pytest.raises(TypeError): - eng.allocate_qubit() - eng2 = _main.MainEngine(backend=ErrorEngine(), engine_list=[], - verbose=True) + qb = eng.allocate_qubit() # noqa: F841 + eng2 = _main.MainEngine(backend=ErrorEngine(), engine_list=[]) with pytest.raises(TypeError): - eng2.allocate_qubit() + qb = eng2.allocate_qubit() # noqa: F841 + + # NB: avoid throwing exceptions when destroying the MainEngine + eng.next_engine = DummyEngine() + eng.next_engine.is_last_engine = True + eng2.next_engine = DummyEngine() + eng2.next_engine.is_last_engine = True diff --git a/projectq/cengines/_manualmapper.py b/projectq/cengines/_manualmapper.py index 323cfe5ed..4e7701021 100755 --- a/projectq/cengines/_manualmapper.py +++ b/projectq/cengines/_manualmapper.py @@ -12,49 +12,45 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains a compiler engine to add mapping information -""" -from projectq.cengines import BasicMapperEngine -from projectq.ops import Measure +"""A compiler engine to add mapping information.""" + +from ._basicmapper import BasicMapperEngine class ManualMapper(BasicMapperEngine): """ - Manual Mapper which adds QubitPlacementTags to Allocate gate commands - according to a user-specified mapping. + Manual Mapper which adds QubitPlacementTags to Allocate gate commands according to a user-specified mapping. Attributes: - map (function): The function which maps a given qubit id to its - location. It gets set when initializing the mapper. + map (function): The function which maps a given qubit id to its location. It gets set when initializing the + mapper. """ def __init__(self, map_fun=lambda x: x): """ - Initialize the mapper to a given mapping. If no mapping function is - provided, the qubit id is used as the location. + Initialize the mapper to a given mapping. + + If no mapping function is provided, the qubit id is used as the location. Args: - map_fun (function): Function which, given the qubit id, returns - an integer describing the physical location (must be constant). + map_fun (function): Function which, given the qubit id, returns an integer describing the physical + location (must be constant). """ - BasicMapperEngine.__init__(self) + super().__init__() self.map = map_fun - self.current_mapping = dict() + self.current_mapping = {} def receive(self, command_list): """ - Receives a command list and passes it to the next engine, adding - qubit placement tags to allocate gates. + Receives a command list and passes it to the next engine, adding qubit placement tags to allocate gates. Args: - command_list (list of Command objects): list of commands to - receive. + command_list (list of Command objects): list of commands to receive. """ for cmd in command_list: ids = [qb.id for qr in cmd.qubits for qb in qr] ids += [qb.id for qb in cmd.control_qubits] - for ID in ids: - if ID not in self.current_mapping: - self._current_mapping[ID] = self.map(ID) + for qubit_id in ids: + if qubit_id not in self.current_mapping: + self._current_mapping[qubit_id] = self.map(qubit_id) self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_manualmapper_test.py b/projectq/cengines/_manualmapper_test.py index efff9a560..f627bea2f 100755 --- a/projectq/cengines/_manualmapper_test.py +++ b/projectq/cengines/_manualmapper_test.py @@ -11,18 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._manualmapper.py.""" -import pytest - from projectq import MainEngine -from projectq.cengines import DummyEngine -from projectq.ops import H, Allocate, Measure, All +from projectq.cengines import DummyEngine, ManualMapper from projectq.meta import LogicalQubitIDTag - -from projectq.cengines import ManualMapper -from projectq.backends import IBMBackend +from projectq.ops import All, H, Measure def test_manualmapper_mapping(): @@ -31,8 +25,7 @@ def test_manualmapper_mapping(): def mapping(qubit_id): return (qubit_id + 1) & 1 - eng = MainEngine(backend=backend, - engine_list=[ManualMapper(mapping)]) + eng = MainEngine(backend=backend, engine_list=[ManualMapper(mapping)]) qb0 = eng.allocate_qubit() qb1 = eng.allocate_qubit() H | qb0 diff --git a/projectq/cengines/_optimize.py b/projectq/cengines/_optimize.py index 2e72540b9..1cce7317a 100755 --- a/projectq/cengines/_optimize.py +++ b/projectq/cengines/_optimize.py @@ -12,88 +12,91 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains a local optimizer engine. -""" +"""A local optimizer engine.""" -from copy import deepcopy as _deepcopy -from projectq.cengines import LastEngineException, BasicEngine -from projectq.ops import FlushGate, FastForwardingGate, NotMergeable +import warnings + +from projectq.ops import FastForwardingGate, FlushGate, NotMergeable + +from ._basics import BasicEngine class LocalOptimizer(BasicEngine): """ - LocalOptimizer is a compiler engine which optimizes locally (merging - rotations, cancelling gates with their inverse) in a local window of user- - defined size. - - It stores all commands in a dict of lists, where each qubit has its own - gate pipeline. After adding a gate, it tries to merge / cancel successive - gates using the get_merged and get_inverse functions of the gate (if - available). For examples, see BasicRotationGate. Once a list corresponding - to a qubit contains >=m gates, the pipeline is sent on to the next engine. + Circuit optimization compiler engine. + + LocalOptimizer is a compiler engine which optimizes locally (merging rotations, cancelling gates with their + inverse) in a local window of user- defined size. + + It stores all commands in a dict of lists, where each qubit has its own gate pipeline. After adding a gate, it + tries to merge / cancel successive gates using the get_merged and get_inverse functions of the gate (if + available). For examples, see BasicRotationGate. Once a list corresponding to a qubit contains >=m gates, the + pipeline is sent on to the next engine. """ - def __init__(self, m=5): + + def __init__(self, cache_size=5, m=None): # pylint: disable=invalid-name """ Initialize a LocalOptimizer object. Args: - m (int): Number of gates to cache per qubit, before sending on the - first gate. + cache_size (int): Number of gates to cache per qubit, before sending on the first gate. """ - BasicEngine.__init__(self) - self._l = dict() # dict of lists containing operations for each qubit - self._m = m # wait for m gates before sending on + super().__init__() + self._l = {} # dict of lists containing operations for each qubit + + if m: + warnings.warn( + 'Pending breaking API change: LocalOptimizer(m=5) will be dropped in a future version in favor of ' + 'LinearMapper(cache_size=5)', + DeprecationWarning, + ) + cache_size = m + self._cache_size = cache_size # wait for m gates before sending on # sends n gate operations of the qubit with index idx - def _send_qubit_pipeline(self, idx, n): - """ - Send n gate operations of the qubit with index idx to the next engine. - """ - il = self._l[idx] # temporary label for readability - for i in range(min(n, len(il))): # loop over first n operations + def _send_qubit_pipeline(self, idx, n_gates): + """Send n gate operations of the qubit with index idx to the next engine.""" + il = self._l[idx] # pylint: disable=invalid-name + for i in range(min(n_gates, len(il))): # loop over first n operations # send all gates before n-qubit gate for other qubits involved # --> recursively call send_helper - other_involved_qubits = [qb - for qreg in il[i].all_qubits - for qb in qreg - if qb.id != idx] + other_involved_qubits = [qb for qreg in il[i].all_qubits for qb in qreg if qb.id != idx] for qb in other_involved_qubits: - Id = qb.id + qubit_id = qb.id try: gateloc = 0 # find location of this gate within its list - while self._l[Id][gateloc] != il[i]: + while self._l[qubit_id][gateloc] != il[i]: gateloc += 1 - gateloc = self._optimize(Id, gateloc) + gateloc = self._optimize(qubit_id, gateloc) # flush the gates before the n-qubit gate - self._send_qubit_pipeline(Id, gateloc) + self._send_qubit_pipeline(qubit_id, gateloc) # delete the n-qubit gate, we're taking care of it # and don't want the other qubit to do so - self._l[Id] = self._l[Id][1:] - except IndexError: - print("Invalid qubit pipeline encountered (in the" - " process of shutting down?).") + self._l[qubit_id] = self._l[qubit_id][1:] + except IndexError: # pragma: no cover + print("Invalid qubit pipeline encountered (in the process of shutting down?).") # all qubits that need to be flushed have been flushed # --> send on the n-qubit gate self.send([il[i]]) # n operations have been sent on --> resize our gate list - self._l[idx] = self._l[idx][n:] + self._l[idx] = self._l[idx][n_gates:] - def _get_gate_indices(self, idx, i, IDs): + def _get_gate_indices(self, idx, i, qubit_ids): """ - Return all indices of a command, each index corresponding to the - command's index in one of the qubits' command lists. + Return all indices of a command. + + Each index corresponding to the command's index in one of the qubits' command lists. Args: idx (int): qubit index i (int): command position in qubit idx's command list IDs (list): IDs of all qubits involved in the command """ - N = len(IDs) + N = len(qubit_ids) # 1-qubit gate: only gate at index i in list #idx is involved if N == 1: return [i] @@ -103,53 +106,63 @@ def _get_gate_indices(self, idx, i, IDs): # count how many there are, and skip over them when looking in the # other lists. cmd = self._l[idx][i] - num_identical_to_skip = sum(1 - for prev_cmd in self._l[idx][:i] - if prev_cmd == cmd) + num_identical_to_skip = sum(1 for prev_cmd in self._l[idx][:i] if prev_cmd == cmd) indices = [] - for Id in IDs: - identical_indices = [i - for i, c in enumerate(self._l[Id]) - if c == cmd] + for qubit_id in qubit_ids: + identical_indices = [i for i, c in enumerate(self._l[qubit_id]) if c == cmd] indices.append(identical_indices[num_identical_to_skip]) return indices def _optimize(self, idx, lim=None): """ - Try to merge or even cancel successive gates using the get_merged and - get_inverse functions of the gate (see, e.g., BasicRotationGate). + Gate cancellation routine. + + Try to remove identity gates using the is_identity function, then merge or even cancel successive gates using + the get_merged and get_inverse functions of the gate (see, e.g., BasicRotationGate). It does so for all qubit command lists. """ # loop over all qubit indices i = 0 - new_gateloc = 0 limit = len(self._l[idx]) if lim is not None: limit = lim - new_gateloc = limit while i < limit - 1: + # can be dropped if the gate is equivalent to an identity gate + if self._l[idx][i].is_identity(): + # determine index of this gate on all qubits + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] + gid = self._get_gate_indices(idx, i, qubitids) + for j, qubit_id in enumerate(qubitids): + new_list = ( + self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 1 :] # noqa: E203 # noqa: E203 + ) + self._l[qubitids[j]] = new_list # pylint: disable=undefined-loop-variable + i = 0 + limit -= 1 + continue + # can be dropped if two in a row are self-inverses inv = self._l[idx][i].get_inverse() if inv == self._l[idx][i + 1]: # determine index of this gate on all qubits - qubitids = [qb.id for sublist in self._l[idx][i].all_qubits - for qb in sublist] + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] gid = self._get_gate_indices(idx, i, qubitids) # check that there are no other gates between this and its # inverse on any of the other qubits involved erase = True - for j in range(len(qubitids)): - erase *= (inv == self._l[qubitids[j]][gid[j] + 1]) + for j, qubit_id in enumerate(qubitids): + erase *= inv == self._l[qubit_id][gid[j] + 1] # drop these two gates if possible and goto next iteration if erase: - for j in range(len(qubitids)): - new_list = (self._l[qubitids[j]][0:gid[j]] + - self._l[qubitids[j]][gid[j] + 2:]) - self._l[qubitids[j]] = new_list + for j, qubit_id in enumerate(qubitids): + new_list = ( + self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 # noqa: E203 + ) + self._l[qubit_id] = new_list i = 0 limit -= 2 continue @@ -157,25 +170,24 @@ def _optimize(self, idx, lim=None): # gates are not each other's inverses --> check if they're # mergeable try: - merged_command = self._l[idx][i].get_merged( - self._l[idx][i + 1]) + merged_command = self._l[idx][i].get_merged(self._l[idx][i + 1]) # determine index of this gate on all qubits - qubitids = [qb.id for sublist in self._l[idx][i].all_qubits - for qb in sublist] + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] gid = self._get_gate_indices(idx, i, qubitids) merge = True - for j in range(len(qubitids)): - m = self._l[qubitids[j]][gid[j]].get_merged( - self._l[qubitids[j]][gid[j] + 1]) - merge *= (m == merged_command) + for j, qubit_id in enumerate(qubitids): + merged = self._l[qubit_id][gid[j]].get_merged(self._l[qubit_id][gid[j] + 1]) + merge *= merged == merged_command if merge: - for j in range(len(qubitids)): - self._l[qubitids[j]][gid[j]] = merged_command - new_list = (self._l[qubitids[j]][0:gid[j] + 1] + - self._l[qubitids[j]][gid[j] + 2:]) - self._l[qubitids[j]] = new_list + for j, qubit_id in enumerate(qubitids): + self._l[qubit_id][gid[j]] = merged_command + new_list = ( + self._l[qubit_id][0 : gid[j] + 1] # noqa: E203 + + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 + ) + self._l[qubit_id] = new_list i = 0 limit -= 1 continue @@ -186,59 +198,58 @@ def _optimize(self, idx, lim=None): return limit def _check_and_send(self): - """ - Check whether a qubit pipeline must be sent on and, if so, - optimize the pipeline and then send it on. - """ - for i in self._l: - if (len(self._l[i]) >= self._m or len(self._l[i]) > 0 and - isinstance(self._l[i][-1].gate, FastForwardingGate)): + """Check whether a qubit pipeline must be sent on and, if so, optimize the pipeline and then send it on.""" + # NB: self.optimize(i) modifies self._l + for i in self._l: # pylint: disable=consider-using-dict-items + if ( + len(self._l[i]) >= self._cache_size + or len(self._l[i]) > 0 + and isinstance(self._l[i][-1].gate, FastForwardingGate) + ): self._optimize(i) - if (len(self._l[i]) >= self._m and not - isinstance(self._l[i][-1].gate, - FastForwardingGate)): - self._send_qubit_pipeline(i, len(self._l[i]) - self._m + 1) - elif (len(self._l[i]) > 0 and - isinstance(self._l[i][-1].gate, FastForwardingGate)): + if len(self._l[i]) >= self._cache_size and not isinstance(self._l[i][-1].gate, FastForwardingGate): + self._send_qubit_pipeline(i, len(self._l[i]) - self._cache_size + 1) + elif len(self._l[i]) > 0 and isinstance(self._l[i][-1].gate, FastForwardingGate): self._send_qubit_pipeline(i, len(self._l[i])) - new_dict = dict() - for idx in self._l: - if len(self._l[idx]) > 0: - new_dict[idx] = self._l[idx] + new_dict = {} + for idx, _l in self._l.items(): + if len(_l) > 0: + new_dict[idx] = _l self._l = new_dict def _cache_cmd(self, cmd): - """ - Cache a command, i.e., inserts it into the command lists of all qubits - involved. - """ + """Cache a command, i.e., inserts it into the command lists of all qubits involved.""" # are there qubit ids that haven't been added to the list? idlist = [qubit.id for sublist in cmd.all_qubits for qubit in sublist] # add gate command to each of the qubits involved - for ID in idlist: - if ID not in self._l: - self._l[ID] = [] - self._l[ID] += [cmd] + for qubit_id in idlist: + if qubit_id not in self._l: + self._l[qubit_id] = [] + self._l[qubit_id] += [cmd] self._check_and_send() def receive(self, command_list): """ - Receive commands from the previous engine and cache them. - If a flush gate arrives, the entire buffer is sent on. + Receive a list of commands. + + Receive commands from the previous engine and cache them. If a flush gate arrives, the entire buffer is sent + on. """ for cmd in command_list: if cmd.gate == FlushGate(): # flush gate --> optimize and flush - for idx in self._l: + # NB: self.optimize(i) modifies self._l + for idx in self._l: # pylint: disable=consider-using-dict-items self._optimize(idx) self._send_qubit_pipeline(idx, len(self._l[idx])) - new_dict = dict() - for idx in self._l: - if len(self._l[idx]) > 0: - new_dict[idx] = self._l[idx] + new_dict = {} + for idx, _l in self._l.items(): + if len(_l) > 0: # pragma: no cover + new_dict[idx] = _l self._l = new_dict - assert self._l == dict() + if self._l: # pragma: no cover + raise RuntimeError('Internal compiler error: qubits remaining in LocalOptimizer after a flush!') self.send([cmd]) else: self._cache_cmd(cmd) diff --git a/projectq/cengines/_optimize_test.py b/projectq/cengines/_optimize_test.py index e0196f83b..72aa4656e 100755 --- a/projectq/cengines/_optimize_test.py +++ b/projectq/cengines/_optimize_test.py @@ -11,21 +11,40 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._optimize.py.""" +import math + import pytest from projectq import MainEngine -from projectq.cengines import DummyEngine -from projectq.ops import (CNOT, H, Rx, Ry, AllocateQubitGate, X, - FastForwardingGate, ClassicalInstructionGate) +from projectq.cengines import DummyEngine, _optimize +from projectq.ops import ( + CNOT, + AllocateQubitGate, + ClassicalInstructionGate, + FastForwardingGate, + H, + Rx, + Ry, + X, +) + + +def test_local_optimizer_init_api_change(): + with pytest.warns(DeprecationWarning): + tmp = _optimize.LocalOptimizer(m=10) + assert tmp._cache_size == 10 + + local_optimizer = _optimize.LocalOptimizer() + assert local_optimizer._cache_size == 5 -from projectq.cengines import _optimize + local_optimizer = _optimize.LocalOptimizer(cache_size=10) + assert local_optimizer._cache_size == 10 def test_local_optimizer_caching(): - local_optimizer = _optimize.LocalOptimizer(m=4) + local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it caches for each qubit 3 gates @@ -56,7 +75,7 @@ def test_local_optimizer_caching(): def test_local_optimizer_flush_gate(): - local_optimizer = _optimize.LocalOptimizer(m=4) + local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it caches for each qubit 3 gates @@ -71,7 +90,7 @@ def test_local_optimizer_flush_gate(): def test_local_optimizer_fast_forwarding_gate(): - local_optimizer = _optimize.LocalOptimizer(m=4) + local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that FastForwardingGate (e.g. Deallocate) flushes that qb0 pipeline @@ -86,7 +105,7 @@ def test_local_optimizer_fast_forwarding_gate(): def test_local_optimizer_cancel_inverse(): - local_optimizer = _optimize.LocalOptimizer(m=4) + local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it cancels inverses (H, CNOT are self-inverse) @@ -103,8 +122,7 @@ def test_local_optimizer_cancel_inverse(): received_commands = [] # Remove Allocate and Deallocate gates for cmd in backend.received_commands: - if not (isinstance(cmd.gate, FastForwardingGate) or - isinstance(cmd.gate, ClassicalInstructionGate)): + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): received_commands.append(cmd) assert len(received_commands) == 2 assert received_commands[0].gate == H @@ -115,7 +133,7 @@ def test_local_optimizer_cancel_inverse(): def test_local_optimizer_mergeable_gates(): - local_optimizer = _optimize.LocalOptimizer(m=4) + local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it merges mergeable gates such as Rx @@ -127,3 +145,22 @@ def test_local_optimizer_mergeable_gates(): # Expect allocate, one Rx gate, and flush gate assert len(backend.received_commands) == 3 assert backend.received_commands[1].gate == Rx(10 * 0.5) + + +def test_local_optimizer_identity_gates(): + local_optimizer = _optimize.LocalOptimizer(cache_size=4) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + # Test that it merges mergeable gates such as Rx + qb0 = eng.allocate_qubit() + for _ in range(10): + Rx(0.0) | qb0 + Ry(0.0) | qb0 + Rx(4 * math.pi) | qb0 + Ry(4 * math.pi) | qb0 + Rx(0.5) | qb0 + assert len(backend.received_commands) == 0 + eng.flush() + # Expect allocate, one Rx gate, and flush gate + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == Rx(0.5) diff --git a/projectq/cengines/_replacer/__init__.py b/projectq/cengines/_replacer/__init__.py index d1d7ba9a8..312e4b22e 100755 --- a/projectq/cengines/_replacer/__init__.py +++ b/projectq/cengines/_replacer/__init__.py @@ -14,6 +14,4 @@ from ._decomposition_rule import DecompositionRule, ThisIsNotAGateClassError from ._decomposition_rule_set import DecompositionRuleSet -from ._replacer import (AutoReplacer, - InstructionFilter, - NoGateDecompositionError) +from ._replacer import AutoReplacer, InstructionFilter, NoGateDecompositionError diff --git a/projectq/cengines/_replacer/_decomposition_rule.py b/projectq/cengines/_replacer/_decomposition_rule.py index d742f24c5..24392fe24 100755 --- a/projectq/cengines/_replacer/_decomposition_rule.py +++ b/projectq/cengines/_replacer/_decomposition_rule.py @@ -12,59 +12,52 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing the definition of a decomposition rule.""" + from projectq.ops import BasicGate class ThisIsNotAGateClassError(TypeError): - pass + """Exception raised when a gate instance is encountered instead of a gate class in a decomposition rule.""" -class DecompositionRule: - """ - A rule for breaking down specific gates into sequences of simpler gates. - """ +class DecompositionRule: # pylint: disable=too-few-public-methods + """A rule for breaking down specific gates into sequences of simpler gates.""" - def __init__(self, - gate_class, - gate_decomposer, - gate_recognizer=lambda cmd: True): + def __init__(self, gate_class, gate_decomposer, gate_recognizer=lambda cmd: True): """ + Initialize a DecompositionRule object. + Args: gate_class (type): The type of gate that this rule decomposes. - The gate class is redundant information used to make lookups - faster when iterating over a circuit and deciding "which rules - apply to this gate?" again and again. + The gate class is redundant information used to make lookups faster when iterating over a circuit and + deciding "which rules apply to this gate?" again and again. - Note that this parameter is a gate type, not a gate instance. - You supply gate_class=MyGate or gate_class=MyGate().__class__, - not gate_class=MyGate(). + Note that this parameter is a gate type, not a gate instance. You supply gate_class=MyGate or + gate_class=MyGate().__class__, not gate_class=MyGate(). - gate_decomposer (function[projectq.ops.Command]): Function which, - given the command to decompose, applies a sequence of gates - corresponding to the high-level function of a gate of type - gate_class. + gate_decomposer (function[projectq.ops.Command]): Function which, given the command to decompose, applies + a sequence of gates corresponding to the high-level function of a gate of type gate_class. - gate_recognizer (function[projectq.ops.Command] : boolean): A - predicate that determines if the decomposition applies to the - given command (on top of the filtering by gate_class). + gate_recognizer (function[projectq.ops.Command] : boolean): A predicate that determines if the + decomposition applies to the given command (on top of the filtering by gate_class). - For example, a decomposition rule may only to apply rotation - gates that rotate by a specific angle. + For example, a decomposition rule may only to apply rotation gates that rotate by a specific angle. - If no gate_recognizer is given, the decomposition applies to - all gates matching the gate_class. + If no gate_recognizer is given, the decomposition applies to all gates matching the gate_class. """ - # Check for common gate_class type mistakes. if isinstance(gate_class, BasicGate): raise ThisIsNotAGateClassError( "gate_class is a gate instance instead of a type of BasicGate." - "\nDid you pass in someGate instead of someGate.__class__?") + "\nDid you pass in someGate instead of someGate.__class__?" + ) if gate_class == type.__class__: raise ThisIsNotAGateClassError( "gate_class is type.__class__ instead of a type of BasicGate." - "\nDid you pass in GateType.__class__ instead of GateType?") + "\nDid you pass in GateType.__class__ instead of GateType?" + ) self.gate_class = gate_class self.gate_decomposer = gate_decomposer diff --git a/projectq/cengines/_replacer/_decomposition_rule_set.py b/projectq/cengines/_replacer/_decomposition_rule_set.py index d7d1a38cb..9cdb5d9da 100755 --- a/projectq/cengines/_replacer/_decomposition_rule_set.py +++ b/projectq/cengines/_replacer/_decomposition_rule_set.py @@ -12,33 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing the definition of a decomposition rule set.""" + from projectq.meta import Dagger class DecompositionRuleSet: - """ - A collection of indexed decomposition rules. - """ + """A collection of indexed decomposition rules.""" + def __init__(self, rules=None, modules=None): """ + Initialize a DecompositionRuleSet object. + Args: rules list[DecompositionRule]: Initial decomposition rules. - modules (iterable[ModuleWithDecompositionRuleSet]): A list of - things with an "all_defined_decomposition_rules" property - containing decomposition rules to add to the rule set. + modules (iterable[ModuleWithDecompositionRuleSet]): A list of things with an + "all_defined_decomposition_rules" property containing decomposition rules to add to the rule set. """ - self.decompositions = dict() + self.decompositions = {} if rules: self.add_decomposition_rules(rules) if modules: - self.add_decomposition_rules([ - rule - for module in modules - for rule in module.all_defined_decomposition_rules]) + self.add_decomposition_rules( + [rule for module in modules for rule in module.all_defined_decomposition_rules] + ) def add_decomposition_rules(self, rules): + """Add some decomposition rules to a decomposition rule set.""" for rule in rules: self.add_decomposition_rule(rule) @@ -56,46 +58,38 @@ def add_decomposition_rule(self, rule): self.decompositions[cls].append(decomp_obj) -class ModuleWithDecompositionRuleSet: - """ - Interface type for explaining one of the parameters that can be given to - DecompositionRuleSet. - """ +class ModuleWithDecompositionRuleSet: # pragma: no cover # pylint: disable=too-few-public-methods + """Interface type for explaining one of the parameters that can be given to DecompositionRuleSet.""" + def __init__(self, all_defined_decomposition_rules): """ + Initialize a ModuleWithDecompositionRuleSet object. + Args: - all_defined_decomposition_rules (list[DecompositionRule]): - A list of decomposition rules. + all_defined_decomposition_rules (list[DecompositionRule]): A list of decomposition rules. """ self.all_defined_decomposition_rules = all_defined_decomposition_rules -class _Decomposition(object): - """ - The Decomposition class can be used to register a decomposition rule (by - calling register_decomposition) - """ +class _Decomposition: # pylint: disable=too-few-public-methods + """The Decomposition class can be used to register a decomposition rule (by calling register_decomposition).""" + def __init__(self, replacement_fun, recogn_fun): """ - Construct the Decomposition object. + Initialize a Decomposition object. Args: - replacement_fun: Function that, when called with a `Command` - object, decomposes this command. - recogn_fun: Function that, when called with a `Command` object, - returns True if and only if the replacement rule can handle - this command. - - Every Decomposition is registered with the gate class. The - Decomposition rule is then potentially valid for all objects which are - an instance of that same class - (i.e., instance of gate_object.__class__). All other parameters have - to be checked by the recogn_fun, i.e., it has to decide whether the - decomposition rule can indeed be applied to replace the given Command. - - As an example, consider recognizing the Toffoli gate, which is a - Pauli-X gate with 2 control qubits. The recognizer function would then - be: + replacement_fun: Function that, when called with a `Command` object, decomposes this command. + recogn_fun: Function that, when called with a `Command` object, returns True if and only if the + replacement rule can handle this command. + + Every Decomposition is registered with the gate class. The Decomposition rule is then potentially valid for + all objects which are an instance of that same class (i.e., instance of gate_object.__class__). All other + parameters have to be checked by the recogn_fun, i.e., it has to decide whether the decomposition rule can + indeed be applied to replace the given Command. + + As an example, consider recognizing the Toffoli gate, which is a Pauli-X gate with 2 control qubits. The + recognizer function would then be: .. code-block:: python @@ -108,8 +102,7 @@ def recogn_toffoli(cmd): .. code-block:: python - register_decomposition(X.__class__, decompose_toffoli, - recogn_toffoli) + register_decomposition(X.__class__, decompose_toffoli, recogn_toffoli) Note: See projectq.setups.decompositions for more example codes. @@ -120,18 +113,16 @@ def recogn_toffoli(cmd): def get_inverse_decomposition(self): """ - Return the Decomposition object which handles the inverse of the - original command. + Return the Decomposition object which handles the inverse of the original command. - This simulates the user having added a decomposition rule for the - inverse as well. Since decomposing the inverse of a command can be - achieved by running the original decomposition inside a - `with Dagger(engine):` statement, this is not necessary - (and will be done automatically by the framework). + This simulates the user having added a decomposition rule for the inverse as well. Since decomposing the + inverse of a command can be achieved by running the original decomposition inside a `with Dagger(engine):` + statement, this is not necessary (and will be done automatically by the framework). Returns: Decomposition handling the inverse of the original command. """ + def decomp(cmd): with Dagger(cmd.engine): self.decompose(cmd.get_inverse()) diff --git a/projectq/cengines/_replacer/_decomposition_rule_test.py b/projectq/cengines/_replacer/_decomposition_rule_test.py index a162735ae..1989275d6 100755 --- a/projectq/cengines/_replacer/_decomposition_rule_test.py +++ b/projectq/cengines/_replacer/_decomposition_rule_test.py @@ -11,12 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._replacer._decomposition_rule.py.""" import pytest from projectq.ops import BasicRotationGate + from . import DecompositionRule, ThisIsNotAGateClassError @@ -25,11 +25,7 @@ class WrongInput(BasicRotationGate): pass with pytest.raises(ThisIsNotAGateClassError): - _ = DecompositionRule(WrongInput.__class__, - lambda cmd: None, - lambda cmd: None) + _ = DecompositionRule(WrongInput.__class__, lambda cmd: None, lambda cmd: None) with pytest.raises(ThisIsNotAGateClassError): - _ = DecompositionRule(WrongInput(0), - lambda cmd: None, - lambda cmd: None) + _ = DecompositionRule(WrongInput(0), lambda cmd: None, lambda cmd: None) diff --git a/projectq/cengines/_replacer/_replacer.py b/projectq/cengines/_replacer/_replacer.py index 29c883123..69cedf7ac 100755 --- a/projectq/cengines/_replacer/_replacer.py +++ b/projectq/cengines/_replacer/_replacer.py @@ -13,49 +13,51 @@ # limitations under the License. """ -Contains an AutoReplacer compiler engine which uses engine.is_available to -determine whether a command can be executed. If not, it uses the loaded setup -(e.g., default) to find an appropriate decomposition. +Definitions of a few compiler engines that handle command filtering and replacement. -The InstructionFilter can be used to further specify which gates to -replace/keep. +Contains an AutoReplacer compiler engine which uses engine.is_available to determine whether a command can be +executed. If not, it uses the loaded setup (e.g., default) to find an appropriate decomposition. + +The InstructionFilter can be used to further specify which gates to replace/keep. """ -from projectq.cengines import (BasicEngine, - ForwarderEngine, - CommandModifier) -from projectq.ops import (FlushGate, - get_inverse) +from projectq.cengines import BasicEngine, CommandModifier, ForwarderEngine +from projectq.ops import FlushGate, get_inverse class NoGateDecompositionError(Exception): - pass + """Exception raised when no gate decomposition rule can be found.""" class InstructionFilter(BasicEngine): """ - The InstructionFilter is a compiler engine which changes the behavior of - is_available according to a filter function. All commands are passed to - this function, which then returns whether this command can be executed - (True) or needs replacement (False). + A compiler engine that implements a user-defined is_available() method. + + The InstructionFilter is a compiler engine which changes the behavior of is_available according to a filter + function. All commands are passed to this function, which then returns whether this command can be executed (True) + or needs replacement (False). """ + def __init__(self, filterfun): """ - Initializer: The provided filterfun returns True for all commands - which do not need replacement and False for commands that do. + Initialize an InstructionFilter object. + + Initializer: The provided filterfun returns True for all commands which do not need replacement and False for + commands that do. Args: - filterfun (function): Filter function which returns True for - available commands, and False otherwise. filterfun will be - called as filterfun(self, cmd). + filterfun (function): Filter function which returns True for available commands, and False + otherwise. filterfun will be called as filterfun(self, cmd). """ - BasicEngine.__init__(self) + super().__init__() self._filterfun = filterfun def is_available(self, cmd): """ - Specialized implementation of BasicBackend.is_available: Forwards this - call to the filter function given to the constructor. + Test whether a Command is supported by a compiler engine. + + Specialized implementation of BasicBackend.is_available: Forwards this call to the filter function given to + the constructor. Args: cmd (Command): Command for which to check availability. @@ -64,7 +66,9 @@ def is_available(self, cmd): def receive(self, command_list): """ - Forward all commands to the next engine. + Receive a list of commands. + + This implementation simply forwards all commands to the next engine. Args: command_list (list): List of commands to receive. @@ -74,14 +78,18 @@ def receive(self, command_list): class AutoReplacer(BasicEngine): """ - The AutoReplacer is a compiler engine which uses engine.is_available in - order to determine which commands need to be replaced/decomposed/compiled - further. The loaded setup is used to find decomposition rules appropriate - for each command (e.g., setups.default). + A compiler engine to automatically replace certain commands. + + The AutoReplacer is a compiler engine which uses engine.is_available in order to determine which commands need to + be replaced/decomposed/compiled further. The loaded setup is used to find decomposition rules appropriate for each + command (e.g., setups.default). """ - def __init__(self, decompositionRuleSet, - decomposition_chooser=lambda cmd, - decomposition_list: decomposition_list[0]): + + def __init__( + self, + decomposition_rule_se, + decomposition_chooser=lambda cmd, decomposition_list: decomposition_list[0], + ): """ Initialize an AutoReplacer. @@ -104,17 +112,20 @@ def __init__(self, decompositionRuleSet, def decomposition_chooser(cmd, decomp_list): return decomp_list[0] + + repl = AutoReplacer(decomposition_chooser) """ - BasicEngine.__init__(self) + super().__init__() self._decomp_chooser = decomposition_chooser - self.decompositionRuleSet = decompositionRuleSet + self.decomposition_rule_set = decomposition_rule_se - def _process_command(self, cmd): + def _process_command(self, cmd): # pylint: disable=too-many-locals,too-many-branches """ - Check whether a command cmd can be handled by further engines and, - if not, replace it using the decomposition rules loaded with the setup - (e.g., setups.default). + Process a command. + + Check whether a command cmd can be handled by further engines and, if not, replace it using the decomposition + rules loaded with the setup (e.g., setups.default). Args: cmd (Command): Command to process. @@ -122,13 +133,9 @@ def _process_command(self, cmd): Raises: Exception if no replacement is available in the loaded setup. """ - if self.is_available(cmd): + if self.is_available(cmd): # pylint: disable=too-many-nested-blocks self.send([cmd]) else: - # check for decomposition rules - decomp_list = [] - potential_decomps = [] - # First check for a decomposition rules of the gate class, then # the gate class of the inverse gate. If nothing is found, do the # same for the first parent class, etc. @@ -136,45 +143,54 @@ def _process_command(self, cmd): # If gate does not have an inverse it's parent classes are # DaggeredGate, BasicGate, object. Hence don't check the last two inverse_mro = type(get_inverse(cmd.gate)).mro()[:-2] - rules = self.decompositionRuleSet.decompositions - for level in range(max(len(gate_mro), len(inverse_mro))): - # Check for forward rules - if level < len(gate_mro): - class_name = gate_mro[level].__name__ - try: - potential_decomps = [d for d in rules[class_name]] - except KeyError: - pass - # throw out the ones which don't recognize the command - for d in potential_decomps: - if d.check(cmd): - decomp_list.append(d) - if len(decomp_list) != 0: - break - # Check for rules implementing the inverse gate - # and run them in reverse - if level < len(inverse_mro): - inv_class_name = inverse_mro[level].__name__ - try: - potential_decomps += [ - d.get_inverse_decomposition() - for d in rules[inv_class_name] - ] - except KeyError: - pass - # throw out the ones which don't recognize the command - for d in potential_decomps: - if d.check(cmd): - decomp_list.append(d) - if len(decomp_list) != 0: - break - - if len(decomp_list) == 0: - raise NoGateDecompositionError("\nNo replacement found for " + - str(cmd) + "!") - - # use decomposition chooser to determine the best decomposition - chosen_decomp = self._decomp_chooser(cmd, decomp_list) + rules = self.decomposition_rule_set.decompositions + + # If the decomposition rule to remove negatively controlled qubits is present in the list of potential + # decompositions, we process it immediately, before any other decompositions. + controlstate_rule = [ + rule for rule in rules.get('BasicGate', []) if rule.decompose.__name__ == '_decompose_controlstate' + ] + if controlstate_rule and controlstate_rule[0].check(cmd): + chosen_decomp = controlstate_rule[0] + else: + # check for decomposition rules + decomp_list = [] + potential_decomps = [] + + for level in range(max(len(gate_mro), len(inverse_mro))): + # Check for forward rules + if level < len(gate_mro): + class_name = gate_mro[level].__name__ + try: + potential_decomps = rules[class_name] + except KeyError: + pass + # throw out the ones which don't recognize the command + for decomp in potential_decomps: + if decomp.check(cmd): + decomp_list.append(decomp) + if len(decomp_list) != 0: + break + # Check for rules implementing the inverse gate + # and run them in reverse + if level < len(inverse_mro): + inv_class_name = inverse_mro[level].__name__ + try: + potential_decomps += [d.get_inverse_decomposition() for d in rules[inv_class_name]] + except KeyError: + pass + # throw out the ones which don't recognize the command + for decomp in potential_decomps: + if decomp.check(cmd): + decomp_list.append(decomp) + if len(decomp_list) != 0: + break + + if len(decomp_list) == 0: + raise NoGateDecompositionError(f"\nNo replacement found for {str(cmd)}!") + + # use decomposition chooser to determine the best decomposition + chosen_decomp = self._decomp_chooser(cmd, decomp_list) # the decomposed command must have the same tags # (plus the ones it gets from meta-statements inside the @@ -186,6 +202,7 @@ def cmd_mod_fun(cmd): # Adds the tags cmd.tags = old_tags[:] + cmd.tags cmd.engine = self.main_engine return cmd + # the CommandModifier calls cmd_mod_fun for each command # --> commands get the right tags. cmod_eng = CommandModifier(cmd_mod_fun) @@ -202,9 +219,10 @@ def cmd_mod_fun(cmd): # Adds the tags def receive(self, command_list): """ - Receive a list of commands from the previous compiler engine and, if - necessary, replace/decompose the gates according to the decomposition - rules in the loaded setup. + Receive a list of commands. + + Receive a list of commands from the previous compiler engine and, if necessary, replace/decompose the gates + according to the decomposition rules in the loaded setup. Args: command_list (list): List of commands to handle. diff --git a/projectq/cengines/_replacer/_replacer_test.py b/projectq/cengines/_replacer/_replacer_test.py index f532cd998..b6dabffa2 100755 --- a/projectq/cengines/_replacer/_replacer_test.py +++ b/projectq/cengines/_replacer/_replacer_test.py @@ -11,18 +11,23 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._replacer._replacer.py.""" import pytest from projectq import MainEngine -from projectq.cengines import (DummyEngine, - DecompositionRuleSet, - DecompositionRule) -from projectq.ops import (BasicGate, ClassicalInstructionGate, Command, H, - NotInvertible, Rx, Ry, S, X) +from projectq.cengines import DecompositionRule, DecompositionRuleSet, DummyEngine from projectq.cengines._replacer import _replacer +from projectq.ops import ( + BasicGate, + ClassicalInstructionGate, + Command, + H, + NotInvertible, + Rx, + S, + X, +) def test_filter_engine(): @@ -30,6 +35,7 @@ def my_filter(self, cmd): if cmd.gate == H: return True return False + filter_eng = _replacer.InstructionFilter(my_filter) eng = MainEngine(backend=DummyEngine(), engine_list=[filter_eng]) qubit = eng.allocate_qubit() @@ -42,7 +48,8 @@ def my_filter(self, cmd): class SomeGateClass(BasicGate): - """ Test gate class """ + """Test gate class""" + pass @@ -63,21 +70,18 @@ def decompose_test1(cmd): def recognize_test(cmd): return True - result.add_decomposition_rule( - DecompositionRule(SomeGate.__class__, decompose_test1, - recognize_test)) + result.add_decomposition_rule(DecompositionRule(SomeGate.__class__, decompose_test1, recognize_test)) def decompose_test2(cmd): qb = cmd.qubits H | qb - result.add_decomposition_rule( - DecompositionRule(SomeGateClass, decompose_test2, - recognize_test)) + result.add_decomposition_rule(DecompositionRule(SomeGateClass, decompose_test2, recognize_test)) assert len(result.decompositions[SomeGate.__class__.__name__]) == 2 return result + rule_set = make_decomposition_rule_set() @@ -88,15 +92,17 @@ def test_gate_filter_func(self, cmd): if cmd.gate == SomeGate: return False return True + return _replacer.InstructionFilter(test_gate_filter_func) def test_auto_replacer_default_chooser(fixture_gate_filter): # Test that default decomposition_chooser takes always first rule. backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set), - fixture_gate_filter]) + eng = MainEngine( + backend=backend, + engine_list=[_replacer.AutoReplacer(rule_set), fixture_gate_filter], + ) assert len(rule_set.decompositions[SomeGate.__class__.__name__]) == 2 assert len(backend.received_commands) == 0 qb = eng.allocate_qubit() @@ -110,11 +116,15 @@ def test_auto_replacer_decomposition_chooser(fixture_gate_filter): # Supply a decomposition chooser which always chooses last rule. def test_decomp_chooser(cmd, decomposition_list): return decomposition_list[-1] + backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set, - test_decomp_chooser), - fixture_gate_filter]) + eng = MainEngine( + backend=backend, + engine_list=[ + _replacer.AutoReplacer(rule_set, test_decomp_chooser), + fixture_gate_filter, + ], + ) assert len(rule_set.decompositions[SomeGate.__class__.__name__]) == 2 assert len(backend.received_commands) == 0 qb = eng.allocate_qubit() @@ -131,10 +141,10 @@ def h_filter(self, cmd): if cmd.gate == H: return False return True + h_filter = _replacer.InstructionFilter(h_filter) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set), h_filter]) + eng = MainEngine(backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), h_filter]) qubit = eng.allocate_qubit() with pytest.raises(_replacer.NoGateDecompositionError): H | qubit @@ -161,9 +171,7 @@ def decompose_no_magic_gate(cmd): def recognize_no_magic_gate(cmd): return True - rule_set.add_decomposition_rule(DecompositionRule(NoMagicGate, - decompose_no_magic_gate, - recognize_no_magic_gate)) + rule_set.add_decomposition_rule(DecompositionRule(NoMagicGate, decompose_no_magic_gate, recognize_no_magic_gate)) def magic_filter(self, cmd): if cmd.gate == MagicGate(): @@ -171,15 +179,17 @@ def magic_filter(self, cmd): return True backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set), - _replacer.InstructionFilter(magic_filter)]) + eng = MainEngine( + backend=backend, + engine_list=[ + _replacer.AutoReplacer(rule_set), + _replacer.InstructionFilter(magic_filter), + ], + ) assert len(backend.received_commands) == 0 qb = eng.allocate_qubit() MagicGate() | qb eng.flush() - for cmd in backend.received_commands: - print(cmd) assert len(backend.received_commands) == 4 assert backend.received_commands[1].gate == H assert backend.received_commands[2].gate == Rx(-0.6) @@ -188,9 +198,10 @@ def magic_filter(self, cmd): def test_auto_replacer_adds_tags(fixture_gate_filter): # Test that AutoReplacer puts back the tags backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set), - fixture_gate_filter]) + eng = MainEngine( + backend=backend, + engine_list=[_replacer.AutoReplacer(rule_set), fixture_gate_filter], + ) assert len(rule_set.decompositions[SomeGate.__class__.__name__]) == 2 assert len(backend.received_commands) == 0 qb = eng.allocate_qubit() @@ -209,18 +220,62 @@ class DerivedSomeGate(SomeGateClass): pass def test_gate_filter_func(self, cmd): - if (cmd.gate == X or cmd.gate == H or - isinstance(cmd.gate, ClassicalInstructionGate)): + if cmd.gate == X or cmd.gate == H or isinstance(cmd.gate, ClassicalInstructionGate): return True return False i_filter = _replacer.InstructionFilter(test_gate_filter_func) backend = DummyEngine(save_commands=True) - eng = MainEngine(backend=backend, - engine_list=[_replacer.AutoReplacer(rule_set), - i_filter]) + eng = MainEngine(backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), i_filter]) qb = eng.allocate_qubit() DerivedSomeGate() | qb eng.flush() received_gate = backend.received_commands[1].gate assert received_gate == X or received_gate == H + + +def test_auto_replacer_priorize_controlstate_rule(): + # Check that when a control state is given and it has negative control, + # Autoreplacer prioritizes the corresponding decomposition rule before anything else. + # (Decomposition rule should have name _decompose_controlstate) + + # Create test gate and inverse + class ControlGate(BasicGate): + pass + + def _decompose_controlstate(cmd): + S | cmd.qubits + + def _decompose_random(cmd): + H | cmd.qubits + + def control_filter(self, cmd): + if cmd.gate == ControlGate(): + return False + return True + + rule_set.add_decomposition_rule(DecompositionRule(BasicGate, _decompose_random)) + + backend = DummyEngine(save_commands=True) + eng = MainEngine( + backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), _replacer.InstructionFilter(control_filter)] + ) + assert len(backend.received_commands) == 0 + qb = eng.allocate_qubit() + ControlGate() | qb + eng.flush() + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == H + + rule_set.add_decomposition_rule(DecompositionRule(BasicGate, _decompose_controlstate)) + + backend = DummyEngine(save_commands=True) + eng = MainEngine( + backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), _replacer.InstructionFilter(control_filter)] + ) + assert len(backend.received_commands) == 0 + qb = eng.allocate_qubit() + ControlGate() | qb + eng.flush() + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == S diff --git a/projectq/cengines/_swapandcnotflipper.py b/projectq/cengines/_swapandcnotflipper.py index e3da39ff3..997274a0a 100755 --- a/projectq/cengines/_swapandcnotflipper.py +++ b/projectq/cengines/_swapandcnotflipper.py @@ -11,32 +11,26 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Contains a compiler engine which flips the directionality of CNOTs according -to the given connectivity graph. It also translates Swap gates to CNOTs if -necessary. +A compiler engine which flips the directionality of CNOTs according to the given connectivity graph. + +It also translates Swap gates to CNOTs if necessary. """ from copy import deepcopy -from projectq.cengines import (BasicEngine, - ForwarderEngine, - CommandModifier) from projectq.meta import get_control_count -from projectq.ops import (All, - NOT, - CNOT, - H, - Swap) +from projectq.ops import CNOT, NOT, All, H, Swap + +from ._basics import BasicEngine, ForwarderEngine +from ._cmdmodifier import CommandModifier class SwapAndCNOTFlipper(BasicEngine): """ - Flips CNOTs and translates Swaps to CNOTs where necessary. + Flip CNOTs and translates Swaps to CNOTs where necessary. Warning: - This engine assumes that CNOT and Hadamard gates are supported by - the following engines. + This engine assumes that CNOT and Hadamard gates are supported by the following engines. Warning: This engine cannot be used as a backend. @@ -47,18 +41,15 @@ def __init__(self, connectivity): Initialize the engine. Args: - connectivity (set): Set of tuples (c, t) where if (c, t) is an - element of the set means that a CNOT can be performed between - the physical ids (c, t) with c being the control and t being - the target qubit. + connectivity (set): Set of tuples (c, t) where if (c, t) is an element of the set means that a CNOT can be + performed between the physical ids (c, t) with c being the control and t being the target qubit. """ - BasicEngine.__init__(self) + super().__init__() self.connectivity = connectivity def is_available(self, cmd): """ - Check if the IBM backend can perform the Command cmd and return True - if so. + Check if the IBM backend can perform the Command cmd and return True if so. Args: cmd (Command): The command to check @@ -72,8 +63,7 @@ def _is_cnot(self, cmd): Args: cmd (Command): Command to check """ - return (isinstance(cmd.gate, NOT.__class__) and - get_control_count(cmd) == 1) + return isinstance(cmd.gate, NOT.__class__) and get_control_count(cmd) == 1 def _is_swap(self, cmd): """ @@ -82,7 +72,7 @@ def _is_swap(self, cmd): Args: cmd (Command): Command to check """ - return (get_control_count(cmd) == 0 and cmd.gate == Swap) + return get_control_count(cmd) == 0 and cmd.gate == Swap def _needs_flipping(self, cmd): """ @@ -98,9 +88,7 @@ def _needs_flipping(self, cmd): control = cmd.control_qubits[0].id is_possible = (control, target) in self.connectivity if not is_possible and (target, control) not in self.connectivity: - raise RuntimeError("The provided connectivity does not " - "allow to execute the CNOT gate {}." - .format(str(cmd))) + raise RuntimeError(f"The provided connectivity does not allow to execute the CNOT gate {str(cmd)}.") return not is_possible def _send_cnot(self, cmd, control, target, flip=False): @@ -108,6 +96,7 @@ def cmd_mod(command): command.tags = deepcopy(cmd.tags) + command.tags command.engine = self.main_engine return command + # We'll have to add all meta tags before sending on cmd_mod_eng = CommandModifier(cmd_mod) cmd_mod_eng.next_engine = self.next_engine @@ -126,14 +115,13 @@ def cmd_mod(command): def receive(self, command_list): """ - Receives a command list and if the command is a CNOT gate, it flips - it using Hadamard gates if necessary; if it is a Swap gate, it - decomposes it using 3 CNOTs. All other gates are simply sent to the - next engine. + Receive a list of commands. + + Receive a command list and if the command is a CNOT gate, it flips it using Hadamard gates if necessary; if it + is a Swap gate, it decomposes it using 3 CNOTs. All other gates are simply sent to the next engine. Args: - command_list (list of Command objects): list of commands to - receive. + command_list (list of Command objects): list of commands to receive. """ for cmd in command_list: if self._needs_flipping(cmd): @@ -141,7 +129,8 @@ def receive(self, command_list): elif self._is_swap(cmd): qubits = [qb for qr in cmd.qubits for qb in qr] ids = [qb.id for qb in qubits] - assert len(ids) == 2 + if len(ids) != 2: + raise RuntimeError('Swap gate is a 2-qubit gate!') if tuple(ids) in self.connectivity: control = [qubits[0]] target = [qubits[1]] @@ -149,9 +138,7 @@ def receive(self, command_list): control = [qubits[1]] target = [qubits[0]] else: - raise RuntimeError("The provided connectivity does not " - "allow to execute the Swap gate {}." - .format(str(cmd))) + raise RuntimeError(f"The provided connectivity does not allow to execute the Swap gate {str(cmd)}.") self._send_cnot(cmd, control, target) self._send_cnot(cmd, target, control, True) self._send_cnot(cmd, control, target) diff --git a/projectq/cengines/_swapandcnotflipper_test.py b/projectq/cengines/_swapandcnotflipper_test.py index b59b88e9b..c00fe0805 100755 --- a/projectq/cengines/_swapandcnotflipper_test.py +++ b/projectq/cengines/_swapandcnotflipper_test.py @@ -11,18 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._swapandcnotflipper.py.""" import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import All, H, CNOT, X, Measure, Swap -from projectq.meta import (Control, Compute, Uncompute, ComputeTag, - UncomputeTag) -from projectq.cengines import _swapandcnotflipper -from projectq.backends import IBMBackend +from projectq.meta import Compute, ComputeTag, Control, Uncompute, UncomputeTag +from projectq.ops import CNOT, All, Command, H, Swap, X +from projectq.types import WeakQubitRef + +from . import _swapandcnotflipper def test_swapandcnotflipper_missing_connection(): @@ -33,6 +32,16 @@ def test_swapandcnotflipper_missing_connection(): Swap | (qubit1, qubit2) +def test_swapandcnotflipper_invalid_swap(): + flipper = _swapandcnotflipper.SwapAndCNOTFlipper(set()) + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + with pytest.raises(RuntimeError): + flipper.receive([Command(engine=None, gate=Swap, qubits=([qb0, qb1], [qb2]))]) + + def test_swapandcnotflipper_is_available(): flipper = _swapandcnotflipper.SwapAndCNOTFlipper(set()) dummy = DummyEngine() @@ -62,7 +71,7 @@ def test_swapandcnotflipper_is_available(): def test_swapandcnotflipper_flips_cnot(): backend = DummyEngine(save_commands=True) - connectivity = set([(0, 1)]) + connectivity = {(0, 1)} flipper = _swapandcnotflipper.SwapAndCNOTFlipper(connectivity) eng = MainEngine(backend=backend, engine_list=[flipper]) qb0 = eng.allocate_qubit() @@ -81,7 +90,7 @@ def test_swapandcnotflipper_flips_cnot(): def test_swapandcnotflipper_invalid_circuit(): backend = DummyEngine(save_commands=True) - connectivity = set([(0, 2)]) + connectivity = {(0, 2)} flipper = _swapandcnotflipper.SwapAndCNOTFlipper(connectivity) eng = MainEngine(backend=backend, engine_list=[flipper]) qb0 = eng.allocate_qubit() @@ -97,7 +106,7 @@ def test_swapandcnotflipper_invalid_circuit(): def test_swapandcnotflipper_optimize_swaps(): backend = DummyEngine(save_commands=True) - connectivity = set([(1, 0)]) + connectivity = {(1, 0)} flipper = _swapandcnotflipper.SwapAndCNOTFlipper(connectivity) eng = MainEngine(backend=backend, engine_list=[flipper]) qb0 = eng.allocate_qubit() @@ -113,7 +122,7 @@ def test_swapandcnotflipper_optimize_swaps(): assert hgates == 4 backend = DummyEngine(save_commands=True) - connectivity = set([(0, 1)]) + connectivity = {(0, 1)} flipper = _swapandcnotflipper.SwapAndCNOTFlipper(connectivity) eng = MainEngine(backend=backend, engine_list=[flipper]) qb0 = eng.allocate_qubit() @@ -131,7 +140,7 @@ def test_swapandcnotflipper_optimize_swaps(): def test_swapandcnotflipper_keeps_tags(): backend = DummyEngine(save_commands=True) - connectivity = set([(1, 0)]) + connectivity = {(1, 0)} flipper = _swapandcnotflipper.SwapAndCNOTFlipper(connectivity) eng = MainEngine(backend=backend, engine_list=[flipper]) qb0 = eng.allocate_qubit() diff --git a/projectq/cengines/_tagremover.py b/projectq/cengines/_tagremover.py index bdfc5b86a..a939366ef 100755 --- a/projectq/cengines/_tagremover.py +++ b/projectq/cengines/_tagremover.py @@ -11,46 +11,53 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Contains a TagRemover engine, which removes temporary command tags (such as -Compute/Uncompute), thus enabling optimization across meta statements (loops -after unrolling, compute/uncompute, ...) +The TagRemover compiler engine. + +A TagRemover engine removes temporary command tags (such as Compute/Uncompute), thus enabling optimization across meta +statements (loops after unrolling, compute/uncompute, ...) """ -from projectq.cengines import BasicEngine from projectq.meta import ComputeTag, UncomputeTag +from ._basics import BasicEngine + class TagRemover(BasicEngine): """ - TagRemover is a compiler engine which removes temporary command tags (see - the tag classes such as LoopTag in projectq.meta._loop). + Compiler engine that remove temporary command tags. - Removing tags is important (after having handled them if necessary) in - order to enable optimizations across meta-function boundaries (compute/ - action/uncompute or loops after unrolling) + TagRemover is a compiler engine which removes temporary command tags (see the tag classes such as LoopTag in + projectq.meta._loop). + + Removing tags is important (after having handled them if necessary) in order to enable optimizations across + meta-function boundaries (compute/ action/uncompute or loops after unrolling) """ - def __init__(self, tags=[ComputeTag, UncomputeTag]): + + def __init__(self, tags=None): """ - Construct the TagRemover. + Initialize a TagRemover object. Args: tags: A list of meta tag classes (e.g., [ComputeTag, UncomputeTag]) denoting the tags to remove """ - BasicEngine.__init__(self) - assert isinstance(tags, list) - self._tags = tags + super().__init__() + if not tags: + self._tags = [ComputeTag, UncomputeTag] + elif isinstance(tags, list): + self._tags = tags + else: + raise TypeError(f'tags should be a list! Got: {tags}') def receive(self, command_list): """ - Receive a list of commands from the previous engine, remove all tags - which are an instance of at least one of the meta tags provided in the - constructor, and then send them on to the next compiler engine. + Receive a list of commands. + + Receive a list of commands from the previous engine, remove all tags which are an instance of at least one of + the meta tags provided in the constructor, and then send them on to the next compiler engine. Args: - command_list (list): List of commands to receive and then - (after removing tags) send on. + command_list (list): List of commands to receive and then (after removing tags) send on. """ for cmd in command_list: for tag in self._tags: diff --git a/projectq/cengines/_tagremover_test.py b/projectq/cengines/_tagremover_test.py index 1318b7bc9..6d61dad8a 100755 --- a/projectq/cengines/_tagremover_test.py +++ b/projectq/cengines/_tagremover_test.py @@ -11,15 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._tagremover.py.""" +import pytest + from projectq import MainEngine +from projectq.cengines import DummyEngine, _tagremover from projectq.meta import ComputeTag, UncomputeTag from projectq.ops import Command, H -from projectq.cengines import DummyEngine - -from projectq.cengines import _tagremover def test_tagremover_default(): @@ -27,9 +26,14 @@ def test_tagremover_default(): assert tag_remover._tags == [ComputeTag, UncomputeTag] +def test_tagremover_invalid(): + with pytest.raises(TypeError): + _tagremover.TagRemover(ComputeTag) + + def test_tagremover(): backend = DummyEngine(save_commands=True) - tag_remover = _tagremover.TagRemover([type("")]) + tag_remover = _tagremover.TagRemover([str]) eng = MainEngine(backend=backend, engine_list=[tag_remover]) # Create a command_list and check if "NewTag" is removed qubit = eng.allocate_qubit() diff --git a/projectq/cengines/_testengine.py b/projectq/cengines/_testengine.py index cf992fcda..3e9b0379b 100755 --- a/projectq/cengines/_testengine.py +++ b/projectq/cengines/_testengine.py @@ -11,40 +11,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """TestEngine and DummyEngine.""" - from copy import deepcopy -from projectq.cengines import BasicEngine -from projectq.ops import FlushGate, Allocate, Deallocate + +from projectq.ops import FlushGate + +from ._basics import BasicEngine + + +def _compare_cmds(cmd1, cmd2): + """Compare two command objects.""" + cmd2 = deepcopy(cmd2) + cmd2.engine = cmd1.engine + return cmd1 == cmd2 class CompareEngine(BasicEngine): """ - CompareEngine is an engine which saves all commands. It is only intended - for testing purposes. Two CompareEngine backends can be compared and - return True if they contain the same commmands. + Command list comparison compiler engine for testing purposes. + + CompareEngine is an engine which saves all commands. It is only intended for testing purposes. Two CompareEngine + backends can be compared and return True if they contain the same commands. """ + def __init__(self): - BasicEngine.__init__(self) + """Initialize a CompareEngine object.""" + super().__init__() self._l = [[]] def is_available(self, cmd): + """All commands are accepted by this compiler engine.""" return True def cache_cmd(self, cmd): + """Cache a command.""" # are there qubit ids that haven't been added to the list? - all_qubit_id_list = [qubit.id for qureg in cmd.all_qubits - for qubit in qureg] + all_qubit_id_list = [qubit.id for qureg in cmd.all_qubits for qubit in qureg] maxidx = int(0) for qubit_id in all_qubit_id_list: maxidx = max(maxidx, qubit_id) # if so, increase size of list to account for these qubits - add = maxidx+1-len(self._l) + add = maxidx + 1 - len(self._l) if add > 0: - for i in range(add): + for _ in range(add): self._l += [[]] # add gate command to each of the qubits involved @@ -52,39 +63,41 @@ def cache_cmd(self, cmd): self._l[qubit_id] += [cmd] def receive(self, command_list): + """ + Receive a list of commands. + + Receive a command list and, for each command, stores it inside the cache before sending it to the next + compiler engine. + + Args: + command_list (list of Command objects): list of commands to receive. + """ for cmd in command_list: if not cmd.gate == FlushGate(): self.cache_cmd(cmd) if not self.is_last_engine: self.send(command_list) - def compare_cmds(self, c1, c2): - c2 = deepcopy(c2) - c2.engine = c1.engine - return c1 == c2 - def __eq__(self, other): - if (not isinstance(other, CompareEngine) or - len(self._l) != len(other._l)): + """Equal operator.""" + if not isinstance(other, CompareEngine) or len(self._l) != len(other._l): return False - for i in range(len(self._l)): - if len(self._l[i]) != len(other._l[i]): + for i, _li in enumerate(self._l): + if len(_li) != len(other._l[i]): return False - for j in range(len(self._l[i])): - if not self.compare_cmds(self._l[i][j], other._l[i][j]): + for j, _lij in enumerate(_li): + if not _compare_cmds(_lij, other._l[i][j]): return False return True - def __ne__(self, other): - return not self.__eq__(other) - def __str__(self): + """Return a string representation of the object.""" string = "" - for qubit_id in range(len(self._l)): - string += "Qubit {0} : ".format(qubit_id) + for qubit_id, _l in enumerate(self._l): + string += f"Qubit {qubit_id} : " for command in self._l[qubit_id]: - string += str(command) + ", " - string = string[:-2] + "\n" + string += f"{str(command)}, " + string = f"{string[:-2]}\n" return string @@ -92,31 +105,39 @@ class DummyEngine(BasicEngine): """ DummyEngine used for testing. - The DummyEngine forwards all commands directly to next engine. - If self.is_last_engine == True it just discards all gates. - By setting save_commands == True all commands get saved as a - list in self.received_commands. Elements are appended to this - list so they are ordered according to when they are received. + The DummyEngine forwards all commands directly to next engine. If self.is_last_engine == True it just discards + all gates. + By setting save_commands == True all commands get saved as a list in self.received_commands. Elements are appended + to this list so they are ordered according to when they are received. """ + def __init__(self, save_commands=False): """ - Initialize DummyEngine + Initialize a DummyEngine. Args: save_commands (default = False): If True, commands are saved in self.received_commands. """ - BasicEngine.__init__(self) + super().__init__() self.save_commands = save_commands self.received_commands = [] def is_available(self, cmd): + """All commands are accepted by this compiler engine.""" return True def receive(self, command_list): + """ + Receive a list of commands. + + Receive a command list and, for each command, stores it internally if requested before sending it to the next + compiler engine. + + Args: + command_list (list of Command objects): list of commands to receive. + """ if self.save_commands: self.received_commands.extend(command_list) if not self.is_last_engine: self.send(command_list) - else: - pass diff --git a/projectq/cengines/_testengine_test.py b/projectq/cengines/_testengine_test.py index 1a7d5bab8..ff0786ff0 100755 --- a/projectq/cengines/_testengine_test.py +++ b/projectq/cengines/_testengine_test.py @@ -11,14 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._testengine.py.""" from projectq import MainEngine -from projectq.cengines import DummyEngine -from projectq.ops import CNOT, H, Rx, Allocate, FlushGate - -from projectq.cengines import _testengine +from projectq.cengines import DummyEngine, _testengine +from projectq.ops import CNOT, Allocate, FlushGate, H, Rx def test_compare_engine_str(): @@ -29,9 +26,10 @@ def test_compare_engine_str(): H | qb0 CNOT | (qb0, qb1) eng.flush() - expected = ("Qubit 0 : Allocate | Qureg[0], H | Qureg[0], " + - "CX | ( Qureg[0], Qureg[1] )\nQubit 1 : Allocate | Qureg[1]," + - " CX | ( Qureg[0], Qureg[1] )\n") + expected = ( + "Qubit 0 : Allocate | Qureg[0], H | Qureg[0], CX | ( Qureg[0], Qureg[1] )\n" + "Qubit 1 : Allocate | Qureg[1], CX | ( Qureg[0], Qureg[1] )\n" + ) assert str(compare_engine) == expected @@ -97,9 +95,9 @@ def test_compare_engine(): CNOT | (qb20, qb21) eng2.flush() # test other branch to fail - qb30 = eng3.allocate_qubit() - qb31 = eng3.allocate_qubit() - qb32 = eng3.allocate_qubit() + qb30 = eng3.allocate_qubit() # noqa: F841 + qb31 = eng3.allocate_qubit() # noqa: F841 + qb32 = eng3.allocate_qubit() # noqa: F841 eng3.flush() assert compare_engine0 == compare_engine1 assert compare_engine1 != compare_engine2 diff --git a/projectq/cengines/_twodmapper.py b/projectq/cengines/_twodmapper.py index 769dbedd8..7a54a0291 100644 --- a/projectq/cengines/_twodmapper.py +++ b/projectq/cengines/_twodmapper.py @@ -11,38 +11,40 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Mapper for a quantum circuit to a 2D square grid. -Input: Quantum circuit with 1 and 2 qubit gates on n qubits. Gates are assumed - to be applied in parallel if they act on disjoint qubit(s) and any pair - of qubits can perform a 2 qubit gate (all-to-all connectivity) -Output: Quantum circuit in which qubits are placed in 2-D square grid in which - only nearest neighbour qubits can perform a 2 qubit gate. The mapper - uses Swap gates in order to move qubits next to each other. +Input: Quantum circuit with 1 and 2 qubit gates on n qubits. Gates are assumed to be applied in parallel if they act + on disjoint qubit(s) and any pair of qubits can perform a 2 qubit gate (all-to-all connectivity) +Output: Quantum circuit in which qubits are placed in 2-D square grid in which only nearest neighbour qubits can + perform a 2 qubit gate. The mapper uses Swap gates in order to move qubits next to each other. """ -from copy import deepcopy import itertools import math import random +from copy import deepcopy import networkx as nx -from projectq.cengines import (BasicMapperEngine, LinearMapper, - return_swap_depth) from projectq.meta import LogicalQubitIDTag -from projectq.ops import (AllocateQubitGate, Command, DeallocateQubitGate, - FlushGate, Swap) +from projectq.ops import ( + AllocateQubitGate, + Command, + DeallocateQubitGate, + FlushGate, + Swap, +) from projectq.types import WeakQubitRef +from ._basicmapper import BasicMapperEngine +from ._linearmapper import LinearMapper, return_swap_depth + -class GridMapper(BasicMapperEngine): +class GridMapper(BasicMapperEngine): # pylint: disable=too-many-instance-attributes """ Mapper to a 2-D grid graph. - Mapped qubits on the grid are numbered in row-major order. E.g. for - 3 rows and 2 columns: + Mapped qubits on the grid are numbered in row-major order. E.g. for 3 rows and 2 columns: 0 - 1 | | @@ -50,62 +52,54 @@ class GridMapper(BasicMapperEngine): | | 4 - 5 - The numbers are the mapped qubit ids. The backend might number - the qubits on the grid differently (e.g. not row-major), we call these - backend qubit ids. If the backend qubit ids are not row-major, one can - pass a dictionary translating from our row-major mapped ids to these - backend ids. + The numbers are the mapped qubit ids. The backend might number the qubits on the grid differently (e.g. not + row-major), we call these backend qubit ids. If the backend qubit ids are not row-major, one can pass a dictionary + translating from our row-major mapped ids to these backend ids. - Note: The algorithm sorts twice inside each column and once inside each - row. + Note: The algorithm sorts twice inside each column and once inside each row. Attributes: - current_mapping: Stores the mapping: key is logical qubit id, value - is backend qubit id. + current_mapping: Stores the mapping: key is logical qubit id, value is backend qubit id. storage(int): Number of gate it caches before mapping. num_rows(int): Number of rows in the grid num_columns(int): Number of columns in the grid num_qubits(int): num_rows x num_columns = number of qubits num_mappings (int): Number of times the mapper changed the mapping - depth_of_swaps (dict): Key are circuit depth of swaps, value is the - number of such mappings which have been + depth_of_swaps (dict): Key are circuit depth of swaps, value is the number of such mappings which have been applied - num_of_swaps_per_mapping (dict): Key are the number of swaps per - mapping, value is the number of such - mappings which have been applied + num_of_swaps_per_mapping (dict): Key are the number of swaps per mapping, value is the number of such mappings + which have been applied """ - def __init__(self, num_rows, num_columns, mapped_ids_to_backend_ids=None, - storage=1000, - optimization_function=lambda x: return_swap_depth(x), - num_optimization_steps=50): + + def __init__( # pylint: disable=too-many-arguments + self, + num_rows, + num_columns, + mapped_ids_to_backend_ids=None, + storage=1000, + optimization_function=return_swap_depth, + num_optimization_steps=50, + ): """ Initialize a GridMapper compiler engine. Args: num_rows(int): Number of rows in the grid num_columns(int): Number of columns in the grid. - mapped_ids_to_backend_ids(dict): Stores a mapping from mapped ids - which are 0,...,self.num_qubits-1 - in row-major order on the grid to - the corresponding qubit ids of the - backend. Key: mapped id. Value: - corresponding backend id. - Default is None which means - backend ids are identical to - mapped ids. + mapped_ids_to_backend_ids(dict): Stores a mapping from mapped ids which are 0,...,self.num_qubits-1 in + row-major order on the grid to the corresponding qubit ids of the + backend. Key: mapped id. Value: corresponding backend id. Default is + None which means backend ids are identical to mapped ids. storage: Number of gates to temporarily store - optimization_function: Function which takes a list of swaps and - returns a cost value. Mapper chooses a - permutation which minimizes this cost. - Default optimizes for circuit depth. - num_optimization_steps(int): Number of different permutations to - of the matching to try and minimize - the cost. + optimization_function: Function which takes a list of swaps and returns a cost value. Mapper chooses a + permutation which minimizes this cost. Default optimizes for circuit depth. + num_optimization_steps(int): Number of different permutations to of the matching to try and minimize the + cost. Raises: RuntimeError: if incorrect `mapped_ids_to_backend_ids` parameter """ - BasicMapperEngine.__init__(self) + super().__init__() self.num_rows = num_rows self.num_columns = num_columns self.num_qubits = num_rows * num_columns @@ -113,15 +107,14 @@ def __init__(self, num_rows, num_columns, mapped_ids_to_backend_ids=None, # Before sending we use this map to translate to backend ids: self._mapped_ids_to_backend_ids = mapped_ids_to_backend_ids if self._mapped_ids_to_backend_ids is None: - self._mapped_ids_to_backend_ids = dict() + self._mapped_ids_to_backend_ids = {} for i in range(self.num_qubits): self._mapped_ids_to_backend_ids[i] = i - if (not (set(self._mapped_ids_to_backend_ids.keys()) == - set(list(range(self.num_qubits)))) or not ( - len(set(self._mapped_ids_to_backend_ids.values())) == - self.num_qubits)): + if not (set(self._mapped_ids_to_backend_ids.keys()) == set(range(self.num_qubits))) or not ( + len(set(self._mapped_ids_to_backend_ids.values())) == self.num_qubits + ): raise RuntimeError("Incorrect mapped_ids_to_backend_ids parameter") - self._backend_ids_to_mapped_ids = dict() + self._backend_ids_to_mapped_ids = {} for mapped_id, backend_id in self._mapped_ids_to_backend_ids.items(): self._backend_ids_to_mapped_ids[backend_id] = mapped_id # As we use internally the mapped ids which are in row-major order, @@ -137,7 +130,7 @@ def __init__(self, num_rows, num_columns, mapped_ids_to_backend_ids=None, # places. self._rng = random.Random(11) # Storing commands - self._stored_commands = list() + self._stored_commands = [] # Logical qubit ids for which the Allocate gate has already been # processed and sent to the next engine but which are not yet # deallocated: @@ -145,8 +138,8 @@ def __init__(self, num_rows, num_columns, mapped_ids_to_backend_ids=None, # Change between 2D and 1D mappings (2D is a snake like 1D chain) # Note it translates to our mapped ids in row major order and not # backend ids which might be different. - self._map_2d_to_1d = dict() - self._map_1d_to_2d = dict() + self._map_2d_to_1d = {} + self._map_1d_to_2d = {} for row_index in range(self.num_rows): for column_index in range(self.num_columns): if row_index % 2 == 0: @@ -155,17 +148,17 @@ def __init__(self, num_rows, num_columns, mapped_ids_to_backend_ids=None, self._map_1d_to_2d[mapped_id] = mapped_id else: mapped_id_2d = row_index * self.num_columns + column_index - mapped_id_1d = ((row_index + 1) * self.num_columns - - column_index - 1) + mapped_id_1d = (row_index + 1) * self.num_columns - column_index - 1 self._map_2d_to_1d[mapped_id_2d] = mapped_id_1d self._map_1d_to_2d[mapped_id_1d] = mapped_id_2d # Statistics: self.num_mappings = 0 - self.depth_of_swaps = dict() - self.num_of_swaps_per_mapping = dict() + self.depth_of_swaps = {} + self.num_of_swaps_per_mapping = {} @property def current_mapping(self): + """Access to the mapping stored inside the mapper engine.""" return deepcopy(self._current_mapping) @current_mapping.setter @@ -174,43 +167,33 @@ def current_mapping(self, current_mapping): if current_mapping is None: self._current_row_major_mapping = None else: - self._current_row_major_mapping = dict() + self._current_row_major_mapping = {} for logical_id, backend_id in current_mapping.items(): - self._current_row_major_mapping[logical_id] = ( - self._backend_ids_to_mapped_ids[backend_id]) + self._current_row_major_mapping[logical_id] = self._backend_ids_to_mapped_ids[backend_id] def is_available(self, cmd): - """ - Only allows 1 or two qubit gates. - """ + """Only allow 1 or two qubit gates.""" num_qubits = 0 for qureg in cmd.all_qubits: num_qubits += len(qureg) - if num_qubits <= 2: - return True - else: - return False + return num_qubits <= 2 def _return_new_mapping(self): """ - Returns a new mapping of the qubits. + Return a new mapping of the qubits. - It goes through self._saved_commands and tries to find a - mapping to apply these gates on a first come first served basis. - It reuses the function of a 1D mapper and creates a mapping for a - 1D linear chain and then wraps it like a snake onto the square grid. + It goes through self._saved_commands and tries to find a mapping to apply these gates on a first come first + served basis. It reuses the function of a 1D mapper and creates a mapping for a 1D linear chain and then + wraps it like a snake onto the square grid. - One might create better mappings by specializing this function for a - square grid. + One might create better mappings by specializing this function for a square grid. - Returns: A new mapping as a dict. key is logical qubit id, - value is mapped id + Returns: A new mapping as a dict. key is logical qubit id, value is mapped id """ # Change old mapping to 1D in order to use LinearChain heuristic if self._current_row_major_mapping: - old_mapping_1d = dict() - for logical_id, mapped_id in ( - self._current_row_major_mapping.items()): + old_mapping_1d = {} + for logical_id, mapped_id in self._current_row_major_mapping.items(): old_mapping_1d[logical_id] = self._map_2d_to_1d[mapped_id] else: old_mapping_1d = self._current_row_major_mapping @@ -220,23 +203,19 @@ def _return_new_mapping(self): cyclic=False, currently_allocated_ids=self._currently_allocated_ids, stored_commands=self._stored_commands, - current_mapping=old_mapping_1d) + current_mapping=old_mapping_1d, + ) - new_mapping_2d = dict() + new_mapping_2d = {} for logical_id, mapped_id in new_mapping_1d.items(): new_mapping_2d[logical_id] = self._map_1d_to_2d[mapped_id] return new_mapping_2d def _compare_and_swap(self, element0, element1, key): - """ - If swapped (inplace), then return swap operation - so that key(element0) < key(element1) - """ + """If swapped (inplace), then return swap operation so that key(element0) < key(element1).""" if key(element0) > key(element1): - mapped_id0 = (element0.current_column + - element0.current_row * self.num_columns) - mapped_id1 = (element1.current_column + - element1.current_row * self.num_columns) + mapped_id0 = element0.current_column + element0.current_row * self.num_columns + mapped_id1 = element1.current_column + element1.current_row * self.num_columns swap_operation = (mapped_id0, mapped_id1) # swap elements but update also current position: tmp_0 = element0.final_row @@ -249,8 +228,7 @@ def _compare_and_swap(self, element0, element1, key): element1.final_column = tmp_1 element1.row_after_step_1 = tmp_2 return swap_operation - else: - return None + return None def _sort_within_rows(self, final_positions, key): swap_operations = [] @@ -258,16 +236,16 @@ def _sort_within_rows(self, final_positions, key): finished_sorting = False while not finished_sorting: finished_sorting = True - for column in range(1, self.num_columns-1, 2): + for column in range(1, self.num_columns - 1, 2): element0 = final_positions[row][column] - element1 = final_positions[row][column+1] + element1 = final_positions[row][column + 1] swap = self._compare_and_swap(element0, element1, key=key) if swap is not None: finished_sorting = False swap_operations.append(swap) - for column in range(0, self.num_columns-1, 2): + for column in range(0, self.num_columns - 1, 2): element0 = final_positions[row][column] - element1 = final_positions[row][column+1] + element1 = final_positions[row][column + 1] swap = self._compare_and_swap(element0, element1, key=key) if swap is not None: finished_sorting = False @@ -280,46 +258,52 @@ def _sort_within_columns(self, final_positions, key): finished_sorting = False while not finished_sorting: finished_sorting = True - for row in range(1, self.num_rows-1, 2): + for row in range(1, self.num_rows - 1, 2): element0 = final_positions[row][column] - element1 = final_positions[row+1][column] + element1 = final_positions[row + 1][column] swap = self._compare_and_swap(element0, element1, key=key) if swap is not None: finished_sorting = False swap_operations.append(swap) - for row in range(0, self.num_rows-1, 2): + for row in range(0, self.num_rows - 1, 2): element0 = final_positions[row][column] - element1 = final_positions[row+1][column] + element1 = final_positions[row + 1][column] swap = self._compare_and_swap(element0, element1, key=key) if swap is not None: finished_sorting = False swap_operations.append(swap) return swap_operations - def return_swaps(self, old_mapping, new_mapping, permutation=None): + def return_swaps( # pylint: disable=too-many-locals,too-many-branches,too-many-statements + self, old_mapping, new_mapping, permutation=None + ): """ - Returns the swap operation to change mapping + Return the swap operation to change mapping. Args: - old_mapping: dict: keys are logical ids and values are mapped - qubit ids - new_mapping: dict: keys are logical ids and values are mapped - qubit ids - permutation: list of int from 0, 1, ..., self.num_rows-1. It is - used to permute the found perfect matchings. Default - is None which keeps the original order. + old_mapping: dict: keys are logical ids and values are mapped qubit ids + new_mapping: dict: keys are logical ids and values are mapped qubit ids + permutation: list of int from 0, 1, ..., self.num_rows-1. It is used to permute the found perfect + matchings. Default is None which keeps the original order. Returns: - List of tuples. Each tuple is a swap operation which needs to be - applied. Tuple contains the two mapped qubit ids for the Swap. + List of tuples. Each tuple is a swap operation which needs to be applied. Tuple contains the two mapped + qubit ids for the Swap. """ if permutation is None: permutation = list(range(self.num_rows)) swap_operations = [] - class Position(object): - """ Custom Container.""" - def __init__(self, current_row, current_column, final_row, - final_column, row_after_step_1=None): + class Position: # pylint: disable=too-few-public-methods + """Custom Container.""" + + def __init__( # pylint: disable=too-many-arguments + self, + current_row, + current_column, + final_row, + final_column, + row_after_step_1=None, + ): self.current_row = current_row self.current_column = current_column self.final_row = final_row @@ -329,8 +313,7 @@ def __init__(self, current_row, current_column, final_row, # final_positions contains info containers # final_position[i][j] contains info container with # current_row == i and current_column == j - final_positions = [[None for i in range(self.num_columns)] - for j in range(self.num_rows)] + final_positions = [[None for i in range(self.num_columns)] for j in range(self.num_rows)] # move qubits which are in both mappings used_mapped_ids = set() for logical_id in old_mapping: @@ -340,10 +323,12 @@ def __init__(self, current_row, current_column, final_row, old_row = old_mapping[logical_id] // self.num_columns new_column = new_mapping[logical_id] % self.num_columns new_row = new_mapping[logical_id] // self.num_columns - info_container = Position(current_row=old_row, - current_column=old_column, - final_row=new_row, - final_column=new_column) + info_container = Position( + current_row=old_row, + current_column=old_column, + final_row=new_row, + final_column=new_column, + ) final_positions[old_row][old_column] = info_container # exchange all remaining None with the not yet used mapped ids all_ids = set(range(self.num_qubits)) @@ -355,26 +340,26 @@ def __init__(self, current_row, current_column, final_row, mapped_id = not_used_mapped_ids.pop() new_column = mapped_id % self.num_columns new_row = mapped_id // self.num_columns - info_container = Position(current_row=row, - current_column=column, - final_row=new_row, - final_column=new_column) + info_container = Position( + current_row=row, + current_column=column, + final_row=new_row, + final_column=new_column, + ) final_positions[row][column] = info_container - assert len(not_used_mapped_ids) == 0 + if len(not_used_mapped_ids) > 0: # pragma: no cover + raise RuntimeError('Internal compiler error: len(not_used_mapped_ids) > 0') # 1. Assign column_after_step_1 for each element # Matching contains the num_columns matchings matchings = [None for i in range(self.num_rows)] - # Build bipartite graph. Nodes are the current columns numbered - # (0, 1, ...) and the destination columns numbered with an offset of - # self.num_columns (0 + offset, 1+offset, ...) + # Build bipartite graph. Nodes are the current columns numbered (0, 1, ...) and the destination columns + # numbered with an offset of self.num_columns (0 + offset, 1+offset, ...) graph = nx.Graph() offset = self.num_columns graph.add_nodes_from(range(self.num_columns), bipartite=0) - graph.add_nodes_from(range(offset, offset + self.num_columns), - bipartite=1) - # Add an edge to the graph from (i, j+offset) for every element - # currently in column i which should go to column j for the new - # mapping + graph.add_nodes_from(range(offset, offset + self.num_columns), bipartite=1) + # Add an edge to the graph from (i, j+offset) for every element currently in column i which should go to + # column j for the new mapping for row in range(self.num_rows): for column in range(self.num_columns): destination_column = final_positions[row][column].final_column @@ -384,8 +369,7 @@ def __init__(self, current_row, current_column, final_row, graph[column][destination_column + offset]['num'] = 1 else: graph[column][destination_column + offset]['num'] += 1 - # Find perfect matching, remove those edges from the graph - # and do it again: + # Find perfect matching, remove those edges from the graph and do it again: for i in range(self.num_rows): top_nodes = range(self.num_columns) matching = nx.bipartite.maximum_matching(graph, top_nodes) @@ -409,32 +393,28 @@ def __init__(self, current_row, current_column, final_row, element = final_positions[row][column] if element.row_after_step_1 is not None: continue - elif element.final_column == dest_column: + if element.final_column == dest_column: if best_element is None: best_element = element elif best_element.final_row > element.final_row: best_element = element best_element.row_after_step_1 = row_after_step_1 # 2. Sort inside all the rows - swaps = self._sort_within_columns(final_positions=final_positions, - key=lambda x: x.row_after_step_1) + swaps = self._sort_within_columns(final_positions=final_positions, key=lambda x: x.row_after_step_1) swap_operations += swaps # 3. Sort inside all the columns - swaps = self._sort_within_rows(final_positions=final_positions, - key=lambda x: x.final_column) + swaps = self._sort_within_rows(final_positions=final_positions, key=lambda x: x.final_column) swap_operations += swaps # 4. Sort inside all the rows - swaps = self._sort_within_columns(final_positions=final_positions, - key=lambda x: x.final_row) + swaps = self._sort_within_columns(final_positions=final_positions, key=lambda x: x.final_row) swap_operations += swaps return swap_operations - def _send_possible_commands(self): + def _send_possible_commands(self): # pylint: disable=too-many-branches """ - Sends the stored commands possible without changing the mapping. + Send the stored commands possible without changing the mapping. - Note: self._current_row_major_mapping (hence also self.current_mapping) - must exist already + Note: self._current_row_major_mapping (hence also self.current_mapping) must exist already """ active_ids = deepcopy(self._currently_allocated_ids) for logical_id in self._current_row_major_mapping: @@ -442,8 +422,7 @@ def _send_possible_commands(self): active_ids.add(logical_id) new_stored_commands = [] - for i in range(len(self._stored_commands)): - cmd = self._stored_commands[i] + for i, cmd in enumerate(self._stored_commands): if len(active_ids) == 0: new_stored_commands += self._stored_commands[i:] break @@ -451,31 +430,27 @@ def _send_possible_commands(self): if cmd.qubits[0][0].id in self._current_row_major_mapping: self._currently_allocated_ids.add(cmd.qubits[0][0].id) - mapped_id = self._current_row_major_mapping[ - cmd.qubits[0][0].id] - qb = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[mapped_id]) + mapped_id = self._current_row_major_mapping[cmd.qubits[0][0].id] + qb = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[mapped_id]) new_cmd = Command( engine=self, gate=AllocateQubitGate(), qubits=([qb],), - tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)], + ) self.send([new_cmd]) else: new_stored_commands.append(cmd) elif isinstance(cmd.gate, DeallocateQubitGate): if cmd.qubits[0][0].id in active_ids: - mapped_id = self._current_row_major_mapping[ - cmd.qubits[0][0].id] - qb = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[mapped_id]) + mapped_id = self._current_row_major_mapping[cmd.qubits[0][0].id] + qb = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[mapped_id]) new_cmd = Command( engine=self, gate=DeallocateQubitGate(), qubits=([qb],), - tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)]) + tags=[LogicalQubitIDTag(cmd.qubits[0][0].id)], + ) self._currently_allocated_ids.remove(cmd.qubits[0][0].id) active_ids.remove(cmd.qubits[0][0].id) self._current_row_major_mapping.pop(cmd.qubits[0][0].id) @@ -491,21 +466,18 @@ def _send_possible_commands(self): if qubit.id not in active_ids: send_gate = False break - mapped_ids.add( - self._current_row_major_mapping[qubit.id]) + mapped_ids.add(self._current_row_major_mapping[qubit.id]) # Check that mapped ids are nearest neighbour on 2D grid if len(mapped_ids) == 2: - qb0, qb1 = sorted(list(mapped_ids)) + qb0, qb1 = sorted(mapped_ids) send_gate = False if qb1 - qb0 == self.num_columns: send_gate = True elif qb1 - qb0 == 1 and qb1 % self.num_columns != 0: send_gate = True if send_gate: - # Note: This sends the cmd correctly with the backend ids - # as it looks up the mapping in self.current_mapping - # and not our internal mapping - # self._current_row_major_mapping + # Note: This sends the cmd correctly with the backend ids as it looks up the mapping in + # self.current_mapping and not our internal mapping self._current_row_major_mapping self._send_cmd_with_mapped_ids(cmd) else: for qureg in cmd.all_qubits: @@ -514,19 +486,17 @@ def _send_possible_commands(self): new_stored_commands.append(cmd) self._stored_commands = new_stored_commands - def _run(self): + def _run(self): # pylint: disable=too-many-locals.too-many-branches,too-many-statements """ - Creates a new mapping and executes possible gates. + Create a new mapping and executes possible gates. - It first allocates all 0, ..., self.num_qubits-1 mapped qubit ids, if - they are not already used because we might need them all for the - swaps. Then it creates a new map, swaps all the qubits to the new map, - executes all possible gates, and finally deallocates mapped qubit ids - which don't store any information. + It first allocates all 0, ..., self.num_qubits-1 mapped qubit ids, if they are not already used because we + might need them all for the swaps. Then it creates a new map, swaps all the qubits to the new map, executes + all possible gates, and finally deallocates mapped qubit ids which don't store any information. """ num_of_stored_commands_before = len(self._stored_commands) if not self.current_mapping: - self.current_mapping = dict() + self.current_mapping = {} else: self._send_possible_commands() if len(self._stored_commands) == 0: @@ -537,18 +507,17 @@ def _run(self): lowest_cost = None matchings_numbers = list(range(self.num_rows)) if self.num_optimization_steps <= math.factorial(self.num_rows): - permutations = itertools.permutations(matchings_numbers, - self.num_rows) + permutations = itertools.permutations(matchings_numbers, self.num_rows) else: permutations = [] for _ in range(self.num_optimization_steps): - permutations.append(self._rng.sample(matchings_numbers, - self.num_rows)) + permutations.append(self._rng.sample(matchings_numbers, self.num_rows)) for permutation in permutations: trial_swaps = self.return_swaps( old_mapping=self._current_row_major_mapping, new_mapping=new_row_major_mapping, - permutation=permutation) + permutation=permutation, + ) if swaps is None: swaps = trial_swaps lowest_cost = self.optimization_function(trial_swaps) @@ -560,26 +529,17 @@ def _run(self): # i.e., contained in self._currently_allocated_ids) mapped_ids_used = set() for logical_id in self._currently_allocated_ids: - mapped_ids_used.add( - self._current_row_major_mapping[logical_id]) - not_allocated_ids = set(range(self.num_qubits)).difference( - mapped_ids_used) + mapped_ids_used.add(self._current_row_major_mapping[logical_id]) + not_allocated_ids = set(range(self.num_qubits)).difference(mapped_ids_used) for mapped_id in not_allocated_ids: - qb = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[mapped_id]) - cmd = Command(engine=self, gate=AllocateQubitGate(), - qubits=([qb],)) + qb = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[mapped_id]) + cmd = Command(engine=self, gate=AllocateQubitGate(), qubits=([qb],)) self.send([cmd]) # Send swap operations to arrive at new_mapping: for qubit_id0, qubit_id1 in swaps: - q0 = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[qubit_id0]) - q1 = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[qubit_id1]) - cmd = Command(engine=self, gate=Swap, qubits=([q0], [q1])) + qb0 = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[qubit_id0]) + qb1 = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[qubit_id1]) + cmd = Command(engine=self, gate=Swap, qubits=([qb0], [qb1])) self.send([cmd]) # Register statistics: self.num_mappings += 1 @@ -597,43 +557,39 @@ def _run(self): mapped_ids_used = set() for logical_id in self._currently_allocated_ids: mapped_ids_used.add(new_row_major_mapping[logical_id]) - not_needed_anymore = set(range(self.num_qubits)).difference( - mapped_ids_used) + not_needed_anymore = set(range(self.num_qubits)).difference(mapped_ids_used) for mapped_id in not_needed_anymore: - qb = WeakQubitRef( - engine=self, - idx=self._mapped_ids_to_backend_ids[mapped_id]) - cmd = Command(engine=self, gate=DeallocateQubitGate(), - qubits=([qb],)) + qb = WeakQubitRef(engine=self, idx=self._mapped_ids_to_backend_ids[mapped_id]) + cmd = Command(engine=self, gate=DeallocateQubitGate(), qubits=([qb],)) self.send([cmd]) # Change to new map: self._current_row_major_mapping = new_row_major_mapping - new_mapping = dict() + new_mapping = {} for logical_id, mapped_id in new_row_major_mapping.items(): - new_mapping[logical_id] = ( - self._mapped_ids_to_backend_ids[mapped_id]) + new_mapping[logical_id] = self._mapped_ids_to_backend_ids[mapped_id] self.current_mapping = new_mapping # Send possible gates: self._send_possible_commands() # Check that mapper actually made progress if len(self._stored_commands) == num_of_stored_commands_before: - raise RuntimeError("Mapper is potentially in an infinite loop. " + - "It is likely that the algorithm requires " + - "too many qubits. Increase the number of " + - "qubits for this mapper.") + raise RuntimeError( + "Mapper is potentially in an infinite loop. It is likely that the algorithm requires too" + "many qubits. Increase the number of qubits for this mapper." + ) def receive(self, command_list): """ - Receives a command list and, for each command, stores it until - we do a mapping (FlushGate or Cache of stored commands is full). + Receive a list of commands. + + Receive a command list and, for each command, stores it until we do a mapping (FlushGate or Cache of stored + commands is full). Args: - command_list (list of Command objects): list of commands to - receive. + command_list (list of Command objects): list of commands to receive. """ for cmd in command_list: if isinstance(cmd.gate, FlushGate): - while(len(self._stored_commands)): + while self._stored_commands: self._run() self.send([cmd]) else: diff --git a/projectq/cengines/_twodmapper_test.py b/projectq/cengines/_twodmapper_test.py index 7e4bfa168..5722b21c9 100644 --- a/projectq/cengines/_twodmapper_test.py +++ b/projectq/cengines/_twodmapper_test.py @@ -11,24 +11,21 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.cengines._2dmapper.py.""" -from copy import deepcopy import itertools import random +from copy import deepcopy import pytest import projectq from projectq.cengines import DummyEngine, LocalOptimizer +from projectq.cengines import _twodmapper as two_d from projectq.meta import LogicalQubitIDTag -from projectq.ops import (Allocate, BasicGate, Command, Deallocate, FlushGate, - X) +from projectq.ops import Allocate, BasicGate, Command, Deallocate, FlushGate, X from projectq.types import WeakQubitRef -from projectq.cengines import _twodmapper as two_d - def test_is_available(): mapper = two_d.GridMapper(num_rows=2, num_columns=2) @@ -48,12 +45,10 @@ def test_is_available(): def test_wrong_init_mapped_ids_to_backend_ids(): with pytest.raises(RuntimeError): test = {0: 1, 1: 0, 2: 2, 3: 3, 4: 4} - two_d.GridMapper(num_rows=2, num_columns=3, - mapped_ids_to_backend_ids=test) + two_d.GridMapper(num_rows=2, num_columns=3, mapped_ids_to_backend_ids=test) with pytest.raises(RuntimeError): test = {0: 1, 1: 0, 2: 2, 3: 3, 4: 4, 5: 2} - two_d.GridMapper(num_rows=2, num_columns=3, - mapped_ids_to_backend_ids=test) + two_d.GridMapper(num_rows=2, num_columns=3, mapped_ids_to_backend_ids=test) def test_resetting_mapping_to_none(): @@ -67,12 +62,23 @@ def test_resetting_mapping_to_none(): @pytest.mark.parametrize("different_backend_ids", [False, True]) def test_return_new_mapping(different_backend_ids): if different_backend_ids: - map_to_backend_ids = {0: 21, 1: 32, 2: 1, 3: 4, 4: 5, 5: 6, 6: 10, - 7: 7, 8: 0, 9: 56, 10: 55, 11: 9} + map_to_backend_ids = { + 0: 21, + 1: 32, + 2: 1, + 3: 4, + 4: 5, + 5: 6, + 6: 10, + 7: 7, + 8: 0, + 9: 56, + 10: 55, + 11: 9, + } else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=4, num_columns=3, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=4, num_columns=3, mapped_ids_to_backend_ids=map_to_backend_ids) eng = projectq.MainEngine(DummyEngine(), [mapper]) linear_chain_ids = [33, 22, 11, 2, 3, 0, 6, 7, 9, 12, 4, 88] mapper._stored_commands = [] @@ -82,42 +88,71 @@ def test_return_new_mapping(different_backend_ids): mapper._stored_commands.append(cmd) for i in range(11): qb0 = WeakQubitRef(engine=None, idx=linear_chain_ids[i]) - qb1 = WeakQubitRef(engine=None, idx=linear_chain_ids[i+1]) + qb1 = WeakQubitRef(engine=None, idx=linear_chain_ids[i + 1]) cmd = Command(None, X, qubits=([qb0],), controls=[qb1]) mapper._stored_commands.append(cmd) new_mapping = mapper._return_new_mapping() - possible_solution_1 = {33: 0, 22: 1, 11: 2, 2: 5, 3: 4, 0: 3, 6: 6, 7: 7, - 9: 8, 12: 11, 4: 10, 88: 9} - possible_solution_2 = {88: 0, 4: 1, 12: 2, 9: 5, 7: 4, 6: 3, 0: 6, 3: 7, - 2: 8, 11: 11, 22: 10, 33: 9} - assert (new_mapping == possible_solution_1 or - new_mapping == possible_solution_2) + possible_solution_1 = { + 33: 0, + 22: 1, + 11: 2, + 2: 5, + 3: 4, + 0: 3, + 6: 6, + 7: 7, + 9: 8, + 12: 11, + 4: 10, + 88: 9, + } + possible_solution_2 = { + 88: 0, + 4: 1, + 12: 2, + 9: 5, + 7: 4, + 6: 3, + 0: 6, + 3: 7, + 2: 8, + 11: 11, + 22: 10, + 33: 9, + } + assert new_mapping == possible_solution_1 or new_mapping == possible_solution_2 eng.flush() if different_backend_ids: - transformed_sol1 = dict() + transformed_sol1 = {} for logical_id, mapped_id in possible_solution_1.items(): transformed_sol1[logical_id] = map_to_backend_ids[mapped_id] - transformed_sol2 = dict() + transformed_sol2 = {} for logical_id, mapped_id in possible_solution_2.items(): transformed_sol2[logical_id] = map_to_backend_ids[mapped_id] - assert (mapper.current_mapping == transformed_sol1 or - mapper.current_mapping == transformed_sol2) + assert mapper.current_mapping == transformed_sol1 or mapper.current_mapping == transformed_sol2 else: - assert (mapper.current_mapping == possible_solution_1 or - mapper.current_mapping == possible_solution_2) - - -@pytest.mark.parametrize("num_rows, num_columns, seed, none_old, none_new", - [(2, 2, 0, 0, 0), (3, 4, 1, 0, 0), (4, 3, 2, 0, 0), - (5, 5, 3, 0, 0), (5, 3, 4, 3, 0), (4, 4, 5, 0, 3), - (6, 6, 7, 2, 3)]) + assert mapper.current_mapping == possible_solution_1 or mapper.current_mapping == possible_solution_2 + + +@pytest.mark.parametrize( + "num_rows, num_columns, seed, none_old, none_new", + [ + (2, 2, 0, 0, 0), + (3, 4, 1, 0, 0), + (4, 3, 2, 0, 0), + (5, 5, 3, 0, 0), + (5, 3, 4, 3, 0), + (4, 4, 5, 0, 3), + (6, 6, 7, 2, 3), + ], +) def test_return_swaps_random(num_rows, num_columns, seed, none_old, none_new): random.seed(seed) num_qubits = num_rows * num_columns old_chain = random.sample(range(num_qubits), num_qubits) new_chain = random.sample(range(num_qubits), num_qubits) - old_mapping = dict() - new_mapping = dict() + old_mapping = {} + new_mapping = {} for i in range(num_qubits): old_mapping[old_chain[i]] = i new_mapping[new_chain[i]] = i @@ -131,16 +166,15 @@ def test_return_swaps_random(num_rows, num_columns, seed, none_old, none_new): for logical_id in new_none_ids: new_mapping.pop(logical_id) - mapper = two_d.GridMapper(num_rows=num_rows, - num_columns=num_columns) + mapper = two_d.GridMapper(num_rows=num_rows, num_columns=num_columns) swaps = mapper.return_swaps(old_mapping, new_mapping) # Check that Swaps are allowed all_allowed_swaps = set() for row in range(num_rows): - for column in range(num_columns-1): + for column in range(num_columns - 1): qb_id = row * num_columns + column all_allowed_swaps.add((qb_id, qb_id + 1)) - for row in range(num_rows-1): + for row in range(num_rows - 1): for column in range(num_columns): qb_id = row * num_columns + column all_allowed_swaps.add((qb_id, qb_id + num_columns)) @@ -161,24 +195,30 @@ def test_return_swaps_random(num_rows, num_columns, seed, none_old, none_new): @pytest.mark.parametrize("different_backend_ids", [False, True]) def test_send_possible_commands(different_backend_ids): if different_backend_ids: - map_to_backend_ids = {0: 21, 1: 32, 2: 1, 3: 4, 4: 5, 5: 6, 6: 10, - 7: 7} + map_to_backend_ids = {0: 21, 1: 32, 2: 1, 3: 4, 4: 5, 5: 6, 6: 10, 7: 7} else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=2, num_columns=4, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=2, num_columns=4, mapped_ids_to_backend_ids=map_to_backend_ids) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend # mapping is identical except 5 <-> 0 if different_backend_ids: - mapper.current_mapping = {0: 6, 1: 32, 2: 1, 3: 4, 4: 5, 5: 21, 6: 10, - 7: 7} + mapper.current_mapping = {0: 6, 1: 32, 2: 1, 3: 4, 4: 5, 5: 21, 6: 10, 7: 7} else: - mapper.current_mapping = {5: 0, 1: 1, 2: 2, 3: 3, 4: 4, 0: 5, 6: 6, - 7: 7} - neighbours = [(5, 1), (1, 2), (2, 3), (4, 0), (0, 6), (6, 7), - (5, 4), (1, 0), (2, 6), (3, 7)] + mapper.current_mapping = {5: 0, 1: 1, 2: 2, 3: 3, 4: 4, 0: 5, 6: 6, 7: 7} + neighbours = [ + (5, 1), + (1, 2), + (2, 3), + (4, 0), + (0, 6), + (6, 7), + (5, 4), + (1, 0), + (2, 6), + (3, 7), + ] for qb0_id, qb1_id in neighbours: qb0 = WeakQubitRef(engine=None, idx=qb0_id) qb1 = WeakQubitRef(engine=None, idx=qb1_id) @@ -188,8 +228,7 @@ def test_send_possible_commands(different_backend_ids): mapper._send_possible_commands() assert len(mapper._stored_commands) == 0 for qb0_id, qb1_id in itertools.permutations(range(8), 2): - if ((qb0_id, qb1_id) not in neighbours and - (qb1_id, qb0_id) not in neighbours): + if (qb0_id, qb1_id) not in neighbours and (qb1_id, qb0_id) not in neighbours: qb0 = WeakQubitRef(engine=None, idx=qb0_id) qb1 = WeakQubitRef(engine=None, idx=qb1_id) cmd = Command(None, X, qubits=([qb0],), controls=[qb1]) @@ -204,18 +243,16 @@ def test_send_possible_commands_allocate(different_backend_ids): map_to_backend_ids = {0: 21, 1: 32, 2: 3, 3: 4, 4: 5, 5: 6} else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=3, num_columns=2, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=3, num_columns=2, mapped_ids_to_backend_ids=map_to_backend_ids) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) mapper._stored_commands = [cmd0] - mapper._currently_allocated_ids = set([10]) + mapper._currently_allocated_ids = {10} # not in mapping: - mapper.current_mapping = dict() + mapper.current_mapping = {} assert len(backend.received_commands) == 0 mapper._send_possible_commands() assert len(backend.received_commands) == 0 @@ -226,10 +263,15 @@ def test_send_possible_commands_allocate(different_backend_ids): assert len(mapper._stored_commands) == 0 # Only self._run() sends Allocate gates mapped0 = WeakQubitRef(engine=None, idx=3) - received_cmd = Command(engine=mapper, gate=Allocate, qubits=([mapped0],), - controls=[], tags=[LogicalQubitIDTag(0)]) + received_cmd = Command( + engine=mapper, + gate=Allocate, + qubits=([mapped0],), + controls=[], + tags=[LogicalQubitIDTag(0)], + ) assert backend.received_commands[0] == received_cmd - assert mapper._currently_allocated_ids == set([10, 0]) + assert mapper._currently_allocated_ids == {10, 0} @pytest.mark.parametrize("different_backend_ids", [False, True]) @@ -238,17 +280,15 @@ def test_send_possible_commands_deallocate(different_backend_ids): map_to_backend_ids = {0: 21, 1: 32, 2: 3, 3: 4, 4: 5, 5: 6} else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=3, num_columns=2, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=3, num_columns=2, mapped_ids_to_backend_ids=map_to_backend_ids) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) - cmd0 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], tags=[]) mapper._stored_commands = [cmd0] - mapper.current_mapping = dict() - mapper._currently_allocated_ids = set([10]) + mapper.current_mapping = {} + mapper._currently_allocated_ids = {10} # not yet allocated: mapper._send_possible_commands() assert len(backend.received_commands) == 0 @@ -259,8 +299,8 @@ def test_send_possible_commands_deallocate(different_backend_ids): mapper._send_possible_commands() assert len(backend.received_commands) == 1 assert len(mapper._stored_commands) == 0 - assert mapper.current_mapping == dict() - assert mapper._currently_allocated_ids == set([10]) + assert mapper.current_mapping == {} + assert mapper._currently_allocated_ids == {10} @pytest.mark.parametrize("different_backend_ids", [False, True]) @@ -269,19 +309,15 @@ def test_send_possible_commands_keep_remaining_gates(different_backend_ids): map_to_backend_ids = {0: 21, 1: 32, 2: 3, 3: 0, 4: 5, 5: 6} else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=3, num_columns=2, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=3, num_columns=2, mapped_ids_to_backend_ids=map_to_backend_ids) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) qb1 = WeakQubitRef(engine=None, idx=1) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) - cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], - tags=[]) - cmd2 = Command(engine=None, gate=Allocate, qubits=([qb1],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) + cmd1 = Command(engine=None, gate=Deallocate, qubits=([qb0],), controls=[], tags=[]) + cmd2 = Command(engine=None, gate=Allocate, qubits=([qb1],), controls=[], tags=[]) mapper._stored_commands = [cmd0, cmd1, cmd2] mapper.current_mapping = {0: 0} @@ -295,15 +331,13 @@ def test_send_possible_commands_one_inactive_qubit(different_backend_ids): map_to_backend_ids = {0: 21, 1: 32, 2: 3, 3: 0, 4: 5, 5: 6} else: map_to_backend_ids = None - mapper = two_d.GridMapper(num_rows=3, num_columns=2, - mapped_ids_to_backend_ids=map_to_backend_ids) + mapper = two_d.GridMapper(num_rows=3, num_columns=2, mapped_ids_to_backend_ids=map_to_backend_ids) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend qb0 = WeakQubitRef(engine=None, idx=0) qb1 = WeakQubitRef(engine=None, idx=1) - cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], - tags=[]) + cmd0 = Command(engine=None, gate=Allocate, qubits=([qb0],), controls=[], tags=[]) cmd1 = Command(engine=None, gate=X, qubits=([qb0],), controls=[qb1]) mapper._stored_commands = [cmd0, cmd1] mapper.current_mapping = {0: 0} @@ -329,7 +363,8 @@ def choose_last_permutation(swaps): num_columns=2, mapped_ids_to_backend_ids=map_to_backend_ids, optimization_function=choose_last_permutation, - num_optimization_steps=num_optimization_steps) + num_optimization_steps=num_optimization_steps, + ) backend = DummyEngine(save_commands=True) backend.is_last_engine = True mapper.next_engine = backend @@ -354,21 +389,25 @@ def choose_last_permutation(swaps): mapper.receive([cmd_flush]) assert mapper._stored_commands == [] assert len(backend.received_commands) == 10 - assert mapper._currently_allocated_ids == set([0, 2, 3]) + assert mapper._currently_allocated_ids == {0, 2, 3} if different_backend_ids: - assert (mapper.current_mapping == {0: 21, 2: 3, 3: 0} or - mapper.current_mapping == {0: 32, 2: 0, 3: 21} or - mapper.current_mapping == {0: 3, 2: 21, 3: 32} or - mapper.current_mapping == {0: 0, 2: 32, 3: 3}) + assert ( + mapper.current_mapping == {0: 21, 2: 3, 3: 0} + or mapper.current_mapping == {0: 32, 2: 0, 3: 21} + or mapper.current_mapping == {0: 3, 2: 21, 3: 32} + or mapper.current_mapping == {0: 0, 2: 32, 3: 3} + ) else: - assert (mapper.current_mapping == {0: 0, 2: 2, 3: 3} or - mapper.current_mapping == {0: 1, 2: 3, 3: 0} or - mapper.current_mapping == {0: 2, 2: 0, 3: 1} or - mapper.current_mapping == {0: 3, 2: 1, 3: 2}) + assert ( + mapper.current_mapping == {0: 0, 2: 2, 3: 3} + or mapper.current_mapping == {0: 1, 2: 3, 3: 0} + or mapper.current_mapping == {0: 2, 2: 0, 3: 1} + or mapper.current_mapping == {0: 3, 2: 1, 3: 2} + ) cmd9 = Command(None, X, qubits=([qb0],), controls=[qb3]) mapper.storage = 1 mapper.receive([cmd9]) - assert mapper._currently_allocated_ids == set([0, 2, 3]) + assert mapper._currently_allocated_ids == {0, 2, 3} assert mapper._stored_commands == [] assert len(mapper.current_mapping) == 3 assert 0 in mapper.current_mapping @@ -419,8 +458,7 @@ def test_correct_stats(): cmd8 = Command(None, X, qubits=([qb1],), controls=[qb2]) qb_flush = WeakQubitRef(engine=None, idx=-1) cmd_flush = Command(engine=None, gate=FlushGate(), qubits=([qb_flush],)) - mapper.receive([cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7, cmd8, - cmd_flush]) + mapper.receive([cmd0, cmd1, cmd2, cmd3, cmd4, cmd5, cmd6, cmd7, cmd8, cmd_flush]) assert mapper.num_mappings == 2 @@ -430,7 +468,8 @@ def test_send_possible_cmds_before_new_mapping(): backend.is_last_engine = True mapper.next_engine = backend - def dont_call_mapping(): raise Exception + def dont_call_mapping(): + raise Exception mapper._return_new_mapping = dont_call_mapping mapper.current_mapping = {0: 1} diff --git a/projectq/cengines/_withflushing_test.py b/projectq/cengines/_withflushing_test.py new file mode 100644 index 000000000..b0e844bf5 --- /dev/null +++ b/projectq/cengines/_withflushing_test.py @@ -0,0 +1,38 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.cengines.__init__.py.""" + +from unittest.mock import MagicMock + +from projectq.cengines import DummyEngine, flushing + + +def test_with_flushing(): + """Test with flushing() as eng:""" + with flushing(DummyEngine()) as engine: + engine.flush = MagicMock() + assert engine.flush.call_count == 0 + assert engine.flush.call_count == 1 + + +def test_with_flushing_with_exception(): + """Test with flushing() as eng: with an exception raised in the 'with' block.""" + try: + with flushing(DummyEngine()) as engine: + engine.flush = MagicMock() + assert engine.flush.call_count == 0 + raise ValueError("An exception is raised in the 'with' block") + except ValueError: + pass + assert engine.flush.call_count == 1 diff --git a/projectq/libs/__init__.py b/projectq/libs/__init__.py index ee1451dcd..ff6f1e495 100755 --- a/projectq/libs/__init__.py +++ b/projectq/libs/__init__.py @@ -11,3 +11,5 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +"""ProjectQ module containing libraries expanding the basic functionalities of ProjectQ.""" diff --git a/projectq/libs/hist/__init__.py b/projectq/libs/hist/__init__.py new file mode 100644 index 000000000..d55514dda --- /dev/null +++ b/projectq/libs/hist/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Measurement histogram plot helper functions. + +Contains a function to plot measurement outcome probabilities as a histogram for the simulator +""" + +from ._histogram import histogram diff --git a/projectq/libs/hist/_histogram.py b/projectq/libs/hist/_histogram.py new file mode 100644 index 000000000..d2bf255aa --- /dev/null +++ b/projectq/libs/hist/_histogram.py @@ -0,0 +1,75 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions to plot a histogram of measured data.""" + +import matplotlib.pyplot as plt + +from projectq.backends import Simulator + + +def histogram(backend, qureg): + """ + Make a measurement outcome probability histogram for the given qubits. + + Args: + backend (BasicEngine): A ProjectQ backend + qureg (list of qubits and/or quregs): The qubits, + for which to make the histogram + + Returns: + A tuple (fig, axes, probabilities), where: + fig: The histogram as figure + axes: The axes of the histogram + probabilities (dict): A dictionary mapping outcomes as string + to their probabilities + + Note: + Don't forget to call eng.flush() before using this function. + """ + qubit_list = [] + for qb in qureg: + if isinstance(qb, list): + qubit_list.extend(qb) + else: + qubit_list.append(qb) + + if len(qubit_list) > 5: + print(f'Warning: For {len(qubit_list)} qubits there are 2^{len(qubit_list)} different outcomes') + print("The resulting histogram may look bad and/or take too long.") + print("Consider calling histogram() with a sublist of the qubits.") + + if hasattr(backend, 'get_probabilities'): + probabilities = backend.get_probabilities(qureg) + elif isinstance(backend, Simulator): + outcome = [0] * len(qubit_list) + n_outcomes = 1 << len(qubit_list) + probabilities = {} + for i in range(n_outcomes): + for pos in range(len(qubit_list)): + if (1 << pos) & i: + outcome[pos] = 1 + else: + outcome[pos] = 0 + probabilities[''.join([str(bit) for bit in outcome])] = backend.get_probability(outcome, qubit_list) + else: + raise RuntimeError('Unable to retrieve probabilities from backend') + + # Empirical figure size for up to 5 qubits + fig, axes = plt.subplots(figsize=(min(21.2, 2 + 0.6 * (1 << len(qubit_list))), 7)) + names = list(probabilities.keys()) + values = list(probabilities.values()) + axes.bar(names, values) + fig.suptitle('Measurement Probabilities') + return (fig, axes, probabilities) diff --git a/projectq/libs/hist/_histogram_test.py b/projectq/libs/hist/_histogram_test.py new file mode 100644 index 000000000..55d6fb470 --- /dev/null +++ b/projectq/libs/hist/_histogram_test.py @@ -0,0 +1,143 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import matplotlib +import matplotlib.pyplot as plt # noqa: F401 +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import BasicEngine, DummyEngine +from projectq.libs.hist import histogram +from projectq.ops import All, AllocateQubitGate, C, FlushGate, H, Measure, X + + +@pytest.fixture(scope="module") +def matplotlib_setup(): + old_backend = matplotlib.get_backend() + matplotlib.use('agg') # avoid showing the histogram plots + yield + matplotlib.use(old_backend) + + +def test_invalid_backend(matplotlib_setup): + eng = MainEngine(backend=DummyEngine()) + qubit = eng.allocate_qubit() + eng.flush() + + with pytest.raises(RuntimeError): + histogram(eng.backend, qubit) + + +def test_backend_get_probabilities_method(matplotlib_setup): + class MyBackend(BasicEngine): + def get_probabilities(self, qureg): + return {'000': 0.5, '111': 0.5} + + def is_available(self, cmd): + return True + + def receive(self, command_list): + for cmd in command_list: + if not isinstance(cmd.gate, FlushGate): + assert isinstance(cmd.gate, AllocateQubitGate) + + eng = MainEngine(backend=MyBackend(), verbose=True) + qureg = eng.allocate_qureg(3) + eng.flush() + _, _, prob = histogram(eng.backend, qureg) + assert prob['000'] == 0.5 + assert prob['111'] == 0.5 + + # NB: avoid throwing exceptions when destroying the MainEngine + eng.next_engine = DummyEngine() + eng.next_engine.is_last_engine = True + + +def test_qubit(matplotlib_setup): + sim = Simulator() + eng = MainEngine(sim) + qubit = eng.allocate_qubit() + eng.flush() + _, _, prob = histogram(sim, qubit) + assert prob["0"] == pytest.approx(1) + assert prob["1"] == pytest.approx(0) + H | qubit + eng.flush() + _, _, prob = histogram(sim, qubit) + assert prob["0"] == pytest.approx(0.5) + Measure | qubit + eng.flush() + _, _, prob = histogram(sim, qubit) + assert prob["0"] == pytest.approx(1) or prob["1"] == pytest.approx(1) + + +def test_qureg(matplotlib_setup): + sim = Simulator() + eng = MainEngine(sim) + qureg = eng.allocate_qureg(3) + eng.flush() + _, _, prob = histogram(sim, qureg) + assert prob["000"] == pytest.approx(1) + assert prob["110"] == pytest.approx(0) + H | qureg[0] + C(X, 1) | (qureg[0], qureg[1]) + H | qureg[2] + eng.flush() + _, _, prob = histogram(sim, qureg) + assert prob["110"] == pytest.approx(0.25) + assert prob["100"] == pytest.approx(0) + All(Measure) | qureg + eng.flush() + _, _, prob = histogram(sim, qureg) + assert ( + prob["000"] == pytest.approx(1) + or prob["001"] == pytest.approx(1) + or prob["110"] == pytest.approx(1) + or prob["111"] == pytest.approx(1) + ) + assert prob["000"] + prob["001"] + prob["110"] + prob["111"] == pytest.approx(1) + + +def test_combination(matplotlib_setup): + sim = Simulator() + eng = MainEngine(sim) + qureg = eng.allocate_qureg(2) + qubit = eng.allocate_qubit() + eng.flush() + _, _, prob = histogram(sim, [qureg, qubit]) + assert prob["000"] == pytest.approx(1) + H | qureg[0] + C(X, 1) | (qureg[0], qureg[1]) + H | qubit + Measure | qureg[0] + eng.flush() + _, _, prob = histogram(sim, [qureg, qubit]) + assert (prob["000"] == pytest.approx(0.5) and prob["001"] == pytest.approx(0.5)) or ( + prob["110"] == pytest.approx(0.5) and prob["111"] == pytest.approx(0.5) + ) + assert prob["100"] == pytest.approx(0) + Measure | qubit + + +def test_too_many_qubits(matplotlib_setup, capsys): + sim = Simulator() + eng = MainEngine(sim) + qureg = eng.allocate_qureg(6) + eng.flush() + l_ref = len(capsys.readouterr().out) + _, _, prob = histogram(sim, qureg) + assert len(capsys.readouterr().out) > l_ref + assert prob["000000"] == pytest.approx(1) + All(Measure) diff --git a/projectq/libs/math/__init__.py b/projectq/libs/math/__init__.py index 1252fe007..87e1eee3e 100755 --- a/projectq/libs/math/__init__.py +++ b/projectq/libs/math/__init__.py @@ -12,9 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Math gate definitions.""" + from ._default_rules import all_defined_decomposition_rules -from ._gates import (AddConstant, - SubConstant, - AddConstantModN, - SubConstantModN, - MultiplyByConstantModN) +from ._gates import ( + AddConstant, + AddConstantModN, + AddQuantum, + ComparatorQuantum, + DivideQuantum, + MultiplyByConstantModN, + MultiplyQuantum, + SubConstant, + SubConstantModN, + SubtractQuantum, +) diff --git a/projectq/libs/math/_constantmath.py b/projectq/libs/math/_constantmath.py index 5bc106ba9..14c87e7b4 100755 --- a/projectq/libs/math/_constantmath.py +++ b/projectq/libs/math/_constantmath.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,48 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing constant math quantum operations.""" + import math + try: from math import gcd -except ImportError: +except ImportError: # pragma: no cover from fractions import gcd -from projectq.ops import R, X, Swap, Measure, CNOT, QFT -from projectq.meta import Control, Compute, Uncompute, CustomUncompute, Dagger -from ._gates import AddConstant, SubConstant, AddConstantModN, SubConstantModN +from projectq.meta import Compute, Control, CustomUncompute, Uncompute +from projectq.ops import CNOT, QFT, R, Swap, X + +from ._gates import AddConstant, AddConstantModN, SubConstant, SubConstantModN # Draper's addition by constant https://arxiv.org/abs/quant-ph/0008033 -def add_constant(eng, c, quint): +def add_constant(eng, constant, quint): """ - Adds a classical constant c to the quantum integer (qureg) quint using - Draper addition. + Add a classical constant c to the quantum integer (qureg) quint using Draper addition. - Note: Uses the Fourier-transform adder from - https://arxiv.org/abs/quant-ph/0008033. + Note: + Uses the Fourier-transform adder from https://arxiv.org/abs/quant-ph/0008033. """ - with Compute(eng): QFT | quint - for i in range(len(quint)): + for i, qubit in enumerate(quint): for j in range(i, -1, -1): - if ((c >> j) & 1): - R(math.pi / (1 << (i - j))) | quint[i] + if (constant >> j) & 1: + R(math.pi / (1 << (i - j))) | qubit Uncompute(eng) # Modular adder by Beauregard https://arxiv.org/abs/quant-ph/0205095 -def add_constant_modN(eng, c, N, quint): +def add_constant_modN(eng, constant, N, quint): # pylint: disable=invalid-name """ - Adds a classical constant c to a quantum integer (qureg) quint modulo N - using Draper addition and the construction from - https://arxiv.org/abs/quant-ph/0205095. + Add a classical constant c to a quantum integer (qureg) quint modulo N using Draper addition. + + This function uses Draper addition and the construction from https://arxiv.org/abs/quant-ph/0205095. """ - assert(c < N and c >= 0) + if constant < 0 or constant > N: + raise ValueError('Pre-condition failed: 0 <= constant < N') - AddConstant(c) | quint + AddConstant(constant) | quint with Compute(eng): SubConstant(N) | quint @@ -62,7 +65,7 @@ def add_constant_modN(eng, c, N, quint): with Control(eng, ancilla): AddConstant(N) | quint - SubConstant(c) | quint + SubConstant(constant) | quint with CustomUncompute(eng): X | quint[-1] @@ -70,43 +73,48 @@ def add_constant_modN(eng, c, N, quint): X | quint[-1] del ancilla - AddConstant(c) | quint + AddConstant(constant) | quint # Modular multiplication by modular addition & shift, followed by uncompute # from https://arxiv.org/abs/quant-ph/0205095 -def mul_by_constant_modN(eng, c, N, quint_in): +def mul_by_constant_modN(eng, constant, N, quint_in): # pylint: disable=invalid-name """ - Multiplies a quantum integer by a classical number a modulo N, i.e., + Multiply a quantum integer by a classical number a modulo N. + + i.e., |x> -> |a*x mod N> (only works if a and N are relative primes, otherwise the modular inverse does not exist). """ - assert(c < N and c >= 0) - assert(gcd(c, N) == 1) + if constant < 0 or constant > N: + raise ValueError('Pre-condition failed: 0 <= constant < N') + if gcd(constant, N) != 1: + raise ValueError('Pre-condition failed: gcd(constant, N) == 1') - n = len(quint_in) - quint_out = eng.allocate_qureg(n + 1) + n_qubits = len(quint_in) + quint_out = eng.allocate_qureg(n_qubits + 1) - for i in range(n): + for i in range(n_qubits): with Control(eng, quint_in[i]): - AddConstantModN((c << i) % N, N) | quint_out + AddConstantModN((constant << i) % N, N) | quint_out - for i in range(n): + for i in range(n_qubits): Swap | (quint_out[i], quint_in[i]) - cinv = inv_mod_N(c, N) + cinv = inv_mod_N(constant, N) - for i in range(n): + for i in range(n_qubits): with Control(eng, quint_in[i]): SubConstantModN((cinv << i) % N, N) | quint_out del quint_out -# calculates the inverse of a modulo N -def inv_mod_N(a, N): +def inv_mod_N(a, N): # pylint: disable=invalid-name + """Calculate the inverse of a modulo N.""" + # pylint: disable=invalid-name s = 0 old_s = 1 r = N diff --git a/projectq/libs/math/_constantmath_test.py b/projectq/libs/math/_constantmath_test.py index 8b0681420..d4ff4cf4d 100755 --- a/projectq/libs/math/_constantmath_test.py +++ b/projectq/libs/math/_constantmath_test.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,24 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.libs.math_constantmath.py.""" import pytest +import projectq.libs.math from projectq import MainEngine -from projectq.cengines import (InstructionFilter, - AutoReplacer, - DecompositionRuleSet) from projectq.backends import Simulator -from projectq.ops import (All, BasicMathGate, ClassicalInstructionGate, - Measure, X) - -import projectq.libs.math +from projectq.cengines import AutoReplacer, DecompositionRuleSet, InstructionFilter +from projectq.libs.math import AddConstant, AddConstantModN, MultiplyByConstantModN +from projectq.ops import All, BasicMathGate, ClassicalInstructionGate, Measure, X from projectq.setups.decompositions import qft2crandhadamard, swap2cnot -from projectq.libs.math import (AddConstant, - AddConstantModN, - MultiplyByConstantModN) def init(engine, quint, value): @@ -44,72 +37,80 @@ def no_math_emulation(eng, cmd): return True try: return len(cmd.gate.matrix) == 2 - except: + except AttributeError: return False -rule_set = DecompositionRuleSet( - modules=[projectq.libs.math, qft2crandhadamard, swap2cnot]) +@pytest.fixture +def eng(): + return MainEngine( + backend=Simulator(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(no_math_emulation)], + ) -def test_adder(): - sim = Simulator() - eng = MainEngine(sim, [AutoReplacer(rule_set), - InstructionFilter(no_math_emulation)]) +rule_set = DecompositionRuleSet(modules=[projectq.libs.math, qft2crandhadamard, swap2cnot]) + + +@pytest.mark.parametrize( + 'gate', (AddConstantModN(-1, 6), MultiplyByConstantModN(-1, 6), MultiplyByConstantModN(4, 4)), ids=str +) +def test_invalid(eng, gate): + qureg = eng.allocate_qureg(4) + init(eng, qureg, 4) + + with pytest.raises(ValueError): + gate | qureg + eng.flush() + + +def test_adder(eng): qureg = eng.allocate_qureg(4) init(eng, qureg, 4) AddConstant(3) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][7])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][7])) init(eng, qureg, 7) # reset init(eng, qureg, 2) # check for overflow -> should be 15+2 = 1 (mod 16) AddConstant(15) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][1])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][1])) All(Measure) | qureg -def test_modadder(): - sim = Simulator() - eng = MainEngine(sim, [AutoReplacer(rule_set), - InstructionFilter(no_math_emulation)]) - +def test_modadder(eng): qureg = eng.allocate_qureg(4) init(eng, qureg, 4) AddConstantModN(3, 6) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][1])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][1])) init(eng, qureg, 1) # reset init(eng, qureg, 7) AddConstantModN(10, 13) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][4])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][4])) All(Measure) | qureg -def test_modmultiplier(): - sim = Simulator() - eng = MainEngine(sim, [AutoReplacer(rule_set), - InstructionFilter(no_math_emulation)]) - +def test_modmultiplier(eng): qureg = eng.allocate_qureg(4) init(eng, qureg, 4) MultiplyByConstantModN(3, 7) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][5])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][5])) init(eng, qureg, 5) # reset init(eng, qureg, 7) MultiplyByConstantModN(4, 13) | qureg - assert 1. == pytest.approx(abs(sim.cheat()[1][2])) + assert 1.0 == pytest.approx(abs(eng.backend.cheat()[1][2])) All(Measure) | qureg diff --git a/projectq/libs/math/_default_rules.py b/projectq/libs/math/_default_rules.py index af0cf979c..7243feae4 100755 --- a/projectq/libs/math/_default_rules.py +++ b/projectq/libs/math/_default_rules.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,54 +12,164 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Registers a few default replacement rules for Shor's algorithm to work -(see Examples). -""" +"""Registers a few default replacement rules for Shor's algorithm to work (see Examples).""" -from projectq.meta import Control, Dagger from projectq.cengines import DecompositionRule +from projectq.meta import Control -from ._gates import (AddConstant, - SubConstant, - AddConstantModN, - SubConstantModN, - MultiplyByConstantModN) -from ._constantmath import (add_constant, - add_constant_modN, - mul_by_constant_modN) +from ._constantmath import add_constant, add_constant_modN, mul_by_constant_modN +from ._gates import ( + AddConstant, + AddConstantModN, + AddQuantum, + ComparatorQuantum, + DivideQuantum, + MultiplyByConstantModN, + MultiplyQuantum, + SubtractQuantum, + _InverseAddQuantumGate, + _InverseDivideQuantumGate, + _InverseMultiplyQuantumGate, +) +from ._quantummath import ( + add_quantum, + comparator, + inverse_add_quantum_carry, + inverse_quantum_division, + inverse_quantum_multiplication, + quantum_conditional_add, + quantum_conditional_add_carry, + quantum_division, + quantum_multiplication, + subtract_quantum, +) def _replace_addconstant(cmd): eng = cmd.engine - c = cmd.gate.a + const = cmd.gate.a quint = cmd.qubits[0] with Control(eng, cmd.control_qubits): - add_constant(eng, c, quint) + add_constant(eng, const, quint) -def _replace_addconstmodN(cmd): +def _replace_addconstmodN(cmd): # pylint: disable=invalid-name eng = cmd.engine - c = cmd.gate.a + const = cmd.gate.a N = cmd.gate.N quint = cmd.qubits[0] with Control(eng, cmd.control_qubits): - add_constant_modN(eng, c, N, quint) + add_constant_modN(eng, const, N, quint) -def _replace_multiplybyconstantmodN(cmd): +def _replace_multiplybyconstantmodN(cmd): # pylint: disable=invalid-name eng = cmd.engine - c = cmd.gate.a + const = cmd.gate.a N = cmd.gate.N quint = cmd.qubits[0] with Control(eng, cmd.control_qubits): - mul_by_constant_modN(eng, c, N, quint) + mul_by_constant_modN(eng, const, N, quint) + + +def _replace_addquantum(cmd): + eng = cmd.engine + if cmd.control_qubits == []: + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + if len(cmd.qubits) == 3: + carry = cmd.qubits[2] + add_quantum(eng, quint_a, quint_b, carry) + else: + add_quantum(eng, quint_a, quint_b) + else: + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + if len(cmd.qubits) == 3: + carry = cmd.qubits[2] + with Control(eng, cmd.control_qubits): + quantum_conditional_add_carry(eng, quint_a, quint_b, cmd.control_qubits, carry) + else: + with Control(eng, cmd.control_qubits): + quantum_conditional_add(eng, quint_a, quint_b, cmd.control_qubits) + + +def _replace_inverse_add_quantum(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + + if len(cmd.qubits) == 3: + quint_c = cmd.qubits[2] + with Control(eng, cmd.control_qubits): + inverse_add_quantum_carry(eng, quint_a, [quint_b, quint_c]) + else: + with Control(eng, cmd.control_qubits): + subtract_quantum(eng, quint_a, quint_b) + + +def _replace_comparator(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + carry = cmd.qubits[2] + + with Control(eng, cmd.control_qubits): + comparator(eng, quint_a, quint_b, carry) + + +def _replace_quantumdivision(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + quint_c = cmd.qubits[2] + + with Control(eng, cmd.control_qubits): + quantum_division(eng, quint_a, quint_b, quint_c) + + +def _replace_inversequantumdivision(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + quint_c = cmd.qubits[2] + + with Control(eng, cmd.control_qubits): + inverse_quantum_division(eng, quint_a, quint_b, quint_c) + + +def _replace_quantummultiplication(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + quint_c = cmd.qubits[2] + + with Control(eng, cmd.control_qubits): + quantum_multiplication(eng, quint_a, quint_b, quint_c) + + +def _replace_inversequantummultiplication(cmd): + eng = cmd.engine + quint_a = cmd.qubits[0] + quint_b = cmd.qubits[1] + quint_c = cmd.qubits[2] + + with Control(eng, cmd.control_qubits): + inverse_quantum_multiplication(eng, quint_a, quint_b, quint_c) + all_defined_decomposition_rules = [ DecompositionRule(AddConstant, _replace_addconstant), DecompositionRule(AddConstantModN, _replace_addconstmodN), DecompositionRule(MultiplyByConstantModN, _replace_multiplybyconstantmodN), + DecompositionRule(AddQuantum.__class__, _replace_addquantum), + DecompositionRule(_InverseAddQuantumGate, _replace_inverse_add_quantum), + DecompositionRule(SubtractQuantum.__class__, _replace_inverse_add_quantum), + DecompositionRule(ComparatorQuantum.__class__, _replace_comparator), + DecompositionRule(DivideQuantum.__class__, _replace_quantumdivision), + DecompositionRule(_InverseDivideQuantumGate, _replace_inversequantumdivision), + DecompositionRule(MultiplyQuantum.__class__, _replace_quantummultiplication), + DecompositionRule(_InverseMultiplyQuantumGate, _replace_inversequantummultiplication), ] diff --git a/projectq/libs/math/_gates.py b/projectq/libs/math/_gates.py index fe1df6784..ad466d2d7 100755 --- a/projectq/libs/math/_gates.py +++ b/projectq/libs/math/_gates.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,24 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Quantum number math gates for ProjectQ.""" + from projectq.ops import BasicMathGate class AddConstant(BasicMathGate): """ - Add a constant to a quantum number represented by a quantum register, - stored from low- to high-bit. + Add a constant to a quantum number represented by a quantum register, stored from low- to high-bit. Example: .. code-block:: python - qunum = eng.allocate_qureg(5) # 5-qubit number - X | qunum[1] # qunum is now equal to 2 - AddConstant(3) | qunum # qunum is now equal to 5 + qunum = eng.allocate_qureg(5) # 5-qubit number + X | qunum[1] # qunum is now equal to 2 + AddConstant(3) | qunum # qunum is now equal to 5 + + Important: if you run with conditional and carry, carry needs to + be a quantum register for the compiler/decomposition to work. """ - def __init__(self, a): + + def __init__(self, a): # pylint: disable=invalid-name """ - Initializes the gate to the number to add. + Initialize the gate to the number to add. Args: a (int): Number to add to a quantum register. @@ -37,32 +42,29 @@ def __init__(self, a): It also initializes its base class, BasicMathGate, with the corresponding function, so it can be emulated efficiently. """ - BasicMathGate.__init__(self, lambda x: ((x + a),)) - self.a = a + super().__init__(lambda x: ((x + a),)) + self.a = a # pylint: disable=invalid-name def get_inverse(self): - """ - Return the inverse gate (subtraction of the same constant). - """ + """Return the inverse gate (subtraction of the same constant).""" return SubConstant(self.a) def __str__(self): - return "AddConstant({})".format(self.a) + """Return a string representation of the object.""" + return f"AddConstant({self.a})" def __eq__(self, other): + """Equal operator.""" return isinstance(other, AddConstant) and self.a == other.a def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) - def __ne__(self, other): - return not self.__eq__(other) - -def SubConstant(a): +def SubConstant(a): # pylint: disable=invalid-name """ - Subtract a constant from a quantum number represented by a quantum - register, stored from low- to high-bit. + Subtract a constant from a quantum number represented by a quantum register, stored from low- to high-bit. Args: a (int): Constant to subtract @@ -70,102 +72,121 @@ def SubConstant(a): Example: .. code-block:: python - qunum = eng.allocate_qureg(5) # 5-qubit number - X | qunum[2] # qunum is now equal to 4 - SubConstant(3) | qunum # qunum is now equal to 1 + qunum = eng.allocate_qureg(5) # 5-qubit number + X | qunum[2] # qunum is now equal to 4 + SubConstant(3) | qunum # qunum is now equal to 1 """ return AddConstant(-a) class AddConstantModN(BasicMathGate): """ - Add a constant to a quantum number represented by a quantum register - modulo N. + Add a constant to a quantum number represented by a quantum register modulo N. The number is stored from low- to high-bit, i.e., qunum[0] is the LSB. Example: .. code-block:: python - qunum = eng.allocate_qureg(5) # 5-qubit number - X | qunum[1] # qunum is now equal to 2 - AddConstantModN(3, 4) | qunum # qunum is now equal to 1 + qunum = eng.allocate_qureg(5) # 5-qubit number + X | qunum[1] # qunum is now equal to 2 + AddConstantModN(3, 4) | qunum # qunum is now equal to 1 + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ + def __init__(self, a, N): """ - Initializes the gate to the number to add modulo N. + Initialize the gate to the number to add modulo N. Args: a (int): Number to add to a quantum register (0 <= a < N). N (int): Number modulo which the addition is carried out. - It also initializes its base class, BasicMathGate, with the - corresponding function, so it can be emulated efficiently. + It also initializes its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. """ - BasicMathGate.__init__(self, lambda x: ((x + a) % N,)) - self.a = a + super().__init__(lambda x: ((x + a) % N,)) + self.a = a # pylint: disable=invalid-name self.N = N def __str__(self): - return "AddConstantModN({}, {})".format(self.a, self.N) + """Return a string representation of the object.""" + return f"AddConstantModN({self.a}, {self.N})" def get_inverse(self): - """ - Return the inverse gate (subtraction of the same number a modulo the - same number N). - """ + """Return the inverse gate (subtraction of the same number a modulo the same number N).""" return SubConstantModN(self.a, self.N) def __eq__(self, other): - return (isinstance(other, AddConstantModN) and self.a == other.a and - self.N == other.N) + """Equal operator.""" + return isinstance(other, AddConstantModN) and self.a == other.a and self.N == other.N def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) - def __ne__(self, other): - return not self.__eq__(other) - -def SubConstantModN(a, N): +def SubConstantModN(a, N): # pylint: disable=invalid-name """ - Subtract a constant from a quantum number represented by a quantum - register modulo N. + Subtract a constant from a quantum number represented by a quantum register modulo N. The number is stored from low- to high-bit, i.e., qunum[0] is the LSB. Args: a (int): Constant to add - N (int): Constant modulo which the addition of a should be carried - out. + N (int): Constant modulo which the addition of a should be carried out. Example: .. code-block:: python - qunum = eng.allocate_qureg(3) # 3-qubit number - X | qunum[1] # qunum is now equal to 2 - SubConstantModN(4,5) | qunum # qunum is now -2 = 6 = 1 (mod 5) + qunum = eng.allocate_qureg(3) # 3-qubit number + X | qunum[1] # qunum is now equal to 2 + SubConstantModN(4, 5) | qunum # qunum is now -2 = 6 = 1 (mod 5) + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * The value stored in the quantum register must be lower than N """ return AddConstantModN(N - a, N) class MultiplyByConstantModN(BasicMathGate): """ - Multiply a quantum number represented by a quantum register by a constant - modulo N. + Multiply a quantum number represented by a quantum register by a constant modulo N. The number is stored from low- to high-bit, i.e., qunum[0] is the LSB. Example: .. code-block:: python - qunum = eng.allocate_qureg(5) # 5-qubit number - X | qunum[2] # qunum is now equal to 4 - MultiplyByConstantModN(3,5) | qunum # qunum is now 2. + qunum = eng.allocate_qureg(5) # 5-qubit number + X | qunum[2] # qunum is now equal to 4 + MultiplyByConstantModN(3, 5) | qunum # qunum is now 2. + + .. note:: + + Pre-conditions: + + * c < N + * c >= 0 + * gcd(c, N) == 1 + * The value stored in the quantum register must be lower than N """ - def __init__(self, a, N): + + def __init__(self, a, N): # pylint: disable=invalid-name """ - Initializes the gate to the number to multiply with modulo N. + Initialize the gate to the number to multiply with modulo N. Args: a (int): Number by which to multiply a quantum register @@ -175,19 +196,359 @@ def __init__(self, a, N): It also initializes its base class, BasicMathGate, with the corresponding function, so it can be emulated efficiently. """ - BasicMathGate.__init__(self, lambda x: ((a * x) % N,)) - self.a = a + super().__init__(lambda x: ((a * x) % N,)) + self.a = a # pylint: disable=invalid-name self.N = N def __str__(self): - return "MultiplyByConstantModN({}, {})".format(self.a, self.N) + """Return a string representation of the object.""" + return f"MultiplyByConstantModN({self.a}, {self.N})" def __eq__(self, other): - return (isinstance(other, MultiplyByConstantModN) and - self.a == other.a and self.N == other.N) + """Equal operator.""" + return isinstance(other, MultiplyByConstantModN) and self.a == other.a and self.N == other.N def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) - def __ne__(self, other): - return not self.__eq__(other) + +class AddQuantumGate(BasicMathGate): + """ + Add up two quantum numbers represented by quantum registers. + + The numbers are stored from low- to high-bit, i.e., qunum[0] is the LSB. + + Example: + .. code-block:: python + + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + carry_bit = eng.allocate_qubit() + + X | qunum_a[2] # qunum_a is now equal to 4 + X | qunum_b[3] # qunum_b is now equal to 8 + AddQuantum | (qunum_a, qunum_b, carry) + # qunum_a remains 4, qunum_b is now 12 and carry_bit is 0 + """ + + def __init__(self): + """Initialize an AddQuantumGate object.""" + super().__init__(None) + + def __str__(self): + """Return a string representation of the object.""" + return "AddQuantum" + + def __eq__(self, other): + """Equal operator.""" + return isinstance(other, AddQuantumGate) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def get_math_function(self, qubits): + """Get the math function associated with an AddQuantumGate.""" + n_qubits = len(qubits[0]) + + def math_fun(a): # pylint: disable=invalid-name + a[1] = a[0] + a[1] + if len(bin(a[1])[2:]) > n_qubits: + a[1] = a[1] % (2**n_qubits) + + if len(a) == 3: + # Flip the last bit of the carry register + a[2] ^= 1 + return a + + return math_fun + + def get_inverse(self): + """Return the inverse gate (subtraction of the same number a modulo the same number N).""" + return _InverseAddQuantumGate() + + +AddQuantum = AddQuantumGate() + + +class _InverseAddQuantumGate(BasicMathGate): + """Internal gate glass to support emulation for inverse addition.""" + + def __init__(self): + """Initialize an _InverseAddQuantumGate object.""" + super().__init__(None) + + def __str__(self): + """Return a string representation of the object.""" + return "_InverseAddQuantum" + + def get_math_function(self, qubits): + def math_fun(a): # pylint: disable=invalid-name + if len(a) == 3: + # Flip the last bit of the carry register + a[2] ^= 1 + + a[1] -= a[0] + return a + + return math_fun + + +class SubtractQuantumGate(BasicMathGate): + """ + Subtract one quantum number from another quantum number both represented by quantum registers. + + Example: + .. code-block:: python + + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + X | qunum_a[2] # qunum_a is now equal to 4 + X | qunum_b[3] # qunum_b is now equal to 8 + SubtractQuantum | (qunum_a, qunum_b) + # qunum_a remains 4, qunum_b is now 4 + + """ + + def __init__(self): + """ + Initialize a SubtractQuantumGate object. + + Initializes the gate to its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. + """ + + def subtract(a, b): # pylint: disable=invalid-name + return (a, b - a) + + super().__init__(subtract) + + def __str__(self): + """Return a string representation of the object.""" + return "SubtractQuantum" + + def __eq__(self, other): + """Equal operator.""" + return isinstance(other, SubtractQuantumGate) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def get_inverse(self): + """Return the inverse gate (subtraction of the same number a modulo the same number N).""" + return AddQuantum + + +SubtractQuantum = SubtractQuantumGate() + + +class ComparatorQuantumGate(BasicMathGate): + """ + Flip a compare qubit if the binary value of first input is higher than the second input. + + The numbers are stored from low- to high-bit, i.e., qunum[0] is the LSB. + Example: + .. code-block:: python + + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + compare_bit = eng.allocate_qubit() + X | qunum_a[4] # qunum_a is now equal to 16 + X | qunum_b[3] # qunum_b is now equal to 8 + ComparatorQuantum | (qunum_a, qunum_b, compare_bit) + # qunum_a and qunum_b remain 16 and 8, qunum_b is now 12 and compare bit is now 1 + """ + + def __init__(self): + """ + Initialize a ComparatorQuantumGate object. + + Initialize the gate and its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. + """ + + def compare(a, b, c): # pylint: disable=invalid-name + # pylint: disable=invalid-name + if b < a: + if c == 0: + c = 1 + else: + c = 0 + return (a, b, c) + + super().__init__(compare) + + def __str__(self): + """Return a string representation of the object.""" + return "Comparator" + + def __eq__(self, other): + """Equal operator.""" + return isinstance(other, ComparatorQuantumGate) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def get_inverse(self): + """Return the inverse of this gate.""" + return AddQuantum + + +ComparatorQuantum = ComparatorQuantumGate() + + +class DivideQuantumGate(BasicMathGate): + """ + Divide one quantum number from another. + + Takes three inputs which should be quantum registers of equal size; a + dividend, a remainder and a divisor. The remainder should be in the state |0...0> and the dividend should be + bigger than the divisor.The gate returns (in this order): the remainder, the quotient and the divisor. + + The numbers are stored from low- to high-bit, i.e., qunum[0] is the LSB. + + Example: + .. code-block:: python + + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + qunum_c = eng.allocate_qureg(5) # 5-qubit number + + All(X) | [qunum_a[0], qunum_a[3]] # qunum_a is now equal to 9 + X | qunum_c[2] # qunum_c is now equal to 4 + + DivideQuantum | (qunum_a, qunum_b, qunum_c) + # qunum_a is now equal to 1 (remainder), qunum_b is now + # equal to 2 (quotient) and qunum_c remains 4 (divisor) + + # |dividend>|remainder>|divisor> -> |remainder>|quotient>|divisor> + """ + + def __init__(self): + """ + Initialize a DivideQuantumGate object. + + Initialize the gate and its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. + """ + + def division(dividend, remainder, divisor): + if divisor == 0 or divisor > dividend: + return (remainder, dividend, divisor) + + quotient = remainder + dividend // divisor + return ((dividend - (quotient * divisor)), quotient, divisor) + + super().__init__(division) + + def get_inverse(self): + """Return the inverse of this gate.""" + return _InverseDivideQuantumGate() + + def __str__(self): + """Return a string representation of the object.""" + return "DivideQuantum" + + def __eq__(self, other): + """Equal operator.""" + return isinstance(other, DivideQuantumGate) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + +DivideQuantum = DivideQuantumGate() + + +class _InverseDivideQuantumGate(BasicMathGate): + """Internal gate glass to support emulation for inverse division.""" + + def __init__(self): + """Initialize an _InverseDivideQuantumGate object.""" + + def inverse_division(remainder, quotient, divisor): + if divisor == 0: + return (quotient, remainder, divisor) + + dividend = remainder + quotient * divisor + remainder = 0 + return (dividend, remainder, divisor) + + super().__init__(inverse_division) + + def __str__(self): + """Return a string representation of the object.""" + return "_InverseDivideQuantum" + + +class MultiplyQuantumGate(BasicMathGate): + """ + Multiply two quantum numbers represented by a quantum registers. + + Requires three quantum registers as inputs, the first two are the numbers to be multiplied and should have the + same size (n qubits). The third register will hold the product and should be of size 2n+1. The numbers are stored + from low- to high-bit, i.e., qunum[0] is the LSB. + + Example: + .. code-block:: python + + qunum_a = eng.allocate_qureg(4) + qunum_b = eng.allocate_qureg(4) + qunum_c = eng.allocate_qureg(9) + X | qunum_a[2] # qunum_a is now 4 + X | qunum_b[3] # qunum_b is now 8 + MultiplyQuantum() | (qunum_a, qunum_b, qunum_c) + # qunum_a remains 4 and qunum_b remains 8, qunum_c is now equal to 32 + """ + + def __init__(self): + """ + Initialize a MultiplyQuantumGate object. + + Initialize the gate and its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. + """ + + def multiply(a, b, c): # pylint: disable=invalid-name + return (a, b, c + a * b) + + super().__init__(multiply) + + def __str__(self): + """Return a string representation of the object.""" + return "MultiplyQuantum" + + def __eq__(self, other): + """Equal operator.""" + return isinstance(other, MultiplyQuantumGate) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def get_inverse(self): + """Return the inverse of this gate.""" + return _InverseMultiplyQuantumGate() + + +MultiplyQuantum = MultiplyQuantumGate() + + +class _InverseMultiplyQuantumGate(BasicMathGate): + """Internal gate glass to support emulation for inverse multiplication.""" + + def __init__(self): + """Initialize an _InverseMultiplyQuantumGate object.""" + + def inverse_multiplication(a, b, c): # pylint: disable=invalid-name + return (a, b, c - a * b) + + super().__init__(inverse_multiplication) + + def __str__(self): + """Return a string representation of the object.""" + return "_InverseMultiplyQuantum" diff --git a/projectq/libs/math/_gates_math_test.py b/projectq/libs/math/_gates_math_test.py new file mode 100644 index 000000000..6bf28f77c --- /dev/null +++ b/projectq/libs/math/_gates_math_test.py @@ -0,0 +1,347 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.libs.math._gates.py.""" + +import pytest + +import projectq.libs.math +import projectq.setups.decompositions +from projectq.backends import CommandPrinter +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + MainEngine, + TagRemover, +) +from projectq.meta import Compute, Control, Uncompute +from projectq.ops import All, BasicMathGate, ClassicalInstructionGate, Measure, X + +from . import ( + AddConstant, + AddQuantum, + ComparatorQuantum, + DivideQuantum, + MultiplyQuantum, + SubtractQuantum, +) + + +def print_all_probabilities(eng, qureg): + i = 0 + y = len(qureg) + while i < (2**y): + qubit_list = [int(x) for x in list((f'{i:0b}').zfill(y))] + qubit_list = qubit_list[::-1] + prob = eng.backend.get_probability(qubit_list, qureg) + if prob != 0.0: + print(prob, qubit_list, i) + + i += 1 + + +def _eng_emulation(): + # Only decomposing native ProjectQ gates + # -> using emulation for gates in projectq.libs.math + rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) + eng = MainEngine( + engine_list=[ + TagRemover(), + AutoReplacer(rule_set), + TagRemover(), + CommandPrinter(), + ], + verbose=True, + ) + return eng + + +def _eng_decomp(): + def no_math_emulation(eng, cmd): + if isinstance(cmd.gate, BasicMathGate): + return False + if isinstance(cmd.gate, ClassicalInstructionGate): + return True + try: + return len(cmd.gate.matrix) > 0 + except AttributeError: + return False + + rule_set = DecompositionRuleSet(modules=[projectq.libs.math, projectq.setups.decompositions.qft2crandhadamard]) + eng = MainEngine( + engine_list=[ + TagRemover(), + AutoReplacer(rule_set), + InstructionFilter(no_math_emulation), + TagRemover(), + CommandPrinter(), + ] + ) + return eng + + +@pytest.fixture(params=['no_decomp', 'full_decomp']) +def eng(request): + if request.param == 'no_decomp': + return _eng_emulation() + elif request.param == 'full_decomp': + return _eng_decomp() + + +def test_constant_addition(eng): + qunum_a = eng.allocate_qureg(5) + X | qunum_a[2] + with Compute(eng): + AddConstant(5) | (qunum_a) + + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + + +def test_addition(eng): + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + carry_bit = eng.allocate_qubit() + X | qunum_a[2] # qunum_a is now equal to 4 + X | qunum_b[3] # qunum_b is now equal to 8 + AddQuantum | (qunum_a, qunum_b, carry_bit) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 1, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0], carry_bit)) + + +def test_inverse_addition(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + X | qunum_a[2] + X | qunum_b[3] + with Compute(eng): + AddQuantum | (qunum_a, qunum_b) + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 1, 0], qunum_b)) + + +def test_inverse_addition_with_control(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + qunum_c = eng.allocate_qubit() + All(X) | qunum_a + All(X) | qunum_b + X | qunum_c + with Compute(eng): + with Control(eng, qunum_c): + AddQuantum | (qunum_a, qunum_b) + + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1, 1], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1, 1], qunum_b)) + + +def test_addition_with_control(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + control_bit = eng.allocate_qubit() + X | qunum_a[1] # qunum_a is now equal to 2 + X | qunum_b[4] # qunum_b is now equal to 16 + X | control_bit + with Control(eng, control_bit): + AddQuantum | (qunum_a, qunum_b) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0, 1], qunum_b)) + + +def test_addition_with_control_carry(eng): + qunum_a = eng.allocate_qureg(4) # 4-qubit number + qunum_b = eng.allocate_qureg(4) # 4-qubit number + control_bit = eng.allocate_qubit() + qunum_c = eng.allocate_qureg(2) + + X | qunum_a[1] # qunum is now equal to 2 + All(X) | qunum_b[0:4] # qunum is now equal to 15 + X | control_bit + + with Control(eng, control_bit): + AddQuantum | (qunum_a, qunum_b, qunum_c) + # qunum_a and ctrl don't change, qunum_b and qunum_c are now both equal + # to 1 so in binary together 10001 (2 + 15 = 17) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1], control_bit)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0], qunum_c)) + + All(Measure) | qunum_a + All(Measure) | qunum_b + + +def test_inverse_addition_with_control_carry(eng): + qunum_a = eng.allocate_qureg(4) + qunum_b = eng.allocate_qureg(4) + + control_bit = eng.allocate_qubit() + qunum_c = eng.allocate_qureg(2) + + X | qunum_a[1] + All(X) | qunum_b[0:4] + X | control_bit + with Compute(eng): + with Control(eng, control_bit): + AddQuantum | (qunum_a, qunum_b, qunum_c) + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1], control_bit)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0], qunum_c)) + + All(Measure) | qunum_a + All(Measure) | qunum_b + Measure | control_bit + All(Measure) | qunum_c + + +def test_subtraction(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + + X | qunum_a[2] + X | qunum_b[3] + + SubtractQuantum | (qunum_a, qunum_b) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_b)) + + +def test_inverse_subtraction(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + + X | qunum_a[2] + X | qunum_b[3] + + with Compute(eng): + SubtractQuantum | (qunum_a, qunum_b) + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 1, 0], qunum_b)) + + +def test_comparator(eng): + qunum_a = eng.allocate_qureg(5) # 5-qubit number + qunum_b = eng.allocate_qureg(5) # 5-qubit number + compare_bit = eng.allocate_qubit() + X | qunum_a[4] # qunum_a is now equal to 16 + X | qunum_b[3] # qunum_b is now equal to 8 + + ComparatorQuantum | (qunum_a, qunum_b, compare_bit) + + eng.flush() + print_all_probabilities(eng, qunum_a) + print_all_probabilities(eng, qunum_b) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 1], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 1, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1], compare_bit)) + + +def test_division(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + qunum_c = eng.allocate_qureg(5) + + All(X) | [qunum_a[0], qunum_a[3]] # qunum_a is now equal to 9 + X | qunum_c[2] # qunum_c is now 4 + + DivideQuantum | (qunum_a, qunum_b, qunum_c) + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 0, 0], qunum_a)) # remainder + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_c)) + + +def test_inverse_division(eng): + qunum_a = eng.allocate_qureg(5) + qunum_b = eng.allocate_qureg(5) + qunum_c = eng.allocate_qureg(5) + + All(X) | [qunum_a[0], qunum_a[3]] + X | qunum_c[2] + + with Compute(eng): + DivideQuantum | (qunum_a, qunum_b, qunum_c) + Uncompute(eng) + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 1, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_c)) + + +def test_multiplication(eng): + qunum_a = eng.allocate_qureg(4) + qunum_b = eng.allocate_qureg(4) + qunum_c = eng.allocate_qureg(9) + X | qunum_a[2] # qunum_a is now 4 + X | qunum_b[3] # qunum_b is now 8 + MultiplyQuantum | (qunum_a, qunum_b, qunum_c) + # qunum_a remains 4 and qunum_b remains 8, qunum_c is now equal to 32 + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 1, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 0, 1, 0, 0, 0], qunum_c)) + + +def test_inverse_multiplication(eng): + qunum_a = eng.allocate_qureg(4) + qunum_b = eng.allocate_qureg(4) + qunum_c = eng.allocate_qureg(9) + X | qunum_a[2] # qunum_a is now 4 + X | qunum_b[3] # qunum_b is now 8 + with Compute(eng): + MultiplyQuantum | (qunum_a, qunum_b, qunum_c) + Uncompute(eng) + + eng.flush() + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 1, 0, 0], qunum_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 1, 0], qunum_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 0, 0, 0, 0, 0], qunum_c)) diff --git a/projectq/libs/math/_gates_test.py b/projectq/libs/math/_gates_test.py index 52852101a..6eb9613e9 100755 --- a/projectq/libs/math/_gates_test.py +++ b/projectq/libs/math/_gates_test.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,23 +11,34 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Tests for projectq.libs.math._gates.py.""" -"""Tests for projectq.libs.math_gates.py.""" +from projectq.libs.math import ( + AddConstant, + AddConstantModN, + AddQuantum, + ComparatorQuantum, + DivideQuantum, + MultiplyByConstantModN, + MultiplyQuantum, + SubConstant, + SubConstantModN, + SubtractQuantum, +) -import pytest - -from projectq.libs.math import (AddConstant, - AddConstantModN, - MultiplyByConstantModN, - SubConstant, - SubConstantModN) +from ._gates import ( + AddQuantumGate, + ComparatorQuantumGate, + DivideQuantumGate, + MultiplyQuantumGate, + SubtractQuantumGate, +) def test_addconstant(): assert AddConstant(3) == AddConstant(3) assert not AddConstant(3) == AddConstant(4) assert AddConstant(7) != AddConstant(3) - assert str(AddConstant(3)) == "AddConstant(3)" @@ -37,7 +48,6 @@ def test_addconstantmodn(): assert not AddConstantModN(3, 5) == AddConstantModN(3, 4) assert AddConstantModN(7, 4) != AddConstantModN(3, 4) assert AddConstantModN(3, 5) != AddConstantModN(3, 4) - assert str(AddConstantModN(3, 4)) == "AddConstantModN(3, 4)" @@ -47,14 +57,52 @@ def test_multiplybyconstmodn(): assert not MultiplyByConstantModN(3, 5) == MultiplyByConstantModN(3, 4) assert MultiplyByConstantModN(7, 4) != MultiplyByConstantModN(3, 4) assert MultiplyByConstantModN(3, 5) != MultiplyByConstantModN(3, 4) - assert str(MultiplyByConstantModN(3, 4)) == "MultiplyByConstantModN(3, 4)" +def test_AddQuantum(): + assert AddQuantum == AddQuantumGate() + assert AddQuantum != SubtractQuantum + assert not AddQuantum == SubtractQuantum + assert str(AddQuantum) == "AddQuantum" + + +def test_SubtractQuantum(): + assert SubtractQuantum == SubtractQuantumGate() + assert SubtractQuantum != AddQuantum + assert not SubtractQuantum == ComparatorQuantum + assert str(SubtractQuantum) == "SubtractQuantum" + + +def test_Comparator(): + assert ComparatorQuantum == ComparatorQuantumGate() + assert ComparatorQuantum != AddQuantum + assert not ComparatorQuantum == AddQuantum + assert str(ComparatorQuantum) == "Comparator" + + +def test_QuantumDivision(): + assert DivideQuantum == DivideQuantumGate() + assert DivideQuantum != MultiplyQuantum + assert not DivideQuantum == MultiplyQuantum + assert str(DivideQuantum) == "DivideQuantum" + + +def test_QuantumMultiplication(): + assert MultiplyQuantum == MultiplyQuantumGate() + assert MultiplyQuantum != DivideQuantum + assert not MultiplyQuantum == DivideQuantum + assert str(MultiplyQuantum) == "MultiplyQuantum" + + def test_hash_function_implemented(): assert hash(AddConstant(3)) == hash(str(AddConstant(3))) assert hash(SubConstant(-3)) == hash(str(AddConstant(3))) assert hash(AddConstantModN(7, 4)) == hash(str(AddConstantModN(7, 4))) assert hash(SubConstantModN(7, 4)) == hash(str(AddConstantModN(-3, 4))) - assert hash(MultiplyByConstantModN(3, 5)) == hash( - MultiplyByConstantModN(3, 5)) + assert hash(MultiplyByConstantModN(3, 5)) == hash(str(MultiplyByConstantModN(3, 5))) + assert hash(AddQuantum) == hash(str(AddQuantum)) + assert hash(SubtractQuantum) == hash(str(SubtractQuantum)) + assert hash(ComparatorQuantum) == hash(str(ComparatorQuantum)) + assert hash(DivideQuantum) == hash(str(DivideQuantum)) + assert hash(MultiplyQuantum) == hash(str(MultiplyQuantum)) diff --git a/projectq/libs/math/_quantummath.py b/projectq/libs/math/_quantummath.py new file mode 100644 index 000000000..59aa7cf9f --- /dev/null +++ b/projectq/libs/math/_quantummath.py @@ -0,0 +1,555 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Definition of some mathematical quantum operations.""" + +from projectq.meta import Control +from projectq.ops import CNOT, All, X + +from ._gates import AddQuantum, SubtractQuantum + + +def add_quantum(eng, quint_a, quint_b, carry=None): + """ + Add two quantum integers. + + i.e., + + |a0...a(n-1)>|b(0)...b(n-1)>|c> -> |a0...a(n-1)>|b+a(0)...b+a(n)> + + (only works if quint_a and quint_b are the same size and carry is a single + qubit) + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + carry (list): Carry qubit + + Notes: + Ancilla: 0, size: 7n-6, toffoli: 2n-1, depth: 5n-3. + + .. rubric:: References + + Quantum addition using ripple carry from: https://arxiv.org/pdf/0910.2530.pdf + """ + # pylint: disable = pointless-statement + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if carry and len(carry) != 1: + raise ValueError('Either no carry bit or a single carry qubit is allowed!') + + n_qubits = len(quint_a) + 1 + + for i in range(1, n_qubits - 1): + CNOT | (quint_a[i], quint_b[i]) + + if carry: + CNOT | (quint_a[n_qubits - 2], carry) + + for j in range(n_qubits - 3, 0, -1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for k in range(0, n_qubits - 2): + with Control(eng, [quint_a[k], quint_b[k]]): + X | (quint_a[k + 1]) + + if carry: + with Control(eng, [quint_a[n_qubits - 2], quint_b[n_qubits - 2]]): + X | carry + + for i in range(n_qubits - 2, 0, -1): # noqa: E741 + CNOT | (quint_a[i], quint_b[i]) + with Control(eng, [quint_a[i - 1], quint_b[i - 1]]): + X | quint_a[i] + + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) + + for n_qubits in range(0, n_qubits - 1): + CNOT | (quint_a[n_qubits], quint_b[n_qubits]) + + +def subtract_quantum(eng, quint_a, quint_b): + """ + Subtract two quantum integers. + + i.e., + + |a>|b> -> |a>|b-a> + + (only works if quint_a and quint_b are the same size) + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + + Notes: + Quantum subtraction using bitwise complementation of quantum adder: b-a = (a + b')'. Same as the quantum + addition circuit except that the steps involving the carry qubit are left out and complement b at the start + and at the end of the circuit is added. + + Ancilla: 0, size: 9n-8, toffoli: 2n-2, depth: 5n-5. + + + .. rubric:: References + + Quantum addition using ripple carry from: + https://arxiv.org/pdf/0910.2530.pdf + """ + # pylint: disable = pointless-statement, expression-not-assigned + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + n_qubits = len(quint_a) + 1 + + All(X) | quint_b + + for i in range(1, n_qubits - 1): + CNOT | (quint_a[i], quint_b[i]) + + for j in range(n_qubits - 3, 0, -1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for k in range(0, n_qubits - 2): + with Control(eng, [quint_a[k], quint_b[k]]): + X | (quint_a[k + 1]) + + for i in range(n_qubits - 2, 0, -1): # noqa: E741 + CNOT | (quint_a[i], quint_b[i]) + with Control(eng, [quint_a[i - 1], quint_b[i - 1]]): + X | quint_a[i] + + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) + + for n_qubits in range(0, n_qubits - 1): + CNOT | (quint_a[n_qubits], quint_b[n_qubits]) + + All(X) | quint_b + + +def inverse_add_quantum_carry(eng, quint_a, quint_b): + """ + Inverse of quantum addition with carry. + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + """ + # pylint: disable = pointless-statement, expression-not-assigned + # pylint: disable = unused-argument + + if len(quint_a) != len(quint_b[0]): + raise ValueError('quint_a and quint_b must have the same size!') + + All(X) | quint_b[0] + X | quint_b[1][0] + + AddQuantum | (quint_a, quint_b[0], quint_b[1]) + + All(X) | quint_b[0] + X | quint_b[1][0] + + +def comparator(eng, quint_a, quint_b, comp): + """ + Compare the size of two quantum integers. + + i.e, + + if a>b: |a>|b>|c> -> |a>|b>|c+1> + + else: |a>|b>|c> -> |a>|b>|c> + + (only works if quint_a and quint_b are the same size and the comparator is 1 qubit) + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + comp (Qubit): Comparator qubit + + Notes: + Comparator flipping a compare qubit by computing the high bit of b-a, which is 1 if and only if a > b. The + high bit is computed using the first half of circuit in AddQuantum (such that the high bit is written to the + carry qubit) and then undoing the first half of the circuit. By complementing b at the start and b+a at the + end the high bit of b-a is calculated. + + Ancilla: 0, size: 8n-3, toffoli: 2n+1, depth: 4n+3. + """ + # pylint: disable = pointless-statement, expression-not-assigned + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if len(comp) != 1: + raise ValueError('Comparator output qubit must be a single qubit!') + + n_qubits = len(quint_a) + 1 + + All(X) | quint_b + + for i in range(1, n_qubits - 1): + CNOT | (quint_a[i], quint_b[i]) + + CNOT | (quint_a[n_qubits - 2], comp) + + for j in range(n_qubits - 3, 0, -1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for k in range(0, n_qubits - 2): + with Control(eng, [quint_a[k], quint_b[k]]): + X | (quint_a[k + 1]) + + with Control(eng, [quint_a[n_qubits - 2], quint_b[n_qubits - 2]]): + X | comp + + for k in range(0, n_qubits - 2): + with Control(eng, [quint_a[k], quint_b[k]]): + X | (quint_a[k + 1]) + + for j in range(n_qubits - 3, 0, -1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for i in range(1, n_qubits - 1): + CNOT | (quint_a[i], quint_b[i]) + + All(X) | quint_b + + +def quantum_conditional_add(eng, quint_a, quint_b, conditional): + """ + Add up two quantum integers if conditional is high. + + i.e., + + |a>|b>|c> -> |a>|b+a>|c> + (without a carry out qubit) + + if conditional is low, no operation is performed, i.e., + |a>|b>|c> -> |a>|b>|c> + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + conditional (list): Conditional qubit + + Notes: + Ancilla: 0, Size: 7n-7, Toffoli: 3n-3, Depth: 5n-3. + + .. rubric:: References + + Quantum Conditional Add from https://arxiv.org/pdf/1609.01241.pdf + """ + # pylint: disable = pointless-statement, expression-not-assigned + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if len(conditional) != 1: + raise ValueError('Conditional qubit must be a single qubit!') + + n_qubits = len(quint_a) + 1 + + for i in range(1, n_qubits - 1): + CNOT | (quint_a[i], quint_b[i]) + + for i in range(n_qubits - 2, 1, -1): + CNOT | (quint_a[i - 1], quint_a[i]) + + for k in range(0, n_qubits - 2): + with Control(eng, [quint_a[k], quint_b[k]]): + X | (quint_a[k + 1]) + + with Control(eng, [quint_a[n_qubits - 2], conditional[0]]): + X | quint_b[n_qubits - 2] + + for i in range(n_qubits - 2, 0, -1): # noqa: E741 + with Control(eng, [quint_a[i - 1], quint_b[i - 1]]): + X | quint_a[i] + with Control(eng, [quint_a[i - 1], conditional[0]]): + X | (quint_b[i - 1]) + + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) + + for k in range(1, n_qubits - 1): + CNOT | (quint_a[k], quint_b[k]) + + +def quantum_division(eng, dividend, remainder, divisor): + """ + Perform restoring integer division. + + i.e., + + |dividend>|remainder>|divisor> -> |remainder>|quotient>|divisor> + + (only works if all three qubits are of equal length) + + Args: + eng (MainEngine): ProjectQ MainEngine + dividend (list): Quantum register (or list of qubits) + remainder (list): Quantum register (or list of qubits) + divisor (list): Quantum register (or list of qubits) + + Notes: + Ancilla: n, size 16n^2 - 13, toffoli: 5n^2 -5 , depth: 10n^2-6. + + .. rubric:: References + + Quantum Restoring Integer Division from: + https://arxiv.org/pdf/1609.01241.pdf. + """ + # The circuit consists of three parts + # i) leftshift + # ii) subtraction + # iii) conditional add operation. + + if not len(dividend) == len(remainder) == len(divisor): + raise ValueError('Size mismatch in dividend, divisor and remainder!') + + j = len(remainder) + n_dividend = len(dividend) + + while j != 0: + combined_reg = [] + + combined_reg.append(dividend[n_dividend - 1]) + + for i in range(0, n_dividend - 1): + combined_reg.append(remainder[i]) + + SubtractQuantum | (divisor[0:n_dividend], combined_reg) + CNOT | (combined_reg[n_dividend - 1], remainder[n_dividend - 1]) + with Control(eng, remainder[n_dividend - 1]): + AddQuantum | (divisor[0:n_dividend], combined_reg) + X | remainder[n_dividend - 1] + + remainder.insert(0, dividend[n_dividend - 1]) + dividend.insert(0, remainder[n_dividend]) + del remainder[n_dividend] + del dividend[n_dividend] + + j -= 1 + + +def inverse_quantum_division(eng, remainder, quotient, divisor): + """ + Perform the inverse of a restoring integer division. + + i.e., + + |remainder>|quotient>|divisor> -> |dividend>|remainder(0)>|divisor> + + Args: + eng (MainEngine): ProjectQ MainEngine + dividend (list): Quantum register (or list of qubits) + remainder (list): Quantum register (or list of qubits) + divisor (list): Quantum register (or list of qubits) + """ + if not len(quotient) == len(remainder) == len(divisor): + raise ValueError('Size mismatch in quotient, divisor and remainder!') + + j = 0 + n_quotient = len(quotient) + + while j != n_quotient: + X | quotient[0] + with Control(eng, quotient[0]): + SubtractQuantum | (divisor, remainder) + CNOT | (remainder[-1], quotient[0]) + + AddQuantum | (divisor, remainder) + + remainder.insert(n_quotient, quotient[0]) + quotient.insert(n_quotient, remainder[0]) + del remainder[0] + del quotient[0] + j += 1 + + +def quantum_conditional_add_carry(eng, quint_a, quint_b, ctrl, z): # pylint: disable=invalid-name + """ + Add up two quantum integers if the control qubit is |1>. + + i.e., + + |a>|b>|ctrl>|z(0)z(1)> -> |a>|s(0)...s(n-1)>|ctrl>|s(n)z(1)> + (where s denotes the sum of a and b) + + If the control qubit is |0> no operation is performed: + + |a>|b>|ctrl>|z(0)z(1)> -> |a>|b>|ctrl>|z(0)z(1)> + + (only works if quint_a and quint_b are of the same size, ctrl is a + single qubit and z is a quantum register with 2 qubits. + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + ctrl (list): Control qubit + z (list): Quantum register with 2 qubits + + Notes: + Ancilla: 2, size: 7n - 4, toffoli: 3n + 2, depth: 5n. + + .. rubric:: References + + Quantum conditional add with no input carry from: https://arxiv.org/pdf/1706.05113.pdf + """ + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if len(ctrl) != 1: + raise ValueError('Only a single control qubit is allowed!') + if len(z) != 2: + raise ValueError('Z quantum register must have 2 qubits!') + + n_a = len(quint_a) + + for i in range(1, n_a): + CNOT | (quint_a[i], quint_b[i]) + + with Control(eng, [quint_a[n_a - 1], ctrl[0]]): + X | z[0] + + for j in range(n_a - 2, 0, -1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for k in range(0, n_a - 1): + with Control(eng, [quint_b[k], quint_a[k]]): + X | quint_a[k + 1] + + with Control(eng, [quint_b[n_a - 1], quint_a[n_a - 1]]): + X | z[1] + + with Control(eng, [ctrl[0], z[1]]): + X | z[0] + + with Control(eng, [quint_b[n_a - 1], quint_a[n_a - 1]]): + X | z[1] + + for i in range(n_a - 1, 0, -1): # noqa: E741 + with Control(eng, [ctrl[0], quint_a[i]]): + X | quint_b[i] + with Control(eng, [quint_a[i - 1], quint_b[i - 1]]): + X | quint_a[i] + + with Control(eng, [quint_a[0], ctrl[0]]): + X | quint_b[0] + + for j in range(1, n_a - 1): + CNOT | (quint_a[j], quint_a[j + 1]) + + for n_a in range(1, n_a): + CNOT | (quint_a[n_a], quint_b[n_a]) + + +def quantum_multiplication(eng, quint_a, quint_b, product): + """ + Multiplies two quantum integers. + + i.e, + + |a>|b>|0> -> |a>|b>|a*b> + + (only works if quint_a and quint_b are of the same size, n qubits and product has size 2n+1). + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + product (list): Quantum register (or list of qubits) storing + the result + + Notes: + Ancilla: 2n + 1, size: 7n^2 - 9n + 4, toffoli: 5n^2 - 4n, depth: 3n^2 - 2. + + .. rubric:: References + + Quantum multiplication from: https://arxiv.org/abs/1706.05113. + + """ + n_a = len(quint_a) + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if len(product) != ((2 * n_a) + 1): + raise ValueError('product size must be 2*n + 1') + + for i in range(0, n_a): + with Control(eng, [quint_a[i], quint_b[0]]): + X | product[i] + + with Control(eng, quint_b[1]): + AddQuantum | ( + quint_a[0 : (n_a - 1)], # noqa: E203 + product[1:n_a], + [product[n_a + 1], product[n_a + 2]], + ) + + for j in range(2, n_a): + with Control(eng, quint_b[j]): + AddQuantum | ( + quint_a[0 : (n_a - 1)], # noqa: E203 + product[(0 + j) : (n_a - 1 + j)], # noqa: E203 + [product[n_a + j], product[n_a + j + 1]], + ) + + +def inverse_quantum_multiplication(eng, quint_a, quint_b, product): + """ + Inverse of the multiplication of two quantum integers. + + i.e, + + |a>|b>|a*b> -> |a>|b>|0> + + (only works if quint_a and quint_b are of the same size, n qubits and product has size 2n+1) + + Args: + eng (MainEngine): ProjectQ MainEngine + quint_a (list): Quantum register (or list of qubits) + quint_b (list): Quantum register (or list of qubits) + product (list): Quantum register (or list of qubits) storing the result + + """ + n_a = len(quint_a) + + if len(quint_a) != len(quint_b): + raise ValueError('quint_a and quint_b must have the same size!') + if len(product) != ((2 * n_a) + 1): + raise ValueError('product size must be 2*n + 1') + + for j in range(2, n_a): + with Control(eng, quint_b[j]): + SubtractQuantum | ( + quint_a[0 : (n_a - 1)], # noqa: E203 + product[(0 + j) : (n_a - 1 + j)], # noqa: E203 + [product[n_a + j], product[n_a + j + 1]], + ) + for i in range(0, n_a): + with Control(eng, [quint_a[i], quint_b[0]]): + X | product[i] + + with Control(eng, quint_b[1]): + SubtractQuantum | ( + quint_a[0 : (n_a - 1)], # noqa: E203 + product[1:n_a], + [product[n_a + 1], product[n_a + 2]], + ) diff --git a/projectq/libs/math/_quantummath_test.py b/projectq/libs/math/_quantummath_test.py new file mode 100644 index 000000000..d32715ca6 --- /dev/null +++ b/projectq/libs/math/_quantummath_test.py @@ -0,0 +1,393 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import projectq.libs.math +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import AutoReplacer, DecompositionRuleSet, InstructionFilter +from projectq.libs.math import ( + AddQuantum, + ComparatorQuantum, + DivideQuantum, + MultiplyQuantum, + SubtractQuantum, +) +from projectq.meta import Compute, Control, Dagger, Uncompute +from projectq.ops import All, BasicMathGate, ClassicalInstructionGate, Measure, X +from projectq.setups.decompositions import swap2cnot + + +def print_all_probabilities(eng, qureg): + i = 0 + y = len(qureg) + while i < (2**y): + qubit_list = [int(x) for x in list((f'{i:0b}').zfill(y))] + qubit_list = qubit_list[::-1] + prob = eng.backend.get_probability(qubit_list, qureg) + if prob != 0.0: + print(prob, qubit_list, i) + i += 1 + + +def init(engine, quint, value): + for i in range(len(quint)): + if ((value >> i) & 1) == 1: + X | quint[i] + + +def no_math_emulation(eng, cmd): + if isinstance(cmd.gate, BasicMathGate): + return False + if isinstance(cmd.gate, ClassicalInstructionGate): + return True + try: + return len(cmd.gate.matrix) == 2 + except AttributeError: + return False + + +rule_set = DecompositionRuleSet(modules=[projectq.libs.math, swap2cnot]) + + +@pytest.fixture +def eng(): + return MainEngine( + backend=Simulator(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(no_math_emulation)], + ) + + +@pytest.mark.parametrize('carry', (False, True)) +@pytest.mark.parametrize('inverse', (False, True)) +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantumadder_size_mismatch(eng, qubit_idx, inverse, carry): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + qureg_c = eng.allocate_qureg(1 if qubit_idx != 2 else 2) + + if carry and inverse: + pytest.skip('Inverse addition with carry not supported') + elif not carry and qubit_idx == 2: + pytest.skip('Invalid test parameter combination') + + with pytest.raises(ValueError): + if inverse: + with Dagger(eng): + AddQuantum | (qureg_a, qureg_b, qureg_c if carry else []) + else: + AddQuantum | (qureg_a, qureg_b, qureg_c if carry else []) + + +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantum_conditional_adder_size_mismatch(eng, qubit_idx): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + control = eng.allocate_qureg(1 if qubit_idx != 2 else 2) + + with pytest.raises(ValueError): + with Control(eng, control): + AddQuantum | (qureg_a, qureg_b) + + +@pytest.mark.parametrize('inverse', (False, True)) +@pytest.mark.parametrize('qubit_idx', (0, 1, 2, 3)) +def test_quantum_conditional_add_carry_size_mismatch(eng, qubit_idx, inverse): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + qureg_c = eng.allocate_qureg(2 if qubit_idx != 2 else 3) + control = eng.allocate_qureg(1 if qubit_idx != 3 else 2) + + with pytest.raises(ValueError): + with Control(eng, control): + if inverse: + with Dagger(eng): + AddQuantum | (qureg_a, qureg_b, qureg_c) + else: + AddQuantum | (qureg_a, qureg_b, qureg_c) + + +def test_quantum_adder(eng): + qureg_a = eng.allocate_qureg(4) + qureg_b = eng.allocate_qureg(4) + control_qubit = eng.allocate_qubit() + + init(eng, qureg_a, 2) + init(eng, qureg_b, 1) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 0], qureg_b)) + + with Control(eng, control_qubit): + AddQuantum | (qureg_a, qureg_b) + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 0], qureg_b)) + + X | control_qubit + + with Control(eng, control_qubit): + AddQuantum | (qureg_a, qureg_b) + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 0], qureg_b)) + + init(eng, qureg_a, 2) # reset + init(eng, qureg_b, 3) # reset + + c = eng.allocate_qubit() + init(eng, qureg_a, 15) + init(eng, qureg_b, 15) + + AddQuantum | (qureg_a, qureg_b, c) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 1, 1], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1], c)) + + with Compute(eng): + with Control(eng, control_qubit): + AddQuantum | (qureg_a, qureg_b) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 1, 1], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1], c)) + + AddQuantum | (qureg_a, qureg_b) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 1], qureg_b)) + + with Compute(eng): + AddQuantum | (qureg_a, qureg_b) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 1], qureg_b)) + + d = eng.allocate_qureg(2) + + with Compute(eng): + with Control(eng, control_qubit): + AddQuantum | (qureg_a, qureg_b, d) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 1], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0], d)) + + All(Measure) | qureg_b + Measure | c + + +def test_quantumsubtraction(eng): + qureg_a = eng.allocate_qureg(4) + qureg_b = eng.allocate_qureg(4) + control_qubit = eng.allocate_qubit() + + init(eng, qureg_a, 5) + init(eng, qureg_b, 7) + + X | control_qubit + with Control(eng, control_qubit): + SubtractQuantum | (qureg_a, qureg_b) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0, 0, 0], qureg_b)) + + # Size mismatch + with pytest.raises(ValueError): + SubtractQuantum | (qureg_a, qureg_b[:-1]) + eng.flush() + + init(eng, qureg_a, 5) # reset + init(eng, qureg_b, 2) # reset + + init(eng, qureg_a, 5) + init(eng, qureg_b, 3) + + SubtractQuantum | (qureg_a, qureg_b) + + print_all_probabilities(eng, qureg_b) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 1, 1, 0], qureg_b)) + + init(eng, qureg_a, 5) # reset + init(eng, qureg_b, 14) # reset + init(eng, qureg_a, 5) + init(eng, qureg_b, 3) + + with Compute(eng): + SubtractQuantum | (qureg_a, qureg_b) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 0, 0], qureg_b)) + All(Measure) | qureg_a + All(Measure) | qureg_b + + +def test_comparator(eng): + qureg_a = eng.allocate_qureg(3) + qureg_b = eng.allocate_qureg(3) + compare_qubit = eng.allocate_qubit() + + init(eng, qureg_a, 5) + init(eng, qureg_b, 3) + + ComparatorQuantum | (qureg_a, qureg_b, compare_qubit) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1], compare_qubit)) + + # Size mismatch in qubit registers + with pytest.raises(ValueError): + ComparatorQuantum | (qureg_a, qureg_b[:-1], compare_qubit) + + # Only single qubit for compare qubit + with pytest.raises(ValueError): + ComparatorQuantum | (qureg_a, qureg_b, [*compare_qubit, *compare_qubit]) + + All(Measure) | qureg_a + All(Measure) | qureg_b + Measure | compare_qubit + + +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantumdivision_size_mismatch(eng, qubit_idx): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + qureg_c = eng.allocate_qureg(4 if qubit_idx != 2 else 3) + + with pytest.raises(ValueError): + DivideQuantum | (qureg_a, qureg_b, qureg_c) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantumdivision_size_mismatch_inverse(eng, qubit_idx): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + qureg_c = eng.allocate_qureg(4 if qubit_idx != 2 else 3) + + with pytest.raises(ValueError): + with Dagger(eng): + DivideQuantum | (qureg_a, qureg_b, qureg_c) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + +def test_quantumdivision(eng): + qureg_a = eng.allocate_qureg(4) + qureg_b = eng.allocate_qureg(4) + qureg_c = eng.allocate_qureg(4) + + init(eng, qureg_a, 10) + init(eng, qureg_c, 3) + + DivideQuantum | (qureg_a, qureg_b, qureg_c) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 0, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 0], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 0], qureg_c)) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + init(eng, qureg_a, 1) # reset + init(eng, qureg_b, 3) # reset + + init(eng, qureg_a, 11) + + with Compute(eng): + DivideQuantum | (qureg_a, qureg_b, qureg_c) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0, 0], qureg_c)) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantummultiplication_size_mismatch(eng, qubit_idx): + qureg_a = eng.allocate_qureg(3 if qubit_idx != 0 else 2) + qureg_b = eng.allocate_qureg(3 if qubit_idx != 1 else 2) + qureg_c = eng.allocate_qureg(7 if qubit_idx != 2 else 6) + + with pytest.raises(ValueError): + MultiplyQuantum | (qureg_a, qureg_b, qureg_c) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + +@pytest.mark.parametrize('qubit_idx', (0, 1, 2)) +def test_quantummultiplication_size_mismatch_inverse(eng, qubit_idx): + qureg_a = eng.allocate_qureg(4 if qubit_idx != 0 else 3) + qureg_b = eng.allocate_qureg(4 if qubit_idx != 1 else 3) + qureg_c = eng.allocate_qureg(4 if qubit_idx != 2 else 3) + + with pytest.raises(ValueError): + with Dagger(eng): + MultiplyQuantum | (qureg_a, qureg_b, qureg_c) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + +def test_quantummultiplication(eng): + qureg_a = eng.allocate_qureg(3) + qureg_b = eng.allocate_qureg(3) + qureg_c = eng.allocate_qureg(7) + + init(eng, qureg_a, 7) + init(eng, qureg_b, 3) + + MultiplyQuantum | (qureg_a, qureg_b, qureg_c) + + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 1], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 0, 1, 0, 1, 0, 0], qureg_c)) + + All(Measure) | qureg_a + All(Measure) | qureg_b + All(Measure) | qureg_c + + init(eng, qureg_a, 7) + init(eng, qureg_b, 3) + init(eng, qureg_c, 21) + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 0, 0, 0], qureg_c)) + init(eng, qureg_a, 2) + init(eng, qureg_b, 3) + + with Compute(eng): + MultiplyQuantum | (qureg_a, qureg_b, qureg_c) + Uncompute(eng) + + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 1, 0], qureg_a)) + assert 1.0 == pytest.approx(eng.backend.get_probability([1, 1, 0], qureg_b)) + assert 1.0 == pytest.approx(eng.backend.get_probability([0, 0, 0, 0, 0, 0, 0], qureg_c)) diff --git a/projectq/libs/revkit/__init__.py b/projectq/libs/revkit/__init__.py index af8c55ae6..f8d80db0e 100644 --- a/projectq/libs/revkit/__init__.py +++ b/projectq/libs/revkit/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._permutation import PermutationOracle +"""Module containing code to interface with RevKit.""" + from ._control_function import ControlFunctionOracle +from ._permutation import PermutationOracle from ._phase import PhaseOracle diff --git a/projectq/libs/revkit/_control_function.py b/projectq/libs/revkit/_control_function.py index 99f7b880d..40a07fe9f 100644 --- a/projectq/libs/revkit/_control_function.py +++ b/projectq/libs/revkit/_control_function.py @@ -12,14 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""RevKit support for control function oracles.""" + + from projectq.ops import BasicGate from ._utils import _exec -class ControlFunctionOracle: +class ControlFunctionOracle: # pylint: disable=too-few-public-methods """ - Synthesizes a negation controlled by an arbitrary control function. + Synthesize a negation controlled by an arbitrary control function. This creates a circuit for a NOT gate which is controlled by an arbitrary Boolean control function. The control function is provided as integer @@ -28,19 +31,18 @@ class ControlFunctionOracle: the value for function can be, e.g., ``0b11101000``, ``0xe8``, or ``232``. Example: - This example creates a circuit that causes to invert qubit ``d``, the majority-of-three function evaluates to true for the control qubits ``a``, ``b``, and ``c``. .. code-block:: python - ControlFunctionOracle(0x8e) | ([a, b, c], d) + ControlFunctionOracle(0x8E) | ([a, b, c], d) """ def __init__(self, function, **kwargs): """ - Initializes a control function oracle. + Initialize a control function oracle. Args: function (int): Function truth table. @@ -56,21 +58,22 @@ def __init__(self, function, **kwargs): self.function = function else: try: - import dormouse + import dormouse # pylint: disable=import-outside-toplevel + self.function = dormouse.to_truth_table(function) - except ImportError: # pragma: no cover + except ImportError as err: # pragma: no cover raise RuntimeError( "The dormouse library needs to be installed in order to " "automatically compile Python code into functions. Try " "to install dormouse with 'pip install dormouse'." - ) + ) from err self.kwargs = kwargs self._check_function() def __or__(self, qubits): """ - Applies control function to qubits (and synthesizes circuit). + Apply control function to qubits (and synthesizes circuit). Args: qubits (tuple): Qubits to which the control function is @@ -79,11 +82,13 @@ def __or__(self, qubits): target qubit. """ try: - import revkit - except ImportError: # pragma: no cover + import revkit # pylint: disable=import-outside-toplevel + except ImportError as err: # pragma: no cover raise RuntimeError( "The RevKit Python library needs to be installed and in the " - "PYTHONPATH in order to call this function") + "PYTHONPATH in order to call this function" + ) from err + # pylint: disable=invalid-name # convert qubits to tuple qs = [] @@ -92,30 +97,26 @@ def __or__(self, qubits): # function truth table cannot be larger than number of control qubits # allow - if 2**(2**(len(qs) - 1)) <= self.function: - raise AttributeError( - "Function truth table exceeds number of control qubits") + if 2 ** (2 ** (len(qs) - 1)) <= self.function: + raise AttributeError("Function truth table exceeds number of control qubits") # create truth table from function integer - hex_length = max(2**(len(qs) - 1) // 4, 1) - revkit.tt(table="{0:#0{1}x}".format(self.function, hex_length)) + hex_length = max(2 ** (len(qs) - 1) // 4, 1) + revkit.tt(table=f"{self.function:#0{hex_length}x}") # create reversible circuit from truth table self.kwargs.get("synth", revkit.esopbs)() # check whether circuit has correct signature if revkit.ps(mct=True, silent=True)['qubits'] != len(qs): - raise RuntimeError("Generated circuit lines does not match " - "provided qubits") + raise RuntimeError("Generated circuit lines does not match provided qubits") # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_function(self): - """ - Checks whether function is valid. - """ + """Check whether function is valid.""" # function must be positive. We check in __or__ whether function is # too large if self.function < 0: - raise AttributeError("Function must be a postive integer") + raise AttributeError("Function must be a positive integer") diff --git a/projectq/libs/revkit/_control_function_test.py b/projectq/libs/revkit/_control_function_test.py index 36164a0b7..ff4d24fe5 100644 --- a/projectq/libs/revkit/_control_function_test.py +++ b/projectq/libs/revkit/_control_function_test.py @@ -11,45 +11,40 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for libs.revkit._control_function.""" import pytest -from projectq.types import Qubit from projectq import MainEngine from projectq.cengines import DummyEngine - from projectq.libs.revkit import ControlFunctionOracle - # run this test only if RevKit Python module can be loaded revkit = pytest.importorskip('revkit') def test_control_function_majority(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() qubit2 = main_engine.allocate_qubit() qubit3 = main_engine.allocate_qubit() - ControlFunctionOracle(0xe8) | (qubit0, qubit1, qubit2, qubit3) + ControlFunctionOracle(0xE8) | (qubit0, qubit1, qubit2, qubit3) assert len(saving_backend.received_commands) == 7 + def test_control_function_majority_from_python(): - dormouse = pytest.importorskip('dormouse') + dormouse = pytest.importorskip('dormouse') # noqa: F841 def maj(a, b, c): return (a and b) or (a and c) or (b and c) # pragma: no cover saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() @@ -60,8 +55,7 @@ def maj(a, b, c): def test_control_function_invalid_function(): - main_engine = MainEngine(backend=DummyEngine(), - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qureg = main_engine.allocate_qureg(3) @@ -69,7 +63,7 @@ def test_control_function_invalid_function(): ControlFunctionOracle(-42) | qureg with pytest.raises(AttributeError): - ControlFunctionOracle(0x8e) | qureg + ControlFunctionOracle(0x8E) | qureg with pytest.raises(RuntimeError): ControlFunctionOracle(0x8, synth=revkit.esopps) | qureg diff --git a/projectq/libs/revkit/_permutation.py b/projectq/libs/revkit/_permutation.py index b4967528a..87f6e09f1 100644 --- a/projectq/libs/revkit/_permutation.py +++ b/projectq/libs/revkit/_permutation.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""RevKit support for permutation oracles.""" + from projectq.ops import BasicGate from ._utils import _exec -class PermutationOracle: +class PermutationOracle: # pylint: disable=too-few-public-methods """ - Synthesizes a permutation using RevKit. + Synthesize a permutation using RevKit. Given a permutation over `2**q` elements (starting from 0), this class helps to automatically find a reversible circuit over `q` qubits that @@ -33,16 +35,15 @@ class PermutationOracle: def __init__(self, permutation, **kwargs): """ - Initializes a permutation oracle. + Initialize a permutation oracle. Args: permutation (list): Permutation (starting from 0). Keyword Args: - synth: A RevKit synthesis command which creates a reversible - circuit based on a reversible truth table (e.g., - ``revkit.tbs`` or ``revkit.dbs``). Can also be a - nullary lambda that calls several RevKit commands. + synth: A RevKit synthesis command which creates a reversible circuit based on a reversible truth table + (e.g., ``revkit.tbs`` or ``revkit.dbs``). Can also be a nullary lambda that calls several RevKit + commands. **Default:** ``revkit.tbs`` """ self.permutation = permutation @@ -52,27 +53,27 @@ def __init__(self, permutation, **kwargs): def __or__(self, qubits): """ - Applies permutation to qubits (and synthesizes circuit). + Apply permutation to qubits (and synthesizes circuit). Args: - qubits (tuple): Qubits to which the permutation is being - applied. + qubits (tuple): Qubits to which the permutation is being applied. """ try: - import revkit - except ImportError: # pragma: no cover + import revkit # pylint: disable=import-outside-toplevel + except ImportError as err: # pragma: no cover raise RuntimeError( "The RevKit Python library needs to be installed and in the " - "PYTHONPATH in order to call this function") + "PYTHONPATH in order to call this function" + ) from err + # pylint: disable=invalid-name # convert qubits to flattened list qs = BasicGate.make_tuple_of_qureg(qubits) qs = sum(qs, []) # permutation must have 2*q elements, where q is the number of qubits - if 2**(len(qs)) != len(self.permutation): - raise AttributeError( - "Number of qubits does not fit to the size of the permutation") + if 2 ** (len(qs)) != len(self.permutation): + raise AttributeError("Number of qubits does not fit to the size of the permutation") # create reversible truth table from permutation revkit.perm(permutation=" ".join(map(str, self.permutation))) @@ -81,14 +82,11 @@ def __or__(self, qubits): self.kwargs.get("synth", revkit.tbs)() # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_permutation(self): - """ - Checks whether permutation is valid. - """ + """Check whether permutation is valid.""" # permutation must start from 0, has no duplicates and all elements are # consecutive - if (sorted(list(set(self.permutation))) != - list(range(len(self.permutation)))): + if sorted(set(self.permutation)) != list(range(len(self.permutation))): raise AttributeError("Invalid permutation (does it start from 0?)") diff --git a/projectq/libs/revkit/_permutation_test.py b/projectq/libs/revkit/_permutation_test.py index eb0cf9bd5..19d7be0ac 100644 --- a/projectq/libs/revkit/_permutation_test.py +++ b/projectq/libs/revkit/_permutation_test.py @@ -11,26 +11,21 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for libs.revkit._permutation.""" import pytest -from projectq.types import Qubit from projectq import MainEngine from projectq.cengines import DummyEngine - from projectq.libs.revkit import PermutationOracle - # run this test only if RevKit Python module can be loaded revkit = pytest.importorskip('revkit') def test_basic_permutation(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() @@ -41,8 +36,7 @@ def test_basic_permutation(): def test_invalid_permutation(): - main_engine = MainEngine(backend=DummyEngine(), - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() @@ -65,14 +59,15 @@ def test_invalid_permutation(): def test_synthesis_with_adjusted_tbs(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() - import revkit - synth = lambda: revkit.tbs() + def synth(): + import revkit + + return revkit.tbs() PermutationOracle([0, 2, 1, 3], synth=synth) | (qubit0, qubit1) @@ -81,14 +76,14 @@ def test_synthesis_with_adjusted_tbs(): def test_synthesis_with_synthesis_script(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = main_engine.allocate_qubit() qubit1 = main_engine.allocate_qubit() def synth(): import revkit + revkit.tbs() PermutationOracle([0, 2, 1, 3], synth=synth) | (qubit0, qubit1) diff --git a/projectq/libs/revkit/_phase.py b/projectq/libs/revkit/_phase.py index 4aadc319e..8d0eb2931 100644 --- a/projectq/libs/revkit/_phase.py +++ b/projectq/libs/revkit/_phase.py @@ -12,81 +12,80 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""RevKit support for phase oracles.""" + from projectq.ops import BasicGate from ._utils import _exec -class PhaseOracle: +class PhaseOracle: # pylint: disable=too-few-public-methods """ - Synthesizes phase circuit from an arbitrary Boolean function. + Synthesize phase circuit from an arbitrary Boolean function. - This creates a phase circuit from a Boolean function. It inverts the phase - of all amplitudes for which the function evaluates to 1. The Boolean - function is provided as integer representation of the function's truth - table in binary notation. For example, for the majority-of-three function, - which truth table 11101000, the value for function can be, e.g., - ``0b11101000``, ``0xe8``, or ``232``. + This creates a phase circuit from a Boolean function. It inverts the phase of all amplitudes for which the + function evaluates to 1. The Boolean function is provided as integer representation of the function's truth table + in binary notation. For example, for the majority-of-three function, which truth table 11101000, the value for + function can be, e.g., ``0b11101000``, ``0xe8``, or ``232``. - Note that a phase circuit can only accurately be found for a normal - function, i.e., a function that maps the input pattern 0, 0, ..., 0 to 0. - The circuits for a function and its inverse are the same. + Note that a phase circuit can only accurately be found for a normal function, i.e., a function that maps the input + pattern 0, 0, ..., 0 to 0. The circuits for a function and its inverse are the same. Example: - - This example creates a phase circuit based on the majority-of-three - function on qubits ``a``, ``b``, and ``c``. + This example creates a phase circuit based on the majority-of-three function on qubits ``a``, ``b``, and + ``c``. .. code-block:: python - PhaseOracle(0x8e) | (a, b, c) + PhaseOracle(0x8E) | (a, b, c) """ def __init__(self, function, **kwargs): """ - Initializes a phase oracle. + Initialize a phase oracle. Args: function (int): Function truth table. Keyword Args: - synth: A RevKit synthesis command which creates a reversible - circuit based on a truth table and requires no additional - ancillae (e.g., ``revkit.esopps``). Can also be a nullary - lambda that calls several RevKit commands. + synth: A RevKit synthesis command which creates a reversible circuit based on a truth table and requires + no additional ancillae (e.g., ``revkit.esopps``). Can also be a nullary lambda that calls several + RevKit commands. **Default:** ``revkit.esopps`` """ if isinstance(function, int): self.function = function else: try: - import dormouse + import dormouse # pylint: disable=import-outside-toplevel + self.function = dormouse.to_truth_table(function) - except ImportError: # pragma: no cover + except ImportError as err: # pragma: no cover raise RuntimeError( "The dormouse library needs to be installed in order to " "automatically compile Python code into functions. Try " "to install dormouse with 'pip install dormouse'." - ) + ) from err self.kwargs = kwargs self._check_function() def __or__(self, qubits): """ - Applies phase circuit to qubits (and synthesizes circuit). + Apply phase circuit to qubits (and synthesizes circuit). Args: - qubits (tuple): Qubits to which the phase circuit is being - applied. + qubits (tuple): Qubits to which the phase circuit is being applied. """ try: - import revkit - except ImportError: # pragma: no cover + import revkit # pylint: disable=import-outside-toplevel + except ImportError as err: # pragma: no cover raise RuntimeError( "The RevKit Python library needs to be installed and in the " - "PYTHONPATH in order to call this function") + "PYTHONPATH in order to call this function" + ) from err + # pylint: disable=invalid-name # convert qubits to tuple qs = [] for item in BasicGate.make_tuple_of_qureg(qubits): @@ -94,30 +93,26 @@ def __or__(self, qubits): # function truth table cannot be larger than number of control qubits # allow - if 2**(2**len(qs)) <= self.function: - raise AttributeError( - "Function truth table exceeds number of control qubits") + if 2 ** (2 ** len(qs)) <= self.function: + raise AttributeError("Function truth table exceeds number of control qubits") # create truth table from function integer - hex_length = max(2**(len(qs) - 1) // 4, 1) - revkit.tt(table="{0:#0{1}x}".format(self.function, hex_length)) + hex_length = max(2 ** (len(qs) - 1) // 4, 1) + revkit.tt(table=f"{self.function:#0{hex_length}x}") # create phase circuit from truth table - self.kwargs.get("synth", lambda: revkit.esopps())() + self.kwargs.get("synth", revkit.esopps)() # check whether circuit has correct signature if revkit.ps(mct=True, silent=True)['qubits'] != len(qs): - raise RuntimeError("Generated circuit lines does not match " - "provided qubits") + raise RuntimeError("Generated circuit lines does not match provided qubits") # convert reversible circuit to ProjectQ code and execute it - _exec(revkit.write_projectq(log=True)["contents"], qs) + _exec(revkit.to_projectq(mct=True), qs) def _check_function(self): - """ - Checks whether function is valid. - """ + """Check whether function is valid.""" # function must be positive. We check in __or__ whether function is # too large if self.function < 0: - raise AttributeError("Function must be a postive integer") + raise AttributeError("Function must be a positive integer") diff --git a/projectq/libs/revkit/_phase_test.py b/projectq/libs/revkit/_phase_test.py index af634890e..37152e027 100644 --- a/projectq/libs/revkit/_phase_test.py +++ b/projectq/libs/revkit/_phase_test.py @@ -11,20 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for libs.revkit._phase.""" +import numpy as np import pytest -from projectq.types import Qubit from projectq import MainEngine from projectq.backends import Simulator from projectq.cengines import DummyEngine -from projectq.ops import All, H, Measure - from projectq.libs.revkit import PhaseOracle - -import numpy as np +from projectq.ops import All, H, Measure # run this test only if RevKit Python module can be loaded revkit = pytest.importorskip('revkit') @@ -36,17 +32,16 @@ def test_phase_majority(): qureg = main_engine.allocate_qureg(3) All(H) | qureg - PhaseOracle(0xe8) | qureg + PhaseOracle(0xE8) | qureg main_engine.flush() - assert np.array_equal(np.sign(sim.cheat()[1]), - [1., 1., 1., -1., 1., -1., -1., -1.]) + assert np.array_equal(np.sign(sim.cheat()[1]), [1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0]) All(Measure) | qureg def test_phase_majority_from_python(): - dormouse = pytest.importorskip('dormouse') + dormouse = pytest.importorskip('dormouse') # noqa: F841 def maj(a, b, c): return (a and b) or (a and c) or (b and c) # pragma: no cover @@ -60,14 +55,12 @@ def maj(a, b, c): main_engine.flush() - assert np.array_equal(np.sign(sim.cheat()[1]), - [1., 1., 1., -1., 1., -1., -1., -1.]) + assert np.array_equal(np.sign(sim.cheat()[1]), [1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0]) All(Measure) | qureg def test_phase_invalid_function(): - main_engine = MainEngine(backend=DummyEngine(), - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) qureg = main_engine.allocate_qureg(3) @@ -75,7 +68,7 @@ def test_phase_invalid_function(): PhaseOracle(-42) | qureg with pytest.raises(AttributeError): - PhaseOracle(0xcafe) | qureg + PhaseOracle(0xCAFE) | qureg with pytest.raises(RuntimeError): - PhaseOracle(0x8e, synth=lambda: revkit.esopbs()) | qureg + PhaseOracle(0x8E, synth=lambda: revkit.esopbs()) | qureg diff --git a/projectq/libs/revkit/_utils.py b/projectq/libs/revkit/_utils.py index d889e9409..1dfb2287b 100644 --- a/projectq/libs/revkit/_utils.py +++ b/projectq/libs/revkit/_utils.py @@ -13,14 +13,20 @@ # limitations under the License. +"""Module containing some utility functions.""" + +# flake8: noqa +# pylint: skip-file + + def _exec(code, qs): """ - Executes the Python code in 'filename'. + Execute the Python code in 'filename'. Args: code (string): ProjectQ code. - qs (tuple): Qubits to which the permutation is being - applied. + qubits (tuple): Qubits to which the permutation is being applied. """ - from projectq.ops import C, X, Z, All + from projectq.ops import All, C, X, Z + exec(code) diff --git a/projectq/meta/__init__.py b/projectq/meta/__init__.py index a136d4124..382148d7f 100755 --- a/projectq/meta/__init__.py +++ b/projectq/meta/__init__.py @@ -11,10 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -The projectq.meta package features meta instructions which help both the user -and the compiler in writing/producing efficient code. It includes, e.g., +Provides meta instructions which help both the user and the compiler in writing/producing efficient code. + +It includes, e.g., * Loop (with Loop(eng): ...) * Compute/Uncompute (with Compute(eng): ..., [...], Uncompute(eng)) @@ -22,17 +22,15 @@ * Dagger (with Dagger(eng): ...) """ - -from ._dirtyqubit import DirtyQubitTag -from ._loop import (LoopTag, - Loop) -from ._compute import (Compute, - Uncompute, - CustomUncompute, - ComputeTag, - UncomputeTag) -from ._control import (Control, - get_control_count) +from ._compute import Compute, ComputeTag, CustomUncompute, Uncompute, UncomputeTag +from ._control import ( + Control, + canonical_ctrl_state, + get_control_count, + has_negative_control, +) from ._dagger import Dagger -from ._util import insert_engine, drop_engine_after +from ._dirtyqubit import DirtyQubitTag from ._logicalqubit import LogicalQubitIDTag +from ._loop import Loop, LoopTag +from ._util import drop_engine_after, insert_engine diff --git a/projectq/meta/_compute.py b/projectq/meta/_compute.py index 5c624524a..84cde8a7a 100755 --- a/projectq/meta/_compute.py +++ b/projectq/meta/_compute.py @@ -11,111 +11,82 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Compute, Uncompute, CustomUncompute. +Definition of Compute, Uncompute and CustomUncompute. -Contains Compute, Uncompute, and CustomUncompute classes which can be used to -annotate Compute / Action / Uncompute sections, facilitating the conditioning -of the entire operation on the value of a qubit / register (only Action needs +Contains Compute, Uncompute, and CustomUncompute classes which can be used to annotate Compute / Action / Uncompute +sections, facilitating the conditioning of the entire operation on the value of a qubit / register (only Action needs controls). This file also defines the corresponding meta tags. """ from copy import deepcopy -import projectq -from projectq.cengines import BasicEngine +from projectq.cengines import BasicEngine, CommandModifier from projectq.ops import Allocate, Deallocate -from ._util import insert_engine, drop_engine_after - -class QubitManagementError(Exception): - pass +from ._exceptions import QubitManagementError +from ._util import drop_engine_after, insert_engine class NoComputeSectionError(Exception): - """ - Exception raised if uncompute is called but no compute section found. - """ - pass + """Exception raised if uncompute is called but no compute section found.""" -class ComputeTag(object): - """ - Compute meta tag. - """ +class ComputeTag: # pylint: disable=too-few-public-methods + """Compute meta tag.""" def __eq__(self, other): + """Equal operator.""" return isinstance(other, ComputeTag) - def __ne__(self, other): - return not self.__eq__(other) - -class UncomputeTag(object): - """ - Uncompute meta tag. - """ +class UncomputeTag: # pylint: disable=too-few-public-methods + """Uncompute meta tag.""" def __eq__(self, other): + """Equal operator.""" return isinstance(other, UncomputeTag) - def __ne__(self, other): - return not self.__eq__(other) - -class ComputeEngine(BasicEngine): +def _add_uncompute_tag(cmd): """ - Adds Compute-tags to all commands and stores them (to later uncompute them - automatically) + Modify the command tags, inserting an UncomputeTag. + + Args: + cmd (Command): Command to modify. """ + cmd.tags.append(UncomputeTag()) + return cmd + + +class ComputeEngine(BasicEngine): + """Add Compute-tags to all commands and stores them (to later uncompute them automatically).""" def __init__(self): - """ - Initialize a ComputeEngine. - """ - BasicEngine.__init__(self) + """Initialize a ComputeEngine.""" + super().__init__() self._l = [] self._compute = True # Save all qubit ids from qubits which are created or destroyed. self._allocated_qubit_ids = set() self._deallocated_qubit_ids = set() - def _add_uncompute_tag(self, cmd): - """ - Modify the command tags, inserting an UncomputeTag. - - Args: - cmd (Command): Command to modify. - """ - cmd.tags.append(UncomputeTag()) - return cmd - - def run_uncompute(self): + def run_uncompute(self): # pylint: disable=too-many-branches,too-many-statements """ Send uncomputing gates. - Sends the inverse of the stored commands in reverse order down to the - next engine. And also deals with allocated qubits in Compute section. - If a qubit has been allocated during compute, it will be deallocated - during uncompute. If a qubit has been allocated and deallocated during - compute, then a new qubit is allocated and deallocated during - uncompute. + Sends the inverse of the stored commands in reverse order down to the next engine. And also deals with + allocated qubits in Compute section. If a qubit has been allocated during compute, it will be deallocated + during uncompute. If a qubit has been allocated and deallocated during compute, then a new qubit is allocated + and deallocated during uncompute. """ - # No qubits allocated during Compute section -> do standard uncompute if len(self._allocated_qubit_ids) == 0: - self.send([self._add_uncompute_tag(cmd.get_inverse()) - for cmd in reversed(self._l)]) + self.send([_add_uncompute_tag(cmd.get_inverse()) for cmd in reversed(self._l)]) return # qubits ids which were allocated and deallocated in Compute section - ids_local_to_compute = self._allocated_qubit_ids.intersection( - self._deallocated_qubit_ids) - # qubit ids which were allocated but not yet deallocated in - # Compute section - ids_still_alive = self._allocated_qubit_ids.difference( - self._deallocated_qubit_ids) + ids_local_to_compute = self._allocated_qubit_ids.intersection(self._deallocated_qubit_ids) # No qubits allocated and already deallocated during compute. # Don't inspect each command as below -> faster uncompute @@ -131,36 +102,39 @@ def run_uncompute(self): for active_qubit in self.main_engine.active_qubits: if active_qubit.id == qubit_id: active_qubit.id = -1 - active_qubit.__del__() + del active_qubit qubit_found = True break if not qubit_found: - raise QubitManagementError( - "\nQubit was not found in " + - "MainEngine.active_qubits.\n") - self.send([self._add_uncompute_tag(cmd.get_inverse())]) + raise QubitManagementError("\nQubit was not found in " + "MainEngine.active_qubits.\n") + self.send([_add_uncompute_tag(cmd.get_inverse())]) else: - self.send([self._add_uncompute_tag(cmd.get_inverse())]) + self.send([_add_uncompute_tag(cmd.get_inverse())]) return # There was at least one qubit allocated and deallocated within # compute section. Handle uncompute in most general case - new_local_id = dict() + new_local_id = {} for cmd in reversed(self._l): if cmd.gate == Deallocate: - assert (cmd.qubits[0][0].id) in ids_local_to_compute + if not cmd.qubits[0][0].id in ids_local_to_compute: # pragma: no cover + raise RuntimeError( + 'Internal compiler error: qubit being deallocated is not found in the list of qubits local to ' + 'the Compute section' + ) + # Create new local qubit which lives within uncompute section # Allocate needs to have old tags + uncompute tag def add_uncompute(command, old_tags=deepcopy(cmd.tags)): command.tags = old_tags + [UncomputeTag()] return command - tagger_eng = projectq.cengines.CommandModifier(add_uncompute) + + tagger_eng = CommandModifier(add_uncompute) insert_engine(self, tagger_eng) new_local_qb = self.allocate_qubit() drop_engine_after(self) - new_local_id[cmd.qubits[0][0].id] = deepcopy( - new_local_qb[0].id) + new_local_id[cmd.qubits[0][0].id] = deepcopy(new_local_qb[0].id) # Set id of new_local_qb to -1 such that it doesn't send a # deallocate gate new_local_qb[0].id = -1 @@ -172,7 +146,7 @@ def add_uncompute(command, old_tags=deepcopy(cmd.tags)): old_id = deepcopy(cmd.qubits[0][0].id) cmd.qubits[0][0].id = new_local_id[cmd.qubits[0][0].id] del new_local_id[old_id] - self.send([self._add_uncompute_tag(cmd.get_inverse())]) + self.send([_add_uncompute_tag(cmd.get_inverse())]) else: # Deallocate qubit which was allocated in compute section: @@ -184,14 +158,12 @@ def add_uncompute(command, old_tags=deepcopy(cmd.tags)): for active_qubit in self.main_engine.active_qubits: if active_qubit.id == qubit_id: active_qubit.id = -1 - active_qubit.__del__() + del active_qubit qubit_found = True break if not qubit_found: - raise QubitManagementError( - "\nQubit was not found in " + - "MainEngine.active_qubits.\n") - self.send([self._add_uncompute_tag(cmd.get_inverse())]) + raise QubitManagementError("\nQubit was not found in " + "MainEngine.active_qubits.\n") + self.send([_add_uncompute_tag(cmd.get_inverse())]) else: # Process commands by replacing each local qubit from @@ -203,32 +175,32 @@ def add_uncompute(command, old_tags=deepcopy(cmd.tags)): if qubit.id in new_local_id: qubit.id = new_local_id[qubit.id] - self.send([self._add_uncompute_tag(cmd.get_inverse())]) + self.send([_add_uncompute_tag(cmd.get_inverse())]) def end_compute(self): """ End the compute step (exit the with Compute() - statement). - Will tell the Compute-engine to stop caching. It then waits for the - uncompute instruction, which is when it sends all cached commands - inverted and in reverse order down to the next compiler engine. + Will tell the Compute-engine to stop caching. It then waits for the uncompute instruction, which is when it + sends all cached commands inverted and in reverse order down to the next compiler engine. Raises: - QubitManagementError: If qubit has been deallocated in Compute - section which has not been allocated in Compute section + QubitManagementError: If qubit has been deallocated in Compute section which has not been allocated in + Compute section """ self._compute = False - if not self._allocated_qubit_ids.issuperset( - self._deallocated_qubit_ids): + if not self._allocated_qubit_ids.issuperset(self._deallocated_qubit_ids): raise QubitManagementError( "\nQubit has been deallocated in with Compute(eng) context \n" - "which has not been allocated within this Compute section") + "which has not been allocated within this Compute section" + ) def receive(self, command_list): """ - If in compute-mode: Receive commands and store deepcopy of each cmd. - Add ComputeTag to received cmd and send it on. - Otherwise: send all received commands directly to next_engine. + Receive a list of commands. + + If in compute-mode, receive commands and store deepcopy of each cmd. Add ComputeTag to received cmd and send + it on. Otherwise, send all received commands directly to next_engine. Args: command_list (list): List of commands to receive. @@ -248,20 +220,19 @@ def receive(self, command_list): class UncomputeEngine(BasicEngine): - """ - Adds Uncompute-tags to all commands. - """ + """Adds Uncompute-tags to all commands.""" + def __init__(self): - """ - Initialize a UncomputeEngine. - """ - BasicEngine.__init__(self) + """Initialize a UncomputeEngine.""" + super().__init__() # Save all qubit ids from qubits which are created or destroyed. self._allocated_qubit_ids = set() self._deallocated_qubit_ids = set() def receive(self, command_list): """ + Receive a list of commands. + Receive commands and add an UncomputeTag to their tags. Args: @@ -277,7 +248,7 @@ def receive(self, command_list): self.send([cmd]) -class Compute(object): +class Compute: """ Start a compute-section. @@ -287,12 +258,11 @@ class Compute(object): with Compute(eng): do_something(qubits) action(qubits) - Uncompute(eng) # runs inverse of the compute section + Uncompute(eng) # runs inverse of the compute section Warning: - If qubits are allocated within the compute section, they must either be - uncomputed and deallocated within that section or, alternatively, - uncomputed and deallocated in the following uncompute section. + If qubits are allocated within the compute section, they must either be uncomputed and deallocated within that + section or, alternatively, uncomputed and deallocated in the following uncompute section. This means that the following examples are valid: @@ -308,7 +278,7 @@ class Compute(object): do_something_else(qubits) Uncompute(eng) # will allocate a new ancilla (with a different id) - # and then deallocate it again + # and then deallocate it again .. code-block:: python @@ -321,12 +291,10 @@ class Compute(object): Uncompute(eng) # will deallocate the ancilla! - After the uncompute section, ancilla qubits allocated within the - compute section will be invalid (and deallocated). The same holds when - using CustomUncompute. + After the uncompute section, ancilla qubits allocated within the compute section will be invalid (and + deallocated). The same holds when using CustomUncompute. - Failure to comply with these rules results in an exception being - thrown. + Failure to comply with these rules results in an exception being thrown. """ def __init__(self, engine): @@ -334,23 +302,24 @@ def __init__(self, engine): Initialize a Compute context. Args: - engine (BasicEngine): Engine which is the first to receive all - commands (normally: MainEngine). + engine (BasicEngine): Engine which is the first to receive all commands (normally: MainEngine). """ self.engine = engine self._compute_eng = None def __enter__(self): + """Context manager enter function.""" self._compute_eng = ComputeEngine() insert_engine(self.engine, self._compute_eng) - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, exc_traceback): + """Context manager exit function.""" # notify ComputeEngine that the compute section is done self._compute_eng.end_compute() self._compute_eng = None -class CustomUncompute(object): +class CustomUncompute: """ Start a custom uncompute-section. @@ -364,8 +333,8 @@ class CustomUncompute(object): do_something_inverse(qubits) Raises: - QubitManagementError: If qubits are allocated within Compute or within - CustomUncompute context but are not deallocated. + QubitManagementError: If qubits are allocated within Compute or within CustomUncompute context but are not + deallocated. """ def __init__(self, engine): @@ -373,21 +342,22 @@ def __init__(self, engine): Initialize a CustomUncompute context. Args: - engine (BasicEngine): Engine which is the first to receive all - commands (normally: MainEngine). + engine (BasicEngine): Engine which is the first to receive all commands (normally: MainEngine). """ self.engine = engine # Save all qubit ids from qubits which are created or destroyed. self._allocated_qubit_ids = set() self._deallocated_qubit_ids = set() + self._uncompute_eng = None def __enter__(self): + """Context manager enter function.""" # first, remove the compute engine compute_eng = self.engine.next_engine if not isinstance(compute_eng, ComputeEngine): raise NoComputeSectionError( - "Invalid call to CustomUncompute: No corresponding" - "'with Compute' statement found.") + "Invalid call to CustomUncompute: No corresponding 'with Compute' statement found." + ) # Make copy so there is not reference to compute_eng anymore # after __enter__ self._allocated_qubit_ids = compute_eng._allocated_qubit_ids.copy() @@ -398,28 +368,28 @@ def __enter__(self): self._uncompute_eng = UncomputeEngine() insert_engine(self.engine, self._uncompute_eng) - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, exc_traceback): + """Context manager exit function.""" # If an error happens in this context, qubits might not have been # deallocated because that code section was not yet executed, # so don't check and raise an additional error. - if type is not None: + if exc_type is not None: return # Check that all qubits allocated within Compute or within # CustomUncompute have been deallocated. - all_allocated_qubits = self._allocated_qubit_ids.union( - self._uncompute_eng._allocated_qubit_ids) - all_deallocated_qubits = self._deallocated_qubit_ids.union( - self._uncompute_eng._deallocated_qubit_ids) + all_allocated_qubits = self._allocated_qubit_ids.union(self._uncompute_eng._allocated_qubit_ids) + all_deallocated_qubits = self._deallocated_qubit_ids.union(self._uncompute_eng._deallocated_qubit_ids) if len(all_allocated_qubits.difference(all_deallocated_qubits)) != 0: raise QubitManagementError( - "\nError. Not all qubits have been deallocated which have \n" + - "been allocated in the with Compute(eng) or with " + - "CustomUncompute(eng) context.") + "\nError. Not all qubits have been deallocated which have \n" + + "been allocated in the with Compute(eng) or with " + + "CustomUncompute(eng) context." + ) # remove uncompute engine drop_engine_after(self.engine) -def Uncompute(engine): +def Uncompute(engine): # pylint: disable=invalid-name """ Uncompute automatically. @@ -429,12 +399,10 @@ def Uncompute(engine): with Compute(eng): do_something(qubits) action(qubits) - Uncompute(eng) # runs inverse of the compute section + Uncompute(eng) # runs inverse of the compute section """ compute_eng = engine.next_engine if not isinstance(compute_eng, ComputeEngine): - raise NoComputeSectionError("Invalid call to Uncompute: No " - "corresponding 'with Compute' statement " - "found.") + raise NoComputeSectionError("Invalid call to Uncompute: No corresponding 'with Compute' statement found.") compute_eng.run_uncompute() drop_engine_after(engine) diff --git a/projectq/meta/_compute_test.py b/projectq/meta/_compute_test.py index 223fefedc..e5caa360b 100755 --- a/projectq/meta/_compute_test.py +++ b/projectq/meta/_compute_test.py @@ -11,27 +11,24 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._compute.py""" -import pytest import types import weakref -from projectq import MainEngine -from projectq.cengines import DummyEngine, CompareEngine -from projectq.ops import H, Rx, Ry, Deallocate, Allocate, CNOT, NOT, FlushGate -from projectq.types import WeakQubitRef -from projectq.meta import DirtyQubitTag +import pytest -from projectq.meta import _compute +from projectq import MainEngine +from projectq.cengines import CompareEngine, DummyEngine +from projectq.meta import DirtyQubitTag, _compute +from projectq.ops import CNOT, NOT, Allocate, Deallocate, FlushGate, H, Rx, Ry def test_compute_tag(): tag0 = _compute.ComputeTag() tag1 = _compute.ComputeTag() - class MyTag(object): + class MyTag: pass assert not tag0 == MyTag() @@ -43,7 +40,7 @@ def test_uncompute_tag(): tag0 = _compute.UncomputeTag() tag1 = _compute.UncomputeTag() - class MyTag(object): + class MyTag: pass assert not tag0 == MyTag() @@ -73,8 +70,7 @@ def test_compute_engine(): assert backend.received_commands[0].gate == Allocate assert backend.received_commands[0].tags == [_compute.ComputeTag()] assert backend.received_commands[1].gate == H - assert backend.received_commands[1].tags == [_compute.ComputeTag(), - "TagAddedLater"] + assert backend.received_commands[1].tags == [_compute.ComputeTag(), "TagAddedLater"] assert backend.received_commands[2].gate == Rx(0.6) assert backend.received_commands[2].tags == [_compute.ComputeTag()] assert backend.received_commands[3].gate == Deallocate @@ -220,10 +216,9 @@ def test_compute_uncompute_with_statement(): def allow_dirty_qubits(self, meta_tag): return meta_tag == DirtyQubitTag - dummy_cengine.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, - dummy_cengine) - eng = MainEngine(backend=backend, - engine_list=[compare_engine0, dummy_cengine]) + + dummy_cengine.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, dummy_cengine) + eng = MainEngine(backend=backend, engine_list=[compare_engine0, dummy_cengine]) qubit = eng.allocate_qubit() with _compute.Compute(eng): Rx(0.9) | qubit @@ -268,19 +263,21 @@ def allow_dirty_qubits(self, meta_tag): # Test that each command has correct tags assert backend.received_commands[0].tags == [] assert backend.received_commands[1].tags == [_compute.ComputeTag()] - assert backend.received_commands[2].tags == [DirtyQubitTag(), - _compute.ComputeTag()] + assert backend.received_commands[2].tags == [DirtyQubitTag(), _compute.ComputeTag()] for cmd in backend.received_commands[3:9]: assert cmd.tags == [_compute.ComputeTag()] - assert backend.received_commands[9].tags == [DirtyQubitTag(), - _compute.ComputeTag()] + assert backend.received_commands[9].tags == [DirtyQubitTag(), _compute.ComputeTag()] assert backend.received_commands[10].tags == [] - assert backend.received_commands[11].tags == [DirtyQubitTag(), - _compute.UncomputeTag()] + assert backend.received_commands[11].tags == [ + DirtyQubitTag(), + _compute.UncomputeTag(), + ] for cmd in backend.received_commands[12:18]: assert cmd.tags == [_compute.UncomputeTag()] - assert backend.received_commands[18].tags == [DirtyQubitTag(), - _compute.UncomputeTag()] + assert backend.received_commands[18].tags == [ + DirtyQubitTag(), + _compute.UncomputeTag(), + ] assert backend.received_commands[19].tags == [_compute.UncomputeTag()] assert backend.received_commands[20].tags == [] assert backend.received_commands[21].tags == [] @@ -295,8 +292,7 @@ def allow_dirty_qubits(self, meta_tag): assert backend.received_commands[4].qubits[0][0].id == qubit_id assert backend.received_commands[5].qubits[0][0].id == ancilla_compt_id assert backend.received_commands[6].qubits[0][0].id == qubit_id - assert (backend.received_commands[6].control_qubits[0].id == - ancilla_compt_id) + assert backend.received_commands[6].control_qubits[0].id == ancilla_compt_id assert backend.received_commands[7].qubits[0][0].id == qubit_id assert backend.received_commands[8].qubits[0][0].id == ancilla_compt_id assert backend.received_commands[9].qubits[0][0].id == ancilla_compt_id @@ -304,15 +300,14 @@ def allow_dirty_qubits(self, meta_tag): assert backend.received_commands[12].qubits[0][0].id == ancilla_uncompt_id assert backend.received_commands[13].qubits[0][0].id == qubit_id assert backend.received_commands[14].qubits[0][0].id == qubit_id - assert (backend.received_commands[14].control_qubits[0].id == - ancilla_uncompt_id) + assert backend.received_commands[14].control_qubits[0].id == ancilla_uncompt_id assert backend.received_commands[15].qubits[0][0].id == ancilla_uncompt_id assert backend.received_commands[16].qubits[0][0].id == qubit_id assert backend.received_commands[17].qubits[0][0].id == ancilla2_id assert backend.received_commands[18].qubits[0][0].id == ancilla_uncompt_id assert backend.received_commands[19].qubits[0][0].id == qubit_id assert backend.received_commands[20].qubits[0][0].id == qubit_id - # Test that ancilla qubits should have seperate ids + # Test that ancilla qubits should have separate ids assert ancilla_uncompt_id != ancilla_compt_id # Do the same thing with CustomUncompute and compare using the @@ -324,10 +319,9 @@ def allow_dirty_qubits(self, meta_tag): def allow_dirty_qubits(self, meta_tag): return meta_tag == DirtyQubitTag - dummy_cengine1.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, - dummy_cengine1) - eng1 = MainEngine(backend=backend1, - engine_list=[compare_engine1, dummy_cengine1]) + + dummy_cengine1.is_meta_tag_handler = types.MethodType(allow_dirty_qubits, dummy_cengine1) + eng1 = MainEngine(backend=backend1, engine_list=[compare_engine1, dummy_cengine1]) qubit = eng1.allocate_qubit() with _compute.Compute(eng1): Rx(0.9) | qubit @@ -361,7 +355,8 @@ def allow_dirty_qubits(self, meta_tag): def test_exception_if_no_compute_but_uncompute(): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) with pytest.raises(_compute.NoComputeSectionError): - with _compute.CustomUncompute(eng): pass + with _compute.CustomUncompute(eng): + pass def test_exception_if_no_compute_but_uncompute2(): @@ -373,7 +368,7 @@ def test_exception_if_no_compute_but_uncompute2(): def test_qubit_management_error(): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) with _compute.Compute(eng): - ancilla = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 eng.active_qubits = weakref.WeakSet() with pytest.raises(_compute.QubitManagementError): _compute.Uncompute(eng) @@ -382,7 +377,7 @@ def test_qubit_management_error(): def test_qubit_management_error2(): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) with _compute.Compute(eng): - ancilla = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 local_ancilla = eng.allocate_qubit() local_ancilla[0].__del__() eng.active_qubits = weakref.WeakSet() @@ -393,7 +388,7 @@ def test_qubit_management_error2(): def test_only_single_error_in_costum_uncompute(): eng = MainEngine(backend=DummyEngine(), engine_list=[]) with _compute.Compute(eng): - qb = eng.allocate_qubit() + eng.allocate_qubit() # Tests that QubitManagementError is not sent in addition with pytest.raises(RuntimeError): with _compute.CustomUncompute(eng): diff --git a/projectq/meta/_control.py b/projectq/meta/_control.py index 76ded4326..176892709 100755 --- a/projectq/meta/_control.py +++ b/projectq/meta/_control.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Contains the tools to make an entire section of operations controlled. @@ -24,18 +23,82 @@ """ from projectq.cengines import BasicEngine -from projectq.meta import ComputeTag, UncomputeTag -from projectq.ops import ClassicalInstructionGate +from projectq.ops import ClassicalInstructionGate, CtrlAll from projectq.types import BasicQubit -from ._util import insert_engine, drop_engine_after +from ._compute import ComputeTag, UncomputeTag +from ._util import drop_engine_after, insert_engine -class ControlEngine(BasicEngine): + +def canonical_ctrl_state(ctrl_state, num_qubits): """ - Adds control qubits to all commands that have no compute / uncompute tags. + Return canonical form for control state. + + Args: + ctrl_state (int,str,CtrlAll): Initial control state representation + num_qubits (int): number of control qubits + + Returns: + Canonical form of control state (currently a string composed of '0' and '1') + + Note: + In case of integer values for `ctrl_state`, the least significant bit applies to the first qubit in the qubit + register, e.g. if ctrl_state == 2, its binary representation if '10' with the least significant bit being 0. + + This means in particular that the following are equivalent: + + .. code-block:: python + + canonical_ctrl_state(6, 3) == canonical_ctrl_state(6, '110') + """ + if not num_qubits: + return '' + + if isinstance(ctrl_state, CtrlAll): + if ctrl_state == CtrlAll.One: + return '1' * num_qubits + return '0' * num_qubits + + if isinstance(ctrl_state, int): + # If the user inputs an integer, convert it to binary bit string + converted_str = f'{ctrl_state:b}'.zfill(num_qubits)[::-1] + if len(converted_str) != num_qubits: + raise ValueError( + f'Control state specified as {ctrl_state} ({converted_str}) is higher than maximum for {num_qubits} ' + f'qubits: {2 ** num_qubits - 1}' + ) + return converted_str + + if isinstance(ctrl_state, str): + # If the user inputs bit string, directly use it + if len(ctrl_state) != num_qubits: + raise ValueError( + f'Control state {ctrl_state} has different length than the number of control qubits {num_qubits}' + ) + if not set(ctrl_state).issubset({'0', '1'}): + raise ValueError(f'Control state {ctrl_state} has string other than 1 and 0') + return ctrl_state + + raise TypeError('Input must be a string, an integer or an enum value of class State') + + +def _has_compute_uncompute_tag(cmd): + """ + Return True if command cmd has a compute/uncompute tag. + + Args: + cmd (Command object): a command object. """ + for tag in cmd.tags: + if tag in [UncomputeTag(), ComputeTag()]: + return True + return False - def __init__(self, qubits): + +class ControlEngine(BasicEngine): + """Add control qubits to all commands that have no compute / uncompute tags.""" + + def __init__(self, qubits, ctrl_state=CtrlAll.One): """ Initialize the control engine. @@ -43,33 +106,22 @@ def __init__(self, qubits): qubits (list of Qubit objects): qubits conditional on which the following operations are executed. """ - BasicEngine.__init__(self) + super().__init__() self._qubits = qubits - - def _has_compute_uncompute_tag(self, cmd): - """ - Return True if command cmd has a compute/uncompute tag. - - Args: - cmd (Command object): a command object. - """ - for t in cmd.tags: - if t in [UncomputeTag(), ComputeTag()]: - return True - return False + self._state = ctrl_state def _handle_command(self, cmd): - if (not self._has_compute_uncompute_tag(cmd) and not - isinstance(cmd.gate, ClassicalInstructionGate)): - cmd.add_control_qubits(self._qubits) + if not _has_compute_uncompute_tag(cmd) and not isinstance(cmd.gate, ClassicalInstructionGate): + cmd.add_control_qubits(self._qubits, self._state) self.send([cmd]) def receive(self, command_list): + """Receive a list of commands.""" for cmd in command_list: self._handle_command(cmd) -class Control(object): +class Control: """ Condition an entire code block on the value of qubits being 1. @@ -80,7 +132,7 @@ class Control(object): do_something(otherqubits) """ - def __init__(self, engine, qubits): + def __init__(self, engine, qubits, ctrl_state=CtrlAll.One): """ Enter a controlled section. @@ -96,24 +148,31 @@ def __init__(self, engine, qubits): ... """ self.engine = engine - assert(not isinstance(qubits, tuple)) + if isinstance(qubits, tuple): + raise TypeError('Control qubits must be a list, not a tuple!') if isinstance(qubits, BasicQubit): qubits = [qubits] self._qubits = qubits + self._state = canonical_ctrl_state(ctrl_state, len(self._qubits)) def __enter__(self): + """Context manager enter function.""" if len(self._qubits) > 0: - ce = ControlEngine(self._qubits) - insert_engine(self.engine, ce) + engine = ControlEngine(self._qubits, self._state) + insert_engine(self.engine, engine) - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, exc_traceback): + """Context manager exit function.""" # remove control handler from engine list (i.e. skip it) if len(self._qubits) > 0: drop_engine_after(self.engine) def get_control_count(cmd): - """ - Return the number of control qubits of the command object cmd - """ + """Return the number of control qubits of the command object cmd.""" return len(cmd.control_qubits) + + +def has_negative_control(cmd): + """Return whether a command has negatively controlled qubits.""" + return get_control_count(cmd) > 0 and '0' in cmd.control_state diff --git a/projectq/meta/_control_test.py b/projectq/meta/_control_test.py index 77bf538e4..73810b95e 100755 --- a/projectq/meta/_control_test.py +++ b/projectq/meta/_control_test.py @@ -11,19 +11,70 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._control.py""" +import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import Command, H, Rx -from projectq.meta import (DirtyQubitTag, - ComputeTag, - UncomputeTag, - Compute, - Uncompute) +from projectq.meta import ( + Compute, + ComputeTag, + DirtyQubitTag, + Uncompute, + UncomputeTag, + _control, +) +from projectq.ops import Command, CtrlAll, H, IncompatibleControlState, Rx, X +from projectq.types import WeakQubitRef + + +def test_canonical_representation(): + assert _control.canonical_ctrl_state(0, 0) == '' + for num_qubits in range(4): + assert _control.canonical_ctrl_state(0, num_qubits) == '0' * num_qubits + + num_qubits = 4 + for i in range(2**num_qubits): + state = f'{i:0b}'.zfill(num_qubits) + assert _control.canonical_ctrl_state(i, num_qubits) == state[::-1] + assert _control.canonical_ctrl_state(state, num_qubits) == state + + for num_qubits in range(10): + assert _control.canonical_ctrl_state(CtrlAll.Zero, num_qubits) == '0' * num_qubits + assert _control.canonical_ctrl_state(CtrlAll.One, num_qubits) == '1' * num_qubits + + with pytest.raises(TypeError): + _control.canonical_ctrl_state(1.1, 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('1', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('11111', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('1a', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state(4, 2) -from projectq.meta import _control + +def test_has_negative_control(): + qubit0 = WeakQubitRef(None, 0) + qubit1 = WeakQubitRef(None, 0) + qubit2 = WeakQubitRef(None, 0) + qubit3 = WeakQubitRef(None, 0) + assert not _control.has_negative_control(Command(None, H, ([qubit0],))) + assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1])) + assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1], control_state=CtrlAll.One)) + assert _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1], control_state=CtrlAll.Zero)) + assert _control.has_negative_control( + Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state=CtrlAll.Zero) + ) + assert not _control.has_negative_control( + Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state='111') + ) + assert _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state='101')) def test_control_engine_has_compute_tag(): @@ -35,10 +86,9 @@ def test_control_engine_has_compute_tag(): test_cmd0.tags = [DirtyQubitTag(), ComputeTag(), DirtyQubitTag()] test_cmd1.tags = [DirtyQubitTag(), UncomputeTag(), DirtyQubitTag()] test_cmd2.tags = [DirtyQubitTag()] - control_eng = _control.ControlEngine("MockEng") - assert control_eng._has_compute_uncompute_tag(test_cmd0) - assert control_eng._has_compute_uncompute_tag(test_cmd1) - assert not control_eng._has_compute_uncompute_tag(test_cmd2) + assert _control._has_compute_uncompute_tag(test_cmd0) + assert _control._has_compute_uncompute_tag(test_cmd1) + assert not _control._has_compute_uncompute_tag(test_cmd2) def test_control(): @@ -66,3 +116,62 @@ def test_control(): assert backend.received_commands[4].control_qubits[0].id == qureg[0].id assert backend.received_commands[4].control_qubits[1].id == qureg[1].id assert backend.received_commands[6].control_qubits[0].id == qureg[0].id + + with pytest.raises(TypeError): + _control.Control(eng, (qureg[0], qureg[1])) + + +def test_control_state(): + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) + + qureg = eng.allocate_qureg(3) + xreg = eng.allocate_qureg(3) + X | qureg[1] + with _control.Control(eng, qureg[0], '0'): + with Compute(eng): + X | xreg[0] + + X | xreg[1] + Uncompute(eng) + + with _control.Control(eng, qureg[1:], 2): + X | xreg[2] + eng.flush() + + assert len(backend.received_commands) == 6 + 5 + 1 + assert len(backend.received_commands[0].control_qubits) == 0 + assert len(backend.received_commands[1].control_qubits) == 0 + assert len(backend.received_commands[2].control_qubits) == 0 + assert len(backend.received_commands[3].control_qubits) == 0 + assert len(backend.received_commands[4].control_qubits) == 0 + assert len(backend.received_commands[5].control_qubits) == 0 + + assert len(backend.received_commands[6].control_qubits) == 0 + assert len(backend.received_commands[7].control_qubits) == 0 + assert len(backend.received_commands[8].control_qubits) == 1 + assert len(backend.received_commands[9].control_qubits) == 0 + assert len(backend.received_commands[10].control_qubits) == 2 + + assert len(backend.received_commands[11].control_qubits) == 0 + + assert backend.received_commands[8].control_qubits[0].id == qureg[0].id + assert backend.received_commands[8].control_state == '0' + assert backend.received_commands[10].control_qubits[0].id == qureg[1].id + assert backend.received_commands[10].control_qubits[1].id == qureg[2].id + assert backend.received_commands[10].control_state == '01' + + assert _control.has_negative_control(backend.received_commands[8]) + assert _control.has_negative_control(backend.received_commands[10]) + + +def test_control_state_contradiction(): + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) + qureg = eng.allocate_qureg(1) + with pytest.raises(IncompatibleControlState): + with _control.Control(eng, qureg[0], '0'): + qubit = eng.allocate_qubit() + with _control.Control(eng, qureg[0], '1'): + H | qubit + eng.flush() diff --git a/projectq/meta/_dagger.py b/projectq/meta/_dagger.py index a27a48920..f2a7dc243 100755 --- a/projectq/meta/_dagger.py +++ b/projectq/meta/_dagger.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tools to easily invert a sequence of gates. @@ -24,42 +23,37 @@ from projectq.cengines import BasicEngine from projectq.ops import Allocate, Deallocate -from ._util import insert_engine, drop_engine_after - -class QubitManagementError(Exception): - pass +from ._exceptions import QubitManagementError +from ._util import drop_engine_after, insert_engine class DaggerEngine(BasicEngine): - """ - Stores all commands and, when done, inverts the circuit & runs it. - """ + """Store all commands and, when done, inverts the circuit & runs it.""" def __init__(self): - BasicEngine.__init__(self) + """Initialize a DaggerEngine object.""" + super().__init__() self._commands = [] self._allocated_qubit_ids = set() self._deallocated_qubit_ids = set() def run(self): - """ - Run the stored circuit in reverse and check that local qubits - have been deallocated. - """ + """Run the stored circuit in reverse and check that local qubits have been deallocated.""" if self._deallocated_qubit_ids != self._allocated_qubit_ids: - raise QubitManagementError( - "\n Error. Qubits have been allocated in 'with " + - "Dagger(eng)' context,\n which have not explicitely " + - "been deallocated.\n" + - "Correct usage:\n" + - "with Dagger(eng):\n" + - " qubit = eng.allocate_qubit()\n" + - " ...\n" + - " del qubit[0]\n") + raise QubitManagementError( + "\n Error. Qubits have been allocated in 'with " + + "Dagger(eng)' context,\n which have not explicitly " + + "been deallocated.\n" + + "Correct usage:\n" + + "with Dagger(eng):\n" + + " qubit = eng.allocate_qubit()\n" + + " ...\n" + + " del qubit[0]\n" + ) for cmd in reversed(self._commands): - self.send([cmd.get_inverse()]) + self.send([cmd.get_inverse()]) def receive(self, command_list): """ @@ -77,7 +71,7 @@ def receive(self, command_list): self._commands.extend(command_list) -class Dagger(object): +class Dagger: """ Invert an entire code block. @@ -86,7 +80,8 @@ class Dagger(object): .. code-block:: python with Dagger(eng): - [code to invert] + # [code to invert] + pass Warning: If the code to invert contains allocation of qubits, those qubits have @@ -98,7 +93,7 @@ class Dagger(object): with Dagger(eng): qb = eng.allocate_qubit() - H | qb # qb is still available!!! + H | qb # qb is still available!!! The **correct way** of handling qubit (de-)allocation is as follows: @@ -107,7 +102,7 @@ class Dagger(object): with Dagger(eng): qb = eng.allocate_qubit() ... - del qb # sends deallocate gate (which becomes an allocate) + del qb # sends deallocate gate (which becomes an allocate) """ def __init__(self, engine): @@ -128,14 +123,16 @@ def __init__(self, engine): self._dagger_eng = None def __enter__(self): + """Context manager enter function.""" self._dagger_eng = DaggerEngine() insert_engine(self.engine, self._dagger_eng) - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, exc_traceback): + """Context manager exit function.""" # If an error happens in this context, qubits might not have been # deallocated because that code section was not yet executed, # so don't check and raise an additional error. - if type is not None: + if exc_type is not None: return # run dagger engine self._dagger_eng.run() diff --git a/projectq/meta/_dagger_test.py b/projectq/meta/_dagger_test.py index d7a285e77..330e0d106 100755 --- a/projectq/meta/_dagger_test.py +++ b/projectq/meta/_dagger_test.py @@ -11,18 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._dagger.py""" -import pytest import types +import pytest + from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import CNOT, X, Rx, H, Allocate, Deallocate -from projectq.meta import DirtyQubitTag - -from projectq.meta import _dagger +from projectq.meta import DirtyQubitTag, _dagger +from projectq.ops import CNOT, Allocate, Deallocate, H, Rx, X def test_dagger_with_dirty_qubits(): @@ -59,7 +57,7 @@ def test_dagger_qubit_management_error(): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) with pytest.raises(_dagger.QubitManagementError): with _dagger.Dagger(eng): - ancilla = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 def test_dagger_raises_only_single_error(): @@ -67,5 +65,5 @@ def test_dagger_raises_only_single_error(): # Tests that QubitManagementError is not sent in addition with pytest.raises(RuntimeError): with _dagger.Dagger(eng): - ancilla = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 raise RuntimeError diff --git a/projectq/meta/_dirtyqubit.py b/projectq/meta/_dirtyqubit.py index e0693e8e6..c13ed2afb 100755 --- a/projectq/meta/_dirtyqubit.py +++ b/projectq/meta/_dirtyqubit.py @@ -12,17 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Defines the DirtyQubitTag meta tag. -""" +"""Define the DirtyQubitTag meta tag.""" -class DirtyQubitTag(object): - """ - Dirty qubit meta tag - """ +class DirtyQubitTag: # pylint: disable=too-few-public-methods + """Dirty qubit meta tag.""" + def __eq__(self, other): + """Equal operator.""" return isinstance(other, DirtyQubitTag) - - def __ne__(self, other): - return not self.__eq__(other) diff --git a/projectq/meta/_dirtyqubit_test.py b/projectq/meta/_dirtyqubit_test.py index c23b49ba5..09e942c1e 100755 --- a/projectq/meta/_dirtyqubit_test.py +++ b/projectq/meta/_dirtyqubit_test.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._dirtyqubit.py""" -from projectq.meta import ComputeTag - -from projectq.meta import _dirtyqubit +from projectq.meta import ComputeTag, _dirtyqubit def test_dirty_qubit_tag(): diff --git a/projectq/meta/_exceptions.py b/projectq/meta/_exceptions.py new file mode 100644 index 000000000..b9c5858b1 --- /dev/null +++ b/projectq/meta/_exceptions.py @@ -0,0 +1,23 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exception classes for projectq.meta.""" + + +class QubitManagementError(Exception): + """ + Exception raised when the lifetime of a qubit is problematic within a context manager. + + This may occur within Loop, Dagger or Compute regions. + """ diff --git a/projectq/meta/_logicalqubit.py b/projectq/meta/_logicalqubit.py index 0e50ada9b..0aaf4ddd3 100644 --- a/projectq/meta/_logicalqubit.py +++ b/projectq/meta/_logicalqubit.py @@ -12,24 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Defines LogicalQubitIDTag to annotate a MeasureGate for mapped qubits. -""" +"""Definition of LogicalQubitIDTag to annotate a MeasureGate for mapped qubits.""" -class LogicalQubitIDTag(object): +class LogicalQubitIDTag: # pylint: disable=too-few-public-methods """ LogicalQubitIDTag for a mapped qubit to annotate a MeasureGate. Attributes: logical_qubit_id (int): Logical qubit id """ + def __init__(self, logical_qubit_id): + """Initialize a LogicalQubitIDTag object.""" self.logical_qubit_id = logical_qubit_id def __eq__(self, other): - return (isinstance(other, LogicalQubitIDTag) and - self.logical_qubit_id == other.logical_qubit_id) - - def __ne__(self, other): - return not self.__eq__(other) + """Equal operator.""" + return isinstance(other, LogicalQubitIDTag) and self.logical_qubit_id == other.logical_qubit_id diff --git a/projectq/meta/_logicalqubit_test.py b/projectq/meta/_logicalqubit_test.py index 14f1f244d..43ed2943a 100644 --- a/projectq/meta/_logicalqubit_test.py +++ b/projectq/meta/_logicalqubit_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._logicalqubit.py.""" from copy import deepcopy diff --git a/projectq/meta/_loop.py b/projectq/meta/_loop.py index e28d64ea8..5fd94157f 100755 --- a/projectq/meta/_loop.py +++ b/projectq/meta/_loop.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tools to implement loops. @@ -20,44 +19,40 @@ with Loop(eng, 4): H | qb - Rz(M_PI/3.) | qb + Rz(M_PI / 3.0) | qb """ from copy import deepcopy from projectq.cengines import BasicEngine from projectq.ops import Allocate, Deallocate -from ._util import insert_engine, drop_engine_after +from ._exceptions import QubitManagementError +from ._util import drop_engine_after, insert_engine -class QubitManagementError(Exception): - pass +class LoopTag: # pylint: disable=too-few-public-methods + """Loop meta tag.""" -class LoopTag(object): - """ - Loop meta tag - """ def __init__(self, num): + """Initialize a LoopTag object.""" self.num = num self.id = LoopTag.loop_tag_id LoopTag.loop_tag_id += 1 def __eq__(self, other): - return (isinstance(other, LoopTag) and self.id == other.id and - self.num == other.num) - - def __ne__(self, other): - return not self.__eq__(other) + """Equal operator.""" + return isinstance(other, LoopTag) and self.id == other.id and self.num == other.num loop_tag_id = 0 class LoopEngine(BasicEngine): """ - Stores all commands and, when done, executes them num times if no loop tag - handler engine is available. - If there is one, it adds a loop_tag to the commands and sends them on. + A compiler engine to represent executing part of the code multiple times. + + Stores all commands and, when done, executes them num times if no loop tag handler engine is available. If there + is one, it adds a loop_tag to the commands and sends them on. """ def __init__(self, num): @@ -67,7 +62,7 @@ def __init__(self, num): Args: num (int): Number of loop iterations. """ - BasicEngine.__init__(self) + super().__init__() self._tag = LoopTag(num) self._cmd_list = [] self._allocated_qubit_ids = set() @@ -76,7 +71,7 @@ def __init__(self, num): # and deallocated within the loop body. # value: list contain reference to each weakref qubit with this qubit # id either within control_qubits or qubits. - self._refs_to_local_qb = dict() + self._refs_to_local_qb = {} self._next_engines_support_loop_tag = False def run(self): @@ -87,16 +82,19 @@ def run(self): engines, i.e., if .. code-block:: python + is_meta_tag_supported(next_engine, LoopTag) == False """ - error_message = ("\n Error. Qubits have been allocated in with " - "Loop(eng, num) context,\n which have not " - "explicitely been deallocated in the Loop context.\n" - "Correct usage:\nwith Loop(eng, 5):\n" - " qubit = eng.allocate_qubit()\n" - " ...\n" - " del qubit[0]\n") - if not self._next_engines_support_loop_tag: + error_message = ( + "\n Error. Qubits have been allocated in with " + "Loop(eng, num) context,\n which have not " + "explicitly been deallocated in the Loop context.\n" + "Correct usage:\nwith Loop(eng, 5):\n" + " qubit = eng.allocate_qubit()\n" + " ...\n" + " del qubit[0]\n" + ) + if not self._next_engines_support_loop_tag: # pylint: disable=too-many-nested-blocks # Unroll the loop # Check that local qubits have been deallocated: if self._deallocated_qubit_ids != self._allocated_qubit_ids: @@ -127,7 +125,7 @@ def run(self): if self._deallocated_qubit_ids != self._allocated_qubit_ids: raise QubitManagementError(error_message) - def receive(self, command_list): + def receive(self, command_list): # pylint: disable=too-many-branches """ Receive (and potentially temporarily store) all commands. @@ -144,8 +142,8 @@ def receive(self, command_list): unroll or, if there is a LoopTag-handling engine, add the LoopTag. """ - if (self._next_engines_support_loop_tag or - self.next_engine.is_meta_tag_supported(LoopTag)): + # pylint: disable=too-many-nested-blocks + if self._next_engines_support_loop_tag or self.next_engine.is_meta_tag_supported(LoopTag): # Loop tag is supported, send everything with a LoopTag # Don't check is_meta_tag_supported anymore self._next_engines_support_loop_tag = True @@ -166,28 +164,24 @@ def receive(self, command_list): if cmd.gate == Allocate: self._allocated_qubit_ids.add(cmd.qubits[0][0].id) # Save reference to this local qubit - self._refs_to_local_qb[cmd.qubits[0][0].id] = ( - [cmd.qubits[0][0]]) + self._refs_to_local_qb[cmd.qubits[0][0].id] = [cmd.qubits[0][0]] elif cmd.gate == Deallocate: self._deallocated_qubit_ids.add(cmd.qubits[0][0].id) # Save reference to this local qubit - self._refs_to_local_qb[cmd.qubits[0][0].id].append( - cmd.qubits[0][0]) + self._refs_to_local_qb[cmd.qubits[0][0].id].append(cmd.qubits[0][0]) else: # Add a reference to each place a local qubit id is # used as within either control_qubit or qubits for control_qubit in cmd.control_qubits: if control_qubit.id in self._allocated_qubit_ids: - self._refs_to_local_qb[control_qubit.id].append( - control_qubit) + self._refs_to_local_qb[control_qubit.id].append(control_qubit) for qureg in cmd.qubits: for qubit in qureg: if qubit.id in self._allocated_qubit_ids: - self._refs_to_local_qb[qubit.id].append( - qubit) + self._refs_to_local_qb[qubit.id].append(qubit) -class Loop(object): +class Loop: """ Loop n times over an entire code block. @@ -196,10 +190,11 @@ class Loop(object): with Loop(eng, 4): # [quantum gates to be executed 4 times] + pass Warning: - If the code in the loop contains allocation of qubits, those qubits - have to be deleted prior to exiting the 'with Loop()' context. + If the code in the loop contains allocation of qubits, those qubits have to be deleted prior to exiting the + 'with Loop()' context. This code is **NOT VALID**: @@ -207,7 +202,7 @@ class Loop(object): with Loop(eng, 4): qb = eng.allocate_qubit() - H | qb # qb is still available!!! + H | qb # qb is still available!!! The **correct way** of handling qubit (de-)allocation is as follows: @@ -215,8 +210,8 @@ class Loop(object): with Loop(eng, 4): qb = eng.allocate_qubit() - ... - del qb # sends deallocate gate + # ... + del qb # sends deallocate gate """ def __init__(self, engine, num): @@ -232,7 +227,7 @@ def __init__(self, engine, num): with Loop(eng, 4): H | qb - Rz(M_PI/3.) | qb + Rz(M_PI / 3.0) | qb Raises: TypeError: If number of iterations (num) is not an integer ValueError: If number of iterations (num) is not >= 0 @@ -246,11 +241,13 @@ def __init__(self, engine, num): self._loop_eng = None def __enter__(self): + """Context manager enter function.""" if self.num != 1: self._loop_eng = LoopEngine(self.num) insert_engine(self.engine, self._loop_eng) - def __exit__(self, type, value, traceback): + def __exit__(self, exc_type, exc_value, exc_traceback): + """Context manager exit function.""" if self.num != 1: # remove loop handler from engine list (i.e. skip it) self._loop_eng.run() diff --git a/projectq/meta/_loop_test.py b/projectq/meta/_loop_test.py index 2755a690c..01ef872ce 100755 --- a/projectq/meta/_loop_test.py +++ b/projectq/meta/_loop_test.py @@ -11,19 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.meta._loop.py""" -import pytest import types - from copy import deepcopy + +import pytest + from projectq import MainEngine -from projectq.meta import ComputeTag, DirtyQubitTag from projectq.cengines import DummyEngine -from projectq.ops import H, CNOT, X, FlushGate, Allocate, Deallocate - -from projectq.meta import _loop +from projectq.meta import ComputeTag, _loop +from projectq.ops import CNOT, Allocate, Deallocate, FlushGate, H, X def test_loop_tag(): @@ -41,14 +39,12 @@ def test_loop_tag(): def test_loop_wrong_input_type(): eng = MainEngine(backend=DummyEngine(), engine_list=[]) - qubit = eng.allocate_qubit() with pytest.raises(TypeError): _loop.Loop(eng, 1.1) def test_loop_negative_iteration_number(): eng = MainEngine(backend=DummyEngine(), engine_list=[]) - qubit = eng.allocate_qubit() with pytest.raises(ValueError): _loop.Loop(eng, -1) @@ -58,7 +54,7 @@ def test_loop_with_supported_loop_tag_and_local_qubits(): eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) def allow_loop_tags(self, meta_tag): - return meta_tag == _loop.LoopTag + return meta_tag == _loop.LoopTag backend.is_meta_tag_handler = types.MethodType(allow_loop_tags, backend) qubit = eng.allocate_qubit() @@ -135,7 +131,7 @@ def test_empty_loop_when_loop_tag_supported_by_backend(): eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) def allow_loop_tags(self, meta_tag): - return meta_tag == _loop.LoopTag + return meta_tag == _loop.LoopTag backend.is_meta_tag_handler = types.MethodType(allow_loop_tags, backend) qubit = eng.allocate_qubit() @@ -152,7 +148,7 @@ def test_loop_with_supported_loop_tag_depending_on_num(): eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) def allow_loop_tags(self, meta_tag): - return meta_tag == _loop.LoopTag + return meta_tag == _loop.LoopTag backend.is_meta_tag_handler = types.MethodType(allow_loop_tags, backend) qubit = eng.allocate_qubit() @@ -193,29 +189,31 @@ def test_loop_unrolling_with_ancillas(): assert backend.received_commands[ii * 4 + 3].gate == X assert backend.received_commands[ii * 4 + 4].gate == Deallocate # Check qubit ids - assert (backend.received_commands[ii * 4 + 1].qubits[0][0].id == - backend.received_commands[ii * 4 + 2].qubits[0][0].id) - assert (backend.received_commands[ii * 4 + 1].qubits[0][0].id == - backend.received_commands[ii * 4 + 3].control_qubits[0].id) - assert (backend.received_commands[ii * 4 + 3].qubits[0][0].id == - qubit_id) - assert (backend.received_commands[ii * 4 + 1].qubits[0][0].id == - backend.received_commands[ii * 4 + 4].qubits[0][0].id) + assert ( + backend.received_commands[ii * 4 + 1].qubits[0][0].id + == backend.received_commands[ii * 4 + 2].qubits[0][0].id + ) + assert ( + backend.received_commands[ii * 4 + 1].qubits[0][0].id + == backend.received_commands[ii * 4 + 3].control_qubits[0].id + ) + assert backend.received_commands[ii * 4 + 3].qubits[0][0].id == qubit_id + assert ( + backend.received_commands[ii * 4 + 1].qubits[0][0].id + == backend.received_commands[ii * 4 + 4].qubits[0][0].id + ) assert backend.received_commands[13].gate == Deallocate assert backend.received_commands[14].gate == FlushGate() - assert (backend.received_commands[1].qubits[0][0].id != - backend.received_commands[5].qubits[0][0].id) - assert (backend.received_commands[1].qubits[0][0].id != - backend.received_commands[9].qubits[0][0].id) - assert (backend.received_commands[5].qubits[0][0].id != - backend.received_commands[9].qubits[0][0].id) + assert backend.received_commands[1].qubits[0][0].id != backend.received_commands[5].qubits[0][0].id + assert backend.received_commands[1].qubits[0][0].id != backend.received_commands[9].qubits[0][0].id + assert backend.received_commands[5].qubits[0][0].id != backend.received_commands[9].qubits[0][0].id def test_nested_loop(): backend = DummyEngine(save_commands=True) def allow_loop_tags(self, meta_tag): - return meta_tag == _loop.LoopTag + return meta_tag == _loop.LoopTag backend.is_meta_tag_handler = types.MethodType(allow_loop_tags, backend) eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) @@ -229,8 +227,7 @@ def allow_loop_tags(self, meta_tag): assert len(backend.received_commands[1].tags) == 2 assert backend.received_commands[1].tags[0].num == 4 assert backend.received_commands[1].tags[1].num == 3 - assert (backend.received_commands[1].tags[0].id != - backend.received_commands[1].tags[1].id) + assert backend.received_commands[1].tags[0].id != backend.received_commands[1].tags[1].id def test_qubit_management_error(): @@ -238,17 +235,17 @@ def test_qubit_management_error(): eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) with pytest.raises(_loop.QubitManagementError): with _loop.Loop(eng, 3): - qb = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 def test_qubit_management_error_when_loop_tag_supported(): backend = DummyEngine(save_commands=True) def allow_loop_tags(self, meta_tag): - return meta_tag == _loop.LoopTag + return meta_tag == _loop.LoopTag backend.is_meta_tag_handler = types.MethodType(allow_loop_tags, backend) eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) with pytest.raises(_loop.QubitManagementError): with _loop.Loop(eng, 3): - qb = eng.allocate_qubit() + ancilla = eng.allocate_qubit() # noqa: F841 diff --git a/projectq/meta/_util.py b/projectq/meta/_util.py index 856ef6728..79f4dc43c 100755 --- a/projectq/meta/_util.py +++ b/projectq/meta/_util.py @@ -12,19 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Tools to add/remove compiler engines to the MainEngine list.""" + def insert_engine(prev_engine, engine_to_insert): """ - Inserts an engine into the singly-linked list of engines. + Insert an engine into the singly-linked list of engines. It also sets the correct main_engine for engine_to_insert. Args: - prev_engine (projectq.cengines.BasicEngine): - The engine just before the insertion point. - engine_to_insert (projectq.cengines.BasicEngine): - The engine to insert at the insertion point. + prev_engine (projectq.cengines.BasicEngine): The engine just before the insertion point. + engine_to_insert (projectq.cengines.BasicEngine): The engine to insert at the insertion point. """ + if prev_engine.main_engine is not None: + prev_engine.main_engine.n_engines += 1 + + if prev_engine.main_engine.n_engines > prev_engine.main_engine.n_engines_max: + raise RuntimeError('Too many compiler engines added to the MainEngine!') + engine_to_insert.main_engine = prev_engine.main_engine engine_to_insert.next_engine = prev_engine.next_engine prev_engine.next_engine = engine_to_insert @@ -32,16 +38,18 @@ def insert_engine(prev_engine, engine_to_insert): def drop_engine_after(prev_engine): """ - Removes an engine from the singly-linked list of engines. + Remove an engine from the singly-linked list of engines. Args: - prev_engine (projectq.cengines.BasicEngine): - The engine just before the engine to drop. + prev_engine (projectq.cengines.BasicEngine): The engine just before the engine to drop. + Returns: Engine: The dropped engine. """ dropped_engine = prev_engine.next_engine prev_engine.next_engine = dropped_engine.next_engine + if prev_engine.main_engine is not None: + prev_engine.main_engine.n_engines -= 1 dropped_engine.next_engine = None dropped_engine.main_engine = None return dropped_engine diff --git a/projectq/meta/_util_test.py b/projectq/meta/_util_test.py index 2b4ec892b..a60d4ac51 100755 --- a/projectq/meta/_util_test.py +++ b/projectq/meta/_util_test.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.meta import insert_engine, drop_engine_after + +from . import _util def test_insert_then_drop(): @@ -30,19 +32,35 @@ def test_insert_then_drop(): assert d1.main_engine is eng assert d2.main_engine is None assert d3.main_engine is eng + assert eng.n_engines == 2 - insert_engine(d1, d2) + _util.insert_engine(d1, d2) assert d1.next_engine is d2 assert d2.next_engine is d3 assert d3.next_engine is None assert d1.main_engine is eng assert d2.main_engine is eng assert d3.main_engine is eng + assert eng.n_engines == 3 - drop_engine_after(d1) + _util.drop_engine_after(d1) assert d1.next_engine is d3 assert d2.next_engine is None assert d3.next_engine is None assert d1.main_engine is eng assert d2.main_engine is None assert d3.main_engine is eng + assert eng.n_engines == 2 + + +def test_too_many_engines(): + N = 10 + + eng = MainEngine(backend=DummyEngine(), engine_list=[]) + eng.n_engines_max = N + + for _ in range(N - 1): + _util.insert_engine(eng, DummyEngine()) + + with pytest.raises(RuntimeError): + _util.insert_engine(eng, DummyEngine()) diff --git a/projectq/ops/__init__.py b/projectq/ops/__init__.py index 56dbec862..2a931b617 100755 --- a/projectq/ops/__init__.py +++ b/projectq/ops/__init__.py @@ -12,24 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ._basics import (NotMergeable, - NotInvertible, - BasicGate, - SelfInverseGate, - BasicRotationGate, - ClassicalInstructionGate, - FastForwardingGate, - BasicMathGate, - BasicPhaseGate) -from ._command import apply_command, Command -from ._metagates import (DaggeredGate, - get_inverse, - ControlledGate, - C, - Tensor, - All) +"""ProjectQ module containing all basic gates (operations).""" + +from ._basics import ( + BasicGate, + BasicMathGate, + BasicPhaseGate, + BasicRotationGate, + ClassicalInstructionGate, + FastForwardingGate, + MatrixGate, + NotInvertible, + NotMergeable, + SelfInverseGate, +) +from ._command import Command, CtrlAll, IncompatibleControlState, apply_command from ._gates import * +from ._metagates import ( + All, + C, + ControlledGate, + DaggeredGate, + Tensor, + get_inverse, + is_identity, +) +from ._qaagate import QAA from ._qftgate import QFT, QFTGate +from ._qpegate import QPE from ._qubit_operator import QubitOperator from ._shortcuts import * +from ._state_prep import StatePreparation from ._time_evolution import TimeEvolution +from ._uniformly_controlled_rotation import UniformlyControlledRy, UniformlyControlledRz diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 052b3abed..854696586 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -11,11 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines the BasicGate class, the base class of all gates, the -BasicRotationGate class, the SelfInverseGate, the FastForwardingGate, the -ClassicalInstruction gate, and the BasicMathGate class. +Definitions of some of the most basic quantum gates. + +Defines the BasicGate class, the base class of all gates, the BasicRotationGate class, the SelfInverseGate, the +FastForwardingGate, the ClassicalInstruction gate, and the BasicMathGate class. Gates overload the | operator to allow the following syntax: @@ -27,41 +27,45 @@ Gate | qubit Gate | (qubit,) -This means that for more than one quantum argument (right side of | ), a tuple -needs to be made explicitely, while for one argument it is optional. +This means that for more than one quantum argument (right side of | ), a tuple needs to be made explicitly, while for +one argument it is optional. """ import math +import unicodedata from copy import deepcopy +import numpy as np + from projectq.types import BasicQubit -from ._command import Command, apply_command +from ._command import Command, apply_command ANGLE_PRECISION = 12 -ANGLE_TOLERANCE = 10 ** -ANGLE_PRECISION +ANGLE_TOLERANCE = 10**-ANGLE_PRECISION +RTOL = 1e-10 +ATOL = 1e-12 class NotMergeable(Exception): """ - Exception thrown when trying to merge two gates which are not mergeable (or - where it is not implemented (yet)). + Exception thrown when trying to merge two gates which are not mergeable. + + This exception is also thrown if the merging is not implemented (yet)). """ - pass class NotInvertible(Exception): """ - Exception thrown when trying to invert a gate which is not invertable (or - where the inverse is not implemented (yet)). + Exception thrown when trying to invert a gate which is not invertable. + + This exception is also thrown if the inverse is not implemented (yet). """ - pass -class BasicGate(object): - """ - Base class of all gates. - """ +class BasicGate: + """Base class of all gates. (Don't use it directly but derive from it).""" + def __init__(self): """ Initialize a basic gate. @@ -74,27 +78,26 @@ def __init__(self): .. code-block:: python - ExampleGate | (a,b,c,d,e) + ExampleGate | (a, b, c, d, e) - where a and b are interchangeable. Then, call this function as - follows: + where a and b are interchangeable. Then, call this function as follows: .. code-block:: python - self.set_interchangeable_qubit_indices([[0,1]]) + self.set_interchangeable_qubit_indices([[0, 1]]) As another example, consider .. code-block:: python - ExampleGate2 | (a,b,c,d,e) + ExampleGate2 | (a, b, c, d, e) - where a and b are interchangeable and, in addition, c, d, and e - are interchangeable among themselves. Then, call this function as + where a and b are interchangeable and, in addition, c, d, and e are interchangeable among + themselves. Then, call this function as .. code-block:: python - self.set_interchangeable_qubit_indices([[0,1],[2,3,4]]) + self.set_interchangeable_qubit_indices([[0, 1], [2, 3, 4]]) """ self.interchangeable_qubit_indices = [] @@ -125,9 +128,8 @@ def make_tuple_of_qureg(qubits): """ Convert quantum input of "gate | quantum input" to internal formatting. - A Command object only accepts tuples of Quregs (list of Qubit objects) - as qubits input parameter. However, with this function we allow the - user to use a more flexible syntax: + A Command object only accepts tuples of Quregs (list of Qubit objects) as qubits input parameter. However, + with this function we allow the user to use a more flexible syntax: 1) Gate | qubit 2) Gate | [qubit0, qubit1] @@ -135,9 +137,8 @@ def make_tuple_of_qureg(qubits): 4) Gate | (qubit, ) 5) Gate | (qureg, qubit) - where qubit is a Qubit object and qureg is a Qureg object. This - function takes the right hand side of | and transforms it to the - correct input parameter of a Command object which is: + where qubit is a Qubit object and qureg is a Qureg object. This function takes the right hand side of | and + transforms it to the correct input parameter of a Command object which is: 1) -> Gate | ([qubit], ) 2) -> Gate | ([qubit0, qubit1], ) @@ -146,27 +147,27 @@ def make_tuple_of_qureg(qubits): 5) -> Gate | (qureg, [qubit]) Args: - qubits: a Qubit object, a list of Qubit objects, a Qureg object, - or a tuple of Qubit or Qureg objects (can be mixed). + qubits: a Qubit object, a list of Qubit objects, a Qureg object, or a tuple of Qubit or Qureg objects (can + be mixed). Returns: - Canonical representation (tuple): A tuple containing Qureg - (or list of Qubits) objects. + Canonical representation (tuple): A tuple containing Qureg (or list of Qubits) objects. """ if not isinstance(qubits, tuple): qubits = (qubits,) qubits = list(qubits) - for i in range(len(qubits)): - if isinstance(qubits[i], BasicQubit): - qubits[i] = [qubits[i]] + for i, qubit in enumerate(qubits): + if isinstance(qubit, BasicQubit): + qubits[i] = [qubit] return tuple(qubits) def generate_command(self, qubits): """ - Helper function to generate a command consisting of the gate and - the qubits being acted upon. + Generate a command. + + The command object created consists of the gate and the qubits being acted upon. Args: qubits: see BasicGate.make_tuple_of_qureg(qubits) @@ -178,7 +179,8 @@ def generate_command(self, qubits): engines = [q.engine for reg in qubits for q in reg] eng = engines[0] - assert all(e is eng for e in engines) + if not all(e is eng for e in engines): + raise ValueError('All qubits must belong to the same engine!') return Command(eng, self, qubits) def __or__(self, qubits): @@ -193,32 +195,116 @@ def __or__(self, qubits): 5) Gate | (qureg, qubit) Args: - qubits: a Qubit object, a list of Qubit objects, a Qureg object, - or a tuple of Qubit or Qureg objects (can be mixed). + qubits: a Qubit object, a list of Qubit objects, a Qureg object, or a tuple of Qubit or Qureg objects (can + be mixed). """ cmd = self.generate_command(qubits) apply_command(cmd) def __eq__(self, other): - """ Return True if equal (i.e., instance of same class). """ - return isinstance(other, self.__class__) + """ + Equal operator. - def __ne__(self, other): - return not self.__eq__(other) + Return True if instance of the same class, unless other is an instance of :class:MatrixGate, in which case + equality is to be checked by testing for existence and (approximate) equality of matrix representations. + """ + if isinstance(other, self.__class__): + return True + if isinstance(other, MatrixGate): + return NotImplemented + return False def __str__(self): + """Return a string representation of the object.""" raise NotImplementedError('This gate does not implement __str__.') + def to_string(self, symbols): # pylint: disable=unused-argument + """ + Return a string representation of the object. + + Achieve same function as str() but can be extended for configurable representation + """ + return str(self) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def is_identity(self): + """Return True if the gate is an identity gate. In this base class, always returns False.""" + return False + + +class MatrixGate(BasicGate): + """ + A gate class whose instances are defined by a matrix. + + Note: + Use this gate class only for gates acting on a small numbers of qubits. In general, consider instead using + one of the provided ProjectQ gates or define a new class as this allows the compiler to work symbolically. + + Example: + .. code-block:: python + + gate = MatrixGate([[0, 1], [1, 0]]) + gate | qubit + """ + + def __init__(self, matrix=None): + """ + Initialize a MatrixGate object. + + Args: + matrix(numpy.matrix): matrix which defines the gate. Default: None + """ + super().__init__() + self._matrix = np.matrix(matrix) if matrix is not None else None + + @property + def matrix(self): + """Access to the matrix property of this gate.""" + return self._matrix + + @matrix.setter + def matrix(self, matrix): + """Set the matrix property of this gate.""" + self._matrix = np.matrix(matrix) + + def __eq__(self, other): + """ + Equal operator. + + Return True only if both gates have a matrix representation and the matrices are (approximately) + equal. Otherwise return False. + """ + if not hasattr(other, 'matrix'): + return False + if not isinstance(self.matrix, np.matrix) or not isinstance(other.matrix, np.matrix): + raise TypeError("One of the gates doesn't have the correct type (numpy.matrix) for the matrix attribute.") + if self.matrix.shape == other.matrix.shape and np.allclose( + self.matrix, other.matrix, rtol=RTOL, atol=ATOL, equal_nan=False + ): + return True + return False + + def __str__(self): + """Return a string representation of the object.""" + return f"MatrixGate({str(self.matrix.tolist())})" + def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) + def get_inverse(self): + """Return the inverse of this gate.""" + return MatrixGate(np.linalg.inv(self.matrix)) + -class SelfInverseGate(BasicGate): +class SelfInverseGate(BasicGate): # pylint: disable=abstract-method """ Self-inverse basic gate class. - Automatic implementation of the get_inverse-member function for self- - inverse gates. + Automatic implementation of the get_inverse-member function for self-inverse gates. Example: .. code-block:: python @@ -226,20 +312,21 @@ class SelfInverseGate(BasicGate): # get_inverse(H) == H, it is a self-inverse gate: get_inverse(H) | qubit """ + def get_inverse(self): + """Return the inverse of this gate.""" return deepcopy(self) class BasicRotationGate(BasicGate): """ - Defines a base class of a rotation gate. + Base class of for all rotation gates. - A rotation gate has a continuous parameter (the angle), labeled 'angle' / - self.angle. Its inverse is the same gate with the negated argument. - Rotation gates of the same class can be merged by adding the angles. - The continuous parameter is modulo 4 * pi, self.angle is in the interval - [0, 4 * pi). + A rotation gate has a continuous parameter (the angle), labeled 'angle' / self.angle. Its inverse is the same gate + with the negated argument. Rotation gates of the same class can be merged by adding the angles. The continuous + parameter is modulo 4 * pi, self.angle is in the interval [0, 4 * pi). """ + def __init__(self, angle): """ Initialize a basic rotation gate. @@ -247,10 +334,10 @@ def __init__(self, angle): Args: angle (float): Angle of rotation (saved modulo 4 * pi) """ - BasicGate.__init__(self) - rounded_angle = round(float(angle) % (4. * math.pi), ANGLE_PRECISION) + super().__init__() + rounded_angle = round(float(angle) % (4.0 * math.pi), ANGLE_PRECISION) if rounded_angle > 4 * math.pi - ANGLE_TOLERANCE: - rounded_angle = 0. + rounded_angle = 0.0 self.angle = rounded_angle def __str__(self): @@ -263,7 +350,21 @@ def __str__(self): [CLASSNAME]([ANGLE]) """ - return str(self.__class__.__name__) + "(" + str(self.angle) + ")" + return self.to_string() + + def to_string(self, symbols=False): + """ + Return the string representation of a BasicRotationGate. + + Args: + symbols (bool): uses the pi character and round the angle for a more user friendly display if True, full + angle written in radian otherwise. + """ + if symbols: + angle = f"({str(round(self.angle / math.pi, 3))}{unicodedata.lookup('GREEK SMALL LETTER PI')})" + else: + angle = f"({str(self.angle)})" + return str(self.__class__.__name__) + angle def tex_str(self): """ @@ -275,31 +376,25 @@ def tex_str(self): [CLASSNAME]$_[ANGLE]$ """ - return str(self.__class__.__name__) + "$_{" + str(self.angle) + "}$" + return f"{str(self.__class__.__name__)}$_{{{str(round(self.angle / math.pi, 3))}\\pi}}$" def get_inverse(self): - """ - Return the inverse of this rotation gate (negate the angle, return new - object). - """ + """Return the inverse of this rotation gate (negate the angle, return new object).""" if self.angle == 0: return self.__class__(0) - else: - return self.__class__(-self.angle + 4 * math.pi) + return self.__class__(-self.angle + 4 * math.pi) def get_merged(self, other): """ Return self merged with another gate. - Default implementation handles rotation gate of the same type, where - angles are simply added. + Default implementation handles rotation gate of the same type, where angles are simply added. Args: other: Rotation gate of same type. Raises: - NotMergeable: For non-rotation gates or rotation gates of - different type. + NotMergeable: For non-rotation gates or rotation gates of different type. Returns: New object representing the merged gates. @@ -309,29 +404,29 @@ def get_merged(self, other): raise NotMergeable("Can't merge different types of rotation gates.") def __eq__(self, other): - """ Return True if same class and same rotation angle. """ + """Return True if same class and same rotation angle.""" if isinstance(other, self.__class__): return self.angle == other.angle - else: - return False - - def __ne__(self, other): - return not self.__eq__(other) + return False def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) + def is_identity(self): + """Return True if the gate is equivalent to an Identity gate.""" + return self.angle in (0.0, 4 * math.pi) + class BasicPhaseGate(BasicGate): """ - Defines a base class of a phase gate. + Base class for all phase gates. - A phase gate has a continuous parameter (the angle), labeled 'angle' / - self.angle. Its inverse is the same gate with the negated argument. - Phase gates of the same class can be merged by adding the angles. - The continuous parameter is modulo 2 * pi, self.angle is in the interval - [0, 2 * pi). + A phase gate has a continuous parameter (the angle), labeled 'angle' / self.angle. Its inverse is the same gate + with the negated argument. Phase gates of the same class can be merged by adding the angles. The continuous + parameter is modulo 2 * pi, self.angle is in the interval [0, 2 * pi). """ + def __init__(self, angle): """ Initialize a basic rotation gate. @@ -339,10 +434,10 @@ def __init__(self, angle): Args: angle (float): Angle of rotation (saved modulo 2 * pi) """ - BasicGate.__init__(self) - rounded_angle = round(float(angle) % (2. * math.pi), ANGLE_PRECISION) + super().__init__() + rounded_angle = round(float(angle) % (2.0 * math.pi), ANGLE_PRECISION) if rounded_angle > 2 * math.pi - ANGLE_TOLERANCE: - rounded_angle = 0. + rounded_angle = 0.0 self.angle = rounded_angle def __str__(self): @@ -355,7 +450,7 @@ def __str__(self): [CLASSNAME]([ANGLE]) """ - return str(self.__class__.__name__) + "(" + str(self.angle) + ")" + return f"{str(self.__class__.__name__)}({str(self.angle)})" def tex_str(self): """ @@ -367,24 +462,19 @@ def tex_str(self): [CLASSNAME]$_[ANGLE]$ """ - return str(self.__class__.__name__) + "$_{" + str(self.angle) + "}$" + return f"{str(self.__class__.__name__)}$_{{{str(self.angle)}}}$" def get_inverse(self): - """ - Return the inverse of this rotation gate (negate the angle, return new - object). - """ + """Return the inverse of this rotation gate (negate the angle, return new object).""" if self.angle == 0: return self.__class__(0) - else: - return self.__class__(-self.angle + 2 * math.pi) + return self.__class__(-self.angle + 2 * math.pi) def get_merged(self, other): """ Return self merged with another gate. - Default implementation handles rotation gate of the same type, where - angles are simply added. + Default implementation handles rotation gate of the same type, where angles are simply added. Args: other: Rotation gate of same type. @@ -401,45 +491,39 @@ def get_merged(self, other): raise NotMergeable("Can't merge different types of rotation gates.") def __eq__(self, other): - """ Return True if same class and same rotation angle. """ + """Return True if same class and same rotation angle.""" if isinstance(other, self.__class__): return self.angle == other.angle - else: - return False - - def __ne__(self, other): - return not self.__eq__(other) + return False def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) # Classical instruction gates never have control qubits. -class ClassicalInstructionGate(BasicGate): +class ClassicalInstructionGate(BasicGate): # pylint: disable=abstract-method """ Classical instruction gate. - Base class for all gates which are not quantum gates in the typical sense, - e.g., measurement, allocation/deallocation, ... + Base class for all gates which are not quantum gates in the typical sense, e.g., measurement, + allocation/deallocation, ... """ - pass -class FastForwardingGate(ClassicalInstructionGate): +class FastForwardingGate(ClassicalInstructionGate): # pylint: disable=abstract-method """ - Base class for classical instruction gates which require a fast-forward - through compiler engines that cache / buffer gates. Examples include - Measure and Deallocate, which both should be executed asap, such - that Measurement results are available and resources are freed, - respectively. + Base class for fast-forward gates. + + Base class for classical instruction gates which require a fast-forward through compiler engines that cache / + buffer gates. Examples include Measure and Deallocate, which both should be executed asap, such that Measurement + results are available and resources are freed, respectively. Note: - The only requirement is that FlushGate commands run the entire - circuit. FastForwardingGate objects can be used but the user cannot - expect a measurement result to be available for all back-ends when - calling only Measure. E.g., for the IBM Quantum Experience back-end, - sending the circuit for each Measure-gate would be too inefficient, - which is why a final + The only requirement is that FlushGate commands run the entire circuit. FastForwardingGate objects can be used + but the user cannot expect a measurement result to be available for all back-ends when calling only + Measure. E.g., for the IBM Quantum Experience back-end, sending the circuit for each Measure-gate would be too + inefficient, which is why a final .. code-block: python @@ -447,54 +531,51 @@ class FastForwardingGate(ClassicalInstructionGate): is required before the circuit gets sent through the API. """ - pass class BasicMathGate(BasicGate): """ Base class for all math gates. - It allows efficient emulation by providing a mathematical representation - which is given by the concrete gate which derives from this base class. + It allows efficient emulation by providing a mathematical representation which is given by the concrete gate which + derives from this base class. The AddConstant gate, for example, registers a function of the form .. code-block:: python def add(x): - return (x+a,) + return (x + a,) - upon initialization. More generally, the function takes integers as - parameters and returns a tuple / list of outputs, each entry corresponding - to the function input. As an example, consider out-of-place - multiplication, which takes two input registers and adds the result into a - third, i.e., (a,b,c) -> (a,b,c+a*b). The corresponding function then is + upon initialization. More generally, the function takes integers as parameters and returns a tuple / list of + outputs, each entry corresponding to the function input. As an example, consider out-of-place multiplication, + which takes two input registers and adds the result into a third, i.e., (a,b,c) -> (a,b,c+a*b). The corresponding + function then is .. code-block:: python - def multiply(a,b,c) - return (a,b,c+a*b) + def multiply(a, b, c): + return (a, b, c + a * b) """ + def __init__(self, math_fun): """ - Initialize a BasicMathGate by providing the mathematical function that - it implements. + Initialize a BasicMathGate by providing the mathematical function that it implements. Args: - math_fun (function): Function which takes as many int values as - input, as the gate takes registers. For each of these values, - it then returns the output (i.e., it returns a list/tuple of - output values). + math_fun (function): Function which takes as many int values as input, as the gate takes registers. For + each of these values, it then returns the output (i.e., it returns a list/tuple of output values). Example: .. code-block:: python - def add(a,b): - return (a,a+b) - BasicMathGate.__init__(self, add) + def add(a, b): + return (a, a + b) + - If the gate acts on, e.g., fixed point numbers, the number of bits per - register is also required in order to describe the action of such a - mathematical gate. For this reason, there is + super().__init__(add) + + If the gate acts on, e.g., fixed point numbers, the number of bits per register is also required in order to + describe the action of such a mathematical gate. For this reason, there is .. code-block:: python @@ -507,32 +588,37 @@ def add(a,b): def get_math_function(self, qubits): n = len(qubits[0]) - scal = 2.**n + scal = 2.0**n + def math_fun(a): return (int(scal * (math.sin(math.pi * a / scal))),) + return math_fun """ - BasicGate.__init__(self) + super().__init__() + + def math_function(arg): + return list(math_fun(*arg)) - def math_function(x): - return list(math_fun(*x)) self._math_function = math_function def __str__(self): + """Return a string representation of the object.""" return "MATH" - def get_math_function(self, qubits): + def get_math_function(self, qubits): # pylint: disable=unused-argument """ - Return the math function which corresponds to the action of this math - gate, given the input to the gate (a tuple of quantum registers). + Get the math function associated with a BasicMathGate. + + Return the math function which corresponds to the action of this math gate, given the input to the gate (a + tuple of quantum registers). Args: - qubits (tuple): Qubits to which the math gate is being - applied. + qubits (tuple): Qubits to which the math gate is being applied. Returns: - math_fun (function): Python function describing the action of this - gate. (See BasicMathGate.__init__ for an example). + math_fun (function): Python function describing the action of this gate. (See BasicMathGate.__init__ for + an example). """ return self._math_function diff --git a/projectq/ops/_basics_test.py b/projectq/ops/_basics_test.py index fead34616..35b19986d 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -11,19 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._basics.""" -import pytest -from copy import deepcopy import math -from projectq.types import Qubit, Qureg -from projectq.ops import Command +import numpy as np +import pytest + from projectq import MainEngine from projectq.cengines import DummyEngine - -from projectq.ops import _basics +from projectq.ops import Command, X, _basics +from projectq.types import Qubit, Qureg, WeakQubitRef @pytest.fixture @@ -66,26 +64,29 @@ def test_basic_gate_generate_command(main_engine): qureg = Qureg([qubit2, qubit3]) basic_gate = _basics.BasicGate() command1 = basic_gate.generate_command(qubit0) - assert command1 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command1 == Command(main_engine, basic_gate, ([qubit0],)) command2 = basic_gate.generate_command([qubit0, qubit1]) - assert command2 == Command(main_engine, basic_gate, - ([qubit0, qubit1],)) + assert command2 == Command(main_engine, basic_gate, ([qubit0, qubit1],)) command3 = basic_gate.generate_command(qureg) - assert command3 == Command(main_engine, basic_gate, - (qureg,)) + assert command3 == Command(main_engine, basic_gate, (qureg,)) command4 = basic_gate.generate_command((qubit0,)) - assert command4 == Command(main_engine, basic_gate, - ([qubit0],)) + assert command4 == Command(main_engine, basic_gate, ([qubit0],)) command5 = basic_gate.generate_command((qureg, qubit0)) - assert command5 == Command(main_engine, basic_gate, - (qureg, [qubit0])) + assert command5 == Command(main_engine, basic_gate, (qureg, [qubit0])) + + +def test_basic_gate_generate_command_invalid(): + qb0 = WeakQubitRef(1, idx=0) + qb1 = WeakQubitRef(2, idx=0) + + basic_gate = _basics.BasicGate() + with pytest.raises(ValueError): + basic_gate.generate_command([qb0, qb1]) def test_basic_gate_or(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) qubit0 = Qubit(main_engine, 0) qubit1 = Qubit(main_engine, 1) qubit2 = Qubit(main_engine, 2) @@ -107,8 +108,7 @@ def test_basic_gate_or(): for cmd in saving_backend.received_commands: if not isinstance(cmd.gate, _basics.FastForwardingGate): received_commands.append(cmd) - assert received_commands == ([command1, command2, command3, command4, - command5]) + assert received_commands == ([command1, command2, command3, command4, command5]) def test_basic_gate_compare(): @@ -116,6 +116,12 @@ def test_basic_gate_compare(): gate2 = _basics.BasicGate() assert gate1 == gate2 assert not (gate1 != gate2) + gate3 = _basics.MatrixGate() + gate3.matrix = np.matrix([[1, 0], [0, -1]]) + assert gate1 != gate3 + gate4 = _basics.MatrixGate() + gate4.matrix = [[1, 0], [0, -1]] + assert gate4 == gate3 def test_comparing_different_gates(): @@ -145,9 +151,15 @@ def test_self_inverse_gate(): assert id(self_inverse_gate.get_inverse()) != id(self_inverse_gate) -@pytest.mark.parametrize("input_angle, modulo_angle", - [(2.0, 2.0), (17., 4.4336293856408275), - (-0.5 * math.pi, 3.5 * math.pi), (4 * math.pi, 0)]) +@pytest.mark.parametrize( + "input_angle, modulo_angle", + [ + (2.0, 2.0), + (17.0, 4.4336293856408275), + (-0.5 * math.pi, 3.5 * math.pi), + (4 * math.pi, 0), + ], +) def test_basic_rotation_gate_init(input_angle, modulo_angle): # Test internal representation gate = _basics.BasicRotationGate(input_angle) @@ -155,19 +167,20 @@ def test_basic_rotation_gate_init(input_angle, modulo_angle): def test_basic_rotation_gate_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert str(basic_rotation_gate) == "BasicRotationGate(0.5)" + gate = _basics.BasicRotationGate(math.pi) + assert str(gate) == "BasicRotationGate(3.14159265359)" + assert gate.to_string(symbols=True) == "BasicRotationGate(1.0π)" + assert gate.to_string(symbols=False) == "BasicRotationGate(3.14159265359)" def test_basic_rotation_tex_str(): - basic_rotation_gate = _basics.BasicRotationGate(0.5) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.5}$" - basic_rotation_gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) - assert basic_rotation_gate.tex_str() == "BasicRotationGate$_{0.0}$" + gate = _basics.BasicRotationGate(0.5 * math.pi) + assert gate.tex_str() == "BasicRotationGate$_{0.5\\pi}$" + gate = _basics.BasicRotationGate(4 * math.pi - 1e-13) + assert gate.tex_str() == "BasicRotationGate$_{0.0\\pi}$" -@pytest.mark.parametrize("input_angle, inverse_angle", - [(2.0, -2.0 + 4 * math.pi), (-0.5, 0.5), (0.0, 0)]) +@pytest.mark.parametrize("input_angle, inverse_angle", [(2.0, -2.0 + 4 * math.pi), (-0.5, 0.5), (0.0, 0)]) def test_basic_rotation_gate_get_inverse(input_angle, inverse_angle): basic_rotation_gate = _basics.BasicRotationGate(input_angle) inverse = basic_rotation_gate.get_inverse() @@ -186,6 +199,19 @@ def test_basic_rotation_gate_get_merged(): assert merged_gate == basic_rotation_gate3 +def test_basic_rotation_gate_is_identity(): + basic_rotation_gate1 = _basics.BasicRotationGate(0.0) + basic_rotation_gate2 = _basics.BasicRotationGate(1.0 * math.pi) + basic_rotation_gate3 = _basics.BasicRotationGate(2.0 * math.pi) + basic_rotation_gate4 = _basics.BasicRotationGate(3.0 * math.pi) + basic_rotation_gate5 = _basics.BasicRotationGate(4.0 * math.pi) + assert basic_rotation_gate1.is_identity() + assert not basic_rotation_gate2.is_identity() + assert not basic_rotation_gate3.is_identity() + assert not basic_rotation_gate4.is_identity() + assert basic_rotation_gate5.is_identity() + + def test_basic_rotation_gate_comparison_and_hash(): basic_rotation_gate1 = _basics.BasicRotationGate(0.5) basic_rotation_gate2 = _basics.BasicRotationGate(0.5) @@ -198,8 +224,8 @@ def test_basic_rotation_gate_comparison_and_hash(): # Test __ne__: assert basic_rotation_gate4 != basic_rotation_gate1 # Test one gate close to 4*pi the other one close to 0 - basic_rotation_gate5 = _basics.BasicRotationGate(1.e-13) - basic_rotation_gate6 = _basics.BasicRotationGate(4 * math.pi - 1.e-13) + basic_rotation_gate5 = _basics.BasicRotationGate(1.0e-13) + basic_rotation_gate6 = _basics.BasicRotationGate(4 * math.pi - 1.0e-13) assert basic_rotation_gate5 == basic_rotation_gate6 assert basic_rotation_gate6 == basic_rotation_gate5 assert hash(basic_rotation_gate5) == hash(basic_rotation_gate6) @@ -209,9 +235,15 @@ def test_basic_rotation_gate_comparison_and_hash(): assert basic_rotation_gate2 != _basics.BasicRotationGate(0.5 + 2 * math.pi) -@pytest.mark.parametrize("input_angle, modulo_angle", - [(2.0, 2.0), (17., 4.4336293856408275), - (-0.5 * math.pi, 1.5 * math.pi), (2 * math.pi, 0)]) +@pytest.mark.parametrize( + "input_angle, modulo_angle", + [ + (2.0, 2.0), + (17.0, 4.4336293856408275), + (-0.5 * math.pi, 1.5 * math.pi), + (2 * math.pi, 0), + ], +) def test_basic_phase_gate_init(input_angle, modulo_angle): # Test internal representation gate = _basics.BasicPhaseGate(input_angle) @@ -230,8 +262,7 @@ def test_basic_phase_tex_str(): assert basic_rotation_gate.tex_str() == "BasicPhaseGate$_{0.0}$" -@pytest.mark.parametrize("input_angle, inverse_angle", - [(2.0, -2.0 + 2 * math.pi), (-0.5, 0.5), (0.0, 0)]) +@pytest.mark.parametrize("input_angle, inverse_angle", [(2.0, -2.0 + 2 * math.pi), (-0.5, 0.5), (0.0, 0)]) def test_basic_phase_gate_get_inverse(input_angle, inverse_angle): basic_phase_gate = _basics.BasicPhaseGate(input_angle) inverse = basic_phase_gate.get_inverse() @@ -262,8 +293,8 @@ def test_basic_phase_gate_comparison_and_hash(): # Test __ne__: assert basic_phase_gate4 != basic_phase_gate1 # Test one gate close to 2*pi the other one close to 0 - basic_phase_gate5 = _basics.BasicPhaseGate(1.e-13) - basic_phase_gate6 = _basics.BasicPhaseGate(2 * math.pi - 1.e-13) + basic_phase_gate5 = _basics.BasicPhaseGate(1.0e-13) + basic_phase_gate6 = _basics.BasicPhaseGate(2 * math.pi - 1.0e-13) assert basic_phase_gate5 == basic_phase_gate6 assert basic_phase_gate6 == basic_phase_gate5 assert hash(basic_phase_gate5) == hash(basic_phase_gate6) @@ -279,10 +310,42 @@ def my_math_function(a, b, c): class MyMultiplyGate(_basics.BasicMathGate): def __init__(self): - _basics.BasicMathGate.__init__(self, my_math_function) + super().__init__(my_math_function) gate = MyMultiplyGate() assert str(gate) == 'MATH' # Test a=2, b=3, and c=5 should give a=2, b=3, c=11 math_fun = gate.get_math_function(("qreg1", "qreg2", "qreg3")) assert math_fun([2, 3, 5]) == [2, 3, 11] + + +def test_matrix_gate(): + gate1 = _basics.MatrixGate() + gate2 = _basics.MatrixGate() + with pytest.raises(TypeError): + assert gate1 == gate2 + gate3 = _basics.MatrixGate([[0, 1], [1, 0]]) + gate4 = _basics.MatrixGate([[0, 1], [1, 0]]) + gate5 = _basics.MatrixGate([[1, 0], [0, -1]]) + assert gate3 == gate4 + assert gate4 != gate5 + with pytest.raises(TypeError): + assert gate1 != gate3 + with pytest.raises(TypeError): + assert gate3 != gate1 + gate6 = _basics.BasicGate() + assert gate6 != gate1 + assert gate6 != gate3 + assert gate1 != gate6 + assert gate3 != gate6 + gate7 = gate5.get_inverse() + gate8 = _basics.MatrixGate([[1, 0], [0, (1 + 1j) / math.sqrt(2)]]) + assert gate7 == gate5 + assert gate7 != gate8 + gate9 = _basics.MatrixGate([[1, 0], [0, (1 - 1j) / math.sqrt(2)]]) + gate10 = gate9.get_inverse() + assert gate10 == gate8 + assert gate3 == X + assert X == gate3 + assert str(gate3) == "MatrixGate([[0, 1], [1, 0]])" + assert hash(gate3) == hash("MatrixGate([[0, 1], [1, 0]])") diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 5186502fa..481efcc16 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -This file defines the apply_command function and the Command class. +The apply_command function and the Command class. When a gate is applied to qubits, e.g., @@ -21,16 +20,15 @@ CNOT | (qubit1, qubit2) -a Command object is generated which represents both the gate, qubits and -control qubits. This Command object then gets sent down the compilation -pipeline. +a Command object is generated which represents both the gate, qubits and control qubits. This Command object then gets +sent down the compilation pipeline. -In detail, the Gate object overloads the operator| (magic method __or__) -to generate a Command object which stores the qubits in a canonical order -using interchangeable qubit indices defined by the gate to allow the -optimizer to cancel the following two gates +In detail, the Gate object overloads the operator| (magic method __or__) to generate a Command object which stores the +qubits in a canonical order using interchangeable qubit indices defined by the gate to allow the optimizer to cancel +the following two gates .. code-block:: python + Swap | (qubit1, qubit2) Swap | (qubit2, qubit1) @@ -38,18 +36,30 @@ apply wrapper (apply_command). """ +import itertools from copy import deepcopy +from enum import IntEnum import projectq -from projectq.types import WeakQubitRef, Qureg +from projectq.types import Qureg, WeakQubitRef + + +class IncompatibleControlState(Exception): + """Exception thrown when trying to set two incompatible states for a control qubit.""" + + +class CtrlAll(IntEnum): + """Enum type to initialise the control state of qubits.""" + + Zero = 0 + One = 1 def apply_command(cmd): """ Apply a command. - Extracts the qubits-owning (target) engine from the Command object - and sends the Command to it. + Extracts the qubits-owning (target) engine from the Command object and sends the Command to it. Args: cmd (Command): Command to apply @@ -58,122 +68,124 @@ def apply_command(cmd): engine.receive([cmd]) -class Command(object): +class Command: # pylint: disable=too-many-instance-attributes """ - Class used as a container to store commands. If a gate is applied to - qubits, then the gate and qubits are saved in a command object. Qubits - are copied into WeakQubitRefs in order to allow early deallocation (would - be kept alive otherwise). WeakQubitRef qubits don't send deallocate gate - when destructed. + Class used as a container to store commands. + + If a gate is applied to qubits, then the gate and qubits are saved in a command object. Qubits are copied into + WeakQubitRefs in order to allow early deallocation (would be kept alive otherwise). WeakQubitRef qubits don't send + deallocate gate when destructed. Attributes: gate: The gate to execute - qubits: Tuple of qubit lists (e.g. Quregs). Interchangeable qubits - are stored in a unique order + qubits: Tuple of qubit lists (e.g. Quregs). Interchangeable qubits are stored in a unique order control_qubits: The Qureg of control qubits in a unique order engine: The engine (usually: MainEngine) - tags: The list of tag objects associated with this command - (e.g., ComputeTag, UncomputeTag, LoopTag, ...). tag objects need to - support ==, != (__eq__ and __ne__) for comparison as used in e.g. - TagRemover. New tags should always be added to the end of the list. - This means that if there are e.g. two LoopTags in a command, tag[0] - is from the inner scope while tag[1] is from the other scope as the - other scope receives the command after the inner scope LoopEngine - and hence adds its LoopTag to the end. + tags: The list of tag objects associated with this command (e.g., ComputeTag, UncomputeTag, LoopTag, ...). tag + objects need to support ==, != (__eq__ and __ne__) for comparison as used in e.g. TagRemover. New tags + should always be added to the end of the list. This means that if there are e.g. two LoopTags in a + command, tag[0] is from the inner scope while tag[1] is from the other scope as the other scope receives + the command after the inner scope LoopEngine and hence adds its LoopTag to the end. all_qubits: A tuple of control_qubits + qubits """ - def __init__(self, engine, gate, qubits, controls=(), tags=()): + def __init__( + self, engine, gate, qubits, controls=(), tags=(), control_state=CtrlAll.One + ): # pylint: disable=too-many-arguments """ Initialize a Command object. Note: - control qubits (Command.control_qubits) are stored as a - list of qubits, and command tags (Command.tags) as a list of tag- - objects. All functions within this class also work if - WeakQubitRefs are supplied instead of normal Qubit objects - (see WeakQubitRef). + control qubits (Command.control_qubits) are stored as a list of qubits, and command tags (Command.tags) as a + list of tag-objects. All functions within this class also work if WeakQubitRefs are supplied instead of + normal Qubit objects (see WeakQubitRef). Args: - engine (projectq.cengines.BasicEngine): - engine which created the qubit (mostly the MainEngine) - gate (projectq.ops.Gate): - Gate to be executed - qubits (tuple[Qureg]): - Tuple of quantum registers (to which the gate is applied) - controls (Qureg|list[Qubit]): - Qubits that condition the command. - tags (list[object]): - Tags associated with the command. + engine (projectq.cengines.BasicEngine): engine which created the qubit (mostly the MainEngine) + gate (projectq.ops.Gate): Gate to be executed + qubits (tuple[Qureg]): Tuple of quantum registers (to which the gate is applied) + controls (Qureg|list[Qubit]): Qubits that condition the command. + tags (list[object]): Tags associated with the command. + control_state(int,str,projectq.meta.CtrlAll) Control state for any control qubits """ - qubits = tuple([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qreg] - for qreg in qubits) + qubits = tuple([WeakQubitRef(qubit.engine, qubit.id) for qubit in qreg] for qreg in qubits) self.gate = gate self.tags = list(tags) self.qubits = qubits # property self.control_qubits = controls # property self.engine = engine # property + self.control_state = control_state # property @property def qubits(self): + """Qubits stored in a Command object.""" return self._qubits @qubits.setter def qubits(self, qubits): + """Set the qubits stored in a Command object.""" self._qubits = self._order_qubits(qubits) def __deepcopy__(self, memo): - """ Deepcopy implementation. Engine should stay a reference.""" - return Command(self.engine, - deepcopy(self.gate), - self.qubits, - list(self.control_qubits), - deepcopy(self.tags)) + """Deepcopy implementation. Engine should stay a reference.""" + return Command( + self.engine, + deepcopy(self.gate), + self.qubits, + list(self.control_qubits), + deepcopy(self.tags), + ) def get_inverse(self): """ Get the command object corresponding to the inverse of this command. - Inverts the gate (if possible) and creates a new command object from - the result. + Inverts the gate (if possible) and creates a new command object from the result. Raises: - NotInvertible: If the gate does not provide an inverse (see - BasicGate.get_inverse) + NotInvertible: If the gate does not provide an inverse (see BasicGate.get_inverse) + """ + return Command( + self._engine, + projectq.ops.get_inverse(self.gate), + self.qubits, + list(self.control_qubits), + deepcopy(self.tags), + ) + + def is_identity(self): + """ + Evaluate if the gate called in the command object is an identity gate. + + Returns: + True if the gate is equivalent to an Identity gate, False otherwise """ - return Command(self._engine, - projectq.ops.get_inverse(self.gate), - self.qubits, - list(self.control_qubits), - deepcopy(self.tags)) + return projectq.ops.is_identity(self.gate) def get_merged(self, other): """ - Merge this command with another one and return the merged command - object. + Merge this command with another one and return the merged command object. Args: other: Other command to merge with this one (self) Raises: - NotMergeable: if the gates don't supply a get_merged()-function - or can't be merged for other reasons. + NotMergeable: if the gates don't supply a get_merged()-function or can't be merged for other reasons. """ - if (self.tags == other.tags and self.all_qubits == other.all_qubits and - self.engine == other.engine): - return Command(self.engine, - self.gate.get_merged(other.gate), - self.qubits, - self.control_qubits, - deepcopy(self.tags)) + if self.tags == other.tags and self.all_qubits == other.all_qubits and self.engine == other.engine: + return Command( + self.engine, + self.gate.get_merged(other.gate), + self.qubits, + self.control_qubits, + deepcopy(self.tags), + ) raise projectq.ops.NotMergeable("Commands not mergeable.") def _order_qubits(self, qubits): """ - Order the given qubits according to their IDs (for unique comparison of - commands). + Order the given qubits according to their IDs (for unique comparison of commands). Args: qubits: Tuple of quantum registers (i.e., tuple of lists of qubits) @@ -184,11 +196,10 @@ def _order_qubits(self, qubits): # e.g. [[0,4],[1,2,3]] interchangeable_qubit_indices = self.interchangeable_qubit_indices for old_positions in interchangeable_qubit_indices: - new_positions = sorted(old_positions, - key=lambda x: ordered_qubits[x][0].id) + new_positions = sorted(old_positions, key=lambda x: ordered_qubits[x][0].id) qubits_new_order = [ordered_qubits[i] for i in new_positions] - for i in range(len(old_positions)): - ordered_qubits[old_positions[i]] = qubits_new_order[i] + for i, pos in enumerate(old_positions): + ordered_qubits[pos] = qubits_new_order[i] return tuple(ordered_qubits) @property @@ -196,34 +207,51 @@ def interchangeable_qubit_indices(self): """ Return nested list of qubit indices which are interchangeable. - Certain qubits can be interchanged (e.g., the qubit order for a Swap - gate). To ensure that only those are sorted when determining the - ordering (see _order_qubits), self.interchangeable_qubit_indices is - used. + Certain qubits can be interchanged (e.g., the qubit order for a Swap gate). To ensure that only those are sorted + when determining the ordering (see _order_qubits), self.interchangeable_qubit_indices is used. + Example: - If we can interchange qubits 0,1 and qubits 3,4,5, - then this function returns [[0,1],[3,4,5]] + If we can interchange qubits 0,1 and qubits 3,4,5, then this function returns [[0,1],[3,4,5]] """ return self.gate.interchangeable_qubit_indices @property def control_qubits(self): - """ Returns Qureg of control qubits.""" + """Return a Qureg of control qubits.""" return self._control_qubits @control_qubits.setter def control_qubits(self, qubits): """ - Set control_qubits to qubits + Set control_qubits to qubits. Args: control_qubits (Qureg): quantum register """ - self._control_qubits = ([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) + self._control_qubits = [WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits] self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) - def add_control_qubits(self, qubits): + @property + def control_state(self): + """Return the state of the control qubits (ie. either positively- or negatively-controlled).""" + return self._control_state + + @control_state.setter + def control_state(self, state): + """ + Set control_state to state. + + Args: + state (int,str,projectq.meta.CtrtAll): state of control qubit (ie. positive or negative) + """ + # NB: avoid circular imports + from projectq.meta import ( # pylint: disable=import-outside-toplevel + canonical_ctrl_state, + ) + + self._control_state = canonical_ctrl_state(state, len(self._control_qubits)) + + def add_control_qubits(self, qubits, state=CtrlAll.One): """ Add (additional) control qubits to this command object. @@ -232,14 +260,31 @@ def add_control_qubits(self, qubits): thus early deallocation of qubits. Args: - qubits (list of Qubit objects): List of qubits which control this - gate, i.e., the gate is only executed if all qubits are - in state 1. + qubits (list of Qubit objects): List of qubits which control this gate + state (int,str,CtrlAll): Control state (ie. positive or negative) for the qubits being added as + control qubits. """ - assert(isinstance(qubits, list)) - self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) - for qubit in qubits]) - self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) + # NB: avoid circular imports + from projectq.meta import ( # pylint: disable=import-outside-toplevel + canonical_ctrl_state, + ) + + if not isinstance(qubits, list): + raise ValueError('Control qubits must be a list of qubits!') + self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits]) + self._control_state += canonical_ctrl_state(state, len(qubits)) + + zipped = sorted(zip(self._control_qubits, self._control_state), key=lambda x: x[0].id) + unzipped_qubit, unzipped_state = zip(*zipped) + self._control_qubits, self._control_state = list(unzipped_qubit), ''.join(unzipped_state) + + # Make sure that we do not have contradicting control states for any control qubits + for _, data in itertools.groupby(zipped, key=lambda x: x[0].id): + qubits, states = list(zip(*data)) + if len(set(states)) != 1: + raise IncompatibleControlState( + f'Control qubits {list(qubits)} cannot have conflicting control states: {states}' + ) @property def all_qubits(self): @@ -254,10 +299,7 @@ def all_qubits(self): @property def engine(self): - """ - Return engine to which the qubits belong / on which the gates are - executed. - """ + """Return engine to which the qubits belong / on which the gates are executed.""" return self._engine @engine.setter @@ -285,21 +327,22 @@ def __eq__(self, other): Returns: True if Command objects are equal (same gate, applied to same qubits; ordered modulo interchangeability; and same tags) """ - if (isinstance(other, self.__class__) and - self.gate == other.gate and - self.tags == other.tags and - self.engine == other.engine and - self.all_qubits == other.all_qubits): + if ( + isinstance(other, self.__class__) + and self.gate == other.gate + and self.tags == other.tags + and self.engine == other.engine + and self.all_qubits == other.all_qubits + ): return True return False - def __ne__(self, other): - return not self.__eq__(other) - def __str__(self): - """ - Get string representation of this Command object. - """ + """Return a string representation of the object.""" + return self.to_string() + + def to_string(self, symbols=False): + """Get string representation of this Command object.""" qubits = self.qubits ctrlqubits = self.control_qubits if len(ctrlqubits) > 0: @@ -312,6 +355,6 @@ def __str__(self): for qreg in qubits: qstring += str(Qureg(qreg)) qstring += ", " - qstring = qstring[:-2] + " )" + qstring = f"{qstring[:-2]} )" cstring = "C" * len(ctrlqubits) - return cstring + str(self.gate) + " | " + qstring + return f"{cstring + self.gate.to_string(symbols)} | {qstring}" diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index ae1407836..19db3644c 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,21 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._command.""" -from copy import deepcopy import math +import sys +from copy import deepcopy + import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.meta import ComputeTag -from projectq.ops import BasicGate, Rx, NotMergeable +from projectq.meta import ComputeTag, canonical_ctrl_state +from projectq.ops import BasicGate, CtrlAll, NotMergeable, Rx, _command from projectq.types import Qubit, Qureg, WeakQubitRef -from projectq.ops import _command - @pytest.fixture def main_engine(): @@ -36,8 +35,8 @@ def test_command_init(main_engine): qureg0 = Qureg([Qubit(main_engine, 0)]) qureg1 = Qureg([Qubit(main_engine, 1)]) qureg2 = Qureg([Qubit(main_engine, 2)]) - qureg3 = Qureg([Qubit(main_engine, 3)]) - qureg4 = Qureg([Qubit(main_engine, 4)]) + # qureg3 = Qureg([Qubit(main_engine, 3)]) + # qureg4 = Qureg([Qubit(main_engine, 4)]) gate = BasicGate() cmd = _command.Command(main_engine, gate, (qureg0, qureg1, qureg2)) assert cmd.gate == gate @@ -46,18 +45,16 @@ def test_command_init(main_engine): for cmd_qureg, expected_qureg in zip(cmd.qubits, expected_tuple): assert cmd_qureg[0].id == expected_qureg[0].id # Testing that Qubits are now WeakQubitRef objects - assert type(cmd_qureg[0]) == WeakQubitRef + assert type(cmd_qureg[0]) is WeakQubitRef assert cmd._engine == main_engine # Test that quregs are ordered if gate has interchangeable qubits: symmetric_gate = BasicGate() symmetric_gate.interchangeable_qubit_indices = [[0, 1]] - symmetric_cmd = _command.Command(main_engine, symmetric_gate, - (qureg2, qureg1, qureg0)) + symmetric_cmd = _command.Command(main_engine, symmetric_gate, (qureg2, qureg1, qureg0)) assert cmd.gate == gate assert cmd.tags == [] expected_ordered_tuple = (qureg1, qureg2, qureg0) - for cmd_qureg, expected_qureg in zip(symmetric_cmd.qubits, - expected_ordered_tuple): + for cmd_qureg, expected_qureg in zip(symmetric_cmd.qubits, expected_ordered_tuple): assert cmd_qureg[0].id == expected_qureg[0].id assert symmetric_cmd._engine == main_engine @@ -121,6 +118,7 @@ def test_command_get_merged(main_engine): expected_cmd = _command.Command(main_engine, Rx(1.0), (qubit,)) expected_cmd.add_control_qubits(ctrl_qubit) expected_cmd.tags = ["TestTag"] + assert merged_cmd == expected_cmd # Don't merge commands as different control qubits cmd3 = _command.Command(main_engine, Rx(0.5), (qubit,)) cmd3.tags = ["TestTag"] @@ -133,6 +131,19 @@ def test_command_get_merged(main_engine): cmd.get_merged(cmd4) +def test_command_is_identity(main_engine): + qubit = main_engine.allocate_qubit() + qubit2 = main_engine.allocate_qubit() + cmd = _command.Command(main_engine, Rx(0.0), (qubit,)) + cmd2 = _command.Command(main_engine, Rx(0.5), (qubit2,)) + inverse_cmd = cmd.get_inverse() + inverse_cmd2 = cmd2.get_inverse() + assert inverse_cmd.gate.is_identity() + assert cmd.gate.is_identity() + assert not inverse_cmd2.gate.is_identity() + assert not cmd2.gate.is_identity() + + def test_command_order_qubits(main_engine): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) @@ -160,18 +171,54 @@ def test_command_interchangeable_qubit_indices(main_engine): qubit5 = Qureg([Qubit(main_engine, 5)]) input_tuple = (qubit4, qubit5, qubit3, qubit2, qubit1, qubit0) cmd = _command.Command(main_engine, gate, input_tuple) - assert (cmd.interchangeable_qubit_indices == [[0, 4, 5], [1, 2]] or - cmd.interchangeable_qubit_indices == [[1, 2], [0, 4, 5]]) + assert cmd.interchangeable_qubit_indices == [ + [0, 4, 5], + [1, 2], + ] or cmd.interchangeable_qubit_indices == [[1, 2], [0, 4, 5]] -def test_commmand_add_control_qubits(main_engine): +@pytest.mark.parametrize( + 'state', + [0, 1, '0', '1', CtrlAll.One, CtrlAll.Zero], + ids=['int(0)', 'int(1)', 'str(0)', 'str(1)', 'CtrlAll.One', 'CtrlAll.Zero'], +) +def test_command_add_control_qubits_one(main_engine, state): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) - qubit2 = Qureg([Qubit(main_engine, 2)]) cmd = _command.Command(main_engine, Rx(0.5), (qubit0,)) - cmd.add_control_qubits(qubit2 + qubit1) + cmd.add_control_qubits(qubit1, state=state) + assert cmd.control_qubits[0].id == 1 + assert cmd.control_state == canonical_ctrl_state(state, 1) + + with pytest.raises(ValueError): + cmd.add_control_qubits(qubit0[0]) + + +@pytest.mark.parametrize( + 'state', + [0, 1, 2, 3, '00', '01', '10', '11', CtrlAll.One, CtrlAll.Zero], + ids=[ + 'int(0)', + 'int(1)', + 'int(2)', + 'int(3)', + 'str(00)', + 'str(01)', + 'str(10)', + 'str(1)', + 'CtrlAll.One', + 'CtrlAll.Zero', + ], +) +def test_command_add_control_qubits_two(main_engine, state): + qubit0 = Qureg([Qubit(main_engine, 0)]) + qubit1 = Qureg([Qubit(main_engine, 1)]) + qubit2 = Qureg([Qubit(main_engine, 2)]) + qubit3 = Qureg([Qubit(main_engine, 3)]) + cmd = _command.Command(main_engine, Rx(0.5), (qubit0,), qubit1) + cmd.add_control_qubits(qubit2 + qubit3, state) assert cmd.control_qubits[0].id == 1 - assert cmd.control_qubits[1].id == 2 + assert cmd.control_state == f"1{canonical_ctrl_state(state, 2)}" def test_command_all_qubits(main_engine): @@ -195,6 +242,10 @@ def test_command_engine(main_engine): assert id(cmd.control_qubits[0].engine) == id(main_engine) assert id(cmd.qubits[0][0].engine) == id(main_engine) + # Avoid raising exception upon Qubit destructions + qubit0[0].id = -1 + qubit1[0].id = -1 + def test_command_comparison(main_engine): qubit = Qureg([Qubit(main_engine, 0)]) @@ -229,12 +280,34 @@ def test_command_comparison(main_engine): assert cmd6 != cmd1 -def test_command_str(): +def test_command_str(main_engine): qubit = Qureg([Qubit(main_engine, 0)]) ctrl_qubit = Qureg([Qubit(main_engine, 1)]) - cmd = _command.Command(main_engine, Rx(0.5), (qubit,)) + cmd = _command.Command(main_engine, Rx(0.5 * math.pi), (qubit,)) cmd.tags = ["TestTag"] cmd.add_control_qubits(ctrl_qubit) - assert str(cmd) == "CRx(0.5) | ( Qureg[1], Qureg[0] )" - cmd2 = _command.Command(main_engine, Rx(0.5), (qubit,)) - assert str(cmd2) == "Rx(0.5) | Qureg[0]" + cmd2 = _command.Command(main_engine, Rx(0.5 * math.pi), (qubit,)) + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert str(cmd2) == "Rx(1.5707963268) | Qureg[0]" + + +def test_command_to_string(main_engine): + qubit = Qureg([Qubit(main_engine, 0)]) + ctrl_qubit = Qureg([Qubit(main_engine, 1)]) + cmd = _command.Command(main_engine, Rx(0.5 * math.pi), (qubit,)) + cmd.tags = ["TestTag"] + cmd.add_control_qubits(ctrl_qubit) + cmd2 = _command.Command(main_engine, Rx(0.5 * math.pi), (qubit,)) + + assert cmd.to_string(symbols=True) == "CRx(0.5π) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=True) == "Rx(0.5π) | Qureg[0]" + if sys.version_info.major == 3: + assert cmd.to_string(symbols=False) == "CRx(1.570796326795) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.570796326795) | Qureg[0]" + else: + assert cmd.to_string(symbols=False) == "CRx(1.5707963268) | ( Qureg[1], Qureg[0] )" + assert cmd2.to_string(symbols=False) == "Rx(1.5707963268) | Qureg[0]" diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py index 530fcce76..10527a868 100755 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -11,176 +11,232 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ +Definition of the basic set of quantum gates. + Contains definitions of standard gates such as * Hadamard (H) * Pauli-X (X / NOT) +* Pauli-Y (Y) * Pauli-Z (Z) +* S and its inverse (S / Sdagger) * T and its inverse (T / Tdagger) +* SqrtX gate (SqrtX) * Swap gate (Swap) +* SqrtSwap gate (SqrtSwap) +* Entangle (Entangle) * Phase gate (Ph) +* Rotation-X (Rx) +* Rotation-Y (Ry) * Rotation-Z (Rz) +* Rotation-XX on two qubits (Rxx) +* Rotation-YY on two qubits (Ryy) +* Rotation-ZZ on two qubits (Rzz) * Phase-shift (R) * Measurement (Measure) and meta gates, i.e., * Allocate / Deallocate qubits * Flush gate (end of circuit) +* Barrier +* FlipBits """ -import math import cmath -import warnings +import math import numpy as np -from projectq.ops import get_inverse -from ._basics import (BasicGate, - SelfInverseGate, - BasicRotationGate, - BasicPhaseGate, - ClassicalInstructionGate, - FastForwardingGate, - BasicMathGate) +from ._basics import ( + BasicGate, + BasicPhaseGate, + BasicRotationGate, + ClassicalInstructionGate, + FastForwardingGate, + SelfInverseGate, +) from ._command import apply_command -from projectq.types import BasicQubit +from ._metagates import get_inverse class HGate(SelfInverseGate): - """ Hadamard gate class """ + """Hadamard gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "H" @property def matrix(self): - return 1. / cmath.sqrt(2.) * np.matrix([[1, 1], [1, -1]]) + """Access to the matrix property of this gate.""" + return 1.0 / cmath.sqrt(2.0) * np.matrix([[1, 1], [1, -1]]) + #: Shortcut (instance of) :class:`projectq.ops.HGate` H = HGate() class XGate(SelfInverseGate): - """ Pauli-X gate class """ + """Pauli-X gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "X" @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[0, 1], [1, 0]]) + #: Shortcut (instance of) :class:`projectq.ops.XGate` X = NOT = XGate() class YGate(SelfInverseGate): - """ Pauli-Y gate class """ + """Pauli-Y gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "Y" @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[0, -1j], [1j, 0]]) + #: Shortcut (instance of) :class:`projectq.ops.YGate` Y = YGate() class ZGate(SelfInverseGate): - """ Pauli-Z gate class """ + """Pauli-Z gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "Z" @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[1, 0], [0, -1]]) + #: Shortcut (instance of) :class:`projectq.ops.ZGate` Z = ZGate() class SGate(BasicGate): - """ S gate class """ + """S gate class.""" + @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[1, 0], [0, 1j]]) def __str__(self): + """Return a string representation of the object.""" return "S" + #: Shortcut (instance of) :class:`projectq.ops.SGate` S = SGate() -#: Shortcut (instance of) :class:`projectq.ops.SGate` +#: Inverse (and shortcut) of :class:`projectq.ops.SGate` Sdag = Sdagger = get_inverse(S) class TGate(BasicGate): - """ T gate class """ + """T gate class.""" + @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[1, 0], [0, cmath.exp(1j * cmath.pi / 4)]]) def __str__(self): + """Return a string representation of the object.""" return "T" + #: Shortcut (instance of) :class:`projectq.ops.TGate` T = TGate() -#: Shortcut (instance of) :class:`projectq.ops.TGate` +#: Inverse (and shortcut) of :class:`projectq.ops.TGate` Tdag = Tdagger = get_inverse(T) class SqrtXGate(BasicGate): - """ Square-root X gate class """ + """Square-root X gate class.""" + @property def matrix(self): - return 0.5 * np.matrix([[1+1j, 1-1j], [1-1j, 1+1j]]) + """Access to the matrix property of this gate.""" + return 0.5 * np.matrix([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]]) def tex_str(self): + """Return the Latex string representation of a SqrtXGate.""" return r'$\sqrt{X}$' def __str__(self): + """Return a string representation of the object.""" return "SqrtX" + #: Shortcut (instance of) :class:`projectq.ops.SqrtXGate` SqrtX = SqrtXGate() -class SwapGate(SelfInverseGate, BasicMathGate): - """ Swap gate class (swaps 2 qubits) """ +class SwapGate(SelfInverseGate): + """Swap gate class (swaps 2 qubits).""" + def __init__(self): - BasicMathGate.__init__(self, lambda x, y: (y, x)) - SelfInverseGate.__init__(self) + """Initialize a Swap gate.""" + super().__init__() self.interchangeable_qubit_indices = [[0, 1]] def __str__(self): + """Return a string representation of the object.""" return "Swap" @property def matrix(self): + """Access to the matrix property of this gate.""" + # fmt: off return np.matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + # fmt: on + #: Shortcut (instance of) :class:`projectq.ops.SwapGate` Swap = SwapGate() class SqrtSwapGate(BasicGate): - """ Square-root Swap gate class """ + """Square-root Swap gate class.""" + def __init__(self): - BasicGate.__init__(self) + """Initialize a SqrtSwap gate.""" + super().__init__() self.interchangeable_qubit_indices = [[0, 1]] def __str__(self): + """Return a string representation of the object.""" return "SqrtSwap" @property def matrix(self): - return np.matrix([[1, 0, 0, 0], - [0, 0.5+0.5j, 0.5-0.5j, 0], - [0, 0.5-0.5j, 0.5+0.5j, 0], - [0, 0, 0, 1]]) + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [1, 0, 0, 0], + [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], + [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], + [0, 0, 0, 1], + ] + ) + #: Shortcut (instance of) :class:`projectq.ops.SqrtSwapGate` SqrtSwap = SqrtSwapGate() @@ -188,56 +244,125 @@ def matrix(self): class EntangleGate(BasicGate): """ - Entangle gate (Hadamard on first qubit, followed by CNOTs applied to all - other qubits). + Entangle gate class. + + (Hadamard on first qubit, followed by CNOTs applied to all other qubits). """ + def __str__(self): + """Return a string representation of the object.""" return "Entangle" + #: Shortcut (instance of) :class:`projectq.ops.EntangleGate` Entangle = EntangleGate() class Ph(BasicPhaseGate): - """ Phase gate (global phase) """ + """Phase gate (global phase).""" + @property def matrix(self): - return np.matrix([[cmath.exp(1j * self.angle), 0], - [0, cmath.exp(1j * self.angle)]]) + """Access to the matrix property of this gate.""" + return np.matrix([[cmath.exp(1j * self.angle), 0], [0, cmath.exp(1j * self.angle)]]) class Rx(BasicRotationGate): - """ RotationX gate class """ + """RotationX gate class.""" + @property def matrix(self): - return np.matrix([[math.cos(0.5 * self.angle), - -1j * math.sin(0.5 * self.angle)], - [-1j * math.sin(0.5 * self.angle), - math.cos(0.5 * self.angle)]]) + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [math.cos(0.5 * self.angle), -1j * math.sin(0.5 * self.angle)], + [-1j * math.sin(0.5 * self.angle), math.cos(0.5 * self.angle)], + ] + ) class Ry(BasicRotationGate): - """ RotationX gate class """ + """RotationY gate class.""" + @property def matrix(self): - return np.matrix([[math.cos(0.5 * self.angle), - -math.sin(0.5 * self.angle)], - [math.sin(0.5 * self.angle), - math.cos(0.5 * self.angle)]]) + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [math.cos(0.5 * self.angle), -math.sin(0.5 * self.angle)], + [math.sin(0.5 * self.angle), math.cos(0.5 * self.angle)], + ] + ) class Rz(BasicRotationGate): - """ RotationZ gate class """ + """RotationZ gate class.""" + @property def matrix(self): - return np.matrix([[cmath.exp(-.5 * 1j * self.angle), 0], - [0, cmath.exp(.5 * 1j * self.angle)]]) + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [cmath.exp(-0.5 * 1j * self.angle), 0], + [0, cmath.exp(0.5 * 1j * self.angle)], + ] + ) + + +class Rxx(BasicRotationGate): + """RotationXX gate class.""" + + @property + def matrix(self): + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [cmath.cos(0.5 * self.angle), 0, 0, -1j * cmath.sin(0.5 * self.angle)], + [0, cmath.cos(0.5 * self.angle), -1j * cmath.sin(0.5 * self.angle), 0], + [0, -1j * cmath.sin(0.5 * self.angle), cmath.cos(0.5 * self.angle), 0], + [-1j * cmath.sin(0.5 * self.angle), 0, 0, cmath.cos(0.5 * self.angle)], + ] + ) + + +class Ryy(BasicRotationGate): + """RotationYY gate class.""" + + @property + def matrix(self): + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [cmath.cos(0.5 * self.angle), 0, 0, 1j * cmath.sin(0.5 * self.angle)], + [0, cmath.cos(0.5 * self.angle), -1j * cmath.sin(0.5 * self.angle), 0], + [0, -1j * cmath.sin(0.5 * self.angle), cmath.cos(0.5 * self.angle), 0], + [1j * cmath.sin(0.5 * self.angle), 0, 0, cmath.cos(0.5 * self.angle)], + ] + ) + + +class Rzz(BasicRotationGate): + """RotationZZ gate class.""" + + @property + def matrix(self): + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [cmath.exp(-0.5 * 1j * self.angle), 0, 0, 0], + [0, cmath.exp(0.5 * 1j * self.angle), 0, 0], + [0, 0, cmath.exp(0.5 * 1j * self.angle), 0], + [0, 0, 0, cmath.exp(-0.5 * 1j * self.angle)], + ] + ) class R(BasicPhaseGate): - """ Phase-shift gate (equivalent to Rz up to a global phase) """ + """Phase-shift gate (equivalent to Rz up to a global phase).""" + @property def matrix(self): + """Access to the matrix property of this gate.""" return np.matrix([[1, 0], [0, cmath.exp(1j * self.angle)]]) @@ -246,9 +371,8 @@ class FlushGate(FastForwardingGate): Flush gate (denotes the end of the circuit). Note: - All compiler engines (cengines) which cache/buffer gates are obligated - to flush and send all gates to the next compiler engine (followed by - the flush command). + All compiler engines (cengines) which cache/buffer gates are obligated to flush and send all gates to the next + compiler engine (followed by the flush command). Note: This gate is sent when calling @@ -261,20 +385,26 @@ class FlushGate(FastForwardingGate): """ def __str__(self): + """Return a string representation of the object.""" return "" class MeasureGate(FastForwardingGate): - """ Measurement gate class (for single qubits).""" + """Measurement gate class (for single qubits).""" + def __str__(self): + """Return a string representation of the object.""" return "Measure" def __or__(self, qubits): """ - Previously (ProjectQ <= v0.3.6) MeasureGate/Measure was allowed to be - applied to any number of quantum registers. Now the MeasureGate/Measure - is strictly a single qubit gate. In the coming releases the backward - compatibility will be removed! + Operator| overload which enables the syntax Gate | qubits. + + Previously (ProjectQ <= v0.3.6) MeasureGate/Measure was allowed to be applied to any number of quantum + registers. Now the MeasureGate/Measure is strictly a single qubit gate. + + Raises: + RuntimeError: Since ProjectQ v0.6.0 if the gate is applied to multiple qubits. """ num_qubits = 0 for qureg in self.make_tuple_of_qureg(qubits): @@ -282,59 +412,129 @@ def __or__(self, qubits): num_qubits += 1 cmd = self.generate_command(([qubit],)) apply_command(cmd) - if num_qubits > 1: - warnings.warn("Pending syntax change in future versions of " - "ProjectQ: \n Measure will be a single qubit gate " - "only. Use `All(Measure) | qureg` instead to " - "measure multiple qubits.") + if num_qubits > 1: # pragma: no cover + raise RuntimeError('Measure is a single qubit gate. Use All(Measure) | qureg instead') + #: Shortcut (instance of) :class:`projectq.ops.MeasureGate` Measure = MeasureGate() class AllocateQubitGate(ClassicalInstructionGate): - """ Qubit allocation gate class """ + """Qubit allocation gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "Allocate" def get_inverse(self): + """Return the inverse of this gate.""" return DeallocateQubitGate() + #: Shortcut (instance of) :class:`projectq.ops.AllocateQubitGate` Allocate = AllocateQubitGate() class DeallocateQubitGate(FastForwardingGate): - """ Qubit deallocation gate class """ + """Qubit deallocation gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "Deallocate" def get_inverse(self): + """Return the inverse of this gate.""" return Allocate + #: Shortcut (instance of) :class:`projectq.ops.DeallocateQubitGate` Deallocate = DeallocateQubitGate() class AllocateDirtyQubitGate(ClassicalInstructionGate): - """ Dirty qubit allocation gate class """ + """Dirty qubit allocation gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "AllocateDirty" def get_inverse(self): + """Return the inverse of this gate.""" return Deallocate + #: Shortcut (instance of) :class:`projectq.ops.AllocateDirtyQubitGate` AllocateDirty = AllocateDirtyQubitGate() class BarrierGate(BasicGate): - """ Barrier gate class """ + """Barrier gate class.""" + def __str__(self): + """Return a string representation of the object.""" return "Barrier" def get_inverse(self): + """Return the inverse of this gate.""" return Barrier + #: Shortcut (instance of) :class:`projectq.ops.BarrierGate` Barrier = BarrierGate() + + +class FlipBits(SelfInverseGate): + """Gate for flipping qubits by means of XGates.""" + + def __init__(self, bits_to_flip): + """ + Initialize a FlipBits gate. + + Example: + .. code-block:: python + + qureg = eng.allocate_qureg(2) + FlipBits([0, 1]) | qureg + + Args: + bits_to_flip(list[int]|list[bool]|str|int): int or array of 0/1, True/False, or string of 0/1 identifying + the qubits to flip. In case of int, the bits to flip are determined from the binary digits, with the + least significant bit corresponding to qureg[0]. If bits_to_flip is negative, exactly all qubits which + would not be flipped for the input -bits_to_flip-1 are flipped, i.e., bits_to_flip=-1 flips all qubits. + """ + super().__init__() + if isinstance(bits_to_flip, int): + self.bits_to_flip = bits_to_flip + else: + self.bits_to_flip = 0 + for i in reversed(list(bits_to_flip)): + bit = 0b1 if i == '1' or i == 1 or i is True else 0b0 + self.bits_to_flip = (self.bits_to_flip << 1) | bit + + def __str__(self): + """Return a string representation of the object.""" + return f"FlipBits({self.bits_to_flip})" + + def __or__(self, qubits): + """Operator| overload which enables the syntax Gate | qubits.""" + quregs_tuple = self.make_tuple_of_qureg(qubits) + if len(quregs_tuple) > 1: + raise ValueError( + f"{str(self)} can only be applied to qubits, quregs, arrays of qubits, " + "and tuples with one individual qubit" + ) + for qureg in quregs_tuple: + for i, qubit in enumerate(qureg): + if (self.bits_to_flip >> i) & 1: + XGate() | qubit + + def __eq__(self, other): + """Equal operator.""" + if isinstance(other, self.__class__): + return self.bits_to_flip == other.bits_to_flip + return False + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) diff --git a/projectq/ops/_gates_test.py b/projectq/ops/_gates_test.py index 002ab8d20..ab431b923 100755 --- a/projectq/ops/_gates_test.py +++ b/projectq/ops/_gates_test.py @@ -11,27 +11,23 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._gates.""" -import math import cmath +import math + import numpy as np import pytest -from projectq.ops import (get_inverse, SelfInverseGate, BasicRotationGate, - ClassicalInstructionGate, FastForwardingGate, - BasicGate) - -from projectq.ops import _gates +from projectq import MainEngine +from projectq.ops import All, FlipBits, Measure, _gates, get_inverse def test_h_gate(): gate = _gates.HGate() assert gate == gate.get_inverse() assert str(gate) == "H" - assert np.array_equal(gate.matrix, - 1. / math.sqrt(2) * np.matrix([[1, 1], [1, -1]])) + assert np.array_equal(gate.matrix, 1.0 / math.sqrt(2) * np.matrix([[1, 1], [1, -1]])) assert isinstance(_gates.H, _gates.HGate) @@ -72,9 +68,7 @@ def test_s_gate(): def test_t_gate(): gate = _gates.TGate() assert str(gate) == "T" - assert np.array_equal(gate.matrix, - np.matrix([[1, 0], - [0, cmath.exp(1j * cmath.pi / 4)]])) + assert np.array_equal(gate.matrix, np.matrix([[1, 0], [0, cmath.exp(1j * cmath.pi / 4)]])) assert isinstance(_gates.T, _gates.TGate) assert isinstance(_gates.Tdag, type(get_inverse(gate))) assert isinstance(_gates.Tdagger, type(get_inverse(gate))) @@ -83,10 +77,8 @@ def test_t_gate(): def test_sqrtx_gate(): gate = _gates.SqrtXGate() assert str(gate) == "SqrtX" - assert np.array_equal(gate.matrix, np.matrix([[0.5 + 0.5j, 0.5 - 0.5j], - [0.5 - 0.5j, 0.5 + 0.5j]])) - assert np.array_equal(gate.matrix * gate.matrix, - np.matrix([[0j, 1], [1, 0]])) + assert np.array_equal(gate.matrix, np.matrix([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]])) + assert np.array_equal(gate.matrix * gate.matrix, np.matrix([[0j, 1], [1, 0]])) assert isinstance(_gates.SqrtX, _gates.SqrtXGate) @@ -95,9 +87,7 @@ def test_swap_gate(): assert gate == gate.get_inverse() assert str(gate) == "Swap" assert gate.interchangeable_qubit_indices == [[0, 1]] - assert np.array_equal(gate.matrix, - np.matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], - [0, 0, 0, 1]])) + assert np.array_equal(gate.matrix, np.matrix([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])) assert isinstance(_gates.Swap, _gates.SwapGate) @@ -105,13 +95,18 @@ def test_sqrtswap_gate(): sqrt_gate = _gates.SqrtSwapGate() swap_gate = _gates.SwapGate() assert str(sqrt_gate) == "SqrtSwap" - assert np.array_equal(sqrt_gate.matrix * sqrt_gate.matrix, - swap_gate.matrix) - assert np.array_equal(sqrt_gate.matrix, - np.matrix([[1, 0, 0, 0], - [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], - [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], - [0, 0, 0, 1]])) + assert np.array_equal(sqrt_gate.matrix * sqrt_gate.matrix, swap_gate.matrix) + assert np.array_equal( + sqrt_gate.matrix, + np.matrix( + [ + [1, 0, 0, 0], + [0, 0.5 + 0.5j, 0.5 - 0.5j, 0], + [0, 0.5 - 0.5j, 0.5 + 0.5j, 0], + [0, 0, 0, 1], + ] + ), + ) assert isinstance(_gates.SqrtSwap, _gates.SqrtSwapGate) @@ -121,36 +116,81 @@ def test_engangle_gate(): assert isinstance(_gates.Entangle, _gates.EntangleGate) -@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, - 4 * math.pi]) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rx(angle): gate = _gates.Rx(angle) - expected_matrix = np.matrix([[math.cos(0.5 * angle), - -1j * math.sin(0.5 * angle)], - [-1j * math.sin(0.5 * angle), - math.cos(0.5 * angle)]]) + expected_matrix = np.matrix( + [ + [math.cos(0.5 * angle), -1j * math.sin(0.5 * angle)], + [-1j * math.sin(0.5 * angle), math.cos(0.5 * angle)], + ] + ) assert gate.matrix.shape == expected_matrix.shape assert np.allclose(gate.matrix, expected_matrix) -@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, - 4 * math.pi]) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_ry(angle): gate = _gates.Ry(angle) - expected_matrix = np.matrix([[math.cos(0.5 * angle), - -math.sin(0.5 * angle)], - [math.sin(0.5 * angle), - math.cos(0.5 * angle)]]) + expected_matrix = np.matrix( + [ + [math.cos(0.5 * angle), -math.sin(0.5 * angle)], + [math.sin(0.5 * angle), math.cos(0.5 * angle)], + ] + ) assert gate.matrix.shape == expected_matrix.shape assert np.allclose(gate.matrix, expected_matrix) -@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, - 4 * math.pi]) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rz(angle): gate = _gates.Rz(angle) - expected_matrix = np.matrix([[cmath.exp(-.5 * 1j * angle), 0], - [0, cmath.exp(.5 * 1j * angle)]]) + expected_matrix = np.matrix([[cmath.exp(-0.5 * 1j * angle), 0], [0, cmath.exp(0.5 * 1j * angle)]]) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +def test_rxx(angle): + gate = _gates.Rxx(angle) + expected_matrix = np.matrix( + [ + [cmath.cos(0.5 * angle), 0, 0, -1j * cmath.sin(0.5 * angle)], + [0, cmath.cos(0.5 * angle), -1j * cmath.sin(0.5 * angle), 0], + [0, -1j * cmath.sin(0.5 * angle), cmath.cos(0.5 * angle), 0], + [-1j * cmath.sin(0.5 * angle), 0, 0, cmath.cos(0.5 * angle)], + ] + ) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +def test_ryy(angle): + gate = _gates.Ryy(angle) + expected_matrix = np.matrix( + [ + [cmath.cos(0.5 * angle), 0, 0, 1j * cmath.sin(0.5 * angle)], + [0, cmath.cos(0.5 * angle), -1j * cmath.sin(0.5 * angle), 0], + [0, -1j * cmath.sin(0.5 * angle), cmath.cos(0.5 * angle), 0], + [1j * cmath.sin(0.5 * angle), 0, 0, cmath.cos(0.5 * angle)], + ] + ) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +def test_rzz(angle): + gate = _gates.Rzz(angle) + expected_matrix = np.matrix( + [ + [cmath.exp(-0.5 * 1j * angle), 0, 0, 0], + [0, cmath.exp(0.5 * 1j * angle), 0, 0], + [0, 0, cmath.exp(0.5 * 1j * angle), 0], + [0, 0, 0, cmath.exp(-0.5 * 1j * angle)], + ] + ) assert gate.matrix.shape == expected_matrix.shape assert np.allclose(gate.matrix, expected_matrix) @@ -159,8 +199,7 @@ def test_rz(angle): def test_ph(angle): gate = _gates.Ph(angle) gate2 = _gates.Ph(angle + 2 * math.pi) - expected_matrix = np.matrix([[cmath.exp(1j * angle), 0], - [0, cmath.exp(1j * angle)]]) + expected_matrix = np.matrix([[cmath.exp(1j * angle), 0], [0, cmath.exp(1j * angle)]]) assert gate.matrix.shape == expected_matrix.shape assert np.allclose(gate.matrix, expected_matrix) assert gate2.matrix.shape == expected_matrix.shape @@ -217,3 +256,78 @@ def test_barrier_gate(): assert str(gate) == "Barrier" assert gate.get_inverse() == _gates.BarrierGate() assert isinstance(_gates.Barrier, _gates.BarrierGate) + + +def test_flip_bits_equality_and_hash(): + gate1 = _gates.FlipBits([1, 0, 0, 1]) + gate2 = _gates.FlipBits([1, 0, 0, 1]) + gate3 = _gates.FlipBits([0, 1, 0, 1]) + assert gate1 == gate2 + assert hash(gate1) == hash(gate2) + assert gate1 != gate3 + assert gate1 != _gates.X + + +def test_flip_bits_str(): + gate1 = _gates.FlipBits([0, 0, 1]) + assert str(gate1) == "FlipBits(4)" + + +def test_error_on_tuple_input(): + with pytest.raises(ValueError): + _gates.FlipBits(2) | (None, None) + + +flip_bits_testdata = [ + ([0, 1, 0, 1], '0101'), + ([1, 0, 1, 0], '1010'), + ([False, True, False, True], '0101'), + ('0101', '0101'), + ('1111', '1111'), + ('0000', '0000'), + (8, '0001'), + (11, '1101'), + (1, '1000'), + (-1, '1111'), + (-2, '0111'), + (-3, '1011'), +] + + +@pytest.mark.parametrize("bits_to_flip, result", flip_bits_testdata) +def test_simulator_flip_bits(bits_to_flip, result): + eng = MainEngine() + qubits = eng.allocate_qureg(4) + FlipBits(bits_to_flip) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability(result, qubits)) == 1.0 + All(Measure) | qubits + + +def test_flip_bits_can_be_applied_to_various_qubit_qureg_formats(): + eng = MainEngine() + qubits = eng.allocate_qureg(4) + eng.flush() + assert pytest.approx(eng.backend.get_probability('0000', qubits)) == 1.0 + FlipBits([0, 1, 1, 0]) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1.0 + FlipBits([1]) | qubits[0] + eng.flush() + assert pytest.approx(eng.backend.get_probability('1110', qubits)) == 1.0 + FlipBits([1]) | (qubits[0],) + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1.0 + FlipBits([1, 1]) | [qubits[0], qubits[1]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('1010', qubits)) == 1.0 + FlipBits(-1) | qubits + eng.flush() + assert pytest.approx(eng.backend.get_probability('0101', qubits)) == 1.0 + FlipBits(-4) | [qubits[0], qubits[1], qubits[2], qubits[3]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('0110', qubits)) == 1.0 + FlipBits(2) | [qubits[0]] + [qubits[1], qubits[2]] + eng.flush() + assert pytest.approx(eng.backend.get_probability('0010', qubits)) == 1.0 + All(Measure) | qubits diff --git a/projectq/ops/_metagates.py b/projectq/ops/_metagates.py old mode 100755 new mode 100644 index c2c20969b..0c1f89f3f --- a/projectq/ops/_metagates.py +++ b/projectq/ops/_metagates.py @@ -11,45 +11,39 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ +Definition of some `meta` gates. + Contains meta gates, i.e., * DaggeredGate (Represents the inverse of an arbitrary gate) * ControlledGate (Represents a controlled version of an arbitrary gate) * Tensor/All (Applies a single qubit gate to all supplied qubits), e.g., - Example: + Example: .. code-block:: python - Tensor(H) | (qubit1, qubit2) # apply H to qubit #1 and #2 + Tensor(H) | (qubit1, qubit2) # apply H to qubit #1 and #2 As well as the meta functions -* get_inverse (Tries to access the get_inverse member function of a gate - and upon failure returns a DaggeredGate) +* get_inverse (Tries to access the get_inverse member function of a gate and upon failure returns a DaggeredGate) * C (Creates an n-ary controlled version of an arbitrary gate) """ from ._basics import BasicGate, NotInvertible -from ._command import Command, apply_command class ControlQubitError(Exception): - """ - Exception thrown when wrong number of control qubits are supplied. - """ - pass + """Exception thrown when wrong number of control qubits are supplied.""" class DaggeredGate(BasicGate): """ - Wrapper class allowing to execute the inverse of a gate, even when it does - not define one. + Wrapper class allowing to execute the inverse of a gate, even when it does not define one. - If there is a replacement available, then there is also one for the - inverse, namely the replacement function run in reverse, while inverting - all gates. This class enables using this emulation automatically. + If there is a replacement available, then there is also one for the inverse, namely the replacement function run + in reverse, while inverting all gates. This class enables using this emulation automatically. - A DaggeredGate is returned automatically when employing the get_inverse- - function on a gate which does not provide a get_inverse() member function. + A DaggeredGate is returned automatically when employing the get_inverse- function on a gate which does not provide + a get_inverse() member function. Example: .. code-block:: python @@ -57,10 +51,9 @@ class DaggeredGate(BasicGate): with Dagger(eng): MySpecialGate | qubits - will create a DaggeredGate if MySpecialGate does not implement - get_inverse. If there is a decomposition function available, an auto- - replacer engine can automatically replace the inverted gate by a call to - the decomposition function inside a "with Dagger"-statement. + will create a DaggeredGate if MySpecialGate does not implement get_inverse. If there is a decomposition function + available, an auto- replacer engine can automatically replace the inverted gate by a call to the decomposition + function inside a "with Dagger"-statement. """ def __init__(self, gate): @@ -70,7 +63,7 @@ def __init__(self, gate): Args: gate: Any gate object of which to represent the inverse. """ - BasicGate.__init__(self) + super().__init__() self._gate = gate try: @@ -80,35 +73,25 @@ def __init__(self, gate): pass def __str__(self): - """ - Return string representation (str(gate) + \"^\dagger\"). - """ - return str(self._gate) + "^\dagger" + r"""Return string representation (str(gate) + \"^\dagger\").""" + return f"{str(self._gate)}^\\dagger" def tex_str(self): - """ - Return the Latex string representation of a Daggered gate. - """ + """Return the Latex string representation of a Daggered gate.""" if hasattr(self._gate, 'tex_str'): - return self._gate.tex_str() + r"${}^\dagger$" - else: - return str(self._gate) + r"${}^\dagger$" + return f"{self._gate.tex_str()}${{}}^\\dagger$" + return f"{str(self._gate)}${{}}^\\dagger$" def get_inverse(self): - """ - Return the inverse gate (the inverse of the inverse of a gate is the - gate itself). - """ + """Return the inverse gate (the inverse of the inverse of a gate is the gate itself).""" return self._gate def __eq__(self, other): - """ - Return True if self is equal to other, i.e., same type and - representing the inverse of the same gate. - """ + """Return True if self is equal to other, i.e., same type and representing the inverse of the same gate.""" return isinstance(other, self.__class__) and self._gate == other._gate def __hash__(self): + """Compute the hash of the object.""" return hash(str(self)) @@ -116,8 +99,7 @@ def get_inverse(gate): """ Return the inverse of a gate. - Tries to call gate.get_inverse and, upon failure, creates a DaggeredGate - instead. + Tries to call gate.get_inverse and, upon failure, creates a DaggeredGate instead. Args: gate: Gate of which to get the inverse @@ -125,7 +107,7 @@ def get_inverse(gate): Example: .. code-block:: python - get_inverse(H) # returns a Hadamard gate (HGate object) + get_inverse(H) # returns a Hadamard gate (HGate object) """ try: return gate.get_inverse() @@ -133,6 +115,24 @@ def get_inverse(gate): return DaggeredGate(gate) +def is_identity(gate): + """ + Return True if the gate is an identity gate. + + Tries to call gate.is_identity and, upon failure, returns False + + Args: + gate: Gate of which to get the inverse + + Example: + .. code-block:: python + + get_inverse(Rx(2 * math.pi)) # returns True + get_inverse(Rx(math.pi)) # returns False + """ + return gate.is_identity() + + class ControlledGate(BasicGate): """ Controlled version of a gate. @@ -140,17 +140,15 @@ class ControlledGate(BasicGate): Note: Use the meta function :func:`C()` to create a controlled gate - A wrapper class which enables (multi-) controlled gates. It overloads - the __or__-operator, using the first qubits provided as control qubits. - The n control-qubits need to be the first n qubits. They can be in - separate quregs. + A wrapper class which enables (multi-) controlled gates. It overloads the __or__-operator, using the first qubits + provided as control qubits. The n control-qubits need to be the first n qubits. They can be in separate quregs. Example: .. code-block:: python - ControlledGate(gate, 2) | (qb0, qb2, qb3) # qb0 & qb2 are controls - C(gate, 2) | (qb0, qb2, qb3) # This is much nicer. - C(gate, 2) | ([qb0,qb2], qb3) # Is equivalent + ControlledGate(gate, 2) | (qb0, qb2, qb3) # qb0 & qb2 are controls + C(gate, 2) | (qb0, qb2, qb3) # This is much nicer. + C(gate, 2) | ([qb0, qb2], qb3) # Is equivalent Note: Use :func:`C` rather than ControlledGate, i.e., @@ -168,7 +166,7 @@ def __init__(self, gate, n=1): gate: Gate to wrap. n (int): Number of control qubits. """ - BasicGate.__init__(self) + super().__init__() if isinstance(gate, ControlledGate): self._gate = gate._gate self._n = gate._n + n @@ -177,24 +175,19 @@ def __init__(self, gate, n=1): self._n = n def __str__(self): - """ Return string representation, i.e., CC...C(gate). """ + """Return a string representation of the object.""" return "C" * self._n + str(self._gate) def get_inverse(self): - """ - Return inverse of a controlled gate, which is the controlled inverse - gate. - """ + """Return inverse of a controlled gate, which is the controlled inverse gate.""" return ControlledGate(get_inverse(self._gate), self._n) def __or__(self, qubits): """ - Apply the controlled gate to qubits, using the first n qubits as - controls. + Apply the controlled gate to qubits, using the first n qubits as controls. - Note: The control qubits can be split across the first quregs. - However, the n-th control qubit needs to be the last qubit in a - qureg. The following quregs belong to the gate. + Note: The control qubits can be split across the first quregs. However, the n-th control qubit needs to be + the last qubit in a qureg. The following quregs belong to the gate. Args: qubits (tuple of lists of Qubit objects): qubits to which to apply @@ -214,82 +207,80 @@ def __or__(self, qubits): # Test that there were enough control quregs and that that # the last control qubit was the last qubit in a qureg. if len(ctrl) != self._n: - raise ControlQubitError("Wrong number of control qubits. " - "First qureg(s) need to contain exactly " - "the required number of control quregs.") + raise ControlQubitError( + "Wrong number of control qubits. " + "First qureg(s) need to contain exactly " + "the required number of control quregs." + ) + + import projectq.meta # pylint: disable=import-outside-toplevel - import projectq.meta with projectq.meta.Control(gate_quregs[0][0].engine, ctrl): self._gate | tuple(gate_quregs) def __eq__(self, other): - """ Compare two ControlledGate objects (return True if equal). """ - return (isinstance(other, self.__class__) and - self._gate == other._gate and self._n == other._n) - - def __ne__(self, other): - return not self.__eq__(other) + """Compare two ControlledGate objects (return True if equal).""" + return isinstance(other, self.__class__) and self._gate == other._gate and self._n == other._n -def C(gate, n=1): +def C(gate, n_qubits=1): """ Return n-controlled version of the provided gate. Args: gate: Gate to turn into its controlled version - n: Number of controls (default: 1) + n_qubits: Number of controls (default: 1) Example: .. code-block:: python - C(NOT) | (c, q) # equivalent to CNOT | (c, q) + C(NOT) | (c, q) # equivalent to CNOT | (c, q) """ - return ControlledGate(gate, n) + return ControlledGate(gate, n_qubits) class Tensor(BasicGate): """ - Wrapper class allowing to apply a (single-qubit) gate to every qubit in a - quantum register. Allowed syntax is to supply either a qureg or a tuple - which contains only one qureg. + Wrapper class allowing to apply a (single-qubit) gate to every qubit in a quantum register. + + Allowed syntax is to supply either a qureg or a tuple which contains only one qureg. Example: .. code-block:: python - Tensor(H) | x # applies H to every qubit in the list of qubits x - Tensor(H) | (x,) # alternative to be consistent with other syntax + Tensor(H) | x # applies H to every qubit in the list of qubits x + Tensor(H) | (x,) # alternative to be consistent with other syntax """ def __init__(self, gate): - """ Initialize a Tensor object for the gate. """ - BasicGate.__init__(self) + """Initialize a Tensor object for the gate.""" + super().__init__() self._gate = gate def __str__(self): - """ Return string representation. """ - return "Tensor(" + str(self._gate) + ")" + """Return a string representation of the object.""" + return f"Tensor({str(self._gate)})" def get_inverse(self): - """ - Return the inverse of this tensored gate (which is the tensored - inverse of the gate). - """ + """Return the inverse of this tensored gate (which is the tensored inverse of the gate).""" return Tensor(get_inverse(self._gate)) def __eq__(self, other): + """Equal operator.""" return isinstance(other, Tensor) and self._gate == other._gate - def __ne__(self, other): - return not self.__eq__(other) - def __or__(self, qubits): - """ Applies the gate to every qubit in the quantum register qubits. """ + """Operator| overload which enables the syntax Gate | qubits.""" if isinstance(qubits, tuple): - assert len(qubits) == 1 + if len(qubits) != 1: + raise ValueError('Tensor/All must be applied to a single quantum register!') qubits = qubits[0] - assert isinstance(qubits, list) + if not isinstance(qubits, list): + raise ValueError('Tensor/All must be applied to a list of qubits!') + for qubit in qubits: self._gate | qubit + #: Shortcut (instance of) :class:`projectq.ops.Tensor` All = Tensor diff --git a/projectq/ops/_metagates_test.py b/projectq/ops/_metagates_test.py index c5a62d239..cacd1fb8c 100755 --- a/projectq/ops/_metagates_test.py +++ b/projectq/ops/_metagates_test.py @@ -11,28 +11,46 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._gates.""" import cmath import math + import numpy as np import pytest -from projectq.types import Qubit, Qureg from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import (T, Y, NotInvertible, Entangle, Rx, - FastForwardingGate, Command, C, - ClassicalInstructionGate, All) +from projectq.ops import ( + All, + C, + ClassicalInstructionGate, + Command, + Entangle, + FastForwardingGate, + NotInvertible, + Rx, + T, + Y, + _metagates, +) +from projectq.types import Qubit, WeakQubitRef + + +def test_tensored_gate_invalid(): + qb0 = WeakQubitRef(None, idx=0) + qb1 = WeakQubitRef(None, idx=1) -from projectq.ops import _metagates + with pytest.raises(ValueError): + _metagates.Tensor(Y) | (qb0, qb1) + + with pytest.raises(ValueError): + _metagates.Tensor(Y) | qb0 def test_tensored_controlled_gate(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) gate = Rx(0.6) qubit0 = Qubit(main_engine, 0) qubit1 = Qubit(main_engine, 1) @@ -55,9 +73,7 @@ def test_daggered_gate_init(): # Test init and matrix dagger_inv = _metagates.DaggeredGate(not_invertible_gate) assert dagger_inv._gate == not_invertible_gate - assert np.array_equal(dagger_inv.matrix, - np.matrix([[1, 0], - [0, cmath.exp(-1j * cmath.pi / 4)]])) + assert np.array_equal(dagger_inv.matrix, np.matrix([[1, 0], [0, cmath.exp(-1j * cmath.pi / 4)]])) inv = _metagates.DaggeredGate(invertible_gate) assert inv._gate == invertible_gate assert np.array_equal(inv.matrix, np.matrix([[0, -1j], [1j, 0]])) @@ -72,7 +88,7 @@ def test_daggered_gate_init(): def test_daggered_gate_str(): daggered_gate = _metagates.DaggeredGate(Y) - assert str(daggered_gate) == str(Y) + "^\dagger" + assert str(daggered_gate) == f"{str(Y)}^\\dagger" def test_daggered_gate_hashable(): @@ -87,13 +103,13 @@ def test_daggered_gate_hashable(): def test_daggered_gate_tex_str(): daggered_gate = _metagates.DaggeredGate(Y) str_Y = Y.tex_str() if hasattr(Y, 'tex_str') else str(Y) - assert daggered_gate.tex_str() == str_Y + "${}^\dagger$" + assert daggered_gate.tex_str() == f"{str_Y}${{}}^\\dagger$" # test for a gate with tex_str method rx = Rx(0.5) daggered_rx = _metagates.DaggeredGate(rx) str_rx = rx.tex_str() if hasattr(rx, 'tex_str') else str(rx) - assert daggered_rx.tex_str() == str_rx + "${}^\dagger$" + assert daggered_rx.tex_str() == f"{str_rx}${{}}^\\dagger$" def test_daggered_gate_get_inverse(): @@ -117,12 +133,22 @@ def test_get_inverse(): assert invertible_gate.get_inverse() == Y # Check get_inverse(gate) inv = _metagates.get_inverse(not_invertible_gate) - assert (isinstance(inv, _metagates.DaggeredGate) and - inv._gate == not_invertible_gate) + assert isinstance(inv, _metagates.DaggeredGate) and inv._gate == not_invertible_gate inv2 = _metagates.get_inverse(invertible_gate) assert inv2 == Y +def test_is_identity(): + # Choose gate which is not an identity gate: + non_identity_gate = Rx(0.5) + assert not non_identity_gate.is_identity() + assert not _metagates.is_identity(non_identity_gate) + # Choose gate which is an identity gate: + identity_gate = Rx(0.0) + assert identity_gate.is_identity() + assert _metagates.is_identity(identity_gate) + + def test_controlled_gate_init(): one_control = _metagates.ControlledGate(Y, 1) two_control = _metagates.ControlledGate(Y, 2) @@ -137,7 +163,7 @@ def test_controlled_gate_init(): def test_controlled_gate_str(): one_control = _metagates.ControlledGate(Y, 2) - assert str(one_control) == "CC" + str(Y) + assert str(one_control) == f"CC{str(Y)}" def test_controlled_gate_get_inverse(): @@ -157,19 +183,16 @@ def test_controlled_gate_empty_controls(): def test_controlled_gate_or(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) gate = Rx(0.6) qubit0 = Qubit(main_engine, 0) qubit1 = Qubit(main_engine, 1) qubit2 = Qubit(main_engine, 2) qubit3 = Qubit(main_engine, 3) - expected_cmd = Command(main_engine, gate, ([qubit3],), - controls=[qubit0, qubit1, qubit2]) + expected_cmd = Command(main_engine, gate, ([qubit3],), controls=[qubit0, qubit1, qubit2]) received_commands = [] # Option 1: - _metagates.ControlledGate(gate, 3) | ([qubit1], [qubit0], - [qubit2], [qubit3]) + _metagates.ControlledGate(gate, 3) | ([qubit1], [qubit0], [qubit2], [qubit3]) # Option 2: _metagates.ControlledGate(gate, 3) | (qubit1, qubit0, qubit2, qubit3) # Option 3: @@ -181,8 +204,7 @@ def test_controlled_gate_or(): _metagates.ControlledGate(gate, 3) | (qubit1, [qubit0, qubit2, qubit3]) # Remove Allocate and Deallocate gates for cmd in saving_backend.received_commands: - if not (isinstance(cmd.gate, FastForwardingGate) or - isinstance(cmd.gate, ClassicalInstructionGate)): + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): received_commands.append(cmd) assert len(received_commands) == 4 for cmd in received_commands: @@ -211,7 +233,7 @@ def test_tensor_init(): def test_tensor_str(): gate = _metagates.Tensor(Y) - assert str(gate) == "Tensor(" + str(Y) + ")" + assert str(gate) == f"Tensor({str(Y)})" def test_tensor_get_inverse(): @@ -230,8 +252,7 @@ def test_tensor_comparison(): def test_tensor_or(): saving_backend = DummyEngine(save_commands=True) - main_engine = MainEngine(backend=saving_backend, - engine_list=[DummyEngine()]) + main_engine = MainEngine(backend=saving_backend, engine_list=[DummyEngine()]) gate = Rx(0.6) qubit0 = Qubit(main_engine, 0) qubit1 = Qubit(main_engine, 1) @@ -243,8 +264,7 @@ def test_tensor_or(): received_commands = [] # Remove Allocate and Deallocate gates for cmd in saving_backend.received_commands: - if not (isinstance(cmd.gate, FastForwardingGate) or - isinstance(cmd.gate, ClassicalInstructionGate)): + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): received_commands.append(cmd) # Check results assert len(received_commands) == 6 diff --git a/projectq/ops/_qaagate.py b/projectq/ops/_qaagate.py new file mode 100755 index 000000000..c5561eaa4 --- /dev/null +++ b/projectq/ops/_qaagate.py @@ -0,0 +1,82 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Definition of the quantum amplitude amplification gate.""" + +from ._basics import BasicGate + + +class QAA(BasicGate): + """ + Quantum Amplitude Amplification gate. + + (Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. Complete reference G. Brassard, P. Hoyer, + M. Mosca, A. Tapp (2000) Quantum Amplitude Amplification and Estimation https://arxiv.org/abs/quant-ph/0005055) + + Quantum Amplitude Amplification (QAA) executes the algorithm, but not the final measurement required to obtain the + marked state(s) with high probability. The starting state on which the QAA algorithm is executed is the one + resulting of applying the Algorithm on the |0> state. + + Example: + .. code-block:: python + + def func_algorithm(eng, system_qubits): + All(H) | system_qubits + + + def func_oracle(eng, system_qubits, qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi / 4.0 * math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + + Warning: + No qubit allocation/deallocation may take place during the call to the defined Algorithm + :code:`func_algorithm` + + Attributes: + func_algorithm: Algorithm that initialite the state and to be used in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the "good" states + + """ + + def __init__(self, algorithm, oracle): + """Initialize a QAA object.""" + super().__init__() + self.algorithm = algorithm + self.oracle = oracle + + def __str__(self): + """Return a string representation of the object.""" + return f'QAA(Algorithm = {str(self.algorithm.__name__)}, Oracle = {str(self.oracle.__name__)})' diff --git a/projectq/ops/_qaagate_test.py b/projectq/ops/_qaagate_test.py new file mode 100755 index 000000000..e6d68688b --- /dev/null +++ b/projectq/ops/_qaagate_test.py @@ -0,0 +1,27 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.ops._qaagate.""" + +from projectq.ops import All, H, X, _qaagate + + +def test_qaa_str(): + def func_algorithm(): + All(H) + + def func_oracle(): + All(X) + + gate = _qaagate.QAA(func_algorithm, func_oracle) + assert str(gate) == "QAA(Algorithm = func_algorithm, Oracle = func_oracle)" diff --git a/projectq/ops/_qftgate.py b/projectq/ops/_qftgate.py index 97b045072..3a14de37a 100755 --- a/projectq/ops/_qftgate.py +++ b/projectq/ops/_qftgate.py @@ -12,15 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Definition of the QFT gate.""" + from ._basics import BasicGate class QFTGate(BasicGate): - """ - Quantum Fourier Transform gate. - """ + """Quantum Fourier Transform gate.""" + def __str__(self): + """Return a string representation of the object.""" return "QFT" + #: Shortcut (instance of) :class:`projectq.ops.QFTGate` QFT = QFTGate() diff --git a/projectq/ops/_qftgate_test.py b/projectq/ops/_qftgate_test.py index a74683006..a790506f0 100755 --- a/projectq/ops/_qftgate_test.py +++ b/projectq/ops/_qftgate_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._qftgate.""" from projectq.ops import _qftgate @@ -20,3 +19,7 @@ def test_qft_gate_str(): gate = _qftgate.QFT assert str(gate) == "QFT" + + +def test_qft_equality(): + assert _qftgate.QFT == _qftgate.QFTGate() diff --git a/projectq/ops/_qpegate.py b/projectq/ops/_qpegate.py new file mode 100755 index 000000000..3eab6e84c --- /dev/null +++ b/projectq/ops/_qpegate.py @@ -0,0 +1,34 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Definition of the quantum phase estimation gate.""" + +from ._basics import BasicGate + + +class QPE(BasicGate): + """ + Quantum Phase Estimation gate. + + See setups.decompositions for the complete implementation + """ + + def __init__(self, unitary): + """Initialize a QPE gate.""" + super().__init__() + self.unitary = unitary + + def __str__(self): + """Return a string representation of the object.""" + return f'QPE({str(self.unitary)})' diff --git a/projectq/ops/_qpegate_test.py b/projectq/ops/_qpegate_test.py new file mode 100755 index 000000000..d404127ff --- /dev/null +++ b/projectq/ops/_qpegate_test.py @@ -0,0 +1,22 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.ops._qpegate.""" + +from projectq.ops import X, _qpegate + + +def test_qpe_str(): + unitary = X + gate = _qpegate.QPE(unitary) + assert str(gate) == "QPE(X)" diff --git a/projectq/ops/_qubit_operator.py b/projectq/ops/_qubit_operator.py index a95e3ea9a..80c901dd3 100644 --- a/projectq/ops/_qubit_operator.py +++ b/projectq/ops/_qubit_operator.py @@ -11,167 +11,163 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """QubitOperator stores a sum of Pauli operators acting on qubits.""" -import copy -import itertools -import numpy +import cmath +import copy +from ._basics import BasicGate, NotInvertible, NotMergeable +from ._command import apply_command +from ._gates import Ph, X, Y, Z EQ_TOLERANCE = 1e-12 - # Define products of all Pauli operators for symbolic multiplication. -_PAULI_OPERATOR_PRODUCTS = {('I', 'I'): (1., 'I'), - ('I', 'X'): (1., 'X'), - ('X', 'I'): (1., 'X'), - ('I', 'Y'): (1., 'Y'), - ('Y', 'I'): (1., 'Y'), - ('I', 'Z'): (1., 'Z'), - ('Z', 'I'): (1., 'Z'), - ('X', 'X'): (1., 'I'), - ('Y', 'Y'): (1., 'I'), - ('Z', 'Z'): (1., 'I'), - ('X', 'Y'): (1.j, 'Z'), - ('X', 'Z'): (-1.j, 'Y'), - ('Y', 'X'): (-1.j, 'Z'), - ('Y', 'Z'): (1.j, 'X'), - ('Z', 'X'): (1.j, 'Y'), - ('Z', 'Y'): (-1.j, 'X')} +_PAULI_OPERATOR_PRODUCTS = { + ('I', 'I'): (1.0, 'I'), + ('I', 'X'): (1.0, 'X'), + ('X', 'I'): (1.0, 'X'), + ('I', 'Y'): (1.0, 'Y'), + ('Y', 'I'): (1.0, 'Y'), + ('I', 'Z'): (1.0, 'Z'), + ('Z', 'I'): (1.0, 'Z'), + ('X', 'X'): (1.0, 'I'), + ('Y', 'Y'): (1.0, 'I'), + ('Z', 'Z'): (1.0, 'I'), + ('X', 'Y'): (1.0j, 'Z'), + ('X', 'Z'): (-1.0j, 'Y'), + ('Y', 'X'): (-1.0j, 'Z'), + ('Y', 'Z'): (1.0j, 'X'), + ('Z', 'X'): (1.0j, 'Y'), + ('Z', 'Y'): (-1.0j, 'X'), +} class QubitOperatorError(Exception): - pass + """Exception raised when a QubitOperator is instantiated with some invalid data.""" -class QubitOperator(object): +class QubitOperator(BasicGate): """ A sum of terms acting on qubits, e.g., 0.5 * 'X0 X5' + 0.3 * 'Z1 Z2'. A term is an operator acting on n qubits and can be represented as: - coefficent * local_operator[0] x ... x local_operator[n-1] + coefficient * local_operator[0] x ... x local_operator[n-1] - where x is the tensor product. A local operator is a Pauli operator - ('I', 'X', 'Y', or 'Z') which acts on one qubit. In math notation a term - is, for example, 0.5 * 'X0 X5', which means that a Pauli X operator acts - on qubit 0 and 5, while the identity operator acts on all other qubits. + where x is the tensor product. A local operator is a Pauli operator ('I', 'X', 'Y', or 'Z') which acts on one + qubit. In math notation a term is, for example, 0.5 * 'X0 X5', which means that a Pauli X operator acts on qubit 0 + and 5, while the identity operator acts on all other qubits. - A QubitOperator represents a sum of terms acting on qubits and overloads - operations for easy manipulation of these objects by the user. + A QubitOperator represents a sum of terms acting on qubits and overloads operations for easy manipulation of these + objects by the user. - Note for a QubitOperator to be a Hamiltonian which is a hermitian - operator, the coefficients of all terms must be real. + Note for a QubitOperator to be a Hamiltonian which is a hermitian operator, the coefficients of all terms must be + real. .. code-block:: python hamiltonian = 0.5 * QubitOperator('X0 X5') + 0.3 * QubitOperator('Z0') + Our Simulator takes a hermitian QubitOperator to directly calculate the expectation value (see + Simulator.get_expectation_value) of this observable. + + A hermitian QubitOperator can also be used as input for the TimeEvolution gate. + + If the QubitOperator is unitary, i.e., it contains only one term with a coefficient, whose absolute value is 1, + then one can apply it directly to qubits: + + .. code-block:: python + + eng = projectq.MainEngine() + qureg = eng.allocate_qureg(6) + QubitOperator('X0 X5', 1.0j) | qureg # Applies X to qubit 0 and 5 with an additional global phase of 1.j + + Attributes: - terms (dict): **key**: A term represented by a tuple containing all - non-trivial local Pauli operators ('X', 'Y', or 'Z'). - A non-trivial local Pauli operator is specified by a - tuple with the first element being an integer - indicating the qubit on which a non-trivial local - operator acts and the second element being a string, - either 'X', 'Y', or 'Z', indicating which non-trivial - Pauli operator acts on that qubit. Examples: - ((1, 'X'),) or ((1, 'X'), (4,'Z')) or the identity (). - The tuples representing the non-trivial local terms - are sorted according to the qubit number they act on, - starting from 0. - **value**: Coefficient of this term as a (complex) float + terms (dict): **key**: A term represented by a tuple containing all non-trivial local Pauli operators ('X', + 'Y', or 'Z'). A non-trivial local Pauli operator is specified by a tuple with the first element + being an integer indicating the qubit on which a non-trivial local operator acts and the second + element being a string, either 'X', 'Y', or 'Z', indicating which non-trivial Pauli operator + acts on that qubit. Examples: ((1, 'X'),) or ((1, 'X'), (4,'Z')) or the identity (). The tuples + representing the non-trivial local terms are sorted according to the qubit number they act on, + starting from 0. **value**: Coefficient of this term as a (complex) float """ - def __init__(self, term=None, coefficient=1.): + def __init__(self, term=None, coefficient=1.0): # pylint: disable=too-many-branches """ - Inits a QubitOperator. + Initialize a QubitOperator object. - The init function only allows to initialize one term. Additional terms - have to be added using += (which is fast) or using + of two - QubitOperator objects: + The init function only allows to initialize one term. Additional terms have to be added using += (which is + fast) or using + of two QubitOperator objects: Example: .. code-block:: python - ham = ((QubitOperator('X0 Y3', 0.5) - + 0.6 * QubitOperator('X0 Y3'))) + ham = QubitOperator('X0 Y3', 0.5) + 0.6 * QubitOperator('X0 Y3') # Equivalently ham2 = QubitOperator('X0 Y3', 0.5) ham2 += 0.6 * QubitOperator('X0 Y3') Note: - Adding terms to QubitOperator is faster using += (as this is done - by in-place addition). Specifying the coefficient in the __init__ - is faster than by multiplying a QubitOperator with a scalar as - calls an out-of-place multiplication. + Adding terms to QubitOperator is faster using += (as this is done by in-place addition). Specifying the + coefficient in the __init__ is faster than by multiplying a QubitOperator with a scalar as calls an + out-of-place multiplication. Args: - coefficient (complex float, optional): The coefficient of the - first term of this QubitOperator. Default is 1.0. - term (optional, empy tuple, a tuple of tuples, or a string): - 1) Default is None which means there are no terms in the - QubitOperator hence it is the "zero" Operator - 2) An empty tuple means there are no non-trivial Pauli - operators acting on the qubits hence only identities - with a coefficient (which by default is 1.0). - 3) A sorted tuple of tuples. The first element of each tuple - is an integer indicating the qubit on which a non-trivial - local operator acts, starting from zero. The second element - of each tuple is a string, either 'X', 'Y' or 'Z', - indicating which local operator acts on that qubit. - 4) A string of the form 'X0 Z2 Y5', indicating an X on - qubit 0, Z on qubit 2, and Y on qubit 5. The string should - be sorted by the qubit number. '' is the identity. + coefficient (complex float, optional): The coefficient of the first term of this QubitOperator. Default is + 1.0. + term (optional, empty tuple, a tuple of tuples, or a string): + 1) Default is None which means there are no terms in the QubitOperator hence it is the "zero" Operator + 2) An empty tuple means there are no non-trivial Pauli operators acting on the qubits hence only + identities with a coefficient (which by default is 1.0). + 3) A sorted tuple of tuples. The first element of each tuple is an integer indicating the qubit on + which a non-trivial local operator acts, starting from zero. The second element of each tuple is a + string, either 'X', 'Y' or 'Z', indicating which local operator acts on that qubit. + 4) A string of the form 'X0 Z2 Y5', indicating an X on qubit 0, Z on qubit 2, and Y on qubit 5. The + string should be sorted by the qubit number. '' is the identity. Raises: QubitOperatorError: Invalid operators provided to QubitOperator. """ + super().__init__() if not isinstance(coefficient, (int, float, complex)): raise ValueError('Coefficient must be a numeric type.') self.terms = {} if term is None: return - elif isinstance(term, tuple): - if term is (): + if isinstance(term, tuple): + if term == (): self.terms[()] = coefficient else: # Test that input is a tuple of tuples and correct action for local_operator in term: - if (not isinstance(local_operator, tuple) or - len(local_operator) != 2): + if not isinstance(local_operator, tuple) or len(local_operator) != 2: raise ValueError("term specified incorrectly.") qubit_num, action = local_operator if not isinstance(action, str) or action not in 'XYZ': - raise ValueError("Invalid action provided: must be " - "string 'X', 'Y', or 'Z'.") + raise ValueError("Invalid action provided: must be string 'X', 'Y', or 'Z'.") if not (isinstance(qubit_num, int) and qubit_num >= 0): - raise QubitOperatorError("Invalid qubit number " - "provided to QubitTerm: " - "must be a non-negative " - "int.") + raise QubitOperatorError( + "Invalid qubit number provided to QubitTerm: must be a non-negative int." + ) # Sort and add to self.terms: term = list(term) term.sort(key=lambda loc_operator: loc_operator[0]) self.terms[tuple(term)] = coefficient elif isinstance(term, str): list_ops = [] - for el in term.split(): - if len(el) < 2: + for element in term.split(): + if len(element) < 2: raise ValueError('term specified incorrectly.') - list_ops.append((int(el[1:]), el[0])) + list_ops.append((int(element[1:]), element[0])) # Test that list_ops has correct format of tuples for local_operator in list_ops: qubit_num, action = local_operator if not isinstance(action, str) or action not in 'XYZ': - raise ValueError("Invalid action provided: must be " - "string 'X', 'Y', or 'Z'.") + raise ValueError("Invalid action provided: must be string 'X', 'Y', or 'Z'.") if not (isinstance(qubit_num, int) and qubit_num >= 0): - raise QubitOperatorError("Invalid qubit number " - "provided to QubitTerm: " - "must be a non-negative " - "int.") + raise QubitOperatorError("Invalid qubit number provided to QubitTerm: must be a non-negative int.") # Sort and add to self.terms: list_ops.sort(key=lambda loc_operator: loc_operator[0]) self.terms[tuple(list_ops)] = coefficient @@ -180,15 +176,16 @@ def __init__(self, term=None, coefficient=1.): def compress(self, abs_tol=1e-12): """ - Eliminates all terms with coefficients close to zero and removes - imaginary parts of coefficients that are close to zero. + Compress the coefficient of a QubitOperator. + + Eliminate all terms with coefficients close to zero and removes imaginary parts of coefficients that are close + to zero. Args: abs_tol(float): Absolute tolerance, must be at least 0.0 """ new_terms = {} - for term in self.terms: - coeff = self.terms[term] + for term, coeff in self.terms.items(): if abs(coeff.imag) <= abs_tol: coeff = coeff.real if abs(coeff) > abs_tol: @@ -197,13 +194,11 @@ def compress(self, abs_tol=1e-12): def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12): """ - Returns True if other (QubitOperator) is close to self. + Return True if other (QubitOperator) is close to self. - Comparison is done for each term individually. Return True - if the difference between each term in self and other is - less than the relative tolerance w.r.t. either other or self - (symmetric test) or if the difference is less than the absolute - tolerance. + Comparison is done for each term individually. Return True if the difference between each term in self and + other is less than the relative tolerance w.r.t. either other or self (symmetric test) or if the difference is + less than the absolute tolerance. Args: other(QubitOperator): QubitOperator to compare against. @@ -212,10 +207,10 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12): """ # terms which are in both: for term in set(self.terms).intersection(set(other.terms)): - a = self.terms[term] - b = other.terms[term] + left = self.terms[term] + right = other.terms[term] # math.isclose does this in Python >=3.5 - if not abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol): + if not abs(left - right) <= max(rel_tol * max(abs(left), abs(right)), abs_tol): return False # terms only in one (compare to 0.0 so only abs_tol) for term in set(self.terms).symmetric_difference(set(other.terms)): @@ -226,7 +221,141 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12): return False return True - def __imul__(self, multiplier): + def __or__(self, qubits): # pylint: disable=too-many-locals + """ + Operator| overload which enables the syntax Gate | qubits. + + In particular, enable the following syntax: + + .. code-block:: python + + QubitOperator(...) | qureg + QubitOperator(...) | (qureg,) + QubitOperator(...) | qubit + QubitOperator(...) | (qubit,) + + Unlike other gates, this gate is only allowed to be applied to one + quantum register or one qubit and only if the QubitOperator is + unitary, i.e., consists of one term with a coefficient whose absolute + values is 1. + + Example: + + .. code-block:: python + + eng = projectq.MainEngine() + qureg = eng.allocate_qureg(6) + QubitOperator('X0 X5', 1.0j) | qureg # Applies X to qubit 0 and 5 + # with an additional global + # phase of 1.j + + While in the above example the QubitOperator gate is applied to 6 + qubits, it only acts non-trivially on the two qubits qureg[0] and + qureg[5]. Therefore, the operator| will create a new rescaled + QubitOperator, i.e, it sends the equivalent of the following new gate + to the MainEngine: + + .. code-block:: python + + QubitOperator('X0 X1', 1.0j) | [qureg[0], qureg[5]] + + which is only a two qubit gate. + + Args: + qubits: one Qubit object, one list of Qubit objects, one Qureg + object, or a tuple of the former three cases. + + Raises: + TypeError: If QubitOperator is not unitary or applied to more than + one quantum register. + ValueError: If quantum register does not have enough qubits + """ + # Check that input is only one qureg or one qubit + qubits = self.make_tuple_of_qureg(qubits) + if len(qubits) != 1: + raise TypeError("Only one qubit or qureg allowed.") + # Check that operator is unitary + if not len(self.terms) == 1: + raise TypeError( + "Too many terms. Only QubitOperators consisting " + "of a single term (single n-qubit Pauli operator) " + "with a coefficient of unit length can be applied " + "to qubits with this function." + ) + ((term, coefficient),) = self.terms.items() + phase = cmath.phase(coefficient) + if abs(coefficient) < 1 - EQ_TOLERANCE or abs(coefficient) > 1 + EQ_TOLERANCE: + raise TypeError( + "abs(coefficient) != 1. Only QubitOperators " + "consisting of a single term (single n-qubit " + "Pauli operator) with a coefficient of unit " + "length can be applied to qubits with this " + "function." + ) + # Test if we need to apply only Ph + if term == (): + Ph(phase) | qubits[0][0] + return + # Check that Qureg has enough qubits: + num_qubits = len(qubits[0]) + non_trivial_qubits = set() + for index, _ in term: + non_trivial_qubits.add(index) + if max(non_trivial_qubits) >= num_qubits: + raise ValueError("QubitOperator acts on more qubits than the gate is applied to.") + # Apply X, Y, Z, if QubitOperator acts only on one qubit + if len(term) == 1: + if term[0][1] == "X": + X | qubits[0][term[0][0]] + elif term[0][1] == "Y": + Y | qubits[0][term[0][0]] + elif term[0][1] == "Z": + Z | qubits[0][term[0][0]] + Ph(phase) | qubits[0][term[0][0]] + return + # Create new QubitOperator gate with rescaled qubit indices in + # 0,..., len(non_trivial_qubits) - 1 + new_index = {} + non_trivial_qubits = sorted(non_trivial_qubits) + for i, qubit in enumerate(non_trivial_qubits): + new_index[qubit] = i + new_qubitoperator = QubitOperator() + new_term = tuple((new_index[index], action) for index, action in term) + new_qubitoperator.terms[new_term] = coefficient + new_qubits = [qubits[0][i] for i in non_trivial_qubits] + # Apply new gate + cmd = new_qubitoperator.generate_command(new_qubits) + apply_command(cmd) + + def get_inverse(self): + """ + Return the inverse gate of a QubitOperator if applied as a gate. + + Raises: + NotInvertible: Not implemented for QubitOperators which have + multiple terms or a coefficient with absolute value + not equal to 1. + """ + if len(self.terms) == 1: + ((term, coefficient),) = self.terms.items() + if not abs(coefficient) < 1 - EQ_TOLERANCE and not abs(coefficient) > 1 + EQ_TOLERANCE: + return QubitOperator(term, coefficient ** (-1)) + raise NotInvertible("BasicGate: No get_inverse() implemented.") + + def get_merged(self, other): + """ + Return this gate merged with another gate. + + Standard implementation of get_merged: + + Raises: + NotMergeable: merging is not possible + """ + if isinstance(other, self.__class__) and len(other.terms) == 1 and len(self.terms) == 1: + return self * other + raise NotMergeable() + + def __imul__(self, multiplier): # pylint: disable=too-many-locals,too-many-branches """ In-place multiply (*=) terms with scalar or QubitOperator. @@ -240,12 +369,11 @@ def __imul__(self, multiplier): return self # Handle QubitOperator. - elif isinstance(multiplier, QubitOperator): - result_terms = dict() - for left_term in self.terms: - for right_term in multiplier.terms: - new_coefficient = (self.terms[left_term] * - multiplier.terms[right_term]) + if isinstance(multiplier, QubitOperator): # pylint: disable=too-many-nested-blocks + result_terms = {} + for left_term, left_coeff in self.terms.items(): + for right_term, right_coeff in multiplier.terms.items(): + new_coefficient = left_coeff * right_coeff # Loop through local operators and create new sorted list # of representing the product local operator: @@ -254,19 +382,15 @@ def __imul__(self, multiplier): right_operator_index = 0 n_operators_left = len(left_term) n_operators_right = len(right_term) - while (left_operator_index < n_operators_left and - right_operator_index < n_operators_right): - (left_qubit, left_loc_op) = ( - left_term[left_operator_index]) - (right_qubit, right_loc_op) = ( - right_term[right_operator_index]) + while left_operator_index < n_operators_left and right_operator_index < n_operators_right: + (left_qubit, left_loc_op) = left_term[left_operator_index] + (right_qubit, right_loc_op) = right_term[right_operator_index] # Multiply local operators acting on the same qubit if left_qubit == right_qubit: left_operator_index += 1 right_operator_index += 1 - (scalar, loc_op) = _PAULI_OPERATOR_PRODUCTS[ - (left_loc_op, right_loc_op)] + (scalar, loc_op) = _PAULI_OPERATOR_PRODUCTS[(left_loc_op, right_loc_op)] # Add new term. if loc_op != 'I': @@ -285,8 +409,7 @@ def __imul__(self, multiplier): # Finish the remainding operators: if left_operator_index == n_operators_left: - product_operators += right_term[ - right_operator_index::] + product_operators += right_term[right_operator_index::] elif right_operator_index == n_operators_right: product_operators += left_term[left_operator_index::] @@ -298,9 +421,7 @@ def __imul__(self, multiplier): result_terms[tmp_key] = new_coefficient self.terms = result_terms return self - else: - raise TypeError('Cannot in-place multiply term of invalid type ' + - 'to QubitTerm.') + raise TypeError('Cannot in-place multiply term of invalid type to QubitTerm.') def __mul__(self, multiplier): """ @@ -315,14 +436,11 @@ def __mul__(self, multiplier): Raises: TypeError: Invalid type cannot be multiply with QubitOperator. """ - if (isinstance(multiplier, (int, float, complex)) or - isinstance(multiplier, QubitOperator)): + if isinstance(multiplier, (int, float, complex, QubitOperator)): product = copy.deepcopy(self) product *= multiplier return product - else: - raise TypeError( - 'Object of invalid type cannot multiply with QubitOperator.') + raise TypeError('Object of invalid type cannot multiply with QubitOperator.') def __rmul__(self, multiplier): """ @@ -342,8 +460,7 @@ def __rmul__(self, multiplier): TypeError: Object of invalid type cannot multiply QubitOperator. """ if not isinstance(multiplier, (int, float, complex)): - raise TypeError( - 'Object of invalid type cannot multiply with QubitOperator.') + raise TypeError('Object of invalid type cannot multiply with QubitOperator.') return self * multiplier def __truediv__(self, divisor): @@ -366,20 +483,13 @@ def __truediv__(self, divisor): raise TypeError('Cannot divide QubitOperator by non-scalar type.') return self * (1.0 / divisor) - def __div__(self, divisor): - """ For compatibility with Python 2. """ - return self.__truediv__(divisor) - def __itruediv__(self, divisor): + """Perform self =/ divisor for a scalar.""" if not isinstance(divisor, (int, float, complex)): raise TypeError('Cannot divide QubitOperator by non-scalar type.') - self *= (1.0 / divisor) + self *= 1.0 / divisor return self - def __idiv__(self, divisor): - """ For compatibility with Python 2. """ - return self.__itruediv__(divisor) - def __iadd__(self, addend): """ In-place method for += addition of QubitOperator. @@ -393,10 +503,10 @@ def __iadd__(self, addend): if isinstance(addend, QubitOperator): for term in addend.terms: if term in self.terms: - if abs(addend.terms[term] + self.terms[term]) > 0.: + if abs(addend.terms[term] + self.terms[term]) > 0.0: self.terms[term] += addend.terms[term] else: - del self.terms[term] + self.terms.pop(term) else: self.terms[term] = addend.terms[term] else: @@ -404,7 +514,7 @@ def __iadd__(self, addend): return self def __add__(self, addend): - """ Return self + addend for a QubitOperator. """ + """Return self + addend for a QubitOperator.""" summand = copy.deepcopy(self) summand += addend return summand @@ -422,10 +532,10 @@ def __isub__(self, subtrahend): if isinstance(subtrahend, QubitOperator): for term in subtrahend.terms: if term in self.terms: - if abs(self.terms[term] - subtrahend.terms[term]) > 0.: + if abs(self.terms[term] - subtrahend.terms[term]) > 0.0: self.terms[term] -= subtrahend.terms[term] else: - del self.terms[term] + self.terms.pop(term) else: self.terms[term] = -subtrahend.terms[term] else: @@ -433,33 +543,44 @@ def __isub__(self, subtrahend): return self def __sub__(self, subtrahend): - """ Return self - subtrahend for a QubitOperator. """ + """Return self - subtrahend for a QubitOperator.""" minuend = copy.deepcopy(self) minuend -= subtrahend return minuend def __neg__(self): - return -1. * self + """ + Opposite operator. + + Return -self for a QubitOperator. + """ + return -1.0 * self def __str__(self): - """Return an easy-to-read string representation.""" + """Return a string representation of the object.""" if not self.terms: return '0' string_rep = '' - for term in self.terms: - tmp_string = '{}'.format(self.terms[term]) + for term, coeff in self.terms.items(): + tmp_string = f'{coeff}' if term == (): tmp_string += ' I' for operator in term: if operator[1] == 'X': - tmp_string += ' X{}'.format(operator[0]) + tmp_string += f' X{operator[0]}' elif operator[1] == 'Y': - tmp_string += ' Y{}'.format(operator[0]) - else: - assert operator[1] == 'Z' - tmp_string += ' Z{}'.format(operator[0]) - string_rep += '{} +\n'.format(tmp_string) + tmp_string += f' Y{operator[0]}' + elif operator[1] == 'Z': + tmp_string += f' Z{operator[0]}' + else: # pragma: no cover + raise ValueError('Internal compiler error: operator must be one of X, Y, Z!') + string_rep += f'{tmp_string} +\n' return string_rep[:-3] def __repr__(self): + """Repr method.""" return str(self) + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) diff --git a/projectq/ops/_qubit_operator_test.py b/projectq/ops/_qubit_operator_test.py index f7f76cd61..3b5e30508 100644 --- a/projectq/ops/_qubit_operator_test.py +++ b/projectq/ops/_qubit_operator_test.py @@ -11,33 +11,41 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for _qubit_operator.py.""" +import cmath import copy +import math import numpy import pytest +from projectq import MainEngine +from projectq.cengines import DummyEngine from projectq.ops import _qubit_operator as qo +from ._basics import NotInvertible, NotMergeable +from ._gates import Ph, T, X, Y, Z + def test_pauli_operator_product_unchanged(): - correct = {('I', 'I'): (1., 'I'), - ('I', 'X'): (1., 'X'), - ('X', 'I'): (1., 'X'), - ('I', 'Y'): (1., 'Y'), - ('Y', 'I'): (1., 'Y'), - ('I', 'Z'): (1., 'Z'), - ('Z', 'I'): (1., 'Z'), - ('X', 'X'): (1., 'I'), - ('Y', 'Y'): (1., 'I'), - ('Z', 'Z'): (1., 'I'), - ('X', 'Y'): (1.j, 'Z'), - ('X', 'Z'): (-1.j, 'Y'), - ('Y', 'X'): (-1.j, 'Z'), - ('Y', 'Z'): (1.j, 'X'), - ('Z', 'X'): (1.j, 'Y'), - ('Z', 'Y'): (-1.j, 'X')} + correct = { + ('I', 'I'): (1.0, 'I'), + ('I', 'X'): (1.0, 'X'), + ('X', 'I'): (1.0, 'X'), + ('I', 'Y'): (1.0, 'Y'), + ('Y', 'I'): (1.0, 'Y'), + ('I', 'Z'): (1.0, 'Z'), + ('Z', 'I'): (1.0, 'Z'), + ('X', 'X'): (1.0, 'I'), + ('Y', 'Y'): (1.0, 'I'), + ('Z', 'Z'): (1.0, 'I'), + ('X', 'Y'): (1.0j, 'Z'), + ('X', 'Z'): (-1.0j, 'Y'), + ('Y', 'X'): (-1.0j, 'Z'), + ('Y', 'Z'): (1.0j, 'X'), + ('Z', 'X'): (1.0j, 'Y'), + ('Z', 'Y'): (-1.0j, 'X'), + } assert qo._PAULI_OPERATOR_PRODUCTS == correct @@ -46,8 +54,7 @@ def test_init_defaults(): assert len(loc_op.terms) == 0 -@pytest.mark.parametrize("coefficient", [0.5, 0.6j, numpy.float64(2.303), - numpy.complex128(-1j)]) +@pytest.mark.parametrize("coefficient", [0.5, 0.6j, numpy.float64(2.303), numpy.complex128(-1j)]) def test_init_tuple(coefficient): loc_op = ((0, 'X'), (5, 'Y'), (6, 'Z')) qubit_op = qo.QubitOperator(loc_op, coefficient) @@ -56,61 +63,61 @@ def test_init_tuple(coefficient): def test_init_str(): - qubit_op = qo.QubitOperator('X0 Y5 Z12', -1.) + qubit_op = qo.QubitOperator('X0 Y5 Z12', -1.0) correct = ((0, 'X'), (5, 'Y'), (12, 'Z')) assert correct in qubit_op.terms assert qubit_op.terms[correct] == -1.0 def test_init_str_identity(): - qubit_op = qo.QubitOperator('', 2.) + qubit_op = qo.QubitOperator('', 2.0) assert len(qubit_op.terms) == 1 assert () in qubit_op.terms - assert qubit_op.terms[()] == pytest.approx(2.) + assert qubit_op.terms[()] == pytest.approx(2.0) def test_init_bad_term(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator(list()) + qo.QubitOperator([]) def test_init_bad_coefficient(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator('X0', "0.5") + qo.QubitOperator('X0', "0.5") def test_init_bad_action(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator('Q0') + qo.QubitOperator('Q0') def test_init_bad_action_in_tuple(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator(((1, 'Q'),)) + qo.QubitOperator(((1, 'Q'),)) def test_init_bad_qubit_num_in_tuple(): with pytest.raises(qo.QubitOperatorError): - qubit_op = qo.QubitOperator((("1", 'X'),)) + qo.QubitOperator((("1", 'X'),)) def test_init_bad_tuple(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator(((0, 1, 'X'),)) + qo.QubitOperator(((0, 1, 'X'),)) def test_init_bad_str(): with pytest.raises(ValueError): - qubit_op = qo.QubitOperator('X') + qo.QubitOperator('X') def test_init_bad_qubit_num(): with pytest.raises(qo.QubitOperatorError): - qubit_op = qo.QubitOperator('X-1') + qo.QubitOperator('X-1') def test_isclose_abs_tol(): - a = qo.QubitOperator('X0', -1.) + a = qo.QubitOperator('X0', -1.0) b = qo.QubitOperator('X0', -1.05) c = qo.QubitOperator('X0', -1.11) assert a.isclose(b, rel_tol=1e-14, abs_tol=0.1) @@ -123,30 +130,30 @@ def test_isclose_abs_tol(): def test_compress(): - a = qo.QubitOperator('X0', .9e-12) + a = qo.QubitOperator('X0', 0.9e-12) assert len(a.terms) == 1 a.compress() assert len(a.terms) == 0 - a = qo.QubitOperator('X0', 1. + 1j) - a.compress(.5) + a = qo.QubitOperator('X0', 1.0 + 1j) + a.compress(0.5) assert len(a.terms) == 1 for term in a.terms: - assert a.terms[term] == 1. + 1j + assert a.terms[term] == 1.0 + 1j a = qo.QubitOperator('X0', 1.1 + 1j) - a.compress(1.) + a.compress(1.0) assert len(a.terms) == 1 for term in a.terms: assert a.terms[term] == 1.1 - a = qo.QubitOperator('X0', 1.1 + 1j) + qo.QubitOperator('X1', 1.e-6j) + a = qo.QubitOperator('X0', 1.1 + 1j) + qo.QubitOperator('X1', 1.0e-6j) a.compress() assert len(a.terms) == 2 for term in a.terms: assert isinstance(a.terms[term], complex) - a.compress(1.e-5) + a.compress(1.0e-5) assert len(a.terms) == 1 for term in a.terms: assert isinstance(a.terms[term], complex) - a.compress(1.) + a.compress(1.0) assert len(a.terms) == 1 for term in a.terms: assert isinstance(a.terms[term], float) @@ -184,15 +191,103 @@ def test_isclose_different_num_terms(): assert not a.isclose(b, rel_tol=1e-12, abs_tol=0.05) +def test_get_inverse(): + qo0 = qo.QubitOperator("X1 Z2", cmath.exp(0.6j)) + qo1 = qo.QubitOperator("", 1j) + assert qo0.get_inverse().isclose(qo.QubitOperator("X1 Z2", cmath.exp(-0.6j))) + assert qo1.get_inverse().isclose(qo.QubitOperator("", -1j)) + qo0 += qo1 + with pytest.raises(NotInvertible): + qo0.get_inverse() + + +def test_get_merged(): + qo0 = qo.QubitOperator("X1 Z2", 1j) + qo1 = qo.QubitOperator("Y3", 1j) + assert qo0.isclose(qo.QubitOperator("X1 Z2", 1j)) + assert qo1.isclose(qo.QubitOperator("Y3", 1j)) + assert qo0.get_merged(qo1).isclose(qo.QubitOperator("X1 Z2 Y3", -1)) + with pytest.raises(NotMergeable): + qo1.get_merged(T) + qo2 = qo0 + qo1 + with pytest.raises(NotMergeable): + qo2.get_merged(qo0) + with pytest.raises(NotMergeable): + qo0.get_merged(qo2) + + +def test_or_one_qubit(): + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend, engine_list=[]) + qureg = eng.allocate_qureg(3) + eng.flush() + identity = qo.QubitOperator("", 1j) + x = qo.QubitOperator("X1", cmath.exp(0.5j)) + y = qo.QubitOperator("Y2", cmath.exp(0.6j)) + z = qo.QubitOperator("Z0", cmath.exp(4.5j)) + identity | qureg + eng.flush() + x | qureg + eng.flush() + y | qureg + eng.flush() + z | qureg + eng.flush() + assert saving_backend.received_commands[4].gate == Ph(math.pi / 2.0) + + assert saving_backend.received_commands[6].gate == X + assert saving_backend.received_commands[6].qubits == ([qureg[1]],) + assert saving_backend.received_commands[7].gate == Ph(0.5) + assert saving_backend.received_commands[7].qubits == ([qureg[1]],) + + assert saving_backend.received_commands[9].gate == Y + assert saving_backend.received_commands[9].qubits == ([qureg[2]],) + assert saving_backend.received_commands[10].gate == Ph(0.6) + assert saving_backend.received_commands[10].qubits == ([qureg[2]],) + + assert saving_backend.received_commands[12].gate == Z + assert saving_backend.received_commands[12].qubits == ([qureg[0]],) + assert saving_backend.received_commands[13].gate == Ph(4.5) + assert saving_backend.received_commands[13].qubits == ([qureg[0]],) + + +def test_wrong_input(): + eng = MainEngine() + qureg = eng.allocate_qureg(3) + op0 = qo.QubitOperator("X1", 0.99) + with pytest.raises(TypeError): + op0 | qureg + op1 = qo.QubitOperator("X2", 1) + with pytest.raises(ValueError): + op1 | qureg[1] + with pytest.raises(TypeError): + op0 | (qureg[1], qureg[2]) + op2 = op0 + op1 + with pytest.raises(TypeError): + op2 | qureg + + +def test_rescaling_of_indices(): + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend, engine_list=[]) + qureg = eng.allocate_qureg(4) + eng.flush() + op = qo.QubitOperator("X0 Y1 Z3", 1j) + op | qureg + eng.flush() + assert saving_backend.received_commands[5].gate.isclose(qo.QubitOperator("X0 Y1 Z2", 1j)) + # test that gate creates a new QubitOperator + assert op.isclose(qo.QubitOperator("X0 Y1 Z3", 1j)) + + def test_imul_inplace(): qubit_op = qo.QubitOperator("X1") prev_id = id(qubit_op) - qubit_op *= 3. + qubit_op *= 3.0 assert id(qubit_op) == prev_id -@pytest.mark.parametrize("multiplier", [0.5, 0.6j, numpy.float64(2.303), - numpy.complex128(-1j)]) +@pytest.mark.parametrize("multiplier", [0.5, 0.6j, numpy.float64(2.303), numpy.complex128(-1j)]) def test_imul_scalar(multiplier): loc_op = ((1, 'X'), (2, 'Y')) qubit_op = qo.QubitOperator(loc_op) @@ -201,13 +296,14 @@ def test_imul_scalar(multiplier): def test_imul_qubit_op(): - op1 = qo.QubitOperator(((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.j) + op1 = qo.QubitOperator(((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.0j) op2 = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) op1 *= op2 - correct_coefficient = 1.j * 3.0j * 0.5 + correct_coefficient = 1.0j * 3.0j * 0.5 correct_term = ((0, 'Y'), (1, 'X'), (3, 'Z'), (11, 'X')) assert len(op1.terms) == 1 assert correct_term in op1.terms + assert op1.terms[correct_term] == correct_coefficient def test_imul_qubit_op_2(): @@ -249,13 +345,12 @@ def test_mul_bad_multiplier(): def test_mul_out_of_place(): - op1 = qo.QubitOperator(((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.j) + op1 = qo.QubitOperator(((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.0j) op2 = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) op3 = op1 * op2 - correct_coefficient = 1.j * 3.0j * 0.5 + correct_coefficient = 1.0j * 3.0j * 0.5 correct_term = ((0, 'Y'), (1, 'X'), (3, 'Z'), (11, 'X')) - assert op1.isclose(qo.QubitOperator( - ((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.j)) + assert op1.isclose(qo.QubitOperator(((0, 'Y'), (3, 'X'), (8, 'Z'), (11, 'X')), 3.0j)) assert op2.isclose(qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5)) assert op3.isclose(qo.QubitOperator(correct_term, correct_coefficient)) @@ -272,13 +367,11 @@ def test_mul_multiple_terms(): op += qo.QubitOperator(((1, 'Z'), (3, 'Y'), (9, 'Z')), 1.4j) res = op * op correct = qo.QubitOperator((), 0.5**2 + 1.2**2 + 1.4j**2) - correct += qo.QubitOperator(((1, 'Y'), (3, 'Z')), - 2j * 1j * 0.5 * 1.2) + correct += qo.QubitOperator(((1, 'Y'), (3, 'Z')), 2j * 1j * 0.5 * 1.2) assert res.isclose(correct) -@pytest.mark.parametrize("multiplier", [0.5, 0.6j, numpy.float64(2.303), - numpy.complex128(-1j)]) +@pytest.mark.parametrize("multiplier", [0.5, 0.6j, numpy.float64(2.303), numpy.complex128(-1j)]) def test_rmul_scalar(multiplier): op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) res1 = op * multiplier @@ -292,20 +385,15 @@ def test_rmul_bad_multiplier(): op = "0.5" * op -@pytest.mark.parametrize("divisor", [0.5, 0.6j, numpy.float64(2.303), - numpy.complex128(-1j), 2]) +@pytest.mark.parametrize("divisor", [0.5, 0.6j, numpy.float64(2.303), numpy.complex128(-1j), 2]) def test_truediv_and_div(divisor): op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) - op2 = copy.deepcopy(op) original = copy.deepcopy(op) res = op / divisor - res2 = op2.__div__(divisor) # To test python 2 version as well - correct = op * (1. / divisor) + correct = op * (1.0 / divisor) assert res.isclose(correct) - assert res2.isclose(correct) # Test if done out of place assert op.isclose(original) - assert op2.isclose(original) def test_truediv_bad_divisor(): @@ -314,20 +402,15 @@ def test_truediv_bad_divisor(): op = op / "0.5" -@pytest.mark.parametrize("divisor", [0.5, 0.6j, numpy.float64(2.303), - numpy.complex128(-1j), 2]) +@pytest.mark.parametrize("divisor", [0.5, 0.6j, numpy.float64(2.303), numpy.complex128(-1j), 2]) def test_itruediv_and_idiv(divisor): op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) - op2 = copy.deepcopy(op) original = copy.deepcopy(op) - correct = op * (1. / divisor) + correct = op * (1.0 / divisor) op /= divisor - op2.__idiv__(divisor) # To test python 2 version as well assert op.isclose(correct) - assert op2.isclose(correct) # Test if done in-place assert not op.isclose(original) - assert not op2.isclose(original) def test_itruediv_bad_divisor(): @@ -417,6 +500,9 @@ def test_isub_different_term(): assert len(a.terms) == 2 assert a.terms[term_a] == pytest.approx(1.0) assert a.terms[term_b] == pytest.approx(-1.0) + b = qo.QubitOperator(term_a, 1.0) + b -= qo.QubitOperator(term_a, 1.0) + assert b.terms == {} def test_isub_bad_addend(): @@ -441,6 +527,11 @@ def test_str(): assert str(op2) == "2 I" +def test_hash(): + op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) + assert hash(op) == hash("0.5 X1 Y3 Z8") + + def test_str_empty(): op = qo.QubitOperator() assert str(op) == '0' @@ -449,8 +540,7 @@ def test_str_empty(): def test_str_multiple_terms(): op = qo.QubitOperator(((1, 'X'), (3, 'Y'), (8, 'Z')), 0.5) op += qo.QubitOperator(((1, 'Y'), (3, 'Y'), (8, 'Z')), 0.6) - assert (str(op) == "0.5 X1 Y3 Z8 +\n0.6 Y1 Y3 Z8" or - str(op) == "0.6 Y1 Y3 Z8 +\n0.5 X1 Y3 Z8") + assert str(op) == "0.5 X1 Y3 Z8 +\n0.6 Y1 Y3 Z8" or str(op) == "0.6 Y1 Y3 Z8 +\n0.5 X1 Y3 Z8" op2 = qo.QubitOperator((), 2) assert str(op2) == "2 I" diff --git a/projectq/ops/_shortcuts.py b/projectq/ops/_shortcuts.py index 0635ca9f3..bc80c6fcc 100755 --- a/projectq/ops/_shortcuts.py +++ b/projectq/ops/_shortcuts.py @@ -11,29 +11,26 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines a few shortcuts for certain gates such as +A few shortcuts for certain gates. + +These include: * CNOT = C(NOT) * CRz = C(Rz) * Toffoli = C(NOT,2) = C(CNOT) """ -from ._metagates import C from ._gates import NOT, Rz, Z +from ._metagates import C def CRz(angle): - """ - Shortcut for C(Rz(angle), n=1). - """ - return C(Rz(angle), n=1) + """Shortcut for C(Rz(angle), n_qubits=1).""" + return C(Rz(angle), n_qubits=1) CNOT = CX = C(NOT) - CZ = C(Z) - Toffoli = C(CNOT) diff --git a/projectq/ops/_shortcuts_test.py b/projectq/ops/_shortcuts_test.py index d6bd8b707..8c55d6625 100755 --- a/projectq/ops/_shortcuts_test.py +++ b/projectq/ops/_shortcuts_test.py @@ -11,12 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.ops._shortcuts.""" -from projectq.ops import ControlledGate, Rz - -from projectq.ops import _shortcuts +from projectq.ops import ControlledGate, Rz, _shortcuts def test_crz(): diff --git a/projectq/ops/_state_prep.py b/projectq/ops/_state_prep.py new file mode 100644 index 000000000..a2f3564df --- /dev/null +++ b/projectq/ops/_state_prep.py @@ -0,0 +1,56 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Definition of the state preparation gate.""" + +from ._basics import BasicGate + + +class StatePreparation(BasicGate): + """Gate for transforming qubits in state |0> to any desired quantum state.""" + + def __init__(self, final_state): + """ + Initialize a StatePreparation gate. + + Example: + .. code-block:: python + + qureg = eng.allocate_qureg(2) + StatePreparation([0.5, -0.5j, -0.5, 0.5]) | qureg + + Note: + final_state[k] is taken to be the amplitude of the computational basis state whose string is equal to the + binary representation of k. + + Args: + final_state(list[complex]): wavefunction of the desired quantum state. len(final_state) must be + 2**len(qureg). Must be normalized! + """ + super().__init__() + self.final_state = list(final_state) + + def __str__(self): + """Return a string representation of the object.""" + return "StatePreparation" + + def __eq__(self, other): + """Equal operator.""" + if isinstance(other, self.__class__): + return self.final_state == other.final_state + return False + + def __hash__(self): + """Compute the hash of the object.""" + return hash(f"StatePreparation({str(self.final_state)})") diff --git a/projectq/ops/_state_prep_test.py b/projectq/ops/_state_prep_test.py new file mode 100644 index 000000000..f6cac5358 --- /dev/null +++ b/projectq/ops/_state_prep_test.py @@ -0,0 +1,31 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.ops._state_prep.""" + +from projectq.ops import X, _state_prep + + +def test_equality_and_hash(): + gate1 = _state_prep.StatePreparation([0.5, -0.5, 0.5, -0.5]) + gate2 = _state_prep.StatePreparation([0.5, -0.5, 0.5, -0.5]) + gate3 = _state_prep.StatePreparation([0.5, -0.5, 0.5, 0.5]) + assert gate1 == gate2 + assert hash(gate1) == hash(gate2) + assert gate1 != gate3 + assert gate1 != X + + +def test_str(): + gate1 = _state_prep.StatePreparation([0, 1]) + assert str(gate1) == "StatePreparation" diff --git a/projectq/ops/_time_evolution.py b/projectq/ops/_time_evolution.py index 46e8979b6..c28de10c2 100644 --- a/projectq/ops/_time_evolution.py +++ b/projectq/ops/_time_evolution.py @@ -12,17 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy +"""Definition of the time evolution gate.""" -from projectq.ops import Ph +import copy from ._basics import BasicGate, NotMergeable -from ._qubit_operator import QubitOperator from ._command import apply_command +from ._gates import Ph +from ._qubit_operator import QubitOperator class NotHermitianOperatorError(Exception): - pass + """Error raised if an operator is non-hermitian.""" class TimeEvolution(BasicGate): @@ -32,7 +33,7 @@ class TimeEvolution(BasicGate): This gate is the unitary time evolution propagator: exp(-i * H * t), where H is the Hamiltonian of the system and t is the time. Note that -i - factor is stored implicitely. + factor is stored implicitly. Example: .. code-block:: python @@ -47,13 +48,13 @@ class TimeEvolution(BasicGate): hamiltonian(QubitOperator): hamiltonaian H """ + def __init__(self, time, hamiltonian): """ Initialize time evolution gate. Note: - The hamiltonian must be hermitian and therefore only terms with - real coefficients are allowed. + The hamiltonian must be hermitian and therefore only terms with real coefficients are allowed. Coefficients are internally converted to float. Args: @@ -61,12 +62,10 @@ def __init__(self, time, hamiltonian): hamiltonian (QubitOperator): hamiltonian to evolve under. Raises: - TypeError: If time is not a numeric type and hamiltonian is not a - QubitOperator. - NotHermitianOperatorError: If the input hamiltonian is not - hermitian (only real coefficients). + TypeError: If time is not a numeric type and hamiltonian is not a QubitOperator. + NotHermitianOperatorError: If the input hamiltonian is not hermitian (only real coefficients). """ - BasicGate.__init__(self) + super().__init__() if not isinstance(time, (float, int)): raise TypeError("time needs to be a (real) numeric type.") if not isinstance(hamiltonian, QubitOperator): @@ -75,17 +74,12 @@ def __init__(self, time, hamiltonian): self.hamiltonian = copy.deepcopy(hamiltonian) for term in hamiltonian.terms: if self.hamiltonian.terms[term].imag == 0: - self.hamiltonian.terms[term] = float( - self.hamiltonian.terms[term].real) + self.hamiltonian.terms[term] = float(self.hamiltonian.terms[term].real) else: - raise NotHermitianOperatorError("hamiltonian must be " - "hermitian and hence only " - "have real coefficients.") + raise NotHermitianOperatorError("hamiltonian must be hermitian and hence only have real coefficients.") def get_inverse(self): - """ - Return the inverse gate. - """ + """Return the inverse gate.""" return TimeEvolution(self.time * -1.0, self.hamiltonian) def get_merged(self, other): @@ -94,54 +88,47 @@ def get_merged(self, other): Two TimeEvolution gates are merged if: 1) both have the same terms - 2) the proportionality factor for each of the terms - must have relative error <= 1e-9 compared to the + 2) the proportionality factor for each of the terms must have relative error <= 1e-9 compared to the proportionality factors of the other terms. Note: - While one could merge gates for which both hamiltonians commute, - we are not doing this as in general the resulting gate would have - to be decomposed again. + While one could merge gates for which both hamiltonians commute, we are not doing this as in general the + resulting gate would have to be decomposed again. Note: - We are not comparing if terms are proportional to each other with - an absolute tolerance. It is up to the user to remove terms close - to zero because we cannot choose a suitable absolute error which - works for everyone. Use, e.g., a decomposition rule for that. + We are not comparing if terms are proportional to each other with an absolute tolerance. It is up to the + user to remove terms close to zero because we cannot choose a suitable absolute error which works for + everyone. Use, e.g., a decomposition rule for that. Args: other: TimeEvolution gate Raises: - NotMergeable: If the other gate is not a TimeEvolution gate or - hamiltonians are not suitable for merging. + NotMergeable: If the other gate is not a TimeEvolution gate or hamiltonians are not suitable for merging. Returns: New TimeEvolution gate equivalent to the two merged gates. """ rel_tol = 1e-9 - if (isinstance(other, TimeEvolution) and - set(self.hamiltonian.terms) == set(other.hamiltonian.terms)): + if isinstance(other, TimeEvolution) and set(self.hamiltonian.terms) == set(other.hamiltonian.terms): factor = None for term in self.hamiltonian.terms: if factor is None: - factor = (self.hamiltonian.terms[term] / - float(other.hamiltonian.terms[term])) + factor = self.hamiltonian.terms[term] / float(other.hamiltonian.terms[term]) else: - tmp = (self.hamiltonian.terms[term] / - float(other.hamiltonian.terms[term])) - if not abs(factor - tmp) <= ( - rel_tol * max(abs(factor), abs(tmp))): + tmp = self.hamiltonian.terms[term] / float(other.hamiltonian.terms[term]) + if not abs(factor - tmp) <= (rel_tol * max(abs(factor), abs(tmp))): raise NotMergeable("Cannot merge these two gates.") # Terms are proportional to each other new_time = self.time + other.time / factor return TimeEvolution(time=new_time, hamiltonian=self.hamiltonian) - else: - raise NotMergeable("Cannot merge these two gates.") + raise NotMergeable("Cannot merge these two gates.") def __or__(self, qubits): """ - Operator| overload which enables the following syntax: + Operator| overload which enables the syntax Gate | qubits. + + In particular, enable the following syntax: .. code-block:: python @@ -150,22 +137,19 @@ def __or__(self, qubits): TimeEvolution(...) | qubit TimeEvolution(...) | (qubit,) - Unlike other gates, this gate is only allowed to be applied to one - quantum register or one qubit. + Unlike other gates, this gate is only allowed to be applied to one quantum register or one qubit. Example: - .. code-block:: python wavefunction = eng.allocate_qureg(5) hamiltonian = QubitOperator("X1 Y3", 0.5) TimeEvolution(time=2.0, hamiltonian=hamiltonian) | wavefunction - While in the above example the TimeEvolution gate is applied to 5 - qubits, the hamiltonian of this TimeEvolution gate acts only - non-trivially on the two qubits wavefunction[1] and wavefunction[3]. - Therefore, the operator| will rescale the indices in the hamiltonian - and sends the equivalent of the following new gate to the MainEngine: + While in the above example the TimeEvolution gate is applied to 5 qubits, the hamiltonian of this + TimeEvolution gate acts only non-trivially on the two qubits wavefunction[1] and wavefunction[3]. Therefore, + the operator| will rescale the indices in the hamiltonian and sends the equivalent of the following new gate + to the MainEngine: .. code-block:: python @@ -175,8 +159,8 @@ def __or__(self, qubits): which is only a two qubit gate. Args: - qubits: one Qubit object, one list of Qubit objects, one Qureg - object, or a tuple of the former three cases. + qubits: one Qubit object, one list of Qubit objects, one Qureg object, or a tuple of the former three + cases. """ # Check that input is only one qureg or one qubit qubits = self.make_tuple_of_qureg(qubits) @@ -190,23 +174,20 @@ def __or__(self, qubits): num_qubits = len(qubits[0]) non_trivial_qubits = set() for term in self.hamiltonian.terms: - for index, action in term: + for index, _ in term: non_trivial_qubits.add(index) if max(non_trivial_qubits) >= num_qubits: - raise ValueError("hamiltonian acts on more qubits than the gate " - "is applied to.") + raise ValueError("hamiltonian acts on more qubits than the gate is applied to.") # create new TimeEvolution gate with rescaled qubit indices in # self.hamiltonian which are ordered from # 0,...,len(non_trivial_qubits) - 1 - new_index = dict() - non_trivial_qubits = sorted(list(non_trivial_qubits)) - for i in range(len(non_trivial_qubits)): - new_index[non_trivial_qubits[i]] = i + new_index = {} + non_trivial_qubits = sorted(non_trivial_qubits) + for i, qubit in enumerate(non_trivial_qubits): + new_index[qubit] = i new_hamiltonian = QubitOperator() - assert len(new_hamiltonian.terms) == 0 for term in self.hamiltonian.terms: - new_term = tuple([(new_index[index], action) - for index, action in term]) + new_term = tuple((new_index[index], action) for index, action in term) new_hamiltonian.terms[new_term] = self.hamiltonian.terms[term] new_gate = TimeEvolution(time=self.time, hamiltonian=new_hamiltonian) new_qubits = [qubits[0][i] for i in non_trivial_qubits] @@ -215,12 +196,9 @@ def __or__(self, qubits): apply_command(cmd) def __eq__(self, other): - """ Not implemented as this object is a floating point type.""" - return NotImplemented - - def __ne__(self, other): - """ Not implemented as this object is a floating point type.""" + """Not implemented as this object is a floating point type.""" return NotImplemented def __str__(self): - return "exp({0} * ({1}))".format(-1j * self.time, self.hamiltonian) + """Return a string representation of the object.""" + return f"exp({-1j * self.time} * ({self.hamiltonian}))" diff --git a/projectq/ops/_time_evolution_test.py b/projectq/ops/_time_evolution_test.py index cbad44fa2..af7914fa1 100644 --- a/projectq/ops/_time_evolution_test.py +++ b/projectq/ops/_time_evolution_test.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests for projectq.ops._time_evolution.""" + import cmath import copy @@ -21,8 +22,7 @@ from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import QubitOperator, BasicGate, NotMergeable, Ph - +from projectq.ops import BasicGate, NotMergeable, Ph, QubitOperator from projectq.ops import _time_evolution as te @@ -54,22 +54,22 @@ def test_init_makes_copy(): def test_init_bad_time(): hamiltonian = QubitOperator("Z2", 0.5) with pytest.raises(TypeError): - gate = te.TimeEvolution(1.5j, hamiltonian) + te.TimeEvolution(1.5j, hamiltonian) def test_init_bad_hamiltonian(): with pytest.raises(TypeError): - gate = te.TimeEvolution(2, "something else") + te.TimeEvolution(2, "something else") def test_init_not_hermitian(): hamiltonian = QubitOperator("Z2", 1e-12j) with pytest.raises(te.NotHermitianOperatorError): - gate = te.TimeEvolution(1, hamiltonian) + te.TimeEvolution(1, hamiltonian) def test_init_cast_complex_to_float(): - hamiltonian = QubitOperator("Z2", 2+0j) + hamiltonian = QubitOperator("Z2", 2 + 0j) gate = te.TimeEvolution(1, hamiltonian) assert isinstance(gate.hamiltonian.terms[((2, 'Z'),)], float) pytest.approx(gate.hamiltonian.terms[((2, 'Z'),)]) == 2.0 @@ -122,10 +122,10 @@ def test_get_merged_not_close_enough(): hamiltonian += QubitOperator("X3", 1) gate = te.TimeEvolution(2, hamiltonian) hamiltonian2 = QubitOperator("Z2", 4) - hamiltonian2 += QubitOperator("X3", 2+1e-8) + hamiltonian2 += QubitOperator("X3", 2 + 1e-8) gate2 = te.TimeEvolution(5, hamiltonian2) with pytest.raises(NotMergeable): - merged = gate.get_merged(gate2) + gate.get_merged(gate2) def test_get_merged_bad_gate(): @@ -254,15 +254,14 @@ def test_or_gate_identity(): eng = MainEngine(backend=saving_backend, engine_list=[]) qureg = eng.allocate_qureg(4) hamiltonian = QubitOperator((), 3.4) - correct_h = copy.deepcopy(hamiltonian) + correct_h = copy.deepcopy(hamiltonian) # noqa: F841 gate = te.TimeEvolution(2.1, hamiltonian) gate | qureg eng.flush() cmd = saving_backend.received_commands[4] assert isinstance(cmd.gate, Ph) assert cmd.gate == Ph(-3.4 * 2.1) - correct = numpy.array([[cmath.exp(-1j * 3.4 * 2.1), 0], - [0, cmath.exp(-1j * 3.4 * 2.1)]]) + correct = numpy.array([[cmath.exp(-1j * 3.4 * 2.1), 0], [0, cmath.exp(-1j * 3.4 * 2.1)]]) print(correct) print(cmd.gate.matrix) assert numpy.allclose(cmd.gate.matrix, correct) @@ -284,5 +283,4 @@ def test_str(): hamiltonian = QubitOperator("X0 Z1") hamiltonian += QubitOperator("Y1", 0.5) gate = te.TimeEvolution(2.1, hamiltonian) - assert (str(gate) == "exp(-2.1j * (0.5 Y1 +\n1.0 X0 Z1))" or - str(gate) == "exp(-2.1j * (1.0 X0 Z1 +\n0.5 Y1))") + assert str(gate) == "exp(-2.1j * (0.5 Y1 +\n1.0 X0 Z1))" or str(gate) == "exp(-2.1j * (1.0 X0 Z1 +\n0.5 Y1))" diff --git a/projectq/ops/_uniformly_controlled_rotation.py b/projectq/ops/_uniformly_controlled_rotation.py new file mode 100644 index 000000000..46cf023f7 --- /dev/null +++ b/projectq/ops/_uniformly_controlled_rotation.py @@ -0,0 +1,143 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Definition of uniformly controlled Ry- and Rz-rotation gates.""" + +import math + +from ._basics import ANGLE_PRECISION, ANGLE_TOLERANCE, BasicGate, NotMergeable + + +class UniformlyControlledRy(BasicGate): + """ + Uniformly controlled Ry gate as introduced in arXiv:quant-ph/0312218. + + This is an n-qubit gate. There are n-1 control qubits and one target qubit. This gate applies Ry(angles(k)) to + the target qubit if the n-1 control qubits are in the classical state k. As there are 2^(n-1) classical states for + the control qubits, this gate requires 2^(n-1) (potentially different) angle parameters. + + Example: + .. code-block:: python + + controls = eng.allocate_qureg(2) + target = eng.allocate_qubit() + UniformlyControlledRy(angles=[0.1, 0.2, 0.3, 0.4]) | (controls, target) + + Note: + The first quantum register contains the control qubits. When converting the classical state k of the control + qubits to an integer, we define controls[0] to be the least significant (qu)bit. controls can also be an empty + list in which case the gate corresponds to an Ry. + + Args: + angles(list[float]): Rotation angles. Ry(angles[k]) is applied conditioned on the control qubits being in + state k. + """ + + def __init__(self, angles): + """Construct a UniformlyControlledRy gate.""" + super().__init__() + rounded_angles = [] + for angle in angles: + new_angle = round(float(angle) % (4.0 * math.pi), ANGLE_PRECISION) + if new_angle > 4 * math.pi - ANGLE_TOLERANCE: + new_angle = 0.0 + rounded_angles.append(new_angle) + self.angles = rounded_angles + + def get_inverse(self): + """Return the inverse of this rotation gate (negate the angles, return new object).""" + return self.__class__([-1 * angle for angle in self.angles]) + + def get_merged(self, other): + """Return self merged with another gate.""" + if isinstance(other, self.__class__): + new_angles = [angle1 + angle2 for (angle1, angle2) in zip(self.angles, other.angles)] + return self.__class__(new_angles) + raise NotMergeable() + + def __str__(self): + """Return a string representation of the object.""" + return f"UniformlyControlledRy({str(self.angles)})" + + def __eq__(self, other): + """Return True if same class, same rotation angles.""" + if isinstance(other, self.__class__): + return self.angles == other.angles + return False + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + +class UniformlyControlledRz(BasicGate): + """ + Uniformly controlled Rz gate as introduced in arXiv:quant-ph/0312218. + + This is an n-qubit gate. There are n-1 control qubits and one target qubit. This gate applies Rz(angles(k)) to + the target qubit if the n-1 control qubits are in the classical state k. As there are 2^(n-1) classical states for + the control qubits, this gate requires 2^(n-1) (potentially different) angle parameters. + + Example: + .. code-block:: python + + controls = eng.allocate_qureg(2) + target = eng.allocate_qubit() + UniformlyControlledRz(angles=[0.1, 0.2, 0.3, 0.4]) | (controls, target) + + Note: + The first quantum register are the contains qubits. When converting the classical state k of the control + qubits to an integer, we define controls[0] to be the least significant (qu)bit. controls can also be an empty + list in which case the gate corresponds to an Rz. + + Args: + angles(list[float]): Rotation angles. Rz(angles[k]) is applied conditioned on the control qubits being in + state k. + """ + + def __init__(self, angles): + """Construct a UniformlyControlledRz gate.""" + super().__init__() + rounded_angles = [] + for angle in angles: + new_angle = round(float(angle) % (4.0 * math.pi), ANGLE_PRECISION) + if new_angle > 4 * math.pi - ANGLE_TOLERANCE: + new_angle = 0.0 + rounded_angles.append(new_angle) + self.angles = rounded_angles + + def get_inverse(self): + """Return the inverse of this rotation gate (negate the angles, return new object).""" + return self.__class__([-1 * angle for angle in self.angles]) + + def get_merged(self, other): + """Return self merged with another gate.""" + if isinstance(other, self.__class__): + new_angles = [angle1 + angle2 for (angle1, angle2) in zip(self.angles, other.angles)] + return self.__class__(new_angles) + raise NotMergeable() + + def __str__(self): + """Return a string representation of the object.""" + return f"UniformlyControlledRz({str(self.angles)})" + + def __eq__(self, other): + """Return True if same class, same rotation angles.""" + if isinstance(other, self.__class__): + return self.angles == other.angles + return False + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) diff --git a/projectq/ops/_uniformly_controlled_rotation_test.py b/projectq/ops/_uniformly_controlled_rotation_test.py new file mode 100644 index 000000000..49bef25ed --- /dev/null +++ b/projectq/ops/_uniformly_controlled_rotation_test.py @@ -0,0 +1,67 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.ops._uniformly_controlled_rotation.""" +import math + +import pytest + +from projectq.ops import Rx +from projectq.ops import _uniformly_controlled_rotation as ucr + +from ._basics import NotMergeable + + +@pytest.mark.parametrize("gate_class", [ucr.UniformlyControlledRy, ucr.UniformlyControlledRz]) +def test_init_rounding(gate_class): + gate = gate_class([0.1 + 4 * math.pi, -1e-14]) + assert gate.angles == [0.1, 0.0] + + +@pytest.mark.parametrize("gate_class", [ucr.UniformlyControlledRy, ucr.UniformlyControlledRz]) +def test_get_inverse(gate_class): + gate = gate_class([0.1, 0.2, 0.3, 0.4]) + inverse = gate.get_inverse() + assert inverse == gate_class([-0.1, -0.2, -0.3, -0.4]) + + +@pytest.mark.parametrize("gate_class", [ucr.UniformlyControlledRy, ucr.UniformlyControlledRz]) +def test_get_merged(gate_class): + gate1 = gate_class([0.1, 0.2, 0.3, 0.4]) + gate2 = gate_class([0.1, 0.2, 0.3, 0.4]) + merged_gate = gate1.get_merged(gate2) + assert merged_gate == gate_class([0.2, 0.4, 0.6, 0.8]) + with pytest.raises(NotMergeable): + gate1.get_merged(Rx(0.1)) + + +def test_str_and_hash(): + gate1 = ucr.UniformlyControlledRy([0.1, 0.2, 0.3, 0.4]) + gate2 = ucr.UniformlyControlledRz([0.1, 0.2, 0.3, 0.4]) + assert str(gate1) == "UniformlyControlledRy([0.1, 0.2, 0.3, 0.4])" + assert str(gate2) == "UniformlyControlledRz([0.1, 0.2, 0.3, 0.4])" + assert hash(gate1) == hash("UniformlyControlledRy([0.1, 0.2, 0.3, 0.4])") + assert hash(gate2) == hash("UniformlyControlledRz([0.1, 0.2, 0.3, 0.4])") + + +@pytest.mark.parametrize("gate_class", [ucr.UniformlyControlledRy, ucr.UniformlyControlledRz]) +def test_equality(gate_class): + gate1 = gate_class([0.1, 0.2]) + gate2 = gate_class([0.1, 0.2 + 1e-14]) + assert gate1 == gate2 + gate3 = gate_class([0.1, 0.2, 0.1, 0.2]) + assert gate2 != gate3 + gate4 = ucr.UniformlyControlledRz([0.1, 0.2]) + gate5 = ucr.UniformlyControlledRy([0.1, 0.2]) + assert gate4 != gate5 + assert not gate5 == gate4 diff --git a/projectq/setups/__init__.py b/projectq/setups/__init__.py index ee1451dcd..32d24fd1e 100755 --- a/projectq/setups/__init__.py +++ b/projectq/setups/__init__.py @@ -11,3 +11,5 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +"""ProjectQ module containing the basic setups for ProjectQ as well as the decomposition rules.""" diff --git a/projectq/setups/_utils.py b/projectq/setups/_utils.py new file mode 100644 index 000000000..20c531abe --- /dev/null +++ b/projectq/setups/_utils.py @@ -0,0 +1,162 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Some utility functions common to some setups.""" + +import inspect + +import projectq.libs.math +import projectq.setups.decompositions +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + LocalOptimizer, + TagRemover, +) +from projectq.ops import ( + CNOT, + QFT, + BasicMathGate, + ClassicalInstructionGate, + ControlledGate, + Swap, + get_inverse, +) + + +def one_and_two_qubit_gates(eng, cmd): # pylint: disable=unused-argument + """Filter out 1- and 2-qubit gates.""" + all_qubits = [qb for qureg in cmd.all_qubits for qb in qureg] + if isinstance(cmd.gate, ClassicalInstructionGate): + # This is required to allow Measure, Allocate, Deallocate, Flush + return True + if eng.next_engine.is_available(cmd): + return True + if len(all_qubits) <= 2: + return True + return False + + +def high_level_gates(eng, cmd): # pylint: disable=unused-argument + """Remove any MathGates.""" + gate = cmd.gate + if eng.next_engine.is_available(cmd): + return True + if gate == QFT or get_inverse(gate) == QFT or gate == Swap: + return True + if isinstance(gate, BasicMathGate): + return False + return True + + +def get_engine_list_linear_grid_base(mapper, one_qubit_gates="any", two_qubit_gates=(CNOT, Swap)): + """ + Return an engine list to compile to a 2-D grid of qubits. + + Note: + If you choose a new gate set for which the compiler does not yet have standard rules, it raises an + `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...`. Also note that even the + gate sets which work might not yet be optimized. So make sure to double check and potentially extend the + decomposition rules. This implementation currently requires that the one qubit gates must contain Rz and at + least one of {Ry(best), Rx, H} and the two qubit gate must contain CNOT (recommended) or CZ. + + Note: + Classical instructions gates such as e.g. Flush and Measure are automatically allowed. + + Example: + get_engine_list(num_rows=2, num_columns=3, + one_qubit_gates=(Rz, Ry, Rx, H), + two_qubit_gates=(CNOT,)) + + Args: + num_rows(int): Number of rows in the grid + num_columns(int): Number of columns in the grid. + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. X), it allows all gates which are equal to it. If the gate is + a class (Rz), it allows all instances of this class. Default is "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate + is a class, it allows all instances of this class. Default is (CNOT, Swap). + Raises: + TypeError: If input is for the gates is not "any" or a tuple. + + Returns: + A list of suitable compiler engines. + """ + if two_qubit_gates != "any" and not isinstance(two_qubit_gates, tuple): + raise TypeError( + "two_qubit_gates parameter must be 'any' or a tuple. When supplying only one gate, make sure to" + "correctly create the tuple (don't miss the comma), e.g. two_qubit_gates=(CNOT,)" + ) + if one_qubit_gates != "any" and not isinstance(one_qubit_gates, tuple): + raise TypeError("one_qubit_gates parameter must be 'any' or a tuple.") + + rule_set = DecompositionRuleSet(modules=[projectq.libs.math, projectq.setups.decompositions]) + allowed_gate_classes = [] + allowed_gate_instances = [] + if one_qubit_gates != "any": + for gate in one_qubit_gates: + if inspect.isclass(gate): + allowed_gate_classes.append(gate) + else: + allowed_gate_instances.append((gate, 0)) + if two_qubit_gates != "any": + for gate in two_qubit_gates: + if inspect.isclass(gate): + # Controlled gate classes don't yet exists and would require + # separate treatment + if isinstance(gate, ControlledGate): # pragma: no cover + raise RuntimeError('Support for controlled gate not implemented!') + allowed_gate_classes.append(gate) + else: + if isinstance(gate, ControlledGate): + allowed_gate_instances.append((gate._gate, gate._n)) # pylint: disable=protected-access + else: + allowed_gate_instances.append((gate, 0)) + allowed_gate_classes = tuple(allowed_gate_classes) + allowed_gate_instances = tuple(allowed_gate_instances) + + def low_level_gates(eng, cmd): # pylint: disable=unused-argument + all_qubits = [q for qr in cmd.all_qubits for q in qr] + if len(all_qubits) > 2: # pragma: no cover + raise ValueError('Filter function cannot handle gates with more than 2 qubits!') + if isinstance(cmd.gate, ClassicalInstructionGate): + # This is required to allow Measure, Allocate, Deallocate, Flush + return True + if one_qubit_gates == "any" and len(all_qubits) == 1: + return True + if two_qubit_gates == "any" and len(all_qubits) == 2: + return True + if isinstance(cmd.gate, allowed_gate_classes): + return True + if (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: + return True + return False + + return [ + AutoReplacer(rule_set), + TagRemover(), + InstructionFilter(high_level_gates), + LocalOptimizer(5), + AutoReplacer(rule_set), + TagRemover(), + InstructionFilter(one_and_two_qubit_gates), + LocalOptimizer(5), + mapper, + AutoReplacer(rule_set), + TagRemover(), + InstructionFilter(low_level_gates), + LocalOptimizer(5), + ] diff --git a/projectq/setups/aqt.py b/projectq/setups/aqt.py new file mode 100644 index 000000000..96465842b --- /dev/null +++ b/projectq/setups/aqt.py @@ -0,0 +1,67 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A setup for AQT trapped ion devices. + +Defines a setup allowing to compile code for the AQT trapped ion devices: +->The 4 qubits device +->The 11 qubits simulator +->The 11 qubits noisy simulator + +It provides the `engine_list` for the `MainEngine' based on the requested +device. Decompose the circuit into a Rx/Ry/Rxx gate set that will be +translated in the backend in the Rx/Ry/MS gate set. +""" + +from projectq.backends._aqt._aqt_http_client import show_devices +from projectq.backends._exceptions import DeviceNotHandledError, DeviceOfflineError +from projectq.cengines import BasicMapperEngine +from projectq.ops import Barrier, Rx, Rxx, Ry +from projectq.setups import restrictedgateset + + +def get_engine_list(token=None, device=None): + """Return the default list of compiler engine for the AQT platform.""" + # Access to the hardware properties via show_devices + # Can also be extended to take into account gate fidelities, new available + # gate, etc.. + devices = show_devices(token) + aqt_setup = [] + if device not in devices: + raise DeviceOfflineError('Error when configuring engine list: device requested for Backend not connected') + if device == 'aqt_simulator': + # The 11 qubit online simulator doesn't need a specific mapping for + # gates. Can also run wider gateset but this setup keep the + # restrictedgateset setup for coherence + mapper = BasicMapperEngine() + # Note: Manual Mapper doesn't work, because its map is updated only if + # gates are applied if gates in the register are not used, then it + # will lead to state errors + res = {} + for i in range(devices[device]['nq']): + res[i] = i + mapper.current_mapping = res + aqt_setup = [mapper] + else: + # If there is an online device not handled into ProjectQ it's not too + # bad, the engine_list can be constructed manually with the + # appropriate mapper and the 'coupling_map' parameter + raise DeviceNotHandledError('Device not yet fully handled by ProjectQ') + + # Most gates need to be decomposed into a subset that is manually converted + # in the backend (until the implementation of the U1,U2,U3) + setup = restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry), two_qubit_gates=(Rxx,), other_gates=(Barrier,)) + setup.extend(aqt_setup) + return setup diff --git a/projectq/setups/aqt_test.py b/projectq/setups/aqt_test.py new file mode 100644 index 000000000..415b5a4a8 --- /dev/null +++ b/projectq/setups/aqt_test.py @@ -0,0 +1,66 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setup.aqt.""" + +import pytest + + +def test_aqt_mapper_in_cengines(monkeypatch): + import projectq.setups.aqt + + def mock_show_devices(*args, **kwargs): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return {'aqt_simulator': {'coupling_map': connections, 'version': '0.0.0', 'nq': 32}} + + monkeypatch.setattr(projectq.setups.aqt, "show_devices", mock_show_devices) + engines_simulator = projectq.setups.aqt.get_engine_list(device='aqt_simulator') + assert len(engines_simulator) == 13 + + +def test_aqt_errors(monkeypatch): + import projectq.setups.aqt + + def mock_show_devices(*args, **kwargs): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return {'aqt_imaginary': {'coupling_map': connections, 'version': '0.0.0', 'nq': 6}} + + monkeypatch.setattr(projectq.setups.aqt, "show_devices", mock_show_devices) + with pytest.raises(projectq.setups.aqt.DeviceOfflineError): + projectq.setups.aqt.get_engine_list(device='simulator') + with pytest.raises(projectq.setups.aqt.DeviceNotHandledError): + projectq.setups.aqt.get_engine_list(device='aqt_imaginary') diff --git a/projectq/setups/awsbraket.py b/projectq/setups/awsbraket.py new file mode 100644 index 000000000..46247ca9d --- /dev/null +++ b/projectq/setups/awsbraket.py @@ -0,0 +1,88 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A setup for AWS Braket devices. + +Defines a setup allowing to compile code for the AWS Braket devices: +->The 11 qubits IonQ device +->The 32 qubits Rigetti device +->The up to 34 qubits SV1 state vector simulator + +It provides the `engine_list` for the `MainEngine' based on the requested +device. Decompose the circuit into the available gate set for each device +that will be used in the backend. +""" + +from projectq.backends._awsbraket._awsbraket_boto3_client import show_devices +from projectq.backends._exceptions import DeviceNotHandledError, DeviceOfflineError +from projectq.ops import ( + Barrier, + H, + R, + Rx, + Ry, + Rz, + S, + Sdag, + SqrtX, + Swap, + T, + Tdag, + X, + Y, + Z, +) +from projectq.setups import restrictedgateset + + +def get_engine_list(credentials=None, device=None): + """Return the default list of compiler engine for the AWS Braket platform.""" + # Access to the hardware properties via show_devices + # Can also be extended to take into account gate fidelities, new available + # gate, etc.. + devices = show_devices(credentials) + if device not in devices: + raise DeviceOfflineError('Error when configuring engine list: device requested for Backend not available') + + # We left the real device to manage the mapping and optimizacion: "The IonQ + # and Rigetti devices compile the provided circuit into their respective + # native gate sets automatically, and they map the abstract qubit indices + # to physical qubits on the respective QPU." + # (see: https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-to-qpu.html) + + # TODO: Investigate if explicit mapping is an advantage + + if device == 'SV1': + setup = restrictedgateset.get_engine_list( + one_qubit_gates=(R, H, Rx, Ry, Rz, S, Sdag, T, Tdag, X, Y, Z, SqrtX), + two_qubit_gates=(Swap,), + other_gates=(Barrier,), + ) + return setup + if device == 'Aspen-8': + setup = restrictedgateset.get_engine_list( + one_qubit_gates=(R, H, Rx, Ry, Rz, S, Sdag, T, Tdag, X, Y, Z), + two_qubit_gates=(Swap,), + other_gates=(Barrier,), + ) + return setup + if device == 'IonQ Device': + setup = restrictedgateset.get_engine_list( + one_qubit_gates=(H, Rx, Ry, Rz, S, Sdag, T, Tdag, X, Y, Z, SqrtX), + two_qubit_gates=(Swap,), + other_gates=(Barrier,), + ) + return setup + raise DeviceNotHandledError(f'Unsupported device type: {device}!') # pragma: no cover diff --git a/projectq/setups/awsbraket_test.py b/projectq/setups/awsbraket_test.py new file mode 100644 index 000000000..a4a95d469 --- /dev/null +++ b/projectq/setups/awsbraket_test.py @@ -0,0 +1,151 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setup.awsbraket.""" + +import json +from unittest.mock import patch + +import pytest + +# ============================================================================== + +_has_boto3 = True +try: + import projectq.setups.awsbraket + +except ImportError: + _has_boto3 = False + +has_boto3 = pytest.mark.skipif(not _has_boto3, reason="boto3 package is not installed") + +# ============================================================================== + +search_value = { + "devices": [ + { + "deviceArn": "arn1", + "deviceName": "SV1", + "deviceType": "SIMULATOR", + "deviceStatus": "ONLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn2", + "deviceName": "Aspen-8", + "deviceType": "QPU", + "deviceStatus": "OFFLINE", + "providerName": "pname1", + }, + { + "deviceArn": "arn3", + "deviceName": "IonQ Device", + "deviceType": "QPU", + "deviceStatus": "ONLINE", + "providerName": "pname2", + }, + ] +} + +device_value_devicecapabilities = json.dumps( + { + "braketSchemaHeader": { + "name": "braket.device_schema.rigetti.rigetti_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + "deviceLocation": "us-east-1", + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": { + "fullyConnected": False, + "connectivityGraph": {"1": ["2", "3"]}, + }, + }, + "deviceParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.rigetti.rigetti_device_parameters", + "version": "1", + } + } + }, + "definitions": { + "GateModelParameters": { + "properties": { + "braketSchemaHeader": { + "const": { + "name": "braket.device_schema.gate_model_parameters", + "version": "1", + } + } + } + } + }, + }, + } +) + +device_value = { + "deviceName": "Aspen-8", + "deviceType": "QPU", + "providerName": "provider1", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_value_devicecapabilities, +} + +creds = { + 'AWS_ACCESS_KEY_ID': 'aws_access_key_id', + 'AWS_SECRET_KEY': 'aws_secret_key', +} + + +@has_boto3 +@patch('boto3.client') +@pytest.mark.parametrize("var_device", ['SV1', 'Aspen-8', 'IonQ Device']) +def test_awsbraket_get_engine_list(mock_boto3_client, var_device): + mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + + engine_list = projectq.setups.awsbraket.get_engine_list(credentials=creds, device=var_device) + assert len(engine_list) == 12 + + +@has_boto3 +@patch('boto3.client') +def test_awsbraket_error(mock_boto3_client): + mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client.search_devices.return_value = search_value + mock_boto3_client.get_device.return_value = device_value + + with pytest.raises(projectq.setups.awsbraket.DeviceOfflineError): + projectq.setups.awsbraket.get_engine_list(credentials=creds, device='Imaginary') diff --git a/projectq/setups/decompositions/__init__.py b/projectq/setups/decompositions/__init__.py index e7dd915cd..f377093ab 100755 --- a/projectq/setups/decompositions/__init__.py +++ b/projectq/setups/decompositions/__init__.py @@ -12,40 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. -from . import (arb1qubit2rzandry, - barrier, - carb1qubit2cnotrzandry, - crz2cxandrz, - cnot2cz, - cnu2toffoliandcu, - entangle, - globalphase, - ph2r, - qft2crandhadamard, - r2rzandph, - rx2rz, - ry2rz, - swap2cnot, - toffoli2cnotandtgate, - time_evolution) +"""ProjectQ's decomposition rules.""" + +from . import ( + amplitudeamplification, + arb1qubit2rzandry, + barrier, + carb1qubit2cnotrzandry, + cnot2cz, + cnot2rxx, + cnu2toffoliandcu, + controlstate, + crz2cxandrz, + entangle, + globalphase, + h2rx, + ph2r, + phaseestimation, + qft2crandhadamard, + qubitop2onequbit, + r2rzandph, + rx2rz, + ry2rz, + rz2rx, + sqrtswap2cnot, + stateprep2cnot, + swap2cnot, + time_evolution, + toffoli2cnotandtgate, + uniformlycontrolledr2cnot, +) all_defined_decomposition_rules = [ rule - for module in [arb1qubit2rzandry, - barrier, - carb1qubit2cnotrzandry, - crz2cxandrz, - cnot2cz, - cnu2toffoliandcu, - entangle, - globalphase, - ph2r, - qft2crandhadamard, - r2rzandph, - rx2rz, - ry2rz, - swap2cnot, - toffoli2cnotandtgate, - time_evolution] + for module in [ + arb1qubit2rzandry, + barrier, + carb1qubit2cnotrzandry, + crz2cxandrz, + cnot2rxx, + cnot2cz, + cnu2toffoliandcu, + controlstate, + entangle, + globalphase, + h2rx, + ph2r, + qubitop2onequbit, + qft2crandhadamard, + r2rzandph, + rx2rz, + ry2rz, + rz2rx, + sqrtswap2cnot, + stateprep2cnot, + swap2cnot, + toffoli2cnotandtgate, + time_evolution, + uniformlycontrolledr2cnot, + phaseestimation, + amplitudeamplification, + ] for rule in module.all_defined_decomposition_rules ] diff --git a/projectq/setups/decompositions/_gates_test.py b/projectq/setups/decompositions/_gates_test.py index 6eb0b93b3..12c05cdc8 100755 --- a/projectq/setups/decompositions/_gates_test.py +++ b/projectq/setups/decompositions/_gates_test.py @@ -11,25 +11,44 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for decompositions rules (using the Simulator). """ import pytest -from projectq.cengines import (MainEngine, - InstructionFilter, - AutoReplacer, - DummyEngine, - DecompositionRuleSet) from projectq.backends import Simulator -from projectq.ops import (All, ClassicalInstructionGate, CRz, Entangle, H, - Measure, Ph, R, Rz, T, Tdag, Toffoli, X) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control -from projectq.setups.decompositions import (crz2cxandrz, entangle, - globalphase, ph2r, r2rzandph, - toffoli2cnotandtgate) +from projectq.ops import ( + All, + ClassicalInstructionGate, + CRz, + Entangle, + H, + Measure, + Ph, + R, + Rz, + T, + Tdag, + Toffoli, + X, +) +from projectq.setups.decompositions import ( + crz2cxandrz, + entangle, + globalphase, + ph2r, + r2rzandph, + toffoli2cnotandtgate, +) def low_level_gates(eng, cmd): @@ -37,8 +56,7 @@ def low_level_gates(eng, cmd): if isinstance(g, ClassicalInstructionGate): return True if len(cmd.control_qubits) == 0: - if (g == T or g == Tdag or g == H or isinstance(g, Rz) or - isinstance(g, Ph)): + if g == T or g == Tdag or g == H or isinstance(g, Rz) or isinstance(g, Ph): return True else: if len(cmd.control_qubits) == 1 and cmd.gate == X: @@ -49,28 +67,27 @@ def low_level_gates(eng, cmd): def test_entangle(): rule_set = DecompositionRuleSet(modules=[entangle]) sim = Simulator() - eng = MainEngine(sim, - [AutoReplacer(rule_set), - InstructionFilter(low_level_gates)]) + eng = MainEngine(sim, [AutoReplacer(rule_set), InstructionFilter(low_level_gates)]) qureg = eng.allocate_qureg(4) Entangle | qureg - assert .5 == pytest.approx(abs(sim.cheat()[1][0])**2) - assert .5 == pytest.approx(abs(sim.cheat()[1][-1])**2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][0]) ** 2) + assert 0.5 == pytest.approx(abs(sim.cheat()[1][-1]) ** 2) All(Measure) | qureg def low_level_gates_noglobalphase(eng, cmd): - return (low_level_gates(eng, cmd) and not isinstance(cmd.gate, Ph) and not - isinstance(cmd.gate, R)) + return low_level_gates(eng, cmd) and not isinstance(cmd.gate, Ph) and not isinstance(cmd.gate, R) def test_globalphase(): rule_set = DecompositionRuleSet(modules=[globalphase, r2rzandph]) dummy = DummyEngine(save_commands=True) - eng = MainEngine(dummy, [AutoReplacer(rule_set), - InstructionFilter(low_level_gates_noglobalphase)]) + eng = MainEngine( + dummy, + [AutoReplacer(rule_set), InstructionFilter(low_level_gates_noglobalphase)], + ) qubit = eng.allocate_qubit() R(1.2) | qubit @@ -99,14 +116,12 @@ def run_circuit(eng): def test_gate_decompositions(): sim = Simulator() eng = MainEngine(sim, []) - rule_set = DecompositionRuleSet( - modules=[r2rzandph, crz2cxandrz, toffoli2cnotandtgate, ph2r]) + rule_set = DecompositionRuleSet(modules=[r2rzandph, crz2cxandrz, toffoli2cnotandtgate, ph2r]) qureg = run_circuit(eng) sim2 = Simulator() - eng_lowlevel = MainEngine(sim2, [AutoReplacer(rule_set), - InstructionFilter(low_level_gates)]) + eng_lowlevel = MainEngine(sim2, [AutoReplacer(rule_set), InstructionFilter(low_level_gates)]) qureg2 = run_circuit(eng_lowlevel) for i in range(len(sim.cheat()[1])): diff --git a/projectq/setups/decompositions/amplitudeamplification.py b/projectq/setups/decompositions/amplitudeamplification.py new file mode 100644 index 000000000..5a8528367 --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification.py @@ -0,0 +1,110 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Registers a decomposition for quantum amplitude amplification. + +(Quick reference https://en.wikipedia.org/wiki/Amplitude_amplification. +Complete reference G. Brassard, P. Hoyer, M. Mosca, A. Tapp (2000) +Quantum Amplitude Amplification and Estimation +https://arxiv.org/abs/quant-ph/0005055) + +Quantum Amplitude Amplification (QAA) executes the algorithm, but not +the final measurement required to obtain the marked state(s) with high +probability. The starting state on which the QAA algorithm is executed +is the one resulting of applying the algorithm on the |0> state. + +Example: + .. code-block:: python + + def func_algorithm(eng, system_qubits): + All(H) | system_qubits + + + def func_oracle(eng, system_qubits, qaa_ancilla): + # This oracle selects the state |010> as the one marked + with Compute(eng): + All(X) | system_qubits[0::2] + with Control(eng, system_qubits): + X | qaa_ancilla + Uncompute(eng) + + + system_qubits = eng.allocate_qureg(3) + # Prepare the qaa_ancilla qubit in the |-> state + qaa_ancilla = eng.allocate_qubit() + X | qaa_ancilla + H | qaa_ancilla + + # Creates the initial state form the Algorithm + func_algorithm(eng, system_qubits) + # Apply Quantum Amplitude Amplification the correct number of times + num_it = int(math.pi / 4.0 * math.sqrt(1 << 3)) + with Loop(eng, num_it): + QAA(func_algorithm, func_oracle) | (system_qubits, qaa_ancilla) + + All(Measure) | system_qubits + +Warning: + No qubit allocation/deallocation may take place during the call + to the defined Algorithm :code:`func_algorithm` + +Attributes: + func_algorithm: Algorithm that initialite the state and to be used + in the QAA algorithm + func_oracle: The Oracle that marks the state(s) as "good" + system_qubits: the system we are interested on + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the + "good" states + +""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, CustomUncompute, Dagger +from projectq.ops import QAA, All, Ph, X, Z + + +def _decompose_QAA(cmd): # pylint: disable=invalid-name + """Decompose the Quantum Amplitude Apmplification algorithm as a gate.""" + eng = cmd.engine + + # System-qubit is the first qubit/qureg. Ancilla qubit is the second qubit + system_qubits = cmd.qubits[0] + qaa_ancilla = cmd.qubits[1] + + # The Oracle and the Algorithm + oracle = cmd.gate.oracle + alg = cmd.gate.algorithm + + # Apply the oracle to invert the amplitude of the good states, S_Chi + oracle(eng, system_qubits, qaa_ancilla) + + # Apply the inversion of the Algorithm, + # the inversion of the amplitude of |0> and the Algorithm + + with Compute(eng): + with Dagger(eng): + alg(eng, system_qubits) + All(X) | system_qubits + with Control(eng, system_qubits[0:-1]): + Z | system_qubits[-1] + with CustomUncompute(eng): + All(X) | system_qubits + alg(eng, system_qubits) + Ph(math.pi) | system_qubits[0] + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(QAA, _decompose_QAA)] diff --git a/projectq/setups/decompositions/amplitudeamplification_test.py b/projectq/setups/decompositions/amplitudeamplification_test.py new file mode 100644 index 000000000..8e68aad1b --- /dev/null +++ b/projectq/setups/decompositions/amplitudeamplification_test.py @@ -0,0 +1,178 @@ +# Copyright 2019 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.amplitudeamplification.py." + +import math + +import pytest + +from projectq.backends import Simulator +from projectq.cengines import AutoReplacer, DecompositionRuleSet, MainEngine +from projectq.meta import Compute, Control, Loop, Uncompute +from projectq.ops import QAA, All, H, Measure, Ry, X +from projectq.setups.decompositions import amplitudeamplification as aa + + +def hache_algorithm(eng, qreg): + All(H) | qreg + + +def simple_oracle(eng, system_q, control): + # This oracle selects the state |1010101> as the one marked + with Compute(eng): + All(X) | system_q[1::2] + with Control(eng, system_q): + X | control + Uncompute(eng) + + +def test_simple_grover(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + + system_qubits = eng.allocate_qureg(7) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + hache_algorithm(eng, system_qubits) + + # Get the amplitude of the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + + total_amp_before = math.sqrt(prob1010101) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4.0 * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.0) * theta_before) ** 2 + with Loop(eng, num_it): + QAA(hache_algorithm, simple_oracle) | (system_qubits, control) + + # Get the probability of getting the marked state after the AA + # to compare with the theoretical probability after the AA + eng.flush() + prob1010101 = eng.backend.get_probability('1010101', system_qubits) + total_prob_after = prob1010101 + + All(Measure) | system_qubits + H | control + Measure | control + + eng.flush() + + assert total_prob_after == pytest.approx( + theoretical_prob, abs=1e-6 + ), f"The obtained probability is less than expected {total_prob_after:f} vs. {theoretical_prob:f}" + + +def complex_algorithm(eng, qreg): + All(H) | qreg + with Control(eng, qreg[0]): + All(X) | qreg[1:] + All(Ry(math.pi / 4)) | qreg[1:] + with Control(eng, qreg[-1]): + All(X) | qreg[1:-1] + + +def complex_oracle(eng, system_q, control): + # This oracle selects the subspace |000000>+|111111> as the good one + with Compute(eng): + with Control(eng, system_q[0]): + All(X) | system_q[1:] + H | system_q[0] + All(X) | system_q + + with Control(eng, system_q): + X | control + + Uncompute(eng) + + +def test_complex_aa(): + rule_set = DecompositionRuleSet(modules=[aa]) + + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + + system_qubits = eng.allocate_qureg(6) + + # Prepare the control qubit in the |-> state + control = eng.allocate_qubit() + X | control + H | control + + # Creates the initial state form the Algorithm + complex_algorithm(eng, system_qubits) + + # Get the probability of getting the marked state before the AA + # to calculate the number of iterations + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + + total_amp_before = math.sqrt(prob000000 + prob111111) + theta_before = math.asin(total_amp_before) + + # Apply Quantum Amplitude Amplification the correct number of times + # Theta is calculated previously using get_probability + # We calculate also the theoretical final probability + # of getting the good state + num_it = int(math.pi / (4.0 * theta_before) + 1) + theoretical_prob = math.sin((2 * num_it + 1.0) * theta_before) ** 2 + with Loop(eng, num_it): + QAA(complex_algorithm, complex_oracle) | (system_qubits, control) + + # Get the probability of getting the marked state after the AA + # to compare with the theoretical probability after the AA + eng.flush() + prob000000 = eng.backend.get_probability('000000', system_qubits) + prob111111 = eng.backend.get_probability('111111', system_qubits) + total_prob_after = prob000000 + prob111111 + + All(Measure) | system_qubits + H | control + Measure | control + + eng.flush() + + assert total_prob_after == pytest.approx( + theoretical_prob, abs=1e-2 + ), f"The obtained probability is less than expected {total_prob_after:f} vs. {theoretical_prob:f}" + + +def test_string_functions(): + algorithm = hache_algorithm + oracle = simple_oracle + gate = QAA(algorithm, oracle) + assert str(gate) == "QAA(Algorithm = hache_algorithm, Oracle = simple_oracle)" diff --git a/projectq/setups/decompositions/arb1qubit2rzandry.py b/projectq/setups/decompositions/arb1qubit2rzandry.py index 6d3ea6e43..fc9d1a4c0 100644 --- a/projectq/setups/decompositions/arb1qubit2rzandry.py +++ b/projectq/setups/decompositions/arb1qubit2rzandry.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Registers the Z-Y decomposition for an arbitrary one qubit gate. +Register the Z-Y decomposition for an arbitrary one qubit gate. See paper "Elementary gates for quantum computing" by Adriano Barenco et al., arXiv:quant-ph/9503016v1. (Note: They use different gate definitions!) @@ -30,14 +29,12 @@ import itertools import math - import numpy from projectq.cengines import DecompositionRule from projectq.meta import Control, get_control_count from projectq.ops import BasicGate, Ph, Ry, Rz - TOLERANCE = 1e-12 @@ -50,19 +47,14 @@ def _recognize_arb1qubit(cmd): carb1qubit2cnotrzandry instead. """ try: - m = cmd.gate.matrix - if len(m) == 2 and get_control_count(cmd) == 0: - return True - else: - return False - except: + return len(cmd.gate.matrix) == 2 and get_control_count(cmd) == 0 + except AttributeError: return False -def _test_parameters(matrix, a, b_half, c_half, d_half): +def _test_parameters(matrix, a, b_half, c_half, d_half): # pylint: disable=invalid-name """ - It builds matrix U with parameters (a, b/2, c/2, d/2) and compares against - matrix. + Build matrix U with parameters (a, b/2, c/2, d/2) and compares against matrix. U = [[exp(j*(a-b/2-d/2))*cos(c/2), -exp(j*(a-b/2+d/2))*sin(c/2)], [exp(j*(a+b/2-d/2))*sin(c/2), exp(j*(a+b/2+d/2))*cos(c/2)]] @@ -77,23 +69,29 @@ def _test_parameters(matrix, a, b_half, c_half, d_half): Returns: True if matrix elements of U and `matrix` are TOLERANCE close. """ - U = [[cmath.exp(1j*(a-b_half-d_half))*math.cos(c_half), - -cmath.exp(1j*(a-b_half+d_half))*math.sin(c_half)], - [cmath.exp(1j*(a+b_half-d_half))*math.sin(c_half), - cmath.exp(1j*(a+b_half+d_half))*math.cos(c_half)]] - return numpy.allclose(U, matrix, rtol=10*TOLERANCE, atol=TOLERANCE) - - -def _find_parameters(matrix): + unitary = [ + [ + cmath.exp(1j * (a - b_half - d_half)) * math.cos(c_half), + -cmath.exp(1j * (a - b_half + d_half)) * math.sin(c_half), + ], + [ + cmath.exp(1j * (a + b_half - d_half)) * math.sin(c_half), + cmath.exp(1j * (a + b_half + d_half)) * math.cos(c_half), + ], + ] + return numpy.allclose(unitary, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) + + +def _find_parameters(matrix): # pylint: disable=too-many-branches,too-many-statements """ - Given a 2x2 unitary matrix, find the parameters - a, b/2, c/2, and d/2 such that + Find decomposition parameters. + + Given a 2x2 unitary matrix, find the parameters a, b/2, c/2, and d/2 such that matrix == [[exp(j*(a-b/2-d/2))*cos(c/2), -exp(j*(a-b/2+d/2))*sin(c/2)], [exp(j*(a+b/2-d/2))*sin(c/2), exp(j*(a+b/2+d/2))*cos(c/2)]] Note: - If the matrix is element of SU(2) (determinant == 1), then - we can choose a = 0. + If the matrix is element of SU(2) (determinant == 1), then we can choose a = 0. Args: matrix(list): 2x2 unitary matrix @@ -105,90 +103,104 @@ def _find_parameters(matrix): # Note: everything is modulo 2pi. # Case 1: sin(c/2) == 0: if abs(matrix[0][1]) < TOLERANCE: - two_a = cmath.phase(matrix[0][0]*matrix[1][1]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: + two_a = cmath.phase(matrix[0][0] * matrix[1][1]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, # w.l.g. we can choose a==0 because (see U above) # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 + a = 0 # pylint: disable=invalid-name else: - a = two_a/2. + a = two_a / 2.0 # pylint: disable=invalid-name d_half = 0 # w.l.g - b = cmath.phase(matrix[1][1])-cmath.phase(matrix[0][0]) - possible_b_half = [(b/2.) % (2*math.pi), (b/2.+math.pi) % (2*math.pi)] + b = cmath.phase(matrix[1][1]) - cmath.phase(matrix[0][0]) # pylint: disable=invalid-name + possible_b_half = [ + (b / 2.0) % (2 * math.pi), + (b / 2.0 + math.pi) % (2 * math.pi), + ] # As we have fixed a, we need to find correct sign for cos(c/2) possible_c_half = [0.0, math.pi] found = False - for b_half, c_half in itertools.product(possible_b_half, - possible_c_half): + for b_half, c_half in itertools.product(possible_b_half, possible_c_half): if _test_parameters(matrix, a, b_half, c_half, d_half): found = True break if not found: - raise Exception("Couldn't find parameters for matrix ", matrix, - "This shouldn't happen. Maybe the matrix is " + - "not unitary?") + raise Exception( + "Couldn't find parameters for matrix ", + matrix, + "This shouldn't happen. Maybe the matrix is not unitary?", + ) # Case 2: cos(c/2) == 0: elif abs(matrix[0][0]) < TOLERANCE: - two_a = cmath.phase(-matrix[0][1]*matrix[1][0]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: + two_a = cmath.phase(-matrix[0][1] * matrix[1][0]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, # w.l.g. we can choose a==0 because (see U above) # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 + a = 0 # pylint: disable=invalid-name else: - a = two_a/2. + a = two_a / 2.0 # pylint: disable=invalid-name d_half = 0 # w.l.g - b = cmath.phase(matrix[1][0])-cmath.phase(matrix[0][1]) + math.pi - possible_b_half = [(b/2.) % (2*math.pi), (b/2.+math.pi) % (2*math.pi)] + b = cmath.phase(matrix[1][0]) - cmath.phase(matrix[0][1]) + math.pi # pylint: disable=invalid-name + possible_b_half = [ + (b / 2.0) % (2 * math.pi), + (b / 2.0 + math.pi) % (2 * math.pi), + ] # As we have fixed a, we need to find correct sign for sin(c/2) - possible_c_half = [math.pi/2., 3./2.*math.pi] + possible_c_half = [math.pi / 2.0, 3.0 / 2.0 * math.pi] found = False - for b_half, c_half in itertools.product(possible_b_half, - possible_c_half): + for b_half, c_half in itertools.product(possible_b_half, possible_c_half): if _test_parameters(matrix, a, b_half, c_half, d_half): found = True break if not found: - raise Exception("Couldn't find parameters for matrix ", matrix, - "This shouldn't happen. Maybe the matrix is " + - "not unitary?") + raise Exception( + "Couldn't find parameters for matrix ", + matrix, + "This shouldn't happen. Maybe the matrix is not unitary?", + ) # Case 3: sin(c/2) != 0 and cos(c/2) !=0: else: - two_a = cmath.phase(matrix[0][0]*matrix[1][1]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: + two_a = cmath.phase(matrix[0][0] * matrix[1][1]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, # w.l.g. we can choose a==0 because (see U above) # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 + a = 0 # pylint: disable=invalid-name else: - a = two_a/2. - two_d = 2.*cmath.phase(matrix[0][1])-2.*cmath.phase(matrix[0][0]) - possible_d_half = [two_d/4. % (2*math.pi), - (two_d/4.+math.pi/2.) % (2*math.pi), - (two_d/4.+math.pi) % (2*math.pi), - (two_d/4.+3./2.*math.pi) % (2*math.pi)] - two_b = 2.*cmath.phase(matrix[1][0])-2.*cmath.phase(matrix[0][0]) - possible_b_half = [two_b/4. % (2*math.pi), - (two_b/4.+math.pi/2.) % (2*math.pi), - (two_b/4.+math.pi) % (2*math.pi), - (two_b/4.+3./2.*math.pi) % (2*math.pi)] + a = two_a / 2.0 # pylint: disable=invalid-name + two_d = 2.0 * cmath.phase(matrix[0][1]) - 2.0 * cmath.phase(matrix[0][0]) + possible_d_half = [ + two_d / 4.0 % (2 * math.pi), + (two_d / 4.0 + math.pi / 2.0) % (2 * math.pi), + (two_d / 4.0 + math.pi) % (2 * math.pi), + (two_d / 4.0 + 3.0 / 2.0 * math.pi) % (2 * math.pi), + ] + two_b = 2.0 * cmath.phase(matrix[1][0]) - 2.0 * cmath.phase(matrix[0][0]) + possible_b_half = [ + two_b / 4.0 % (2 * math.pi), + (two_b / 4.0 + math.pi / 2.0) % (2 * math.pi), + (two_b / 4.0 + math.pi) % (2 * math.pi), + (two_b / 4.0 + 3.0 / 2.0 * math.pi) % (2 * math.pi), + ] tmp = math.acos(abs(matrix[1][1])) - possible_c_half = [tmp % (2*math.pi), - (tmp+math.pi) % (2*math.pi), - (-1.*tmp) % (2*math.pi), - (-1.*tmp+math.pi) % (2*math.pi)] + possible_c_half = [ + tmp % (2 * math.pi), + (tmp + math.pi) % (2 * math.pi), + (-1.0 * tmp) % (2 * math.pi), + (-1.0 * tmp + math.pi) % (2 * math.pi), + ] found = False - for b_half, c_half, d_half in itertools.product(possible_b_half, - possible_c_half, - possible_d_half): + for b_half, c_half, d_half in itertools.product(possible_b_half, possible_c_half, possible_d_half): if _test_parameters(matrix, a, b_half, c_half, d_half): found = True break if not found: - raise Exception("Couldn't find parameters for matrix ", matrix, - "This shouldn't happen. Maybe the matrix is " + - "not unitary?") + raise Exception( + "Couldn't find parameters for matrix ", + matrix, + "This shouldn't happen. Maybe the matrix is not unitary?", + ) return (a, b_half, c_half, d_half) @@ -196,7 +208,7 @@ def _decompose_arb1qubit(cmd): """ Use Z-Y decomposition of Nielsen and Chuang (Theorem 4.1). - An arbitrary one qubit gate matrix can be writen as + An arbitrary one qubit gate matrix can be written as U = [[exp(j*(a-b/2-d/2))*cos(c/2), -exp(j*(a-b/2+d/2))*sin(c/2)], [exp(j*(a+b/2-d/2))*sin(c/2), exp(j*(a+b/2+d/2))*cos(c/2)]] where a,b,c,d are real numbers. @@ -205,21 +217,19 @@ def _decompose_arb1qubit(cmd): we can choose a = 0. """ matrix = cmd.gate.matrix.tolist() - a, b_half, c_half, d_half = _find_parameters(matrix) + a, b_half, c_half, d_half = _find_parameters(matrix) # pylint: disable=invalid-name qb = cmd.qubits eng = cmd.engine with Control(eng, cmd.control_qubits): - if Rz(2*d_half) != Rz(0): - Rz(2*d_half) | qb - if Ry(2*c_half) != Ry(0): - Ry(2*c_half) | qb - if Rz(2*b_half) != Rz(0): - Rz(2*b_half) | qb + if Rz(2 * d_half) != Rz(0): + Rz(2 * d_half) | qb + if Ry(2 * c_half) != Ry(0): + Ry(2 * c_half) | qb + if Rz(2 * b_half) != Rz(0): + Rz(2 * b_half) | qb if a != 0: Ph(a) | qb #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(BasicGate, _decompose_arb1qubit, _recognize_arb1qubit) -] +all_defined_decomposition_rules = [DecompositionRule(BasicGate, _decompose_arb1qubit, _recognize_arb1qubit)] diff --git a/projectq/setups/decompositions/arb1qubit2rzandry_test.py b/projectq/setups/decompositions/arb1qubit2rzandry_test.py index 90eba5edf..452e4d9c9 100644 --- a/projectq/setups/decompositions/arb1qubit2rzandry_test.py +++ b/projectq/setups/decompositions/arb1qubit2rzandry_test.py @@ -14,18 +14,33 @@ "Tests for projectq.setups.decompositions.arb1qubit2rzandry.py." -from cmath import exp import math +from cmath import exp import numpy as np import pytest from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - DummyEngine, InstructionFilter, MainEngine) -from projectq.ops import (BasicGate, ClassicalInstructionGate, Measure, Ph, R, - Rx, Ry, Rz, X) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control +from projectq.ops import ( + BasicGate, + ClassicalInstructionGate, + MatrixGate, + Measure, + Ph, + R, + Rx, + Ry, + Rz, + X, +) from . import arb1qubit2rzandry as arb1q @@ -51,9 +66,8 @@ def test_recognize_incorrect_gates(): # Does not have matrix attribute: BasicGate() | qubit # Two qubit gate: - two_qubit_gate = BasicGate() - two_qubit_gate.matrix = [[1, 0, 0, 0], [0, 1, 0, 0], - [0, 0, 1, 0], [0, 0, 0, 1]] + two_qubit_gate = MatrixGate() + two_qubit_gate.matrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] two_qubit_gate | qubit # Controlled single qubit gate: ctrl_qubit = eng.allocate_qubit() @@ -69,9 +83,7 @@ def z_y_decomp_gates(eng, cmd): if isinstance(g, ClassicalInstructionGate): return True if len(cmd.control_qubits) == 0: - if (isinstance(cmd.gate, Ry) or - isinstance(cmd.gate, Rz) or - isinstance(cmd.gate, Ph)): + if isinstance(cmd.gate, Ry) or isinstance(cmd.gate, Rz) or isinstance(cmd.gate, Ph): return True return False @@ -93,24 +105,28 @@ def create_unitary_matrix(a, b, c, d): Returns: 2x2 matrix as nested lists """ - ph = exp(1j*a) # global phase - return [[ph * exp(1j*b) * math.cos(d), ph * exp(1j*c) * math.sin(d)], - [ph * -exp(-1j*c) * math.sin(d), ph * exp(-1j*b) * math.cos(d)]] + ph = exp(1j * a) # global phase + return [ + [ph * exp(1j * b) * math.cos(d), ph * exp(1j * c) * math.sin(d)], + [ph * -exp(-1j * c) * math.sin(d), ph * exp(-1j * b) * math.cos(d)], + ] def create_test_matrices(): - params = [(0.2, 0.3, 0.5, math.pi * 0.4), - (1e-14, 0.3, 0.5, 0), - (0.4, 0.0, math.pi * 2, 0.7), - (0.0, 0.2, math.pi * 1.2, 1.5), # element of SU(2) - (0.4, 0.0, math.pi * 1.3, 0.8), - (0.4, 4.1, math.pi * 1.3, 0), - (5.1, 1.2, math.pi * 1.5, math.pi/2.), - (1e-13, 1.2, math.pi * 3.7, math.pi/2.), - (0, math.pi/2., 0, 0), - (math.pi/2., -math.pi/2., 0, 0), - (math.pi/2., math.pi/2., 0.1, 0.4), - (math.pi*1.5, math.pi/2., 0, 0.4)] + params = [ + (0.2, 0.3, 0.5, math.pi * 0.4), + (1e-14, 0.3, 0.5, 0), + (0.4, 0.0, math.pi * 2, 0.7), + (0.0, 0.2, math.pi * 1.2, 1.5), # element of SU(2) + (0.4, 0.0, math.pi * 1.3, 0.8), + (0.4, 4.1, math.pi * 1.3, 0), + (5.1, 1.2, math.pi * 1.5, math.pi / 2.0), + (1e-13, 1.2, math.pi * 3.7, math.pi / 2.0), + (0, math.pi / 2.0, 0, 0), + (math.pi / 2.0, -math.pi / 2.0, 0, 0), + (math.pi / 2.0, math.pi / 2.0, 0.1, 0.4), + (math.pi * 1.5, math.pi / 2.0, 0, 0.4), + ] matrices = [] for a, b, c, d in params: matrices.append(create_unitary_matrix(a, b, c, d)) @@ -121,19 +137,22 @@ def create_test_matrices(): def test_decomposition(gate_matrix): for basis_state in ([1, 0], [0, 1]): # Create single qubit gate with gate_matrix - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[arb1q]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(z_y_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(z_y_decomp_gates), + test_dummy_eng, + ], + ) correct_qb = correct_eng.allocate_qubit() correct_eng.flush() @@ -161,16 +180,15 @@ def test_decomposition(gate_matrix): Measure | correct_qb -@pytest.mark.parametrize("gate_matrix", [[[2, 0], [0, 4]], - [[0, 2], [4, 0]], - [[1, 2], [4, 0]]]) +@pytest.mark.parametrize("gate_matrix", [[[2, 0], [0, 4]], [[0, 2], [4, 0]], [[1, 2], [4, 0]]]) def test_decomposition_errors(gate_matrix): - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) rule_set = DecompositionRuleSet(modules=[arb1q]) - eng = MainEngine(backend=DummyEngine(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(z_y_decomp_gates)]) + eng = MainEngine( + backend=DummyEngine(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(z_y_decomp_gates)], + ) qb = eng.allocate_qubit() with pytest.raises(Exception): test_gate | qb diff --git a/projectq/setups/decompositions/barrier.py b/projectq/setups/decompositions/barrier.py index 359e5e1aa..6711e838b 100755 --- a/projectq/setups/decompositions/barrier.py +++ b/projectq/setups/decompositions/barrier.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition rule for barriers. @@ -22,17 +21,14 @@ from projectq.ops import BarrierGate -def _decompose_barrier(cmd): - """ Throw out all barriers if they are not supported. """ - pass +def _decompose_barrier(cmd): # pylint: disable=unused-argument + """Throw out all barriers if they are not supported.""" -def _recognize_barrier(cmd): - """ Recognize all barriers. """ +def _recognize_barrier(cmd): # pylint: disable=unused-argument + """Recognize all barriers.""" return True #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(BarrierGate, _decompose_barrier, _recognize_barrier) -] +all_defined_decomposition_rules = [DecompositionRule(BarrierGate, _decompose_barrier, _recognize_barrier)] diff --git a/projectq/setups/decompositions/barrier_test.py b/projectq/setups/decompositions/barrier_test.py index 646d6fabe..a09bb2f70 100755 --- a/projectq/setups/decompositions/barrier_test.py +++ b/projectq/setups/decompositions/barrier_test.py @@ -1,3 +1,4 @@ +# -*- codingf53: utf-8 -*- # Copyright 2017 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,7 +12,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Tests for barrier.py """ @@ -50,7 +50,6 @@ def my_is_available(cmd): Barrier | qubit eng.flush(deallocate_qubits=True) # Don't test initial allocate and trailing deallocate and flush gate. - count = 0 for cmd in saving_backend.received_commands[1:-2]: assert not cmd.gate == Barrier assert len(saving_backend.received_commands[1:-2]) == 1 diff --git a/projectq/setups/decompositions/carb1qubit2cnotrzandry.py b/projectq/setups/decompositions/carb1qubit2cnotrzandry.py index deaddd9db..549db3af1 100644 --- a/projectq/setups/decompositions/carb1qubit2cnotrzandry.py +++ b/projectq/setups/decompositions/carb1qubit2cnotrzandry.py @@ -1,4 +1,4 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# Copyright 2017, 2021 ProjectQ-Framework (www.projectq.ch) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Registers the decomposition of an controlled arbitary single qubit gate. +Register the decomposition of an controlled arbitrary single qubit gate. See paper "Elementary gates for quantum computing" by Adriano Barenco et al., arXiv:quant-ph/9503016v1. (Note: They use different gate definitions!) or @@ -27,30 +26,26 @@ import numpy from projectq.cengines import DecompositionRule -from projectq.meta import get_control_count, Control -from projectq.ops import BasicGate, CNOT, Ph, Ry, Rz, X +from projectq.meta import Control, get_control_count +from projectq.ops import BasicGate, Ph, Ry, Rz, X from projectq.setups.decompositions import arb1qubit2rzandry as arb1q - TOLERANCE = 1e-12 def _recognize_carb1qubit(cmd): - """ Recognize single controlled one qubit gates with a matrix.""" + """Recognize single controlled one qubit gates with a matrix.""" if get_control_count(cmd) == 1: try: - m = cmd.gate.matrix - if len(m) == 2: - return True - except: + return len(cmd.gate.matrix) == 2 + except AttributeError: return False return False -def _test_parameters(matrix, a, b, c_half): +def _test_parameters(matrix, a, b, c_half): # pylint: disable=invalid-name """ - It builds matrix V with parameters (a, b, c/2) and compares against - matrix. + Build matrix V with parameters (a, b, c/2) and compares against matrix. V = [[-sin(c/2) * exp(j*a), exp(j*(a-b)) * cos(c/2)], [exp(j*(a+b)) * cos(c/2), exp(j*a) * sin(c/2)]] @@ -64,16 +59,24 @@ def _test_parameters(matrix, a, b, c_half): Returns: True if matrix elements of V and `matrix` are TOLERANCE close. """ - V = [[-math.sin(c_half)*cmath.exp(1j*a), - cmath.exp(1j*(a-b))*math.cos(c_half)], - [cmath.exp(1j*(a+b))*math.cos(c_half), - cmath.exp(1j*a) * math.sin(c_half)]] - return numpy.allclose(V, matrix, rtol=10*TOLERANCE, atol=TOLERANCE) - - -def _recognize_v(matrix): + v_matrix = [ + [ + -math.sin(c_half) * cmath.exp(1j * a), + cmath.exp(1j * (a - b)) * math.cos(c_half), + ], + [ + cmath.exp(1j * (a + b)) * math.cos(c_half), + cmath.exp(1j * a) * math.sin(c_half), + ], + ] + return numpy.allclose(v_matrix, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) + + +def _recognize_v(matrix): # pylint: disable=too-many-branches """ - Recognizes a matrix which can be written in the following form: + Test whether a matrix has the correct form. + + Recognize a matrix which can be written in the following form: V = [[-sin(c/2) * exp(j*a), exp(j*(a-b)) * cos(c/2)], [exp(j*(a+b)) * cos(c/2), exp(j*a) * sin(c/2)]] @@ -84,76 +87,76 @@ def _recognize_v(matrix): False if it is not possible otherwise (a, b, c/2) """ if abs(matrix[0][0]) < TOLERANCE: - two_a = cmath.phase(matrix[0][1]*matrix[1][0]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: + two_a = cmath.phase(matrix[0][1] * matrix[1][0]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, # w.l.g. we can choose a==0 because (see U above) # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 + a = 0 # pylint: disable=invalid-name else: - a = two_a/2. - two_b = cmath.phase(matrix[1][0])-cmath.phase(matrix[0][1]) - possible_b = [(two_b/2.) % (2*math.pi), - (two_b/2.+math.pi) % (2*math.pi)] + a = two_a / 2.0 # pylint: disable=invalid-name + two_b = cmath.phase(matrix[1][0]) - cmath.phase(matrix[0][1]) + possible_b = [ + (two_b / 2.0) % (2 * math.pi), + (two_b / 2.0 + math.pi) % (2 * math.pi), + ] possible_c_half = [0, math.pi] - found = False - for b, c_half in itertools.product(possible_b, possible_c_half): + + for b, c_half in itertools.product(possible_b, possible_c_half): # pylint: disable=invalid-name if _test_parameters(matrix, a, b, c_half): - found = True - break - assert found # It should work for all matrices with matrix[0][0]==0. - return (a, b, c_half) - - elif abs(matrix[0][1]) < TOLERANCE: - two_a = cmath.phase(-matrix[0][0] * matrix[1][1]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: + return (a, b, c_half) + raise RuntimeError('Case matrix[0][0]==0 should work in all cases, but did not!') # pragma: no cover + + if abs(matrix[0][1]) < TOLERANCE: + two_a = cmath.phase(-matrix[0][0] * matrix[1][1]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, # w.l.g. we can choose a==0 because (see U above) # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 + a = 0 # pylint: disable=invalid-name else: - a = two_a/2. - b = 0 - possible_c_half = [math.pi/2., 3./2.*math.pi] - found = False + a = two_a / 2.0 # pylint: disable=invalid-name + b = 0 # pylint: disable=invalid-name + possible_c_half = [math.pi / 2.0, 3.0 / 2.0 * math.pi] + for c_half in possible_c_half: if _test_parameters(matrix, a, b, c_half): - found = True return (a, b, c_half) return False + two_a = cmath.phase(-1.0 * matrix[0][0] * matrix[1][1]) % (2 * math.pi) + if abs(two_a) < TOLERANCE or abs(two_a) > 2 * math.pi - TOLERANCE: + # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, + # w.l.g. we can choose a==0 because (see U above) + # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. + a = 0 # pylint: disable=invalid-name else: - two_a = cmath.phase(-1.*matrix[0][0]*matrix[1][1]) % (2*math.pi) - if abs(two_a) < TOLERANCE or abs(two_a) > 2*math.pi-TOLERANCE: - # from 2a==0 (mod 2pi), it follows that a==0 or a==pi, - # w.l.g. we can choose a==0 because (see U above) - # c/2 -> c/2 + pi would have the same effect as as a==0 -> a==pi. - a = 0 - else: - a = two_a/2. - two_b = cmath.phase(matrix[1][0])-cmath.phase(matrix[0][1]) - possible_b = [(two_b/2.) % (2*math.pi), - (two_b/2.+math.pi) % (2*math.pi)] - tmp = math.acos(abs(matrix[1][0])) - possible_c_half = [tmp % (2*math.pi), - (tmp+math.pi) % (2*math.pi), - (-1.*tmp) % (2*math.pi), - (-1.*tmp+math.pi) % (2*math.pi)] - found = False - for b, c_half in itertools.product(possible_b, possible_c_half): - if _test_parameters(matrix, a, b, c_half): - found = True - return (a, b, c_half) - return False + a = two_a / 2.0 # pylint: disable=invalid-name + two_b = cmath.phase(matrix[1][0]) - cmath.phase(matrix[0][1]) + possible_b = [ + (two_b / 2.0) % (2 * math.pi), + (two_b / 2.0 + math.pi) % (2 * math.pi), + ] + tmp = math.acos(abs(matrix[1][0])) + possible_c_half = [ + tmp % (2 * math.pi), + (tmp + math.pi) % (2 * math.pi), + (-1.0 * tmp) % (2 * math.pi), + (-1.0 * tmp + math.pi) % (2 * math.pi), + ] + for b, c_half in itertools.product(possible_b, possible_c_half): # pylint: disable=invalid-name + if _test_parameters(matrix, a, b, c_half): + return (a, b, c_half) + return False -def _decompose_carb1qubit(cmd): +def _decompose_carb1qubit(cmd): # pylint: disable=too-many-branches """ Decompose the single controlled 1 qubit gate into CNOT, Rz, Ry, C(Ph). See Nielsen and Chuang chapter 4.3. - An arbitrary one qubit gate matrix can be writen as + An arbitrary one qubit gate matrix can be written as U = [[exp(j*(a-b/2-d/2))*cos(c/2), -exp(j*(a-b/2+d/2))*sin(c/2)], [exp(j*(a+b/2-d/2))*sin(c/2), exp(j*(a+b/2+d/2))*cos(c/2)]] where a,b,c,d are real numbers. @@ -167,7 +170,7 @@ def _decompose_carb1qubit(cmd): the controlled phase C(exp(ia)) can be implemented with single qubit gates. - If the one qubit gate matrix can be writen as + If the one qubit gate matrix can be written as V = [[-sin(c/2) * exp(j*a), exp(j*(a-b)) * cos(c/2)], [exp(j*(a+b)) * cos(c/2), exp(j*a) * sin(c/2)]] Then C(V) = C(exp(ja))* E * CNOT * D with @@ -184,7 +187,7 @@ def _decompose_carb1qubit(cmd): # Case 1: Unitary matrix which can be written in the form of V: parameters_for_v = _recognize_v(matrix) if parameters_for_v: - a, b, c_half = parameters_for_v + a, b, c_half = parameters_for_v # pylint: disable=invalid-name if Rz(-b) != Rz(0): Rz(-b) | qb if Ry(-c_half) != Ry(0): @@ -201,15 +204,15 @@ def _decompose_carb1qubit(cmd): # Case 2: General matrix U: else: - a, b_half, c_half, d_half = arb1q._find_parameters(matrix) - d = 2*d_half - b = 2*b_half - if Rz((d-b)/2.) != Rz(0): - Rz((d-b)/2.) | qb + a, b_half, c_half, d_half = arb1q._find_parameters(matrix) # pylint: disable=invalid-name, protected-access + d = 2 * d_half # pylint: disable=invalid-name + b = 2 * b_half # pylint: disable=invalid-name + if Rz((d - b) / 2.0) != Rz(0): + Rz((d - b) / 2.0) | qb with Control(eng, cmd.control_qubits): X | qb - if Rz(-(d+b)/2.) != Rz(0): - Rz(-(d+b)/2.) | qb + if Rz(-(d + b) / 2.0) != Rz(0): + Rz(-(d + b) / 2.0) | qb if Ry(-c_half) != Ry(0): Ry(-c_half) | qb with Control(eng, cmd.control_qubits): @@ -224,6 +227,4 @@ def _decompose_carb1qubit(cmd): #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(BasicGate, _decompose_carb1qubit, _recognize_carb1qubit) -] +all_defined_decomposition_rules = [DecompositionRule(BasicGate, _decompose_carb1qubit, _recognize_carb1qubit)] diff --git a/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py b/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py index 686c4991e..f402f7027 100644 --- a/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py +++ b/projectq/setups/decompositions/carb1qubit2cnotrzandry_test.py @@ -18,11 +18,28 @@ import pytest from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - DummyEngine, InstructionFilter, MainEngine) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control -from projectq.ops import (All, BasicGate, ClassicalInstructionGate, Measure, - Ph, R, Rx, Ry, Rz, X, XGate) +from projectq.ops import ( + All, + BasicGate, + ClassicalInstructionGate, + MatrixGate, + Measure, + Ph, + R, + Rx, + Ry, + Rz, + X, + XGate, +) from projectq.setups.decompositions import arb1qubit2rzandry_test as arb1q_t from . import carb1qubit2cnotrzandry as carb1q @@ -57,9 +74,8 @@ def test_recognize_incorrect_gates(): # Does not have matrix attribute: BasicGate() | qubit # Two qubit gate: - two_qubit_gate = BasicGate() - two_qubit_gate.matrix = [[1, 0, 0, 0], [0, 1, 0, 0], - [0, 0, 1, 0], [0, 0, 0, 1]] + two_qubit_gate = MatrixGate() + two_qubit_gate.matrix = np.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) two_qubit_gate | qubit with Control(eng, ctrl_qureg): # Too many Control qubits: @@ -74,19 +90,15 @@ def _decomp_gates(eng, cmd): if isinstance(g, ClassicalInstructionGate): return True if len(cmd.control_qubits) == 0: - if (isinstance(cmd.gate, Ry) or - isinstance(cmd.gate, Rz) or - isinstance(cmd.gate, Ph)): + if isinstance(cmd.gate, Ry) or isinstance(cmd.gate, Rz) or isinstance(cmd.gate, Ph): return True if len(cmd.control_qubits) == 1: - if (isinstance(cmd.gate, XGate) or - isinstance(cmd.gate, Ph)): + if isinstance(cmd.gate, XGate) or isinstance(cmd.gate, Ph): return True return False -@pytest.mark.parametrize("gate_matrix", [[[1, 0], [0, -1]], - [[0, -1j], [1j, 0]]]) +@pytest.mark.parametrize("gate_matrix", [[[1, 0], [0, -1]], [[0, -1j], [1j, 0]]]) def test_recognize_v(gate_matrix): assert carb1q._recognize_v(gate_matrix) @@ -94,21 +106,23 @@ def test_recognize_v(gate_matrix): @pytest.mark.parametrize("gate_matrix", arb1q_t.create_test_matrices()) def test_decomposition(gate_matrix): # Create single qubit gate with gate_matrix - test_gate = BasicGate() + test_gate = MatrixGate() test_gate.matrix = np.matrix(gate_matrix) - for basis_state in ([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], - [0, 0, 0, 1]): + for basis_state in ([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]): correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[carb1q]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) test_sim = test_eng.backend correct_sim = correct_eng.backend @@ -119,8 +133,7 @@ def test_decomposition(gate_matrix): test_ctrl_qb = test_eng.allocate_qubit() test_eng.flush() - correct_sim.set_wavefunction(basis_state, correct_qb + - correct_ctrl_qb) + correct_sim.set_wavefunction(basis_state, correct_qb + correct_ctrl_qb) test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qb) with Control(test_eng, test_ctrl_qb): @@ -136,8 +149,7 @@ def test_decomposition(gate_matrix): for fstate in ['00', '01', '10', '11']: test = test_sim.get_amplitude(fstate, test_qb + test_ctrl_qb) - correct = correct_sim.get_amplitude(fstate, correct_qb + - correct_ctrl_qb) + correct = correct_sim.get_amplitude(fstate, correct_qb + correct_ctrl_qb) assert correct == pytest.approx(test, rel=1e-12, abs=1e-12) All(Measure) | test_qb + test_ctrl_qb diff --git a/projectq/setups/decompositions/cnot2cz.py b/projectq/setups/decompositions/cnot2cz.py index e2ad2ef30..950b12b68 100644 --- a/projectq/setups/decompositions/cnot2cz.py +++ b/projectq/setups/decompositions/cnot2cz.py @@ -12,17 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Registers a decomposition to for a CNOT gate in terms of CZ and Hadamard. -""" +"""Registers a decomposition to for a CNOT gate in terms of CZ and Hadamard.""" from projectq.cengines import DecompositionRule -from projectq.meta import Compute, get_control_count, Uncompute +from projectq.meta import Compute, Uncompute, get_control_count from projectq.ops import CZ, H, X def _decompose_cnot(cmd): - """ Decompose CNOT gates. """ + """Decompose CNOT gates.""" ctrl = cmd.control_qubits eng = cmd.engine with Compute(eng): @@ -36,6 +34,4 @@ def _recognize_cnot(cmd): #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(X.__class__, _decompose_cnot, _recognize_cnot) -] +all_defined_decomposition_rules = [DecompositionRule(X.__class__, _decompose_cnot, _recognize_cnot)] diff --git a/projectq/setups/decompositions/cnot2cz_test.py b/projectq/setups/decompositions/cnot2cz_test.py index f838cf4c5..6738f0483 100644 --- a/projectq/setups/decompositions/cnot2cz_test.py +++ b/projectq/setups/decompositions/cnot2cz_test.py @@ -16,14 +16,16 @@ import pytest -import projectq from projectq import MainEngine from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, DummyEngine, - InstructionFilter) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) from projectq.meta import Control -from projectq.ops import All, CNOT, CZ, Measure, X, Z - +from projectq.ops import CNOT, CZ, All, Measure, X, Z from projectq.setups.decompositions import cnot2cz @@ -50,7 +52,6 @@ def test_recognize_gates(): def _decomp_gates(eng, cmd): - g = cmd.gate if len(cmd.control_qubits) == 1 and isinstance(cmd.gate, X.__class__): return False return True @@ -59,16 +60,19 @@ def _decomp_gates(eng, cmd): def test_cnot_decomposition(): for basis_state_index in range(0, 4): basis_state = [0] * 4 - basis_state[basis_state_index] = 1. + basis_state[basis_state_index] = 1.0 correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[cnot2cz]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) test_sim = test_eng.backend correct_sim = correct_eng.backend correct_qb = correct_eng.allocate_qubit() @@ -78,8 +82,7 @@ def test_cnot_decomposition(): test_ctrl_qb = test_eng.allocate_qubit() test_eng.flush() - correct_sim.set_wavefunction(basis_state, correct_qb + - correct_ctrl_qb) + correct_sim.set_wavefunction(basis_state, correct_qb + correct_ctrl_qb) test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qb) CNOT | (test_ctrl_qb, test_qb) CNOT | (correct_ctrl_qb, correct_qb) @@ -92,10 +95,8 @@ def test_cnot_decomposition(): for fstate in range(4): binary_state = format(fstate, '02b') - test = test_sim.get_amplitude(binary_state, - test_qb + test_ctrl_qb) - correct = correct_sim.get_amplitude(binary_state, correct_qb + - correct_ctrl_qb) + test = test_sim.get_amplitude(binary_state, test_qb + test_ctrl_qb) + correct = correct_sim.get_amplitude(binary_state, correct_qb + correct_ctrl_qb) assert correct == pytest.approx(test, rel=1e-12, abs=1e-12) All(Measure) | test_qb + test_ctrl_qb diff --git a/projectq/setups/decompositions/cnot2rxx.py b/projectq/setups/decompositions/cnot2rxx.py new file mode 100644 index 000000000..f60ee89ed --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx.py @@ -0,0 +1,61 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 + +"""Register a decomposition to for a CNOT gate in terms of Rxx, Rx and Ry gates.""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import Ph, Rx, Rxx, Ry, X + + +def _decompose_cnot2rxx_M(cmd): # pylint: disable=invalid-name + """Decompose CNOT gate into Rxx gate.""" + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + ctrl = cmd.control_qubits + Ry(math.pi / 2) | ctrl[0] + Ph(7 * math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(-math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(-1 * math.pi / 2) | ctrl[0] + + +def _decompose_cnot2rxx_P(cmd): # pylint: disable=invalid-name + """Decompose CNOT gate into Rxx gate.""" + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + ctrl = cmd.control_qubits + Ry(-math.pi / 2) | ctrl[0] + Ph(math.pi / 4) | ctrl[0] + Rx(-math.pi / 2) | ctrl[0] + Rx(math.pi / 2) | cmd.qubits[0][0] + Rxx(math.pi / 2) | (ctrl[0], cmd.qubits[0][0]) + Ry(math.pi / 2) | ctrl[0] + + +def _recognize_cnot2(cmd): + """Identify that the command is a CNOT gate (control - X gate).""" + return get_control_count(cmd) == 1 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(X.__class__, _decompose_cnot2rxx_M, _recognize_cnot2), + DecompositionRule(X.__class__, _decompose_cnot2rxx_P, _recognize_cnot2), +] diff --git a/projectq/setups/decompositions/cnot2rxx_test.py b/projectq/setups/decompositions/cnot2rxx_test.py new file mode 100644 index 000000000..e131ab928 --- /dev/null +++ b/projectq/setups/decompositions/cnot2rxx_test.py @@ -0,0 +1,126 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.cnot2rxx.py." + +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Control +from projectq.ops import CNOT, CZ, All, Measure, X, Z + +from . import cnot2rxx + + +def test_recognize_correct_gates(): + """Test that recognize_cnot recognizes cnot gates.""" + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + eng.flush() + # Create a control function in 3 different ways + CZ | (qubit1, qubit2) + with Control(eng, qubit2): + Z | qubit1 + X | qubit1 + with Control(eng, qubit2 + qubit3): + Z | qubit1 + eng.flush() + eng.flush(deallocate_qubits=True) + for cmd in saving_backend.received_commands[4:7]: + assert cnot2rxx._recognize_cnot2(cmd) + for cmd in saving_backend.received_commands[7:9]: + assert not cnot2rxx._recognize_cnot2(cmd) + + +def _decomp_gates(eng, cmd): + """Test that the cmd.gate is a gate of class X""" + if len(cmd.control_qubits) == 1 and isinstance(cmd.gate, X.__class__): + return False + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements CNOT gate. +# test_eng implements the decomposition of the CNOT gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as CNOT up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """Test that this decomposition of CNOT produces correct amplitudes + + Function tests each DecompositionRule in + cnot2rxx.all_defined_decomposition_rules + """ + decomposition_rule_list = cnot2rxx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(0, 4): + basis_state = [0] * 4 + basis_state[basis_state_index] = 1.0 + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) + test_sim = test_eng.backend + correct_sim = correct_eng.backend + correct_qb = correct_eng.allocate_qubit() + correct_ctrl_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_ctrl_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_sim.set_wavefunction(basis_state, correct_qb + correct_ctrl_qb) + test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qb) + CNOT | (test_ctrl_qb, test_qb) + CNOT | (correct_ctrl_qb, correct_qb) + + test_eng.flush() + correct_eng.flush() + + assert len(correct_dummy_eng.received_commands) == 5 + assert len(test_dummy_eng.received_commands) == 10 + + assert correct_eng.backend.cheat()[1] == pytest.approx(test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + All(Measure) | test_qb + test_ctrl_qb + All(Measure) | correct_qb + correct_ctrl_qb + test_eng.flush(deallocate_qubits=True) + correct_eng.flush(deallocate_qubits=True) diff --git a/projectq/setups/decompositions/cnu2toffoliandcu.py b/projectq/setups/decompositions/cnu2toffoliandcu.py index 5f7c72761..a463de0c2 100644 --- a/projectq/setups/decompositions/cnu2toffoliandcu.py +++ b/projectq/setups/decompositions/cnu2toffoliandcu.py @@ -13,7 +13,7 @@ # limitations under the License. """ -Registers a decomposition rule for multi-controlled gates. +Register a decomposition rule for multi-controlled gates. Implements the decomposition of Nielsen and Chuang (Fig. 4.10) which decomposes a C^n(U) gate into a sequence of 2 * (n-1) Toffoli gates and one @@ -21,15 +21,12 @@ """ from projectq.cengines import DecompositionRule -from projectq.meta import get_control_count, Compute, Control, Uncompute +from projectq.meta import Compute, Control, Uncompute, get_control_count from projectq.ops import BasicGate, Toffoli, XGate -def _recognize_CnU(cmd): - """ - Recognize an arbitrary gate which has n>=2 control qubits, except a - Toffoli gate. - """ +def _recognize_CnU(cmd): # pylint: disable=invalid-name + """Recognize an arbitrary gate which has n>=2 control qubits, except a Toffoli gate.""" if get_control_count(cmd) == 2: if not isinstance(cmd.gate, XGate): return True @@ -38,32 +35,42 @@ def _recognize_CnU(cmd): return False -def _decompose_CnU(cmd): +def _decompose_CnU(cmd): # pylint: disable=invalid-name """ - Decompose a multi-controlled gate U into a single-controlled U. + Decompose a multi-controlled gate U with n control qubits into a single- controlled U. - It uses (n-1) work qubits and 2 * (n-1) Toffoli gates. + It uses (n-1) work qubits and 2 * (n-1) Toffoli gates for general U and (n-2) work qubits and 2n - 3 Toffoli gates + if U is an X-gate. """ eng = cmd.engine qubits = cmd.qubits ctrl_qureg = cmd.control_qubits gate = cmd.gate - n = get_control_count(cmd) - ancilla_qureg = eng.allocate_qureg(n-1) + n_controls = get_control_count(cmd) + + # specialized for X-gate + if gate == XGate() and n_controls > 2: + n_controls -= 1 + ancilla_qureg = eng.allocate_qureg(n_controls - 1) with Compute(eng): Toffoli | (ctrl_qureg[0], ctrl_qureg[1], ancilla_qureg[0]) - for ctrl_index in range(2, n): - Toffoli | (ctrl_qureg[ctrl_index], ancilla_qureg[ctrl_index-2], - ancilla_qureg[ctrl_index-1]) + for ctrl_index in range(2, n_controls): + Toffoli | ( + ctrl_qureg[ctrl_index], + ancilla_qureg[ctrl_index - 2], + ancilla_qureg[ctrl_index - 1], + ) + ctrls = [ancilla_qureg[-1]] - with Control(eng, ancilla_qureg[-1]): + # specialized for X-gate + if gate == XGate() and get_control_count(cmd) > 2: + ctrls += [ctrl_qureg[-1]] + with Control(eng, ctrls): gate | qubits Uncompute(eng) #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(BasicGate, _decompose_CnU, _recognize_CnU) -] +all_defined_decomposition_rules = [DecompositionRule(BasicGate, _decompose_CnU, _recognize_CnU)] diff --git a/projectq/setups/decompositions/cnu2toffoliandcu_test.py b/projectq/setups/decompositions/cnu2toffoliandcu_test.py index 5c815bfd2..b64203bfd 100644 --- a/projectq/setups/decompositions/cnu2toffoliandcu_test.py +++ b/projectq/setups/decompositions/cnu2toffoliandcu_test.py @@ -17,11 +17,25 @@ import pytest from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - DummyEngine, InstructionFilter, MainEngine) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control -from projectq.ops import (All, ClassicalInstructionGate, Measure, Ph, QFT, Rx, - Ry, X, XGate) +from projectq.ops import ( + QFT, + All, + ClassicalInstructionGate, + Measure, + Ph, + Rx, + Ry, + X, + XGate, +) from . import cnu2toffoliandcu @@ -78,16 +92,19 @@ def _decomp_gates(eng, cmd): def test_decomposition(): for basis_state_index in range(0, 16): basis_state = [0] * 16 - basis_state[basis_state_index] = 1. + basis_state[basis_state_index] = 1.0 correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[cnu2toffoliandcu]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) test_sim = test_eng.backend correct_sim = correct_eng.backend correct_qb = correct_eng.allocate_qubit() @@ -97,32 +114,33 @@ def test_decomposition(): test_ctrl_qureg = test_eng.allocate_qureg(3) test_eng.flush() - correct_sim.set_wavefunction(basis_state, correct_qb + - correct_ctrl_qureg) + correct_sim.set_wavefunction(basis_state, correct_qb + correct_ctrl_qureg) test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qureg) with Control(test_eng, test_ctrl_qureg[:2]): Rx(0.4) | test_qb with Control(test_eng, test_ctrl_qureg): Ry(0.6) | test_qb + with Control(test_eng, test_ctrl_qureg): + X | test_qb with Control(correct_eng, correct_ctrl_qureg[:2]): Rx(0.4) | correct_qb with Control(correct_eng, correct_ctrl_qureg): Ry(0.6) | correct_qb + with Control(correct_eng, correct_ctrl_qureg): + X | correct_qb test_eng.flush() correct_eng.flush() - assert len(correct_dummy_eng.received_commands) == 8 - assert len(test_dummy_eng.received_commands) == 20 + assert len(correct_dummy_eng.received_commands) == 9 + assert len(test_dummy_eng.received_commands) == 25 for fstate in range(16): binary_state = format(fstate, '04b') - test = test_sim.get_amplitude(binary_state, - test_qb + test_ctrl_qureg) - correct = correct_sim.get_amplitude(binary_state, correct_qb + - correct_ctrl_qureg) + test = test_sim.get_amplitude(binary_state, test_qb + test_ctrl_qureg) + correct = correct_sim.get_amplitude(binary_state, correct_qb + correct_ctrl_qureg) assert correct == pytest.approx(test, rel=1e-12, abs=1e-12) All(Measure) | test_qb + test_ctrl_qureg diff --git a/projectq/setups/decompositions/controlstate.py b/projectq/setups/decompositions/controlstate.py new file mode 100755 index 000000000..b8d180ecc --- /dev/null +++ b/projectq/setups/decompositions/controlstate.py @@ -0,0 +1,43 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Register a decomposition to replace turn negatively controlled qubits into positively controlled qubits. + +This achieved by applying X gates to selected qubits. +""" + +from copy import deepcopy + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Uncompute, has_negative_control +from projectq.ops import BasicGate, X + + +def _decompose_controlstate(cmd): + """Decompose commands with control qubits in negative state (ie. control qubits with state '0' instead of '1').""" + with Compute(cmd.engine): + for state, ctrl in zip(cmd.control_state, cmd.control_qubits): + if state == '0': + X | ctrl + + # Resend the command with the `control_state` cleared + cmd.ctrl_state = '1' * len(cmd.control_state) + orig_engine = cmd.engine + cmd.engine.receive([deepcopy(cmd)]) # NB: deepcopy required here to workaround infinite recursion detection + Uncompute(orig_engine) + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(BasicGate, _decompose_controlstate, has_negative_control)] diff --git a/projectq/setups/decompositions/controlstate_test.py b/projectq/setups/decompositions/controlstate_test.py new file mode 100755 index 000000000..02512e5e7 --- /dev/null +++ b/projectq/setups/decompositions/controlstate_test.py @@ -0,0 +1,52 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the controlstate decomposition rule. +""" + +from projectq import MainEngine +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Control, has_negative_control +from projectq.ops import X +from projectq.setups.decompositions import cnot2cz, controlstate + + +def filter_func(eng, cmd): + if has_negative_control(cmd): + return False + return True + + +def test_controlstate_priority(): + saving_backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet(modules=[cnot2cz, controlstate]) + eng = MainEngine(backend=saving_backend, engine_list=[AutoReplacer(rule_set), InstructionFilter(filter_func)]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + with Control(eng, qubit2, ctrl_state='0'): + X | qubit1 + with Control(eng, qubit3, ctrl_state='1'): + X | qubit1 + eng.flush() + + assert len(saving_backend.received_commands) == 8 + for cmd in saving_backend.received_commands: + assert not has_negative_control(cmd) diff --git a/projectq/setups/decompositions/crz2cxandrz.py b/projectq/setups/decompositions/crz2cxandrz.py index c12f43f63..b56603cc9 100755 --- a/projectq/setups/decompositions/crz2cxandrz.py +++ b/projectq/setups/decompositions/crz2cxandrz.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition for controlled z-rotation gates. @@ -20,28 +19,26 @@ from projectq.cengines import DecompositionRule from projectq.meta import get_control_count -from projectq.ops import NOT, Rz, C +from projectq.ops import NOT, C, Rz -def _decompose_CRz(cmd): - """ Decompose the controlled Rz gate (into CNOT and Rz). """ +def _decompose_CRz(cmd): # pylint: disable=invalid-name + """Decompose the controlled Rz gate (into CNOT and Rz).""" qubit = cmd.qubits[0] ctrl = cmd.control_qubits gate = cmd.gate - n = get_control_count(cmd) + n_controls = get_control_count(cmd) Rz(0.5 * gate.angle) | qubit - C(NOT, n) | (ctrl, qubit) + C(NOT, n_controls) | (ctrl, qubit) Rz(-0.5 * gate.angle) | qubit - C(NOT, n) | (ctrl, qubit) + C(NOT, n_controls) | (ctrl, qubit) -def _recognize_CRz(cmd): - """ Recognize the controlled Rz gate. """ +def _recognize_CRz(cmd): # pylint: disable=invalid-name + """Recognize the controlled Rz gate.""" return get_control_count(cmd) >= 1 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Rz, _decompose_CRz, _recognize_CRz) -] +all_defined_decomposition_rules = [DecompositionRule(Rz, _decompose_CRz, _recognize_CRz)] diff --git a/projectq/setups/decompositions/entangle.py b/projectq/setups/decompositions/entangle.py index b7049ea99..0f692f669 100755 --- a/projectq/setups/decompositions/entangle.py +++ b/projectq/setups/decompositions/entangle.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition for the Entangle gate. @@ -20,22 +19,20 @@ """ from projectq.cengines import DecompositionRule -from projectq.meta import Control, get_control_count -from projectq.ops import X, H, Entangle, All +from projectq.meta import Control +from projectq.ops import All, Entangle, H, X def _decompose_entangle(cmd): - """ Decompose the entangle gate. """ - qr = cmd.qubits[0] + """Decompose the entangle gate.""" + qureg = cmd.qubits[0] eng = cmd.engine with Control(eng, cmd.control_qubits): - H | qr[0] - with Control(eng, qr[0]): - All(X) | qr[1:] + H | qureg[0] + with Control(eng, qureg[0]): + All(X) | qureg[1:] #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Entangle.__class__, _decompose_entangle) -] +all_defined_decomposition_rules = [DecompositionRule(Entangle.__class__, _decompose_entangle)] diff --git a/projectq/setups/decompositions/globalphase.py b/projectq/setups/decompositions/globalphase.py index b75bd13a4..679c08a51 100755 --- a/projectq/setups/decompositions/globalphase.py +++ b/projectq/setups/decompositions/globalphase.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition rule for global phases. @@ -23,17 +22,14 @@ from projectq.ops import Ph -def _decompose_PhNoCtrl(cmd): - """ Throw out global phases (no controls). """ - pass +def _decompose_PhNoCtrl(cmd): # pylint: disable=invalid-name,unused-argument + """Throw out global phases (no controls).""" -def _recognize_PhNoCtrl(cmd): - """ Recognize global phases (no controls). """ +def _recognize_PhNoCtrl(cmd): # pylint: disable=invalid-name + """Recognize global phases (no controls).""" return get_control_count(cmd) == 0 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Ph, _decompose_PhNoCtrl, _recognize_PhNoCtrl) -] +all_defined_decomposition_rules = [DecompositionRule(Ph, _decompose_PhNoCtrl, _recognize_PhNoCtrl)] diff --git a/projectq/setups/decompositions/h2rx.py b/projectq/setups/decompositions/h2rx.py new file mode 100644 index 000000000..608629b10 --- /dev/null +++ b/projectq/setups/decompositions/h2rx.py @@ -0,0 +1,56 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 + +"""Register a decomposition for the H gate into an Ry and Rx gate.""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import get_control_count +from projectq.ops import H, Ph, Rx, Ry + + +def _decompose_h2rx_M(cmd): # pylint: disable=invalid-name + """Decompose the Ry gate.""" + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + Rx(math.pi) | qubit + Ph(math.pi / 2) | qubit + Ry(-1 * math.pi / 2) | qubit + + +def _decompose_h2rx_N(cmd): # pylint: disable=invalid-name + """Decompose the Ry gate.""" + # Labelled 'N' for 'neutral' because decomposition doesn't end with + # Ry(pi/2) or Ry(-pi/2) + qubit = cmd.qubits[0] + Ry(math.pi / 2) | qubit + Ph(3 * math.pi / 2) | qubit + Rx(-1 * math.pi) | qubit + + +def _recognize_HNoCtrl(cmd): # pylint: disable=invalid-name + """For efficiency reasons only if no control qubits.""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(H.__class__, _decompose_h2rx_N, _recognize_HNoCtrl), + DecompositionRule(H.__class__, _decompose_h2rx_M, _recognize_HNoCtrl), +] diff --git a/projectq/setups/decompositions/h2rx_test.py b/projectq/setups/decompositions/h2rx_test.py new file mode 100644 index 000000000..fb3d2045b --- /dev/null +++ b/projectq/setups/decompositions/h2rx_test.py @@ -0,0 +1,117 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.h2rx.py" + +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Control +from projectq.ops import H, HGate, Measure + +from . import h2rx + + +def test_recognize_correct_gates(): + """Test that recognize_HNoCtrl recognizes ctrl qubits""" + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + H | qubit + with Control(eng, ctrl_qubit): + H | qubit + eng.flush(deallocate_qubits=True) + assert h2rx._recognize_HNoCtrl(saving_backend.received_commands[3]) + assert not h2rx._recognize_HNoCtrl(saving_backend.received_commands[4]) + + +def h_decomp_gates(eng, cmd): + """Test that cmd.gate is a gate of class HGate""" + g = cmd.gate + if isinstance(g, HGate): # H is just a shortcut to HGate + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements H gate. +# test_eng implements the decomposition of the H gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition in this case only produces the same state as H up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +def test_decomposition(): + """Test that this decomposition of H produces correct amplitudes + + Function tests each DecompositionRule in + h2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = h2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state_index in range(2): + basis_state = [0] * 2 + basis_state[basis_state_index] = 1.0 + + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(h_decomp_gates), + test_dummy_eng, + ], + ) + + correct_qb = correct_eng.allocate_qubit() + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_eng.flush() + + correct_eng.backend.set_wavefunction(basis_state, correct_qb) + test_eng.backend.set_wavefunction(basis_state, test_qb) + + H | correct_qb + H | test_qb + + correct_eng.flush() + test_eng.flush() + + assert H in (cmd.gate for cmd in correct_dummy_eng.received_commands) + assert H not in (cmd.gate for cmd in test_dummy_eng.received_commands) + + assert correct_eng.backend.cheat()[1] == pytest.approx(test_eng.backend.cheat()[1], rel=1e-12, abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/decompositions/ph2r.py b/projectq/setups/decompositions/ph2r.py index 5db5d6f6a..2fefe0dcd 100755 --- a/projectq/setups/decompositions/ph2r.py +++ b/projectq/setups/decompositions/ph2r.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition for the controlled global phase gate. @@ -24,8 +23,8 @@ from projectq.ops import Ph, R -def _decompose_Ph(cmd): - """ Decompose the controlled phase gate (C^nPh(phase)). """ +def _decompose_Ph(cmd): # pylint: disable=invalid-name + """Decompose the controlled phase gate (C^nPh(phase)).""" ctrl = cmd.control_qubits gate = cmd.gate eng = cmd.engine @@ -34,12 +33,10 @@ def _decompose_Ph(cmd): R(gate.angle) | ctrl[0] -def _recognize_Ph(cmd): - """ Recognize the controlled phase gate. """ +def _recognize_Ph(cmd): # pylint: disable=invalid-name + """Recognize the controlled phase gate.""" return get_control_count(cmd) >= 1 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Ph, _decompose_Ph, _recognize_Ph) -] +all_defined_decomposition_rules = [DecompositionRule(Ph, _decompose_Ph, _recognize_Ph)] diff --git a/projectq/setups/decompositions/phaseestimation.py b/projectq/setups/decompositions/phaseestimation.py new file mode 100644 index 000000000..0913064e5 --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation.py @@ -0,0 +1,125 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Registers a decomposition for phase estimation. + +(reference https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + +The Quantum Phase Estimation (QPE) executes the algorithm up to the inverse +QFT included. The following steps measuring the ancillas and computing the +phase should be executed outside of the QPE. + +The decomposition uses as ancillas (qpe_ancillas) the first qubit/qureg in +the Command and as system qubits the second qubit/qureg in the Command. + +The unitary operator for which the phase estimation is estimated (unitary) +is the gate in Command + +Example: + .. code-block:: python + + # Example using a ProjectQ gate + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(1) + angle = cmath.pi * 2.0 * 0.125 + U = Ph(angle) # unitary_specfic_to_the_problem() + + # Apply Quantum Phase Estimation + QPE(U) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + # (https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin, 2) + phase = phase_int / (2**n_qpe_ancillas) + print(phase) + + # Example using a function (two_qubit_gate). + # Instead of applying QPE on a gate U one could provide a function + + + def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0 * cmath.pi * (time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + + n_qpe_ancillas = 3 + qpe_ancillas = eng.allocate_qureg(n_qpe_ancillas) + system_qubits = eng.allocate_qureg(2) + X | system_qubits[0] + + # Apply Quantum Phase Estimation + QPE(two_qubit_gate) | (qpe_ancillas, system_qubits) + + All(Measure) | qpe_ancillas + # Compute the phase from the ancilla measurement + # (https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) + phasebinlist = [int(q) for q in qpe_ancillas] + phase_in_bin = ''.join(str(j) for j in phasebinlist) + phase_int = int(phase_in_bin, 2) + phase = phase_int / (2**n_qpe_ancillas) + print(phase) + +Attributes: + unitary (BasicGate): Unitary Operation either a ProjectQ gate or a function f. + Calling the function with the parameters system_qubits(Qureg) and time (integer), + i.e. f(system_qubits, time), applies to the system qubits a unitary defined in f + with parameter time. + + +""" + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Loop +from projectq.ops import QFT, QPE, H, Tensor, get_inverse + + +def _decompose_QPE(cmd): # pylint: disable=invalid-name + """Decompose the Quantum Phase Estimation gate.""" + eng = cmd.engine + + # Ancillas is the first qubit/qureg. System-qubit is the second qubit/qureg + qpe_ancillas = cmd.qubits[0] + system_qubits = cmd.qubits[1] + + # Hadamard on the ancillas + Tensor(H) | qpe_ancillas + + # The Unitary Operator + unitary = cmd.gate.unitary + + # Control U on the system_qubits + if callable(unitary): + # If U is a function + for i, ancilla in enumerate(qpe_ancillas): + with Control(eng, ancilla): + unitary(system_qubits, time=2**i) + else: + for i, ancilla in enumerate(qpe_ancillas): + ipower = int(2**i) + with Loop(eng, ipower): + with Control(eng, ancilla): + unitary | system_qubits + + # Inverse QFT on the ancillas + get_inverse(QFT) | qpe_ancillas + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(QPE, _decompose_QPE)] diff --git a/projectq/setups/decompositions/phaseestimation_test.py b/projectq/setups/decompositions/phaseestimation_test.py new file mode 100644 index 000000000..08e33e587 --- /dev/null +++ b/projectq/setups/decompositions/phaseestimation_test.py @@ -0,0 +1,178 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.phaseestimation.py." + +import cmath + +import numpy as np +import pytest +from flaky import flaky + +import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot +import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot +from projectq.backends import Simulator +from projectq.cengines import AutoReplacer, DecompositionRuleSet, MainEngine +from projectq.ops import CNOT, QPE, All, H, Measure, Ph, StatePreparation, Tensor, X +from projectq.setups.decompositions import phaseestimation as pe +from projectq.setups.decompositions import qft2crandhadamard as dqft + + +@flaky(max_runs=5, min_passes=2) +def test_simple_test_X_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + N = 150 + results = np.array([]) + for i in range(N): + autovector = eng.allocate_qureg(1) + X | autovector + H | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2.0 ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.5).sum() + assert num_phase / N >= 0.35, f"Statistics phase calculation are not correct ({num_phase / N:f} vs. {0.35:f})" + + +@flaky(max_runs=5, min_passes=2) +def test_Ph_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + N = 150 + results = np.array([]) + for i in range(N): + autovector = eng.allocate_qureg(1) + theta = cmath.pi * 2.0 * 0.125 + unit = Ph(theta) + ancillas = eng.allocate_qureg(3) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2.0 ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase / N >= 0.35, f"Statistics phase calculation are not correct ({num_phase / N:f} vs. {0.35:f})" + + +def two_qubit_gate(system_q, time): + CNOT | (system_q[0], system_q[1]) + Ph(2.0 * cmath.pi * (time * 0.125)) | system_q[1] + CNOT | (system_q[0], system_q[1]) + + +@flaky(max_runs=5, min_passes=2) +def test_2qubitsPh_andfunction_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft]) + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + N = 150 + results = np.array([]) + for i in range(N): + autovector = eng.allocate_qureg(2) + X | autovector[0] + ancillas = eng.allocate_qureg(3) + QPE(two_qubit_gate) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2.0 ** (len(ancillas))) + results = np.append(results, phase) + All(Measure) | autovector + eng.flush() + + num_phase = (results == 0.125).sum() + assert num_phase / N >= 0.34, f"Statistics phase calculation are not correct ({num_phase / N:f} vs. {0.34:f})" + + +def test_X_no_eigenvectors(): + rule_set = DecompositionRuleSet(modules=[pe, dqft, stateprep2cnot, ucr2cnot]) + eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + ], + ) + N = 100 + results = np.array([]) + results_plus = np.array([]) + results_minus = np.array([]) + for i in range(N): + autovector = eng.allocate_qureg(1) + amplitude0 = (np.sqrt(2) + np.sqrt(6)) / 4.0 + amplitude1 = (np.sqrt(2) - np.sqrt(6)) / 4.0 + StatePreparation([amplitude0, amplitude1]) | autovector + unit = X + ancillas = eng.allocate_qureg(1) + QPE(unit) | (ancillas, autovector) + All(Measure) | ancillas + fasebinlist = [int(q) for q in ancillas] + fasebin = ''.join(str(j) for j in fasebinlist) + faseint = int(fasebin, 2) + phase = faseint / (2.0 ** (len(ancillas))) + results = np.append(results, phase) + Tensor(H) | autovector + if np.allclose(phase, 0.0, rtol=1e-1): + results_plus = np.append(results_plus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 0 + elif np.allclose(phase, 0.5, rtol=1e-1): + results_minus = np.append(results_minus, phase) + All(Measure) | autovector + autovector_result = int(autovector) + assert autovector_result == 1 + eng.flush() + + total = len(results_plus) + len(results_minus) + plus_probability = len(results_plus) / N + assert total == pytest.approx(N, abs=5) + assert plus_probability == pytest.approx( + 1.0 / 4.0, abs=1e-1 + ), f"Statistics on |+> probability are not correct ({plus_probability:f} vs. {1.0 / 4.0:f})" + + +def test_string(): + unit = X + gate = QPE(unit) + assert str(gate) == "QPE(X)" diff --git a/projectq/setups/decompositions/qft2crandhadamard.py b/projectq/setups/decompositions/qft2crandhadamard.py index 63537dea8..de7346bbf 100755 --- a/projectq/setups/decompositions/qft2crandhadamard.py +++ b/projectq/setups/decompositions/qft2crandhadamard.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition rule for the quantum Fourier transform. @@ -25,11 +24,11 @@ import math from projectq.cengines import DecompositionRule -from projectq.ops import H, R, QFT from projectq.meta import Control +from projectq.ops import QFT, H, R -def _decompose_QFT(cmd): +def _decompose_QFT(cmd): # pylint: disable=invalid-name qb = cmd.qubits[0] eng = cmd.engine with Control(eng, cmd.control_qubits): @@ -41,6 +40,4 @@ def _decompose_QFT(cmd): #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(QFT.__class__, _decompose_QFT) -] +all_defined_decomposition_rules = [DecompositionRule(QFT.__class__, _decompose_QFT)] diff --git a/projectq/setups/decompositions/qubitop2onequbit.py b/projectq/setups/decompositions/qubitop2onequbit.py new file mode 100644 index 000000000..5fd68e345 --- /dev/null +++ b/projectq/setups/decompositions/qubitop2onequbit.py @@ -0,0 +1,49 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Register a decomposition rule for a unitary QubitOperator to one qubit gates.""" + +import cmath + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, get_control_count +from projectq.ops import Ph, QubitOperator, X, Y, Z + + +def _recognize_qubitop(cmd): + """For efficiency only use this if at most 1 control qubit.""" + return get_control_count(cmd) <= 1 + + +def _decompose_qubitop(cmd): + if len(cmd.qubits) != 1: + raise ValueError('QubitOperator decomposition can only accept a single quantum register') + qureg = cmd.qubits[0] + eng = cmd.engine + qubit_op = cmd.gate + with Control(eng, cmd.control_qubits): + ((term, coefficient),) = qubit_op.terms.items() + phase = cmath.phase(coefficient) + Ph(phase) | qureg[0] + for index, action in term: + if action == "X": + X | qureg[index] + elif action == "Y": + Y | qureg[index] + elif action == "Z": + Z | qureg[index] + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(QubitOperator, _decompose_qubitop, _recognize_qubitop)] diff --git a/projectq/setups/decompositions/qubitop2onequbit_test.py b/projectq/setups/decompositions/qubitop2onequbit_test.py new file mode 100644 index 000000000..ed124c963 --- /dev/null +++ b/projectq/setups/decompositions/qubitop2onequbit_test.py @@ -0,0 +1,106 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cmath + +import pytest + +import projectq.setups.decompositions.qubitop2onequbit as qubitop2onequbit +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Control +from projectq.ops import All, Command, Measure, Ph, QubitOperator, X, Y, Z +from projectq.types import WeakQubitRef + + +def test_recognize(): + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend, engine_list=[]) + ctrl_qureg = eng.allocate_qureg(2) + qureg = eng.allocate_qureg(2) + with Control(eng, ctrl_qureg): + QubitOperator("X0 Y1") | qureg + with Control(eng, ctrl_qureg[0]): + QubitOperator("X0 Y1") | qureg + eng.flush() + cmd0 = saving_backend.received_commands[4] + cmd1 = saving_backend.received_commands[5] + assert not qubitop2onequbit._recognize_qubitop(cmd0) + assert qubitop2onequbit._recognize_qubitop(cmd1) + + +def _decomp_gates(eng, cmd): + if isinstance(cmd.gate, QubitOperator): + return False + else: + return True + + +def test_qubitop2singlequbit_invalid(): + qb0 = WeakQubitRef(None, idx=0) + qb1 = WeakQubitRef(None, idx=1) + with pytest.raises(ValueError): + qubitop2onequbit._decompose_qubitop(Command(None, QubitOperator(), ([qb0], [qb1]))) + + +def test_qubitop2singlequbit(): + num_qubits = 4 + random_initial_state = [0.2 + 0.1 * x * cmath.exp(0.1j + 0.2j * x) for x in range(2 ** (num_qubits + 1))] + rule_set = DecompositionRuleSet(modules=[qubitop2onequbit]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(_decomp_gates)], + ) + test_qureg = test_eng.allocate_qureg(num_qubits) + test_ctrl_qb = test_eng.allocate_qubit() + test_eng.flush() + test_eng.backend.set_wavefunction(random_initial_state, test_qureg + test_ctrl_qb) + correct_eng = MainEngine() + correct_qureg = correct_eng.allocate_qureg(num_qubits) + correct_ctrl_qb = correct_eng.allocate_qubit() + correct_eng.flush() + correct_eng.backend.set_wavefunction(random_initial_state, correct_qureg + correct_ctrl_qb) + + qubit_op_0 = QubitOperator("X0 Y1 Z3", -1.0j) + qubit_op_1 = QubitOperator("Z0 Y1 X3", cmath.exp(0.6j)) + + qubit_op_0 | test_qureg + with Control(test_eng, test_ctrl_qb): + qubit_op_1 | test_qureg + test_eng.flush() + + correct_eng.backend.apply_qubit_operator(qubit_op_0, correct_qureg) + with Control(correct_eng, correct_ctrl_qb): + Ph(0.6) | correct_qureg[0] + Z | correct_qureg[0] + Y | correct_qureg[1] + X | correct_qureg[3] + correct_eng.flush() + + for fstate in range(2 ** (num_qubits + 1)): + binary_state = format(fstate, f"0{num_qubits + 1}b") + test = test_eng.backend.get_amplitude(binary_state, test_qureg + test_ctrl_qb) + correct = correct_eng.backend.get_amplitude(binary_state, correct_qureg + correct_ctrl_qb) + assert correct == pytest.approx(test, rel=1e-10, abs=1e-10) + + All(Measure) | correct_qureg + correct_ctrl_qb + All(Measure) | test_qureg + test_ctrl_qb + correct_eng.flush() + test_eng.flush() diff --git a/projectq/setups/decompositions/r2rzandph.py b/projectq/setups/decompositions/r2rzandph.py index ab7b4c95e..9fca9ed52 100755 --- a/projectq/setups/decompositions/r2rzandph.py +++ b/projectq/setups/decompositions/r2rzandph.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition rule for the phase-shift gate. @@ -21,21 +20,19 @@ from projectq.cengines import DecompositionRule from projectq.meta import Control -from projectq.ops import Ph, Rz, R +from projectq.ops import Ph, R, Rz -def _decompose_R(cmd): - """ Decompose the (controlled) phase-shift gate, denoted by R(phase). """ +def _decompose_R(cmd): # pylint: disable=invalid-name + """Decompose the (controlled) phase-shift gate, denoted by R(phase).""" ctrl = cmd.control_qubits eng = cmd.engine gate = cmd.gate with Control(eng, ctrl): - Ph(.5 * gate.angle) | cmd.qubits + Ph(0.5 * gate.angle) | cmd.qubits Rz(gate.angle) | cmd.qubits #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(R, _decompose_R) -] +all_defined_decomposition_rules = [DecompositionRule(R, _decompose_R)] diff --git a/projectq/setups/decompositions/rx2rz.py b/projectq/setups/decompositions/rx2rz.py index 657afa28b..6fe1f22be 100644 --- a/projectq/setups/decompositions/rx2rz.py +++ b/projectq/setups/decompositions/rx2rz.py @@ -12,17 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Registers a decomposition for the Rx gate into an Rz gate and Hadamard. -""" +"""Register a decomposition for the Rx gate into an Rz gate and Hadamard.""" from projectq.cengines import DecompositionRule -from projectq.meta import Compute, Control, get_control_count, Uncompute -from projectq.ops import Rx, Rz, H +from projectq.meta import Compute, Control, Uncompute, get_control_count +from projectq.ops import H, Rx, Rz def _decompose_rx(cmd): - """ Decompose the Rx gate.""" + """Decompose the Rx gate.""" qubit = cmd.qubits[0] eng = cmd.engine angle = cmd.gate.angle @@ -34,12 +32,10 @@ def _decompose_rx(cmd): Uncompute(eng) -def _recognize_RxNoCtrl(cmd): - """ For efficiency reasons only if no control qubits.""" +def _recognize_RxNoCtrl(cmd): # pylint: disable=invalid-name + """For efficiency reasons only if no control qubits.""" return get_control_count(cmd) == 0 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Rx, _decompose_rx, _recognize_RxNoCtrl) -] +all_defined_decomposition_rules = [DecompositionRule(Rx, _decompose_rx, _recognize_RxNoCtrl)] diff --git a/projectq/setups/decompositions/rx2rz_test.py b/projectq/setups/decompositions/rx2rz_test.py index 2d6e5eacb..767c805a0 100644 --- a/projectq/setups/decompositions/rx2rz_test.py +++ b/projectq/setups/decompositions/rx2rz_test.py @@ -18,12 +18,16 @@ import pytest -from projectq import MainEngine from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - DummyEngine, InstructionFilter, MainEngine) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control -from projectq.ops import Measure, Ph, Rx +from projectq.ops import Measure, Rx from . import rx2rz @@ -50,19 +54,22 @@ def rx_decomp_gates(eng, cmd): return True -@pytest.mark.parametrize("angle", [0, math.pi, 2*math.pi, 4*math.pi, 0.5]) +@pytest.mark.parametrize("angle", [0, math.pi, 2 * math.pi, 4 * math.pi, 0.5]) def test_decomposition(angle): for basis_state in ([1, 0], [0, 1]): correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[rx2rz]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(rx_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(rx_decomp_gates), + test_dummy_eng, + ], + ) correct_qb = correct_eng.allocate_qubit() Rx(angle) | correct_qb diff --git a/projectq/setups/decompositions/ry2rz.py b/projectq/setups/decompositions/ry2rz.py index 7ee9e6e01..089d9c018 100644 --- a/projectq/setups/decompositions/ry2rz.py +++ b/projectq/setups/decompositions/ry2rz.py @@ -12,36 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Registers a decomposition for the Ry gate into an Rz and Rx(pi/2) gate. -""" +"""Register a decomposition for the Ry gate into an Rz and Rx(pi/2) gate.""" import math from projectq.cengines import DecompositionRule -from projectq.meta import Compute, Control, get_control_count, Uncompute -from projectq.ops import Rx, Ry, Rz, H +from projectq.meta import Compute, Control, Uncompute, get_control_count +from projectq.ops import Rx, Ry, Rz def _decompose_ry(cmd): - """ Decompose the Ry gate.""" + """Decompose the Ry gate.""" qubit = cmd.qubits[0] eng = cmd.engine angle = cmd.gate.angle with Control(eng, cmd.control_qubits): with Compute(eng): - Rx(math.pi/2.) | qubit + Rx(math.pi / 2.0) | qubit Rz(angle) | qubit Uncompute(eng) -def _recognize_RyNoCtrl(cmd): - """ For efficiency reasons only if no control qubits.""" +def _recognize_RyNoCtrl(cmd): # pylint: disable=invalid-name + """For efficiency reasons only if no control qubits.""" return get_control_count(cmd) == 0 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Ry, _decompose_ry, _recognize_RyNoCtrl) -] +all_defined_decomposition_rules = [DecompositionRule(Ry, _decompose_ry, _recognize_RyNoCtrl)] diff --git a/projectq/setups/decompositions/ry2rz_test.py b/projectq/setups/decompositions/ry2rz_test.py index d24692d63..61f9e1cfd 100644 --- a/projectq/setups/decompositions/ry2rz_test.py +++ b/projectq/setups/decompositions/ry2rz_test.py @@ -18,12 +18,16 @@ import pytest -from projectq import MainEngine from projectq.backends import Simulator -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - DummyEngine, InstructionFilter, MainEngine) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, +) from projectq.meta import Control -from projectq.ops import Measure, Ph, Ry +from projectq.ops import Measure, Ry from . import ry2rz @@ -50,19 +54,22 @@ def ry_decomp_gates(eng, cmd): return True -@pytest.mark.parametrize("angle", [0, math.pi, 2*math.pi, 4*math.pi, 0.5]) +@pytest.mark.parametrize("angle", [0, math.pi, 2 * math.pi, 4 * math.pi, 0.5]) def test_decomposition(angle): for basis_state in ([1, 0], [0, 1]): correct_dummy_eng = DummyEngine(save_commands=True) - correct_eng = MainEngine(backend=Simulator(), - engine_list=[correct_dummy_eng]) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) rule_set = DecompositionRuleSet(modules=[ry2rz]) test_dummy_eng = DummyEngine(save_commands=True) - test_eng = MainEngine(backend=Simulator(), - engine_list=[AutoReplacer(rule_set), - InstructionFilter(ry_decomp_gates), - test_dummy_eng]) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(ry_decomp_gates), + test_dummy_eng, + ], + ) correct_qb = correct_eng.allocate_qubit() Ry(angle) | correct_qb diff --git a/projectq/setups/decompositions/rz2rx.py b/projectq/setups/decompositions/rz2rx.py new file mode 100644 index 000000000..d3116ed23 --- /dev/null +++ b/projectq/setups/decompositions/rz2rx.py @@ -0,0 +1,65 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques for an +# ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 + +"""Registers a decomposition for the Rz gate into an Rx and Ry(pi/2) or Ry(-pi/2) gate.""" + +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, Uncompute, get_control_count +from projectq.ops import Rx, Ry, Rz + + +def _decompose_rz2rx_P(cmd): # pylint: disable=invalid-name + """Decompose the Rz using negative angle.""" + # Labelled 'P' for 'plus' because decomposition ends with a Ry(+pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(-math.pi / 2.0) | qubit + Rx(-angle) | qubit + Uncompute(eng) + + +def _decompose_rz2rx_M(cmd): # pylint: disable=invalid-name + """Decompose the Rz using positive angle.""" + # Labelled 'M' for 'minus' because decomposition ends with a Ry(-pi/2) + qubit = cmd.qubits[0] + eng = cmd.engine + angle = cmd.gate.angle + + with Control(eng, cmd.control_qubits): + with Compute(eng): + Ry(math.pi / 2.0) | qubit + Rx(angle) | qubit + Uncompute(eng) + + +def _recognize_RzNoCtrl(cmd): # pylint: disable=invalid-name + """Decompose the gate only if the command represents a single qubit gate (if it is not part of a control gate).""" + return get_control_count(cmd) == 0 + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(Rz, _decompose_rz2rx_P, _recognize_RzNoCtrl), + DecompositionRule(Rz, _decompose_rz2rx_M, _recognize_RzNoCtrl), +] diff --git a/projectq/setups/decompositions/rz2rx_test.py b/projectq/setups/decompositions/rz2rx_test.py new file mode 100644 index 000000000..7418c5a1d --- /dev/null +++ b/projectq/setups/decompositions/rz2rx_test.py @@ -0,0 +1,129 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.decompositions.rz2rx.py" + +import math + +import numpy as np +import pytest + +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Control +from projectq.ops import Measure, Rz + +from . import rz2rx + + +def test_recognize_correct_gates(): + """Test that recognize_RzNoCtrl recognizes ctrl qubits""" + saving_backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=saving_backend) + qubit = eng.allocate_qubit() + ctrl_qubit = eng.allocate_qubit() + eng.flush() + Rz(0.3) | qubit + with Control(eng, ctrl_qubit): + Rz(0.4) | qubit + eng.flush(deallocate_qubits=True) + assert rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[3]) + assert not rz2rx._recognize_RzNoCtrl(saving_backend.received_commands[4]) + + +def rz_decomp_gates(eng, cmd): + """Test that cmd.gate is the gate Rz""" + g = cmd.gate + if isinstance(g, Rz): + return False + else: + return True + + +# ------------test_decomposition function-------------# +# Creates two engines, correct_eng and test_eng. +# correct_eng implements Rz(angle) gate. +# test_eng implements the decomposition of the Rz(angle) gate. +# correct_qb and test_qb represent results of these two engines, respectively. +# +# The decomposition only needs to produce the same state in a qubit up to a +# global phase. +# test_vector and correct_vector represent the final wave states of correct_qb +# and test_qb. +# +# The dot product of correct_vector and test_vector should have absolute value +# 1, if the two vectors are the same up to a global phase. + + +@pytest.mark.parametrize("angle", [0, math.pi, 2 * math.pi, 4 * math.pi, 0.5]) +def test_decomposition(angle): + """ + Test that this decomposition of Rz produces correct amplitudes + + Note that this function tests each DecompositionRule in + rz2rx.all_defined_decomposition_rules + """ + decomposition_rule_list = rz2rx.all_defined_decomposition_rules + for rule in decomposition_rule_list: + for basis_state in ([1, 0], [0, 1]): + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) + + rule_set = DecompositionRuleSet(rules=[rule]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(rz_decomp_gates), + test_dummy_eng, + ], + ) + + correct_qb = correct_eng.allocate_qubit() + Rz(angle) | correct_qb + correct_eng.flush() + + test_qb = test_eng.allocate_qubit() + Rz(angle) | test_qb + test_eng.flush() + + # Create empty vectors for the wave vectors for the correct and + # test qubits + correct_vector = np.zeros((2, 1), dtype=np.complex_) + test_vector = np.zeros((2, 1), dtype=np.complex_) + + i = 0 + for fstate in ['0', '1']: + test = test_eng.backend.get_amplitude(fstate, test_qb) + correct = correct_eng.backend.get_amplitude(fstate, correct_qb) + correct_vector[i] = correct + test_vector[i] = test + i += 1 + + # Necessary to transpose vector to use matrix dot product + test_vector = test_vector.transpose() + # Remember that transposed vector should come first in product + vector_dot_product = np.dot(test_vector, correct_vector) + + assert np.absolute(vector_dot_product) == pytest.approx(1, rel=1e-12, abs=1e-12) + + Measure | test_qb + Measure | correct_qb diff --git a/projectq/setups/decompositions/sqrtswap2cnot.py b/projectq/setups/decompositions/sqrtswap2cnot.py new file mode 100644 index 000000000..e2b645eaa --- /dev/null +++ b/projectq/setups/decompositions/sqrtswap2cnot.py @@ -0,0 +1,42 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Register a decomposition to achieve a SqrtSwap gate.""" + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, Uncompute +from projectq.ops import CNOT, SqrtSwap, SqrtX + + +def _decompose_sqrtswap(cmd): + """Decompose (controlled) swap gates.""" + if len(cmd.qubits) != 2: + raise ValueError('SqrtSwap gate requires two quantum registers') + if not (len(cmd.qubits[0]) == 1 and len(cmd.qubits[1]) == 1): + raise ValueError('SqrtSwap gate requires must act on only 2 qubits') + ctrl = cmd.control_qubits + qubit0 = cmd.qubits[0][0] + qubit1 = cmd.qubits[1][0] + eng = cmd.engine + + with Control(eng, ctrl): + with Compute(eng): + CNOT | (qubit0, qubit1) + with Control(eng, qubit1): + SqrtX | qubit0 + Uncompute(eng) + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(SqrtSwap.__class__, _decompose_sqrtswap)] diff --git a/projectq/setups/decompositions/sqrtswap2cnot_test.py b/projectq/setups/decompositions/sqrtswap2cnot_test.py new file mode 100644 index 000000000..2e920a70f --- /dev/null +++ b/projectq/setups/decompositions/sqrtswap2cnot_test.py @@ -0,0 +1,88 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setups.decompositions.sqrtswap2cnot.""" + +import pytest + +import projectq.setups.decompositions.sqrtswap2cnot as sqrtswap2cnot +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.ops import All, Command, Measure, SqrtSwap +from projectq.types import WeakQubitRef + + +def _decomp_gates(eng, cmd): + if isinstance(cmd.gate, SqrtSwap.__class__): + return False + return True + + +def test_sqrtswap_invalid(): + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + with pytest.raises(ValueError): + sqrtswap2cnot._decompose_sqrtswap(Command(None, SqrtSwap, ([qb0], [qb1], [qb2]))) + + with pytest.raises(ValueError): + sqrtswap2cnot._decompose_sqrtswap(Command(None, SqrtSwap, ([qb0], [qb1, qb2]))) + + +def test_sqrtswap(): + for basis_state in ([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]): + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) + rule_set = DecompositionRuleSet(modules=[sqrtswap2cnot]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) + test_sim = test_eng.backend + correct_sim = correct_eng.backend + correct_qureg = correct_eng.allocate_qureg(2) + correct_eng.flush() + test_qureg = test_eng.allocate_qureg(2) + test_eng.flush() + + correct_sim.set_wavefunction(basis_state, correct_qureg) + test_sim.set_wavefunction(basis_state, test_qureg) + + SqrtSwap | (test_qureg[0], test_qureg[1]) + test_eng.flush() + SqrtSwap | (correct_qureg[0], correct_qureg[1]) + correct_eng.flush() + + assert len(test_dummy_eng.received_commands) != len(correct_dummy_eng.received_commands) + for fstate in range(4): + binary_state = format(fstate, '02b') + test = test_sim.get_amplitude(binary_state, test_qureg) + correct = correct_sim.get_amplitude(binary_state, correct_qureg) + assert correct == pytest.approx(test, rel=1e-10, abs=1e-10) + + All(Measure) | test_qureg + All(Measure) | correct_qureg + test_eng.flush(deallocate_qubits=True) + correct_eng.flush(deallocate_qubits=True) diff --git a/projectq/setups/decompositions/stateprep2cnot.py b/projectq/setups/decompositions/stateprep2cnot.py new file mode 100644 index 000000000..80e399acd --- /dev/null +++ b/projectq/setups/decompositions/stateprep2cnot.py @@ -0,0 +1,90 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Register decomposition for StatePreparation.""" + +import cmath +import math + +from projectq.cengines import DecompositionRule +from projectq.meta import Control, Dagger +from projectq.ops import ( + Ph, + StatePreparation, + UniformlyControlledRy, + UniformlyControlledRz, +) + + +def _decompose_state_preparation(cmd): # pylint: disable=too-many-locals + """Implement state preparation based on arXiv:quant-ph/0407010v1.""" + eng = cmd.engine + if len(cmd.qubits) != 1: + raise ValueError('StatePreparation does not support multiple quantum registers!') + num_qubits = len(cmd.qubits[0]) + qureg = cmd.qubits[0] + final_state = cmd.gate.final_state + if len(final_state) != 2**num_qubits: + raise ValueError("Length of final_state is invalid.") + norm = 0.0 + for amplitude in final_state: + norm += abs(amplitude) ** 2 + if norm < 1 - 1e-10 or norm > 1 + 1e-10: + raise ValueError("final_state is not normalized.") + with Control(eng, cmd.control_qubits): + # As in the paper reference, we implement the inverse: + with Dagger(eng): + # Cancel all the relative phases + phase_of_blocks = [] + for amplitude in final_state: + phase_of_blocks.append(cmath.phase(amplitude)) + for qubit_idx, qubit in enumerate(qureg): + angles = [] + phase_of_next_blocks = [] + for block in range(2 ** (len(qureg) - qubit_idx - 1)): + phase0 = phase_of_blocks[2 * block] + phase1 = phase_of_blocks[2 * block + 1] + angles.append(phase0 - phase1) + phase_of_next_blocks.append((phase0 + phase1) / 2.0) + UniformlyControlledRz(angles) | ( + qureg[(qubit_idx + 1) :], # noqa: E203 + qubit, + ) + phase_of_blocks = phase_of_next_blocks + # Cancel global phase + Ph(-phase_of_blocks[0]) | qureg[-1] + # Remove amplitudes from states which contain a bit value 1: + abs_of_blocks = [] + for amplitude in final_state: + abs_of_blocks.append(abs(amplitude)) + for qubit_idx, qubit in enumerate(qureg): + angles = [] + abs_of_next_blocks = [] + for block in range(2 ** (len(qureg) - qubit_idx - 1)): + a0 = abs_of_blocks[2 * block] # pylint: disable=invalid-name + a1 = abs_of_blocks[2 * block + 1] # pylint: disable=invalid-name + if a0 == 0 and a1 == 0: + angles.append(0) + else: + angles.append(-2.0 * math.acos(a0 / math.sqrt(a0**2 + a1**2))) + abs_of_next_blocks.append(math.sqrt(a0**2 + a1**2)) + UniformlyControlledRy(angles) | ( + qureg[(qubit_idx + 1) :], # noqa: E203 + qubit, + ) + abs_of_blocks = abs_of_next_blocks + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(StatePreparation, _decompose_state_preparation)] diff --git a/projectq/setups/decompositions/stateprep2cnot_test.py b/projectq/setups/decompositions/stateprep2cnot_test.py new file mode 100644 index 000000000..dd1c1aced --- /dev/null +++ b/projectq/setups/decompositions/stateprep2cnot_test.py @@ -0,0 +1,76 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setups.decompositions.stateprep2cnot.""" + +import cmath +import math +from copy import deepcopy + +import numpy as np +import pytest + +import projectq +import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot +from projectq.ops import All, Command, Measure, Ph, Ry, Rz, StatePreparation +from projectq.setups import restrictedgateset +from projectq.types import WeakQubitRef + + +def test_invalid_arguments(): + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + cmd = Command(None, StatePreparation([0, 1j]), qubits=([qb0], [qb1])) + with pytest.raises(ValueError): + stateprep2cnot._decompose_state_preparation(cmd) + + +def test_wrong_final_state(): + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + cmd = Command(None, StatePreparation([0, 1j]), qubits=([qb0, qb1],)) + with pytest.raises(ValueError): + stateprep2cnot._decompose_state_preparation(cmd) + cmd2 = Command(None, StatePreparation([0, 0.999j]), qubits=([qb0],)) + with pytest.raises(ValueError): + stateprep2cnot._decompose_state_preparation(cmd2) + + +@pytest.mark.parametrize("zeros", [True, False]) +@pytest.mark.parametrize("n_qubits", [1, 2, 3, 4]) +def test_state_preparation(n_qubits, zeros): + engine_list = restrictedgateset.get_engine_list(one_qubit_gates=(Ry, Rz, Ph)) + eng = projectq.MainEngine(engine_list=engine_list) + qureg = eng.allocate_qureg(n_qubits) + eng.flush() + + f_state = [0.2 + 0.1 * x * cmath.exp(0.1j + 0.2j * x) for x in range(2**n_qubits)] + if zeros: + for i in range(2 ** (n_qubits - 1)): + f_state[i] = 0 + norm = 0 + for amplitude in f_state: + norm += abs(amplitude) ** 2 + f_state = [x / math.sqrt(norm) for x in f_state] + + StatePreparation(f_state) | qureg + eng.flush() + + wavefunction = deepcopy(eng.backend.cheat()[1]) + # Test that simulator hasn't reordered wavefunction + mapping = eng.backend.cheat()[0] + for key in mapping: + assert mapping[key] == key + All(Measure) | qureg + eng.flush() + assert np.allclose(wavefunction, f_state, rtol=1e-10, atol=1e-10) diff --git a/projectq/setups/decompositions/swap2cnot.py b/projectq/setups/decompositions/swap2cnot.py index c48cbcc6c..23ffa8c69 100755 --- a/projectq/setups/decompositions/swap2cnot.py +++ b/projectq/setups/decompositions/swap2cnot.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition to achieve a Swap gate. @@ -20,12 +19,12 @@ """ from projectq.cengines import DecompositionRule -from projectq.meta import Compute, Uncompute, Control, get_control_count -from projectq.ops import Swap, CNOT +from projectq.meta import Compute, Control, Uncompute +from projectq.ops import CNOT, Swap def _decompose_swap(cmd): - """ Decompose (controlled) swap gates. """ + """Decompose (controlled) swap gates.""" ctrl = cmd.control_qubits eng = cmd.engine with Compute(eng): @@ -36,6 +35,4 @@ def _decompose_swap(cmd): #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(Swap.__class__, _decompose_swap) -] +all_defined_decomposition_rules = [DecompositionRule(Swap.__class__, _decompose_swap)] diff --git a/projectq/setups/decompositions/time_evolution.py b/projectq/setups/decompositions/time_evolution.py index ce5a91443..9e029391e 100644 --- a/projectq/setups/decompositions/time_evolution.py +++ b/projectq/setups/decompositions/time_evolution.py @@ -11,10 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - """ -Registers decomposition for the TimeEvolution gates. +Register decomposition for the TimeEvolution gates. An exact straight forward decomposition of a TimeEvolution gate is possible if the hamiltonian has only one term or if all the terms commute with each @@ -23,28 +21,23 @@ import math from projectq.cengines import DecompositionRule -from projectq.meta import Control, Compute, Uncompute -from projectq.ops import TimeEvolution, QubitOperator, H, Y, CNOT, Rz, Rx, Ry +from projectq.meta import Compute, Control, Uncompute +from projectq.ops import CNOT, H, QubitOperator, Rx, Ry, Rz, TimeEvolution def _recognize_time_evolution_commuting_terms(cmd): - """ - Recognize all TimeEvolution gates with >1 terms but which all commute. - """ + """Recognize all TimeEvolution gates with >1 terms but which all commute.""" hamiltonian = cmd.gate.hamiltonian if len(hamiltonian.terms) == 1: return False - else: - id_op = QubitOperator((), 0.0) - for term in hamiltonian.terms: - test_op = QubitOperator(term, hamiltonian.terms[term]) - for other in hamiltonian.terms: - other_op = QubitOperator(other, hamiltonian.terms[other]) - commutator = test_op * other_op - other_op * test_op - if not commutator.isclose(id_op, - rel_tol=1e-9, - abs_tol=1e-9): - return False + id_op = QubitOperator((), 0.0) + for term in hamiltonian.terms: + test_op = QubitOperator(term, hamiltonian.terms[term]) + for other in hamiltonian.terms: + other_op = QubitOperator(other, hamiltonian.terms[other]) + commutator = test_op * other_op - other_op * test_op + if not commutator.isclose(id_op, rel_tol=1e-9, abs_tol=1e-9): + return False return True @@ -63,9 +56,9 @@ def _recognize_time_evolution_individual_terms(cmd): return len(cmd.gate.hamiltonian.terms) == 1 -def _decompose_time_evolution_individual_terms(cmd): +def _decompose_time_evolution_individual_terms(cmd): # pylint: disable=too-many-branches """ - Implements a TimeEvolution gate with a hamiltonian having only one term. + Implement a TimeEvolution gate with a hamiltonian having only one term. To implement exp(-i * t * hamiltonian), where the hamiltonian is only one term, e.g., hamiltonian = X0 x Y1 X Z2, we first perform local @@ -81,29 +74,32 @@ def _decompose_time_evolution_individual_terms(cmd): Nielsen and Chuang, Quantum Computation and Information. """ - assert len(cmd.qubits) == 1 + if len(cmd.qubits) != 1: + raise ValueError('TimeEvolution gate can only accept a single quantum register') qureg = cmd.qubits[0] eng = cmd.engine time = cmd.gate.time hamiltonian = cmd.gate.hamiltonian - assert len(hamiltonian.terms) == 1 + if len(hamiltonian.terms) != 1: + raise ValueError('This decomposition function only accepts single-term hamiltonians!') term = list(hamiltonian.terms)[0] coefficient = hamiltonian.terms[term] check_indices = set() # Check that hamiltonian is not identity term, # Previous __or__ operator should have apply a global phase instead: - assert not term == () + if term == (): + raise ValueError('This decomposition function cannot accept a hamiltonian with an empty term!') # hamiltonian has only a single local operator if len(term) == 1: with Control(eng, cmd.control_qubits): if term[0][1] == 'X': - Rx(time * coefficient * 2.) | qureg[term[0][0]] + Rx(time * coefficient * 2.0) | qureg[term[0][0]] elif term[0][1] == 'Y': - Ry(time * coefficient * 2.) | qureg[term[0][0]] + Ry(time * coefficient * 2.0) | qureg[term[0][0]] else: - Rz(time * coefficient * 2.) | qureg[term[0][0]] + Rz(time * coefficient * 2.0) | qureg[term[0][0]] # hamiltonian has more than one local operator else: with Control(eng, cmd.control_qubits): @@ -114,13 +110,15 @@ def _decompose_time_evolution_individual_terms(cmd): if action == 'X': H | qureg[index] elif action == 'Y': - Rx(math.pi / 2.) | qureg[index] + Rx(math.pi / 2.0) | qureg[index] + print(check_indices, set(range(len(qureg)))) # Check that qureg had exactly as many qubits as indices: - assert check_indices == set((range(len(qureg)))) + if check_indices != set(range(len(qureg))): + raise ValueError('Indices mismatch between hamiltonian terms and qubits') # Compute parity - for i in range(len(qureg)-1): - CNOT | (qureg[i], qureg[i+1]) - Rz(time * coefficient * 2.) | qureg[-1] + for i in range(len(qureg) - 1): + CNOT | (qureg[i], qureg[i + 1]) + Rz(time * coefficient * 2.0) | qureg[-1] # Uncompute parity and basis change Uncompute(eng) @@ -128,15 +126,14 @@ def _decompose_time_evolution_individual_terms(cmd): rule_commuting_terms = DecompositionRule( gate_class=TimeEvolution, gate_decomposer=_decompose_time_evolution_commuting_terms, - gate_recognizer=_recognize_time_evolution_commuting_terms) - + gate_recognizer=_recognize_time_evolution_commuting_terms, +) rule_individual_terms = DecompositionRule( gate_class=TimeEvolution, gate_decomposer=_decompose_time_evolution_individual_terms, - gate_recognizer=_recognize_time_evolution_individual_terms) - + gate_recognizer=_recognize_time_evolution_individual_terms, +) #: Decomposition rules -all_defined_decomposition_rules = [rule_commuting_terms, - rule_individual_terms] +all_defined_decomposition_rules = [rule_commuting_terms, rule_individual_terms] diff --git a/projectq/setups/decompositions/time_evolution_test.py b/projectq/setups/decompositions/time_evolution_test.py index 4a5e30f54..8d0197a56 100644 --- a/projectq/setups/decompositions/time_evolution_test.py +++ b/projectq/setups/decompositions/time_evolution_test.py @@ -18,17 +18,30 @@ import numpy import pytest import scipy -from scipy import sparse as sps import scipy.sparse.linalg +from scipy import sparse as sps from projectq import MainEngine from projectq.backends import Simulator -from projectq.cengines import (DummyEngine, AutoReplacer, InstructionFilter, - InstructionFilter, DecompositionRuleSet) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) from projectq.meta import Control -from projectq.ops import (QubitOperator, TimeEvolution, - ClassicalInstructionGate, Ph, Rx, Ry, Rz, All, - Measure) +from projectq.ops import ( + All, + ClassicalInstructionGate, + Command, + Measure, + Ph, + QubitOperator, + Rx, + Ry, + TimeEvolution, +) +from projectq.types import WeakQubitRef from . import time_evolution as te @@ -43,10 +56,10 @@ def test_recognize_commuting_terms(): op4 = QubitOperator("X1 Y2", 0.5) + QubitOperator("X2", 1e-10) op5 = QubitOperator("X1 Y2", 0.5) + QubitOperator("X2", 1e-8) op6 = QubitOperator("X2", 1.0) - TimeEvolution(1., op1 + op2 + op3 + op4) | wavefunction - TimeEvolution(1., op1 + op5) | wavefunction - TimeEvolution(1., op1 + op6) | wavefunction - TimeEvolution(1., op1) | wavefunction + TimeEvolution(1.0, op1 + op2 + op3 + op4) | wavefunction + TimeEvolution(1.0, op1 + op5) | wavefunction + TimeEvolution(1.0, op1 + op6) | wavefunction + TimeEvolution(1.0, op1) | wavefunction cmd1 = saving_backend.received_commands[5] cmd2 = saving_backend.received_commands[6] @@ -63,16 +76,14 @@ def test_decompose_commuting_terms(): saving_backend = DummyEngine(save_commands=True) def my_filter(self, cmd): - if (len(cmd.qubits[0]) <= 2 or - isinstance(cmd.gate, ClassicalInstructionGate)): + if len(cmd.qubits[0]) <= 2 or isinstance(cmd.gate, ClassicalInstructionGate): return True return False rules = DecompositionRuleSet([te.rule_commuting_terms]) replacer = AutoReplacer(rules) filter_eng = InstructionFilter(my_filter) - eng = MainEngine(backend=saving_backend, - engine_list=[replacer, filter_eng]) + eng = MainEngine(backend=saving_backend, engine_list=[replacer, filter_eng]) qureg = eng.allocate_qureg(5) with Control(eng, qureg[3]): op1 = QubitOperator("X1 Y2", 0.7) @@ -88,23 +99,29 @@ def my_filter(self, cmd): scaled_op1 = QubitOperator("X0 Y1", 0.7) scaled_op2 = QubitOperator("Y0 X1", -0.8) for cmd in [cmd1, cmd2, cmd3]: - if (cmd.gate == Ph(- 1.5 * 0.6) and - cmd.qubits[0][0].id == qureg[1].id and # 1st qubit of [1,2,4] - cmd.control_qubits[0].id == qureg[3].id): + if ( + cmd.gate == Ph(-1.5 * 0.6) + and cmd.qubits[0][0].id == qureg[1].id + and cmd.control_qubits[0].id == qureg[3].id # 1st qubit of [1,2,4] + ): found[0] = True - elif (isinstance(cmd.gate, TimeEvolution) and - cmd.gate.hamiltonian.isclose(scaled_op1) and - cmd.gate.time == pytest.approx(1.5) and - cmd.qubits[0][0].id == qureg[1].id and - cmd.qubits[0][1].id == qureg[2].id and - cmd.control_qubits[0].id == qureg[3].id): + elif ( + isinstance(cmd.gate, TimeEvolution) + and cmd.gate.hamiltonian.isclose(scaled_op1) + and cmd.gate.time == pytest.approx(1.5) + and cmd.qubits[0][0].id == qureg[1].id + and cmd.qubits[0][1].id == qureg[2].id + and cmd.control_qubits[0].id == qureg[3].id + ): found[1] = True - elif (isinstance(cmd.gate, TimeEvolution) and - cmd.gate.hamiltonian.isclose(scaled_op2) and - cmd.gate.time == pytest.approx(1.5) and - cmd.qubits[0][0].id == qureg[2].id and - cmd.qubits[0][1].id == qureg[4].id and - cmd.control_qubits[0].id == qureg[3].id): + elif ( + isinstance(cmd.gate, TimeEvolution) + and cmd.gate.hamiltonian.isclose(scaled_op2) + and cmd.gate.time == pytest.approx(1.5) + and cmd.qubits[0][0].id == qureg[2].id + and cmd.qubits[0][1].id == qureg[4].id + and cmd.control_qubits[0].id == qureg[3].id + ): found[2] = True assert all(found) @@ -116,9 +133,9 @@ def test_recognize_individual_terms(): op1 = QubitOperator("X1 Y2", 0.5) op2 = QubitOperator("Y2 X4", -0.5) op3 = QubitOperator("X2", 1.0) - TimeEvolution(1., op1 + op2) | wavefunction - TimeEvolution(1., op2) | wavefunction - TimeEvolution(1., op3) | wavefunction + TimeEvolution(1.0, op1 + op2) | wavefunction + TimeEvolution(1.0, op2) | wavefunction + TimeEvolution(1.0, op3) | wavefunction cmd1 = saving_backend.received_commands[5] cmd2 = saving_backend.received_commands[6] @@ -129,19 +146,40 @@ def test_recognize_individual_terms(): assert te.rule_individual_terms.gate_recognizer(cmd3) +def test_decompose_individual_terms_invalid(): + eng = MainEngine(backend=DummyEngine(), engine_list=[]) + qb0 = WeakQubitRef(eng, idx=0) + qb1 = WeakQubitRef(eng, idx=1) + op1 = QubitOperator("X0 Y1", 0.5) + op2 = op1 + QubitOperator("Y2 X4", -0.5) + op3 = QubitOperator((), 0.5) + op4 = QubitOperator("X0 Y0", 0.5) + + with pytest.raises(ValueError): + te._decompose_time_evolution_individual_terms(Command(eng, TimeEvolution(1, op1), ([qb0], [qb1]))) + + with pytest.raises(ValueError): + te._decompose_time_evolution_individual_terms(Command(eng, TimeEvolution(1, op2), ([qb0],))) + + with pytest.raises(ValueError): + te._decompose_time_evolution_individual_terms(Command(eng, TimeEvolution(1, op3), ([qb0],))) + + with pytest.raises(ValueError): + te._decompose_time_evolution_individual_terms(Command(eng, TimeEvolution(1, op4), ([qb0, qb1],))) + + def test_decompose_individual_terms(): saving_eng = DummyEngine(save_commands=True) def my_filter(self, cmd): - if (isinstance(cmd.gate, TimeEvolution)): + if isinstance(cmd.gate, TimeEvolution): return False return True rules = DecompositionRuleSet([te.rule_individual_terms]) replacer = AutoReplacer(rules) filter_eng = InstructionFilter(my_filter) - eng = MainEngine(backend=Simulator(), - engine_list=[replacer, filter_eng, saving_eng]) + eng = MainEngine(backend=Simulator(), engine_list=[replacer, filter_eng, saving_eng]) qureg = eng.allocate_qureg(5) # initialize in random wavefunction by applying some gates: Rx(0.1) | qureg[0] @@ -174,6 +212,7 @@ def my_filter(self, cmd): eng.flush() qbit_to_bit_map5, final_wavefunction5 = copy.deepcopy(eng.backend.cheat()) All(Measure) | qureg + # Check manually: def build_matrix(list_single_matrices): @@ -183,12 +222,11 @@ def build_matrix(list_single_matrices): return res.tocsc() id_sp = sps.identity(2, format="csc", dtype=complex) - x_sp = sps.csc_matrix([[0., 1.], [1., 0.]], dtype=complex) - y_sp = sps.csc_matrix([[0., -1.j], [1.j, 0.]], dtype=complex) - z_sp = sps.csc_matrix([[1., 0.], [0., -1.]], dtype=complex) + x_sp = sps.csc_matrix([[0.0, 1.0], [1.0, 0.0]], dtype=complex) + y_sp = sps.csc_matrix([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) + z_sp = sps.csc_matrix([[1.0, 0.0], [0.0, -1.0]], dtype=complex) - matrix1 = (sps.identity(2**5, format="csc", dtype=complex) * 0.6 * - 1.1 * -1.0j) + matrix1 = sps.identity(2**5, format="csc", dtype=complex) * 0.6 * 1.1 * -1.0j step1 = scipy.sparse.linalg.expm(matrix1).dot(init_wavefunction) assert numpy.allclose(step1, final_wavefunction1) diff --git a/projectq/setups/decompositions/toffoli2cnotandtgate.py b/projectq/setups/decompositions/toffoli2cnotandtgate.py index 38c33a8be..b46c92ba8 100755 --- a/projectq/setups/decompositions/toffoli2cnotandtgate.py +++ b/projectq/setups/decompositions/toffoli2cnotandtgate.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Registers a decomposition rule for the Toffoli gate. @@ -20,41 +19,36 @@ from projectq.cengines import DecompositionRule from projectq.meta import get_control_count -from projectq.ops import NOT, CNOT, T, Tdag, H +from projectq.ops import CNOT, NOT, H, T, Tdag def _decompose_toffoli(cmd): - """ Decompose the Toffoli gate into CNOT, H, T, and Tdagger gates. """ + """Decompose the Toffoli gate into CNOT, H, T, and Tdagger gates.""" ctrl = cmd.control_qubits - eng = cmd.engine target = cmd.qubits[0] - c1 = ctrl[0] - c2 = ctrl[1] H | target - CNOT | (c1, target) - T | c1 + CNOT | (ctrl[0], target) + T | ctrl[0] Tdag | target - CNOT | (c2, target) - CNOT | (c2, c1) - Tdag | c1 + CNOT | (ctrl[1], target) + CNOT | (ctrl[1], ctrl[0]) + Tdag | ctrl[0] T | target - CNOT | (c2, c1) - CNOT | (c1, target) + CNOT | (ctrl[1], ctrl[0]) + CNOT | (ctrl[0], target) Tdag | target - CNOT | (c2, target) + CNOT | (ctrl[1], target) T | target - T | c2 + T | ctrl[1] H | target def _recognize_toffoli(cmd): - """ Recognize the Toffoli gate. """ + """Recognize the Toffoli gate.""" return get_control_count(cmd) == 2 #: Decomposition rules -all_defined_decomposition_rules = [ - DecompositionRule(NOT.__class__, _decompose_toffoli, _recognize_toffoli) -] +all_defined_decomposition_rules = [DecompositionRule(NOT.__class__, _decompose_toffoli, _recognize_toffoli)] diff --git a/projectq/setups/decompositions/uniformlycontrolledr2cnot.py b/projectq/setups/decompositions/uniformlycontrolledr2cnot.py new file mode 100644 index 000000000..e865e939d --- /dev/null +++ b/projectq/setups/decompositions/uniformlycontrolledr2cnot.py @@ -0,0 +1,143 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Register decomposition for UnformlyControlledRy and UnformlyControlledRz.""" + +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Control, CustomUncompute +from projectq.ops import CNOT, Ry, Rz, UniformlyControlledRy, UniformlyControlledRz + + +def _apply_ucr_n( + angles, ucontrol_qubits, target_qubit, eng, gate_class, rightmost_cnot +): # pylint: disable=too-many-arguments + if len(ucontrol_qubits) == 0: + gate = gate_class(angles[0]) + if gate != gate_class(0): + gate | target_qubit + else: + if rightmost_cnot[len(ucontrol_qubits)]: + angles1 = [] + angles2 = [] + for lower_bits in range(2 ** (len(ucontrol_qubits) - 1)): + leading_0 = angles[lower_bits] + leading_1 = angles[lower_bits + 2 ** (len(ucontrol_qubits) - 1)] + angles1.append((leading_0 + leading_1) / 2.0) + angles2.append((leading_0 - leading_1) / 2.0) + else: + angles1 = [] + angles2 = [] + for lower_bits in range(2 ** (len(ucontrol_qubits) - 1)): + leading_0 = angles[lower_bits] + leading_1 = angles[lower_bits + 2 ** (len(ucontrol_qubits) - 1)] + angles1.append((leading_0 - leading_1) / 2.0) + angles2.append((leading_0 + leading_1) / 2.0) + _apply_ucr_n( + angles=angles1, + ucontrol_qubits=ucontrol_qubits[:-1], + target_qubit=target_qubit, + eng=eng, + gate_class=gate_class, + rightmost_cnot=rightmost_cnot, + ) + # Very custom usage of Compute/CustomUncompute in the following. + if rightmost_cnot[len(ucontrol_qubits)]: + with Compute(eng): + CNOT | (ucontrol_qubits[-1], target_qubit) + else: + with CustomUncompute(eng): + CNOT | (ucontrol_qubits[-1], target_qubit) + _apply_ucr_n( + angles=angles2, + ucontrol_qubits=ucontrol_qubits[:-1], + target_qubit=target_qubit, + eng=eng, + gate_class=gate_class, + rightmost_cnot=rightmost_cnot, + ) + # Next iteration on this level do the other cnot placement + rightmost_cnot[len(ucontrol_qubits)] = not rightmost_cnot[len(ucontrol_qubits)] + + +def _decompose_ucr(cmd, gate_class): + """ + Decomposition for an uniformly controlled single qubit rotation gate. + + Follows decomposition in arXiv:quant-ph/0407010 section II and + arXiv:quant-ph/0410066v2 Fig. 9a. + + For Ry and Rz it uses 2**len(ucontrol_qubits) CNOT and also + 2**len(ucontrol_qubits) single qubit rotations. + + Args: + cmd: CommandObject to decompose. + gate_class: Ry or Rz + """ + eng = cmd.engine + with Control(eng, cmd.control_qubits): + if not (len(cmd.qubits) == 2 and len(cmd.qubits[1]) == 1): + raise TypeError("Wrong number of qubits ") + ucontrol_qubits = cmd.qubits[0] + target_qubit = cmd.qubits[1] + if not len(cmd.gate.angles) == 2 ** len(ucontrol_qubits): + raise ValueError("Wrong len(angles).") + if len(ucontrol_qubits) == 0: + gate_class(cmd.gate.angles[0]) | target_qubit + return + angles1 = [] + angles2 = [] + for lower_bits in range(2 ** (len(ucontrol_qubits) - 1)): + leading_0 = cmd.gate.angles[lower_bits] + leading_1 = cmd.gate.angles[lower_bits + 2 ** (len(ucontrol_qubits) - 1)] + angles1.append((leading_0 + leading_1) / 2.0) + angles2.append((leading_0 - leading_1) / 2.0) + rightmost_cnot = {} + for i in range(len(ucontrol_qubits) + 1): + rightmost_cnot[i] = True + _apply_ucr_n( + angles=angles1, + ucontrol_qubits=ucontrol_qubits[:-1], + target_qubit=target_qubit, + eng=eng, + gate_class=gate_class, + rightmost_cnot=rightmost_cnot, + ) + # Very custom usage of Compute/CustomUncompute in the following. + with Compute(cmd.engine): + CNOT | (ucontrol_qubits[-1], target_qubit) + _apply_ucr_n( + angles=angles2, + ucontrol_qubits=ucontrol_qubits[:-1], + target_qubit=target_qubit, + eng=eng, + gate_class=gate_class, + rightmost_cnot=rightmost_cnot, + ) + with CustomUncompute(eng): + CNOT | (ucontrol_qubits[-1], target_qubit) + + +def _decompose_ucry(cmd): + return _decompose_ucr(cmd, gate_class=Ry) + + +def _decompose_ucrz(cmd): + return _decompose_ucr(cmd, gate_class=Rz) + + +#: Decomposition rules +all_defined_decomposition_rules = [ + DecompositionRule(UniformlyControlledRy, _decompose_ucry), + DecompositionRule(UniformlyControlledRz, _decompose_ucrz), +] diff --git a/projectq/setups/decompositions/uniformlycontrolledr2cnot_test.py b/projectq/setups/decompositions/uniformlycontrolledr2cnot_test.py new file mode 100644 index 000000000..08564ecd9 --- /dev/null +++ b/projectq/setups/decompositions/uniformlycontrolledr2cnot_test.py @@ -0,0 +1,152 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setups.decompositions.uniformlycontrolledr2cnot.""" + +import pytest + +import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot +from projectq import MainEngine +from projectq.backends import Simulator +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, +) +from projectq.meta import Compute, Control, Uncompute +from projectq.ops import ( + All, + Measure, + Ry, + Rz, + UniformlyControlledRy, + UniformlyControlledRz, + X, +) + + +def slow_implementation(angles, control_qubits, target_qubit, eng, gate_class): + """ + Assumption is that control_qubits[0] is lowest order bit + We apply angles[0] to state |0> + """ + assert len(angles) == 2 ** len(control_qubits) + for index in range(2 ** len(control_qubits)): + with Compute(eng): + for bit_pos in range(len(control_qubits)): + if not (index >> bit_pos) & 1: + X | control_qubits[bit_pos] + with Control(eng, control_qubits): + gate_class(angles[index]) | target_qubit + Uncompute(eng) + + +def _decomp_gates(eng, cmd): + if isinstance(cmd.gate, UniformlyControlledRy) or isinstance(cmd.gate, UniformlyControlledRz): + return False + return True + + +def test_no_control_qubits(): + rule_set = DecompositionRuleSet(modules=[ucr2cnot]) + eng = MainEngine( + backend=DummyEngine(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(_decomp_gates)], + ) + qb = eng.allocate_qubit() + with pytest.raises(TypeError): + UniformlyControlledRy([0.1]) | qb + + +def test_wrong_number_of_angles(): + rule_set = DecompositionRuleSet(modules=[ucr2cnot]) + eng = MainEngine( + backend=DummyEngine(), + engine_list=[AutoReplacer(rule_set), InstructionFilter(_decomp_gates)], + ) + qb = eng.allocate_qubit() + with pytest.raises(ValueError): + UniformlyControlledRy([0.1, 0.2]) | ([], qb) + + +@pytest.mark.parametrize("gate_classes", [(Ry, UniformlyControlledRy), (Rz, UniformlyControlledRz)]) +@pytest.mark.parametrize("n", [0, 1, 2, 3, 4]) +def test_uniformly_controlled_ry(n, gate_classes): + random_angles = [ + 0.5, + 0.8, + 1.2, + 2.5, + 4.4, + 2.32, + 6.6, + 15.12, + 1, + 2, + 9.54, + 2.1, + 3.1415, + 1.1, + 0.01, + 0.99, + ] + angles = random_angles[: 2**n] + for basis_state_index in range(0, 2 ** (n + 1)): + basis_state = [0] * 2 ** (n + 1) + basis_state[basis_state_index] = 1.0 + correct_dummy_eng = DummyEngine(save_commands=True) + correct_eng = MainEngine(backend=Simulator(), engine_list=[correct_dummy_eng]) + rule_set = DecompositionRuleSet(modules=[ucr2cnot]) + test_dummy_eng = DummyEngine(save_commands=True) + test_eng = MainEngine( + backend=Simulator(), + engine_list=[ + AutoReplacer(rule_set), + InstructionFilter(_decomp_gates), + test_dummy_eng, + ], + ) + test_sim = test_eng.backend + correct_sim = correct_eng.backend + correct_qb = correct_eng.allocate_qubit() + correct_ctrl_qureg = correct_eng.allocate_qureg(n) + correct_eng.flush() + test_qb = test_eng.allocate_qubit() + test_ctrl_qureg = test_eng.allocate_qureg(n) + test_eng.flush() + + correct_sim.set_wavefunction(basis_state, correct_qb + correct_ctrl_qureg) + test_sim.set_wavefunction(basis_state, test_qb + test_ctrl_qureg) + + gate_classes[1](angles) | (test_ctrl_qureg, test_qb) + slow_implementation( + angles=angles, + control_qubits=correct_ctrl_qureg, + target_qubit=correct_qb, + eng=correct_eng, + gate_class=gate_classes[0], + ) + test_eng.flush() + correct_eng.flush() + + for fstate in range(2 ** (n + 1)): + binary_state = format(fstate, f"0{n + 1}b") + test = test_sim.get_amplitude(binary_state, test_qb + test_ctrl_qureg) + correct = correct_sim.get_amplitude(binary_state, correct_qb + correct_ctrl_qureg) + assert correct == pytest.approx(test, rel=1e-10, abs=1e-10) + + All(Measure) | test_qb + test_ctrl_qureg + All(Measure) | correct_qb + correct_ctrl_qureg + test_eng.flush(deallocate_qubits=True) + correct_eng.flush(deallocate_qubits=True) diff --git a/projectq/setups/default.py b/projectq/setups/default.py index c3e9da9db..109f9c10b 100755 --- a/projectq/setups/default.py +++ b/projectq/setups/default.py @@ -13,24 +13,23 @@ # limitations under the License. """ -Defines the default setup which provides an `engine_list` for the `MainEngine` +The default setup which provides an `engine_list` for the `MainEngine`. -It contains `LocalOptimizers` and an `AutoReplacer` which uses most of the -decompositions rules defined in projectq.setups.decompositions +It contains `LocalOptimizers` and an `AutoReplacer` which uses most of the decompositions rules defined in +projectq.setups.decompositions """ import projectq import projectq.setups.decompositions -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - DecompositionRuleSet) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + LocalOptimizer, + TagRemover, +) def get_engine_list(): + """Return the default list of compiler engine.""" rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - return [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - LocalOptimizer(10)] + return [TagRemover(), LocalOptimizer(10), AutoReplacer(rule_set), TagRemover(), LocalOptimizer(10)] diff --git a/projectq/setups/grid.py b/projectq/setups/grid.py index 674462fed..6d58e7924 100644 --- a/projectq/setups/grid.py +++ b/projectq/setups/grid.py @@ -11,71 +11,35 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines a setup to compile to qubits placed in 2-D grid. +A setup to compile to qubits placed in 2-D grid. -It provides the `engine_list` for the `MainEngine`. This engine list contains -an AutoReplacer with most of the gate decompositions of ProjectQ, which are -used to decompose a circuit into only two qubit gates and arbitrary single -qubit gates. ProjectQ's GridMapper is then used to introduce the -necessary Swap operations to route interacting qubits next to each other. -This setup allows to choose the final gate set (with some limitations). +It provides the `engine_list` for the `MainEngine`. This engine list contains an AutoReplacer with most of the gate +decompositions of ProjectQ, which are used to decompose a circuit into only two qubit gates and arbitrary single qubit +gates. ProjectQ's GridMapper is then used to introduce the necessary Swap operations to route interacting qubits next +to each other. This setup allows to choose the final gate set (with some limitations). """ -import inspect - -import projectq -import projectq.libs.math -import projectq.setups.decompositions -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, GridMapper, - LocalOptimizer, TagRemover) -from projectq.ops import (BasicMathGate, ClassicalInstructionGate, CNOT, - ControlledGate, get_inverse, QFT, Swap) - -def high_level_gates(eng, cmd): - """ - Remove any MathGates. - """ - g = cmd.gate - if g == QFT or get_inverse(g) == QFT or g == Swap: - return True - elif isinstance(g, BasicMathGate): - return False - return True +from projectq.cengines import GridMapper +from projectq.ops import CNOT, Swap +from ._utils import get_engine_list_linear_grid_base -def one_and_two_qubit_gates(eng, cmd): - all_qubits = [q for qr in cmd.all_qubits for q in qr] - if isinstance(cmd.gate, ClassicalInstructionGate): - # This is required to allow Measure, Allocate, Deallocate, Flush - return True - elif len(all_qubits) <= 2: - return True - else: - return False - -def get_engine_list(num_rows, num_columns, one_qubit_gates="any", - two_qubit_gates=(CNOT, Swap)): +def get_engine_list(num_rows, num_columns, one_qubit_gates="any", two_qubit_gates=(CNOT, Swap)): """ - Returns an engine list to compile to a 2-D grid of qubits. + Return an engine list to compile to a 2-D grid of qubits. Note: - If you choose a new gate set for which the compiler does not yet have - standard rules, it raises an `NoGateDecompositionError` or a - `RuntimeError: maximum recursion depth exceeded...`. Also note that - even the gate sets which work might not yet be optimized. So make sure - to double check and potentially extend the decomposition rules. - This implemention currently requires that the one qubit gates must - contain Rz and at least one of {Ry(best), Rx, H} and the two qubit gate - must contain CNOT (recommended) or CZ. + If you choose a new gate set for which the compiler does not yet have standard rules, it raises an + `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...`. Also note that even the + gate sets which work might not yet be optimized. So make sure to double check and potentially extend the + decomposition rules. This implementation currently requires that the one qubit gates must contain Rz and at + least one of {Ry(best), Rx, H} and the two qubit gate must contain CNOT (recommended) or CZ. Note: - Classical instructions gates such as e.g. Flush and Measure are - automatically allowed. + Classical instructions gates such as e.g. Flush and Measure are automatically allowed. Example: get_engine_list(num_rows=2, num_columns=3, @@ -85,84 +49,18 @@ def get_engine_list(num_rows, num_columns, one_qubit_gates="any", Args: num_rows(int): Number of rows in the grid num_columns(int): Number of columns in the grid. - one_qubit_gates: "any" allows any one qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. X), it allows all gates - which are equal to it. If the gate is a class (Rz), it - allows all instances of this class. Default is "any" - two_qubit_gates: "any" allows any two qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. CNOT), it allows all gates - which are equal to it. If the gate is a class, it - allows all instances of this class. - Default is (CNOT, Swap). + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. X), it allows all gates which are equal to it. If the gate is + a class (Rz), it allows all instances of this class. Default is "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate + is a class, it allows all instances of this class. Default is (CNOT, Swap). Raises: TypeError: If input is for the gates is not "any" or a tuple. Returns: A list of suitable compiler engines. """ - if two_qubit_gates != "any" and not isinstance(two_qubit_gates, tuple): - raise TypeError("two_qubit_gates parameter must be 'any' or a tuple. " - "When supplying only one gate, make sure to correctly " - "create the tuple (don't miss the comma), " - "e.g. two_qubit_gates=(CNOT,)") - if one_qubit_gates != "any" and not isinstance(one_qubit_gates, tuple): - raise TypeError("one_qubit_gates parameter must be 'any' or a tuple.") - - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - allowed_gate_classes = [] - allowed_gate_instances = [] - if one_qubit_gates != "any": - for gate in one_qubit_gates: - if inspect.isclass(gate): - allowed_gate_classes.append(gate) - else: - allowed_gate_instances.append((gate, 0)) - if two_qubit_gates != "any": - for gate in two_qubit_gates: - if inspect.isclass(gate): - # Controlled gate classes don't yet exists and would require - # separate treatment - assert not isinstance(gate, ControlledGate) - allowed_gate_classes.append(gate) - else: - if isinstance(gate, ControlledGate): - allowed_gate_instances.append((gate._gate, gate._n)) - else: - allowed_gate_instances.append((gate, 0)) - allowed_gate_classes = tuple(allowed_gate_classes) - allowed_gate_instances = tuple(allowed_gate_instances) - - def low_level_gates(eng, cmd): - all_qubits = [q for qr in cmd.all_qubits for q in qr] - assert len(all_qubits) <= 2 - if isinstance(cmd.gate, ClassicalInstructionGate): - # This is required to allow Measure, Allocate, Deallocate, Flush - return True - elif one_qubit_gates == "any" and len(all_qubits) == 1: - return True - elif two_qubit_gates == "any" and len(all_qubits) == 2: - return True - elif isinstance(cmd.gate, allowed_gate_classes): - return True - elif (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: - return True - else: - return False - - return [AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(high_level_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), - GridMapper(num_rows=num_rows, num_columns=num_columns), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(low_level_gates), - LocalOptimizer(5), - ] + return get_engine_list_linear_grid_base( + GridMapper(num_rows=num_rows, num_columns=num_columns), one_qubit_gates, two_qubit_gates + ) diff --git a/projectq/setups/grid_test.py b/projectq/setups/grid_test.py index 13373570c..7760d487f 100644 --- a/projectq/setups/grid_test.py +++ b/projectq/setups/grid_test.py @@ -11,17 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.setups.squaregrid.""" import pytest import projectq +import projectq.setups.grid as grid_setup from projectq.cengines import DummyEngine, GridMapper from projectq.libs.math import AddConstant -from projectq.ops import BasicGate, CNOT, H, Measure, Rx, Rz, Swap, X - -import projectq.setups.grid as grid_setup +from projectq.ops import CNOT, BasicGate, H, Measure, Rx, Rz, Swap, X def test_mapper_present_and_correct_params(): @@ -37,9 +35,7 @@ def test_mapper_present_and_correct_params(): def test_parameter_any(): - engine_list = grid_setup.get_engine_list(num_rows=3, num_columns=2, - one_qubit_gates="any", - two_qubit_gates="any") + engine_list = grid_setup.get_engine_list(num_rows=3, num_columns=2, one_qubit_gates="any", two_qubit_gates="any") backend = DummyEngine(save_commands=True) eng = projectq.MainEngine(backend, engine_list) qubit1 = eng.allocate_qubit() @@ -54,10 +50,12 @@ def test_parameter_any(): def test_restriction(): - engine_list = grid_setup.get_engine_list(num_rows=3, num_columns=2, - one_qubit_gates=(Rz, H), - two_qubit_gates=(CNOT, - AddConstant)) + engine_list = grid_setup.get_engine_list( + num_rows=3, + num_columns=2, + one_qubit_gates=(Rz, H), + two_qubit_gates=(CNOT, AddConstant), + ) backend = DummyEngine(save_commands=True) eng = projectq.MainEngine(backend, engine_list) qubit1 = eng.allocate_qubit() @@ -85,12 +83,6 @@ def test_restriction(): def test_wrong_init(): with pytest.raises(TypeError): - engine_list = grid_setup.get_engine_list(num_rows=3, - num_columns=2, - one_qubit_gates="any", - two_qubit_gates=(CNOT)) + grid_setup.get_engine_list(num_rows=3, num_columns=2, one_qubit_gates="any", two_qubit_gates=(CNOT)) with pytest.raises(TypeError): - engine_list = grid_setup.get_engine_list(num_rows=3, - num_columns=2, - one_qubit_gates="Any", - two_qubit_gates=(CNOT,)) + grid_setup.get_engine_list(num_rows=3, num_columns=2, one_qubit_gates="Any", two_qubit_gates=(CNOT,)) diff --git a/projectq/setups/ibm.py b/projectq/setups/ibm.py index a5fb2c802..122febaa6 100755 --- a/projectq/setups/ibm.py +++ b/projectq/setups/ibm.py @@ -11,46 +11,111 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines a setup useful for the IBM QE chip with 5 qubits. - -It provides the `engine_list` for the `MainEngine`, and contains an -AutoReplacer with most of the gate decompositions of ProjectQ, among others -it includes: +A setup for IBM quantum chips. - * Controlled z-rotations --> Controlled NOTs and single-qubit rotations - * Toffoli gate --> CNOT and single-qubit gates - * m-Controlled global phases --> (m-1)-controlled phase-shifts - * Global phases --> ignore - * (controlled) Swap gates --> CNOTs and Toffolis - * Arbitrary single qubit gates --> Rz and Ry - * Controlled arbitrary single qubit gates --> Rz, Ry, and CNOT gates +Defines a setup allowing to compile code for the IBM quantum chips: +* Any 5 qubit devices +* the ibmq online simulator +* the melbourne 15 qubit device -Moreover, it contains `LocalOptimizers` and a custom mapper for the CNOT -gates. +It provides the `engine_list` for the `MainEngine' based on the requested device. +Decompose the circuit into a Rx/Ry/Rz/H/CNOT gate set that will be translated in the backend in the U1/U2/U3/CX gate +set. """ -import projectq -import projectq.setups.decompositions -from projectq.cengines import (TagRemover, - LocalOptimizer, - AutoReplacer, - IBM5QubitMapper, - SwapAndCNOTFlipper, - DecompositionRuleSet) +from projectq.backends._exceptions import DeviceNotHandledError, DeviceOfflineError +from projectq.backends._ibm._ibm_http_client import show_devices +from projectq.cengines import ( + BasicMapperEngine, + GridMapper, + IBM5QubitMapper, + LocalOptimizer, + SwapAndCNOTFlipper, +) +from projectq.ops import CNOT, Barrier, H, Rx, Ry, Rz +from projectq.setups import restrictedgateset + +def get_engine_list(token=None, device=None): + """Return the default list of compiler engine for the IBM QE platform.""" + # Access to the hardware properties via show_devices + # Can also be extended to take into account gate fidelities, new available + # gate, etc.. + devices = show_devices(token) + ibm_setup = [] + if device not in devices: + raise DeviceOfflineError('Error when configuring engine list: device requested for Backend not connected') + if devices[device]['nq'] == 5: + # The requested device is a 5 qubit processor + # Obtain the coupling map specific to the device + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + mapper = IBM5QubitMapper(coupling_map) + ibm_setup = [mapper, SwapAndCNOTFlipper(coupling_map), LocalOptimizer(10)] + elif device == 'ibmq_qasm_simulator': + # The 32 qubit online simulator doesn't need a specific mapping for + # gates. Can also run wider gateset but this setup keep the + # restrictedgateset setup for coherence + mapper = BasicMapperEngine() + # Note: Manual Mapper doesn't work, because its map is updated only if + # gates are applied if gates in the register are not used, then it + # will lead to state errors + res = {} + for i in range(devices[device]['nq']): + res[i] = i + mapper.current_mapping = res + ibm_setup = [mapper] + elif device == 'ibmq_16_melbourne': + # Only 15 qubits available on this ibmqx2 unit(in particular qubit 7 + # on the grid), therefore need custom grid mapping + grid_to_physical = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 15, + 8: 14, + 9: 13, + 10: 12, + 11: 11, + 12: 10, + 13: 9, + 14: 8, + 15: 7, + } + coupling_map = devices[device]['coupling_map'] + coupling_map = list2set(coupling_map) + ibm_setup = [ + GridMapper(2, 8, grid_to_physical), + LocalOptimizer(5), + SwapAndCNOTFlipper(coupling_map), + LocalOptimizer(5), + ] + else: + # If there is an online device not handled into ProjectQ it's not too + # bad, the engine_list can be constructed manually with the + # appropriate mapper and the 'coupling_map' parameter + raise DeviceNotHandledError('Device not yet fully handled by ProjectQ') -ibmqx4_connections = set([(2, 1), (4, 2), (2, 0), (3, 2), (3, 4), (1, 0)]) + # Most IBM devices accept U1,U2,U3,CX gates. + # Most gates need to be decomposed into a subset that is manually converted + # in the backend (until the implementation of the U1,U2,U3) + # available gates decomposable now for U1,U2,U3: Rx,Ry,Rz and H + setup = restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry, Rz, H), two_qubit_gates=(CNOT,), other_gates=(Barrier,) + ) + setup.extend(ibm_setup) + return setup -def get_engine_list(): - rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - return [TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - IBM5QubitMapper(), - SwapAndCNOTFlipper(ibmqx4_connections), - LocalOptimizer(10)] +def list2set(coupling_list): + """Convert a list() to a set().""" + result = [] + for element in coupling_list: + result.append(tuple(element)) + return set(result) diff --git a/projectq/setups/ibm16.py b/projectq/setups/ibm16.py deleted file mode 100755 index a00551a21..000000000 --- a/projectq/setups/ibm16.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Defines a setup useful for the IBM QE chip with 16 qubits. - -It provides the `engine_list` for the `MainEngine`, and contains an -AutoReplacer with most of the gate decompositions of ProjectQ, among others -it includes: - - * Controlled z-rotations --> Controlled NOTs and single-qubit rotations - * Toffoli gate --> CNOT and single-qubit gates - * m-Controlled global phases --> (m-1)-controlled phase-shifts - * Global phases --> ignore - * (controlled) Swap gates --> CNOTs and Toffolis - * Arbitrary single qubit gates --> Rz and Ry - * Controlled arbitrary single qubit gates --> Rz, Ry, and CNOT gates - -Moreover, it contains `LocalOptimizers`. -""" - -import projectq -import projectq.libs.math -import projectq.setups.decompositions -from projectq.cengines import (AutoReplacer, - DecompositionRuleSet, - GridMapper, - InstructionFilter, - LocalOptimizer, - SwapAndCNOTFlipper, - TagRemover) -from projectq.setups.grid import high_level_gates - - -ibmqx5_connections = set([(1, 0), (1, 2), (2, 3), (3, 4), (3, 14), (5, 4), - (6, 5), (6, 7), (6, 11), (7, 10), (8, 7), (9, 8), - (9, 10), (11, 10), (12, 5), (12, 11), (12, 13), - (13, 4), (13, 14), (15, 0), (15, 2), (15, 14)]) - - -grid_to_physical = {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 0, - 9: 15, 10: 14, 11: 13, 12: 12, 13: 11, 14: 10, 15: 9} - - -def get_engine_list(): - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - return [TagRemover(), - LocalOptimizer(5), - AutoReplacer(rule_set), - InstructionFilter(high_level_gates), - TagRemover(), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - GridMapper(2, 8, grid_to_physical), - LocalOptimizer(5), - SwapAndCNOTFlipper(ibmqx5_connections), - LocalOptimizer(5)] diff --git a/projectq/setups/ibm16_test.py b/projectq/setups/ibm16_test.py deleted file mode 100644 index cae50b168..000000000 --- a/projectq/setups/ibm16_test.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2017 ProjectQ-Framework (www.projectq.ch) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Tests for projectq.setup.ibm16.""" - -import projectq -import projectq.setups.ibm16 -from projectq import MainEngine -from projectq.cengines import GridMapper, SwapAndCNOTFlipper, DummyEngine -from projectq.libs.math import AddConstant -from projectq.ops import QFT, get_inverse - - -def test_mappers_in_cengines(): - found = 0 - for engine in projectq.setups.ibm16.get_engine_list(): - if isinstance(engine, GridMapper): - found |= 1 - if isinstance(engine, SwapAndCNOTFlipper): - found |= 2 - assert found == 3 - - -def test_high_level_gate_set(): - mod_list = projectq.setups.ibm16.get_engine_list() - saving_engine = DummyEngine(save_commands=True) - mod_list = mod_list[:6] + [saving_engine] + mod_list[6:] - eng = MainEngine(DummyEngine(), - engine_list=mod_list) - qureg = eng.allocate_qureg(3) - AddConstant(3) | qureg - QFT | qureg - eng.flush() - received_gates = [cmd.gate for cmd in saving_engine.received_commands] - assert sum([1 for g in received_gates if g == QFT]) == 1 - assert get_inverse(QFT) not in received_gates - assert AddConstant(3) not in received_gates diff --git a/projectq/setups/ibm_test.py b/projectq/setups/ibm_test.py index 598b949cb..1cdef3425 100644 --- a/projectq/setups/ibm_test.py +++ b/projectq/setups/ibm_test.py @@ -13,17 +13,74 @@ # limitations under the License. """Tests for projectq.setup.ibm.""" -import projectq -from projectq import MainEngine -from projectq.cengines import IBM5QubitMapper, SwapAndCNOTFlipper +import pytest -def test_ibm_cnot_mapper_in_cengines(): +def test_ibm_cnot_mapper_in_cengines(monkeypatch): import projectq.setups.ibm - found = 0 - for engine in projectq.setups.ibm.get_engine_list(): - if isinstance(engine, IBM5QubitMapper): - found |= 1 - if isinstance(engine, SwapAndCNOTFlipper): - found |= 2 - assert found == 3 + + def mock_show_devices(*args, **kwargs): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return { + 'ibmq_burlington': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 5, + }, + 'ibmq_16_melbourne': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 15, + }, + 'ibmq_qasm_simulator': { + 'coupling_map': connections, + 'version': '0.0.0', + 'nq': 32, + }, + } + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + engines_5qb = projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + engines_15qb = projectq.setups.ibm.get_engine_list(device='ibmq_16_melbourne') + engines_simulator = projectq.setups.ibm.get_engine_list(device='ibmq_qasm_simulator') + assert len(engines_5qb) == 15 + assert len(engines_15qb) == 16 + assert len(engines_simulator) == 13 + + +def test_ibm_errors(monkeypatch): + import projectq.setups.ibm + + def mock_show_devices(*args, **kwargs): + connections = { + (0, 1), + (1, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (2, 3), + (2, 4), + (3, 1), + (3, 4), + (4, 3), + } + return {'ibmq_imaginary': {'coupling_map': connections, 'version': '0.0.0', 'nq': 6}} + + monkeypatch.setattr(projectq.setups.ibm, "show_devices", mock_show_devices) + with pytest.raises(projectq.setups.ibm.DeviceOfflineError): + projectq.setups.ibm.get_engine_list(device='ibmq_burlington') + with pytest.raises(projectq.setups.ibm.DeviceNotHandledError): + projectq.setups.ibm.get_engine_list(device='ibmq_imaginary') diff --git a/projectq/setups/ionq.py b/projectq/setups/ionq.py new file mode 100644 index 000000000..3c451048c --- /dev/null +++ b/projectq/setups/ionq.py @@ -0,0 +1,74 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A setup for IonQ trapped ion devices. + +Defines a setup allowing to compile code for IonQ trapped ion devices: +->The 11 qubit device +->The 29 qubits simulator +""" +from projectq.backends._exceptions import DeviceOfflineError +from projectq.backends._ionq._ionq_http_client import IonQ +from projectq.backends._ionq._ionq_mapper import BoundedQubitMapper +from projectq.ops import ( + Barrier, + H, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + S, + Sdag, + SqrtX, + Swap, + T, + Tdag, + X, + Y, + Z, +) +from projectq.setups import restrictedgateset + + +def get_engine_list(token=None, device=None): + """Return the default list of compiler engine for the IonQ platform.""" + service = IonQ() + if token is not None: + service.authenticate(token=token) + devices = service.show_devices() + if not device or device not in devices: + raise DeviceOfflineError(f"Error checking engine list: no '{device}' devices available") + + # + # Qubit mapper + # + mapper = BoundedQubitMapper(devices[device]['nq']) + + # + # Basis Gates + # + + # Declare the basis gateset for the IonQ's API. + engine_list = restrictedgateset.get_engine_list( + one_qubit_gates=(X, Y, Z, Rx, Ry, Rz, H, S, Sdag, T, Tdag, SqrtX), + two_qubit_gates=(Swap, Rxx, Ryy, Rzz), + other_gates=(Barrier,), + ) + return engine_list + [mapper] + + +__all__ = ['get_engine_list'] diff --git a/projectq/setups/ionq_test.py b/projectq/setups/ionq_test.py new file mode 100644 index 000000000..2b632bef7 --- /dev/null +++ b/projectq/setups/ionq_test.py @@ -0,0 +1,55 @@ +# Copyright 2021 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for projectq.setup.ionq.""" + +import pytest + +from projectq.backends._exceptions import DeviceOfflineError +from projectq.backends._ionq._ionq_http_client import IonQ +from projectq.backends._ionq._ionq_mapper import BoundedQubitMapper + + +def test_basic_ionq_mapper(monkeypatch): + import projectq.setups.ionq + + def mock_show_devices(*args, **kwargs): + return {'dummy': {'nq': 3, 'target': 'dummy'}} + + monkeypatch.setattr( + IonQ, + 'show_devices', + mock_show_devices, + ) + engine_list = projectq.setups.ionq.get_engine_list(device='dummy') + assert len(engine_list) > 1 + mapper = engine_list[-1] + assert isinstance(mapper, BoundedQubitMapper) + # to match nq in the backend + assert mapper.max_qubits == 3 + + +def test_ionq_errors(monkeypatch): + import projectq.setups.ionq + + def mock_show_devices(*args, **kwargs): + return {'dummy': {'nq': 3, 'target': 'dummy'}} + + monkeypatch.setattr( + IonQ, + 'show_devices', + mock_show_devices, + ) + + with pytest.raises(DeviceOfflineError): + projectq.setups.ionq.get_engine_list(token='TOKEN', device='simulator') diff --git a/projectq/setups/linear.py b/projectq/setups/linear.py index 0f5c1d172..5ee1e0193 100644 --- a/projectq/setups/linear.py +++ b/projectq/setups/linear.py @@ -11,71 +11,34 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -Defines a setup to compile to qubits placed in a linear chain or a circle. +A setup to compile to qubits placed in a linear chain or a circle. -It provides the `engine_list` for the `MainEngine`. This engine list contains -an AutoReplacer with most of the gate decompositions of ProjectQ, which are -used to decompose a circuit into only two qubit gates and arbitrary single -qubit gates. ProjectQ's LinearMapper is then used to introduce the necessary -Swap operations to route interacting qubits next to each other. This setup -allows to choose the final gate set (with some limitations). +It provides the `engine_list` for the `MainEngine`. This engine list contains an AutoReplacer with most of the gate +decompositions of ProjectQ, which are used to decompose a circuit into only two qubit gates and arbitrary single qubit +gates. ProjectQ's LinearMapper is then used to introduce the necessary Swap operations to route interacting qubits +next to each other. This setup allows to choose the final gate set (with some limitations). """ -import inspect - -import projectq -import projectq.libs.math -import projectq.setups.decompositions -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, LinearMapper, LocalOptimizer, - TagRemover) -from projectq.ops import (BasicMathGate, ClassicalInstructionGate, CNOT, - ControlledGate, get_inverse, QFT, Swap) - - -def high_level_gates(eng, cmd): - """ - Remove any MathGates. - """ - g = cmd.gate - if g == QFT or get_inverse(g) == QFT or g == Swap: - return True - elif isinstance(g, BasicMathGate): - return False - return True - +from projectq.cengines import LinearMapper +from projectq.ops import CNOT, Swap -def one_and_two_qubit_gates(eng, cmd): - all_qubits = [q for qr in cmd.all_qubits for q in qr] - if isinstance(cmd.gate, ClassicalInstructionGate): - # This is required to allow Measure, Allocate, Deallocate, Flush - return True - elif len(all_qubits) <= 2: - return True - else: - return False +from ._utils import get_engine_list_linear_grid_base -def get_engine_list(num_qubits, cyclic=False, one_qubit_gates="any", - two_qubit_gates=(CNOT, Swap)): +def get_engine_list(num_qubits, cyclic=False, one_qubit_gates="any", two_qubit_gates=(CNOT, Swap)): """ - Returns an engine list to compile to a linear chain of qubits. + Return an engine list to compile to a linear chain of qubits. Note: - If you choose a new gate set for which the compiler does not yet have - standard rules, it raises an `NoGateDecompositionError` or a - `RuntimeError: maximum recursion depth exceeded...`. Also note that - even the gate sets which work might not yet be optimized. So make sure - to double check and potentially extend the decomposition rules. - This implemention currently requires that the one qubit gates must - contain Rz and at least one of {Ry(best), Rx, H} and the two qubit gate - must contain CNOT (recommended) or CZ. + If you choose a new gate set for which the compiler does not yet have standard rules, it raises an + `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...`. Also note that even the + gate sets which work might not yet be optimized. So make sure to double check and potentially extend the + decomposition rules. This implementation currently requires that the one qubit gates must contain Rz and at + least one of {Ry(best), Rx, H} and the two qubit gate must contain CNOT (recommended) or CZ. Note: - Classical instructions gates such as e.g. Flush and Measure are - automatically allowed. + Classical instructions gates such as e.g. Flush and Measure are automatically allowed. Example: get_engine_list(num_qubits=10, cyclic=False, @@ -85,84 +48,18 @@ def get_engine_list(num_qubits, cyclic=False, one_qubit_gates="any", Args: num_qubits(int): Number of qubits in the chain cyclic(bool): If a circle or not. Default is False - one_qubit_gates: "any" allows any one qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. X), it allows all gates - which are equal to it. If the gate is a class (Rz), it - allows all instances of this class. Default is "any" - two_qubit_gates: "any" allows any two qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. CNOT), it allows all gates - which are equal to it. If the gate is a class, it - allows all instances of this class. - Default is (CNOT, Swap). + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. X), it allows all gates which are equal to it. If the gate is + a class (Rz), it allows all instances of this class. Default is "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate + is a class, it allows all instances of this class. Default is (CNOT, Swap). Raises: TypeError: If input is for the gates is not "any" or a tuple. Returns: A list of suitable compiler engines. """ - if two_qubit_gates != "any" and not isinstance(two_qubit_gates, tuple): - raise TypeError("two_qubit_gates parameter must be 'any' or a tuple. " - "When supplying only one gate, make sure to correctly " - "create the tuple (don't miss the comma), " - "e.g. two_qubit_gates=(CNOT,)") - if one_qubit_gates != "any" and not isinstance(one_qubit_gates, tuple): - raise TypeError("one_qubit_gates parameter must be 'any' or a tuple.") - - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - allowed_gate_classes = [] - allowed_gate_instances = [] - if one_qubit_gates != "any": - for gate in one_qubit_gates: - if inspect.isclass(gate): - allowed_gate_classes.append(gate) - else: - allowed_gate_instances.append((gate, 0)) - if two_qubit_gates != "any": - for gate in two_qubit_gates: - if inspect.isclass(gate): - # Controlled gate classes don't yet exists and would require - # separate treatment - assert not isinstance(gate, ControlledGate) - allowed_gate_classes.append(gate) - else: - if isinstance(gate, ControlledGate): - allowed_gate_instances.append((gate._gate, gate._n)) - else: - allowed_gate_instances.append((gate, 0)) - allowed_gate_classes = tuple(allowed_gate_classes) - allowed_gate_instances = tuple(allowed_gate_instances) - - def low_level_gates(eng, cmd): - all_qubits = [q for qr in cmd.all_qubits for q in qr] - assert len(all_qubits) <= 2 - if isinstance(cmd.gate, ClassicalInstructionGate): - # This is required to allow Measure, Allocate, Deallocate, Flush - return True - elif one_qubit_gates == "any" and len(all_qubits) == 1: - return True - elif two_qubit_gates == "any" and len(all_qubits) == 2: - return True - elif isinstance(cmd.gate, allowed_gate_classes): - return True - elif (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: - return True - else: - return False - - return [AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(high_level_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), - LinearMapper(num_qubits=num_qubits, cyclic=cyclic), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(low_level_gates), - LocalOptimizer(5), - ] + return get_engine_list_linear_grid_base( + LinearMapper(num_qubits=num_qubits, cyclic=cyclic), one_qubit_gates, two_qubit_gates + ) diff --git a/projectq/setups/linear_test.py b/projectq/setups/linear_test.py index ff162fcf5..a5e1ed773 100644 --- a/projectq/setups/linear_test.py +++ b/projectq/setups/linear_test.py @@ -11,17 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.setups.linear.""" import pytest import projectq +import projectq.setups.linear as linear_setup from projectq.cengines import DummyEngine, LinearMapper from projectq.libs.math import AddConstant -from projectq.ops import BasicGate, CNOT, H, Measure, Rx, Rz, Swap, X - -import projectq.setups.linear as linear_setup +from projectq.ops import CNOT, BasicGate, H, Measure, Rx, Rz, Swap, X def test_mapper_present_and_correct_params(): @@ -37,9 +35,9 @@ def test_mapper_present_and_correct_params(): def test_parameter_any(): - engine_list = linear_setup.get_engine_list(num_qubits=10, cyclic=False, - one_qubit_gates="any", - two_qubit_gates="any") + engine_list = linear_setup.get_engine_list( + num_qubits=10, cyclic=False, one_qubit_gates="any", two_qubit_gates="any" + ) backend = DummyEngine(save_commands=True) eng = projectq.MainEngine(backend, engine_list) qubit1 = eng.allocate_qubit() @@ -54,10 +52,12 @@ def test_parameter_any(): def test_restriction(): - engine_list = linear_setup.get_engine_list(num_qubits=10, cyclic=False, - one_qubit_gates=(Rz, H), - two_qubit_gates=(CNOT, - AddConstant)) + engine_list = linear_setup.get_engine_list( + num_qubits=10, + cyclic=False, + one_qubit_gates=(Rz, H), + two_qubit_gates=(CNOT, AddConstant), + ) backend = DummyEngine(save_commands=True) eng = projectq.MainEngine(backend, engine_list) qubit1 = eng.allocate_qubit() @@ -85,10 +85,6 @@ def test_restriction(): def test_wrong_init(): with pytest.raises(TypeError): - engine_list = linear_setup.get_engine_list(num_qubits=10, cyclic=False, - one_qubit_gates="any", - two_qubit_gates=(CNOT)) + linear_setup.get_engine_list(num_qubits=10, cyclic=False, one_qubit_gates="any", two_qubit_gates=(CNOT)) with pytest.raises(TypeError): - engine_list = linear_setup.get_engine_list(num_qubits=10, cyclic=False, - one_qubit_gates="Any", - two_qubit_gates=(CNOT,)) + linear_setup.get_engine_list(num_qubits=10, cyclic=False, one_qubit_gates="Any", two_qubit_gates=(CNOT,)) diff --git a/projectq/setups/restrictedgateset.py b/projectq/setups/restrictedgateset.py index f02268fd7..202bb990d 100644 --- a/projectq/setups/restrictedgateset.py +++ b/projectq/setups/restrictedgateset.py @@ -11,14 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Defines a setup to compile to a restricted gate set. -It provides the `engine_list` for the `MainEngine`. This engine list contains -an AutoReplacer with most of the gate decompositions of ProjectQ, which are -used to decompose a circuit into a restricted gate set (with some limitions -on the choice of gates). +It provides the `engine_list` for the `MainEngine`. This engine list contains an AutoReplacer with most of the gate +decompositions of ProjectQ, which are used to decompose a circuit into a restricted gate set (with some limitations on +the choice of gates). """ import inspect @@ -26,59 +24,41 @@ import projectq import projectq.libs.math import projectq.setups.decompositions -from projectq.cengines import (AutoReplacer, DecompositionRuleSet, - InstructionFilter, LocalOptimizer, - TagRemover) -from projectq.ops import (BasicMathGate, ClassicalInstructionGate, CNOT, - ControlledGate, get_inverse, QFT, Swap) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + LocalOptimizer, + TagRemover, +) +from projectq.ops import CNOT, BasicGate, ClassicalInstructionGate, ControlledGate +from ._utils import high_level_gates, one_and_two_qubit_gates -def high_level_gates(eng, cmd): - """ - Remove any MathGates. - """ - g = cmd.gate - if eng.next_engine.is_available(cmd): - return True - elif g == QFT or get_inverse(g) == QFT or g == Swap: - return True - elif isinstance(g, BasicMathGate): - return False - return True - - -def one_and_two_qubit_gates(eng, cmd): - all_qubits = [q for qr in cmd.all_qubits for q in qr] - if isinstance(cmd.gate, ClassicalInstructionGate): - # This is required to allow Measure, Allocate, Deallocate, Flush - return True - elif eng.next_engine.is_available(cmd): - return True - elif len(all_qubits) <= 2: - return True - else: - return False +def default_chooser(cmd, decomposition_list): # pylint: disable=unused-argument + """Provide the default chooser function for the AutoReplacer compiler engine.""" + return decomposition_list[0] -def get_engine_list(one_qubit_gates="any", - two_qubit_gates=(CNOT,), - other_gates=()): + +def get_engine_list( # pylint: disable=too-many-branches,too-many-statements + one_qubit_gates="any", + two_qubit_gates=(CNOT,), + other_gates=(), + compiler_chooser=default_chooser, +): """ - Returns an engine list to compile to a restricted gate set. + Return an engine list to compile to a restricted gate set. Note: - If you choose a new gate set for which the compiler does not yet have - standard rules, it raises an `NoGateDecompositionError` or a - `RuntimeError: maximum recursion depth exceeded...`. Also note that - even the gate sets which work might not yet be optimized. So make sure - to double check and potentially extend the decomposition rules. - This implemention currently requires that the one qubit gates must - contain Rz and at least one of {Ry(best), Rx, H} and the two qubit gate - must contain CNOT (recommended) or CZ. + If you choose a new gate set for which the compiler does not yet have standard rules, it raises an + `NoGateDecompositionError` or a `RuntimeError: maximum recursion depth exceeded...`. Also note that even the + gate sets which work might not yet be optimized. So make sure to double check and potentially extend the + decomposition rules. This implementation currently requires that the one qubit gates must contain Rz and at + least one of {Ry(best), Rx, H} and the two qubit gate must contain CNOT (recommended) or CZ. Note: - Classical instructions gates such as e.g. Flush and Measure are - automatically allowed. + Classical instructions gates such as e.g. Flush and Measure are automatically allowed. Example: get_engine_list(one_qubit_gates=(Rz, Ry, Rx, H), @@ -86,99 +66,123 @@ def get_engine_list(one_qubit_gates="any", other_gates=(TimeEvolution,)) Args: - one_qubit_gates: "any" allows any one qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. X), it allows all gates - which are equal to it. If the gate is a class (Rz), it - allows all instances of this class. Default is "any" - two_qubit_gates: "any" allows any two qubit gate, otherwise provide - a tuple of the allowed gates. If the gates are - instances of a class (e.g. CNOT), it allows all gates - which are equal to it. If the gate is a class, it - allows all instances of this class. + one_qubit_gates: "any" allows any one qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. X), it allows all gates which are equal to it. If the gate is + a class (Rz), it allows all instances of this class. + Default is "any" + two_qubit_gates: "any" allows any two qubit gate, otherwise provide a tuple of the allowed gates. If the gates + are instances of a class (e.g. CNOT), it allows all gates which are equal to it. If the gate + is a class, it allows all instances of this class. Default is (CNOT,). - other_gates: A tuple of the allowed gates. If the gates are - instances of a class (e.g. QFT), it allows - all gates which are equal to it. If the gate is a - class, it allows all instances of this class. + other_gates: A tuple of the allowed gates. If the gates are instances of a class (e.g. QFT), it allows all + gates which are equal to it. If the gate is a class, it allows all instances of this class. + compiler_chooser:function selecting the decomposition to use in the Autoreplacer engine + Raises: - TypeError: If input is for the gates is not "any" or a tuple. + TypeError: If input is for the gates is not "any" or a tuple. Also if element within tuple is not a class or + instance of BasicGate (e.g. CRz which is a shortcut function) Returns: A list of suitable compiler engines. """ if two_qubit_gates != "any" and not isinstance(two_qubit_gates, tuple): - raise TypeError("two_qubit_gates parameter must be 'any' or a tuple. " - "When supplying only one gate, make sure to correctly " - "create the tuple (don't miss the comma), " - "e.g. two_qubit_gates=(CNOT,)") + raise TypeError( + "two_qubit_gates parameter must be 'any' or a tuple. " + "When supplying only one gate, make sure to correctly " + "create the tuple (don't miss the comma), " + "e.g. two_qubit_gates=(CNOT,)" + ) if one_qubit_gates != "any" and not isinstance(one_qubit_gates, tuple): raise TypeError("one_qubit_gates parameter must be 'any' or a tuple.") if not isinstance(other_gates, tuple): raise TypeError("other_gates parameter must be a tuple.") - rule_set = DecompositionRuleSet(modules=[projectq.libs.math, - projectq.setups.decompositions]) - allowed_gate_classes = [] + rule_set = DecompositionRuleSet(modules=[projectq.libs.math, projectq.setups.decompositions]) + allowed_gate_classes = [] # n-qubit gates allowed_gate_instances = [] + allowed_gate_classes1 = [] # 1-qubit gates + allowed_gate_instances1 = [] + allowed_gate_classes2 = [] # 2-qubit gates + allowed_gate_instances2 = [] + if one_qubit_gates != "any": for gate in one_qubit_gates: if inspect.isclass(gate): - allowed_gate_classes.append(gate) + allowed_gate_classes1.append(gate) + elif isinstance(gate, BasicGate): + allowed_gate_instances1.append(gate) else: - allowed_gate_instances.append((gate, 0)) + raise TypeError("unsupported one_qubit_gates argument") if two_qubit_gates != "any": for gate in two_qubit_gates: if inspect.isclass(gate): # Controlled gate classes don't yet exists and would require # separate treatment - assert not isinstance(gate, ControlledGate) - allowed_gate_classes.append(gate) - else: + if isinstance(gate, ControlledGate): # pragma: no cover + raise RuntimeError('Support for controlled gate not implemented!') + allowed_gate_classes2.append(gate) + elif isinstance(gate, BasicGate): if isinstance(gate, ControlledGate): - allowed_gate_instances.append((gate._gate, gate._n)) + allowed_gate_instances2.append((gate._gate, gate._n)) # pylint: disable=protected-access else: - allowed_gate_instances.append((gate, 0)) + allowed_gate_instances2.append((gate, 0)) + else: + raise TypeError("unsupported two_qubit_gates argument") for gate in other_gates: if inspect.isclass(gate): # Controlled gate classes don't yet exists and would require # separate treatment - assert not isinstance(gate, ControlledGate) + if isinstance(gate, ControlledGate): # pragma: no cover + raise RuntimeError('Support for controlled gate not implemented!') allowed_gate_classes.append(gate) - else: + elif isinstance(gate, BasicGate): if isinstance(gate, ControlledGate): - allowed_gate_instances.append((gate._gate, gate._n)) + allowed_gate_instances.append((gate._gate, gate._n)) # pylint: disable=protected-access else: allowed_gate_instances.append((gate, 0)) + else: + raise TypeError("unsupported other_gates argument") allowed_gate_classes = tuple(allowed_gate_classes) allowed_gate_instances = tuple(allowed_gate_instances) + allowed_gate_classes1 = tuple(allowed_gate_classes1) + allowed_gate_instances1 = tuple(allowed_gate_instances1) + allowed_gate_classes2 = tuple(allowed_gate_classes2) + allowed_gate_instances2 = tuple(allowed_gate_instances2) - def low_level_gates(eng, cmd): + def low_level_gates(eng, cmd): # pylint: disable=unused-argument,too-many-return-statements all_qubits = [q for qr in cmd.all_qubits for q in qr] if isinstance(cmd.gate, ClassicalInstructionGate): # This is required to allow Measure, Allocate, Deallocate, Flush return True - elif one_qubit_gates == "any" and len(all_qubits) == 1: + if one_qubit_gates == "any" and len(all_qubits) == 1: return True - elif two_qubit_gates == "any" and len(all_qubits) == 2: + if two_qubit_gates == "any" and len(all_qubits) == 2: return True - elif isinstance(cmd.gate, allowed_gate_classes): + if isinstance(cmd.gate, allowed_gate_classes): return True - elif (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: + if (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances: return True - else: - return False - - return [AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(high_level_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), - AutoReplacer(rule_set), - TagRemover(), - InstructionFilter(low_level_gates), - LocalOptimizer(5), - ] + if isinstance(cmd.gate, allowed_gate_classes1) and len(all_qubits) == 1: + return True + if isinstance(cmd.gate, allowed_gate_classes2) and len(all_qubits) == 2: + return True + if cmd.gate in allowed_gate_instances1 and len(all_qubits) == 1: + return True + if (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances2 and len(all_qubits) == 2: + return True + return False + + return [ + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(high_level_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(one_and_two_qubit_gates), + LocalOptimizer(5), + AutoReplacer(rule_set, compiler_chooser), + TagRemover(), + InstructionFilter(low_level_gates), + LocalOptimizer(5), + ] diff --git a/projectq/setups/restrictedgateset_test.py b/projectq/setups/restrictedgateset_test.py index ebe767c1e..92144bde7 100644 --- a/projectq/setups/restrictedgateset_test.py +++ b/projectq/setups/restrictedgateset_test.py @@ -11,24 +11,34 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.setups.restrictedgateset.""" import pytest import projectq -from projectq.cengines import DummyEngine -from projectq.libs.math import (AddConstant, AddConstantModN, - MultiplyByConstantModN) -from projectq.ops import (BasicGate, CNOT, H, Measure, QFT, QubitOperator, Rx, - Rz, Swap, TimeEvolution, Toffoli, X) - import projectq.setups.restrictedgateset as restrictedgateset +from projectq.cengines import DummyEngine +from projectq.libs.math import AddConstant, AddConstantModN, MultiplyByConstantModN +from projectq.meta import Control +from projectq.ops import ( + CNOT, + QFT, + BasicGate, + CRz, + H, + Measure, + QubitOperator, + Rx, + Rz, + Swap, + TimeEvolution, + Toffoli, + X, +) def test_parameter_any(): - engine_list = restrictedgateset.get_engine_list(one_qubit_gates="any", - two_qubit_gates="any") + engine_list = restrictedgateset.get_engine_list(one_qubit_gates="any", two_qubit_gates="any") backend = DummyEngine(save_commands=True) eng = projectq.MainEngine(backend, engine_list) qubit1 = eng.allocate_qubit() @@ -46,19 +56,21 @@ def test_restriction(): engine_list = restrictedgateset.get_engine_list( one_qubit_gates=(Rz, H), two_qubit_gates=(CNOT, AddConstant, Swap), - other_gates=(Toffoli, AddConstantModN, MultiplyByConstantModN(2, 8))) + other_gates=(Toffoli, AddConstantModN, MultiplyByConstantModN(2, 8)), + ) backend = DummyEngine(save_commands=True) - eng = projectq.MainEngine(backend, engine_list) + eng = projectq.MainEngine(backend, engine_list, verbose=True) qubit1 = eng.allocate_qubit() qubit2 = eng.allocate_qubit() qubit3 = eng.allocate_qubit() eng.flush() CNOT | (qubit1, qubit2) H | qubit1 - Rz(0.2) | qubit1 + with Control(eng, qubit2): + Rz(0.2) | qubit1 Measure | qubit1 - AddConstant(1) | qubit1 + qubit2 - AddConstantModN(1, 9) | qubit1 + qubit2 + qubit3 + AddConstant(1) | (qubit1 + qubit2) + AddConstantModN(1, 9) | (qubit1 + qubit2 + qubit3) Toffoli | (qubit1 + qubit2, qubit3) Swap | (qubit1, qubit2) MultiplyByConstantModN(2, 8) | qubit1 + qubit2 + qubit3 @@ -70,15 +82,15 @@ def test_restriction(): assert backend.received_commands[4].gate == X assert len(backend.received_commands[4].control_qubits) == 1 assert backend.received_commands[5].gate == H - assert backend.received_commands[6].gate == Rz(0.2) - assert backend.received_commands[7].gate == Measure - assert backend.received_commands[8].gate == AddConstant(1) - assert backend.received_commands[9].gate == AddConstantModN(1, 9) - assert backend.received_commands[10].gate == X - assert len(backend.received_commands[10].control_qubits) == 2 - assert backend.received_commands[11].gate == Swap - assert backend.received_commands[12].gate == MultiplyByConstantModN(2, 8) - for cmd in backend.received_commands[13:]: + assert backend.received_commands[6].gate == Rz(0.1) + assert backend.received_commands[10].gate == Measure + assert backend.received_commands[11].gate == AddConstant(1) + assert backend.received_commands[12].gate == AddConstantModN(1, 9) + assert backend.received_commands[13].gate == X + assert len(backend.received_commands[13].control_qubits) == 2 + assert backend.received_commands[14].gate == Swap + assert backend.received_commands[15].gate == MultiplyByConstantModN(2, 8) + for cmd in backend.received_commands[16:]: assert cmd.gate != QFT assert not isinstance(cmd.gate, Rx) assert not isinstance(cmd.gate, MultiplyByConstantModN) @@ -87,8 +99,14 @@ def test_restriction(): def test_wrong_init(): with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) + restrictedgateset.get_engine_list(two_qubit_gates=(CNOT)) + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(one_qubit_gates="Any") + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(other_gates="any") + with pytest.raises(TypeError): + restrictedgateset.get_engine_list(one_qubit_gates=(CRz,)) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(one_qubit_gates="Any") + restrictedgateset.get_engine_list(two_qubit_gates=(CRz,)) with pytest.raises(TypeError): - engine_list = restrictedgateset.get_engine_list(other_gates="any") + restrictedgateset.get_engine_list(other_gates=(CRz,)) diff --git a/projectq/setups/trapped_ion_decomposer.py b/projectq/setups/trapped_ion_decomposer.py new file mode 100644 index 000000000..320c8a6ad --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer.py @@ -0,0 +1,139 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Module uses ideas from "Basic circuit compilation techniques +# for an ion-trap quantum machine" by Dmitri Maslov (2017) at +# https://iopscience.iop.org/article/10.1088/1367-2630/aa5e47 +""" +Apply the restricted gate set setup for trapped ion based quantum computers. + +It provides the `engine_list` for the `MainEngine`, restricting the gate set to Rx and Ry single qubit gates and the +Rxx two qubit gates. + +A decomposition chooser is implemented following the ideas in QUOTE for reducing the number of Ry gates in the new +circuit. + +Note: + Because the decomposition chooser is only called when a gate has to be decomposed, this reduction will work better + when the entire circuit has to be decomposed. Otherwise, If the circuit has both superconding gates and native ion + trapped gates the decomposed circuit will not be optimal. +""" + +from projectq.ops import Rx, Rxx, Ry +from projectq.setups import restrictedgateset + +# ------------------chooser_Ry_reducer-------------------# +# If the qubit is not in the prev_Ry_sign dictionary, then no decomposition occurred +# If the value is: +# -1 then the last gate applied (during a decomposition!) was Ry(-math.pi/2) +# 1 then the last gate applied (during a decomposition!) was Ry(+math.pi/2) +# 0 then the last gate applied (during a decomposition!) was a Rx + +prev_Ry_sign = {} # Keeps track of most recent Ry sign, i.e. +# whether we had Ry(-pi/2) or Ry(pi/2) +# prev_Ry_sign[qubit_index] should hold -1 or +# +1 + + +def chooser_Ry_reducer(cmd, decomposition_list): # pylint: disable=invalid-name, too-many-return-statements + """ + Choose the decomposition to maximise Ry cancellations. + + Choose the decomposition so as to maximise Ry cancellations, based on the previous decomposition used for the + given qubit. + + Note: + Classical instructions gates e.g. Flush and Measure are automatically + allowed. + + Returns: + A decomposition object from the decomposition_list. + """ + decomp_rule = {} + name = 'default' + + for decomp in decomposition_list: + try: + # NB: need to (possibly) raise an exception before setting the + # name variable below + decomposition = decomp.decompose.__name__.split('_') + decomp_rule[decomposition[3]] = decomp + name = decomposition[2] + # 'M' stands for minus, 'P' stands for plus 'N' stands for neutral + # e.g. decomp_rule['M'] will give you the decomposition_rule that + # ends with a Ry(-pi/2) + except IndexError: + pass + + local_prev_Ry_sign = prev_Ry_sign.setdefault(cmd.engine, {}) # pylint: disable=invalid-name + + if name == 'cnot2rxx': + ctrl_id = cmd.control_qubits[0].id + + if local_prev_Ry_sign.get(ctrl_id, -1) <= 0: + # If the previous qubit had Ry(-pi/2) choose the decomposition + # that starts with Ry(pi/2) + local_prev_Ry_sign[ctrl_id] = -1 + # Now the prev_Ry_sign is set to -1 since at the end of the + # decomposition we will have a Ry(-pi/2) + return decomp_rule['M'] + + # Previous qubit had Ry(pi/2) choose decomposition that starts + # with Ry(-pi/2) and ends with R(pi/2) + local_prev_Ry_sign[ctrl_id] = 1 + return decomp_rule['P'] + + if name == 'h2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, 0) == 0: + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 0 + return decomp_rule['N'] + + if name == 'rz2rx': + qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] + qubit_id = qubit_id[0] + + if local_prev_Ry_sign.get(qubit_id, -1) <= 0: + local_prev_Ry_sign[qubit_id] = -1 + return decomp_rule['M'] + + local_prev_Ry_sign[qubit_id] = 1 + return decomp_rule['P'] + + # No decomposition chosen, so use the first decomposition in the list like the default function + return decomposition_list[0] + + +def get_engine_list(): + """ + Return an engine list compiling code into a trapped ion based compiled circuit code. + + Note: + - Classical instructions gates such as e.g. Flush and Measure are automatically allowed. + - The restricted gate set engine does not work with Rxx gates, as ProjectQ will by default bounce back and + forth between Cz gates and Cx gates. An appropriate decomposition chooser needs to be used! + + Returns: + A list of suitable compiler engines. + """ + return restrictedgateset.get_engine_list( + one_qubit_gates=(Rx, Ry), + two_qubit_gates=(Rxx,), + compiler_chooser=chooser_Ry_reducer, + ) diff --git a/projectq/setups/trapped_ion_decomposer_test.py b/projectq/setups/trapped_ion_decomposer_test.py new file mode 100644 index 000000000..8670ef1e9 --- /dev/null +++ b/projectq/setups/trapped_ion_decomposer_test.py @@ -0,0 +1,151 @@ +# Copyright 2018 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"Tests for projectq.setups.trapped_ion_decomposer.py." + +import projectq +from projectq.cengines import ( + AutoReplacer, + DecompositionRule, + DecompositionRuleSet, + DummyEngine, + InstructionFilter, + MainEngine, + TagRemover, +) +from projectq.meta import get_control_count +from projectq.ops import CNOT, ClassicalInstructionGate, H, Measure, Rx, Rxx, Ry, Rz, X + +from . import restrictedgateset +from .trapped_ion_decomposer import chooser_Ry_reducer, get_engine_list + + +def filter_gates(eng, cmd): + if isinstance(cmd.gate, ClassicalInstructionGate): + return True + if (cmd.gate == X and get_control_count(cmd) == 1) or cmd.gate == H or isinstance(cmd.gate, Rz): + return False + return True + + +def test_chooser_Ry_reducer_synthetic(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet(modules=[projectq.libs.math, projectq.setups.decompositions]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + CNOT | (control, target) + CNOT | (control, target) + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + CNOT | (control, target) + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert backend.received_commands[idx0].gate.get_inverse() == backend.received_commands[idx1].gate + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + H | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + H | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert backend.received_commands[idx0].gate.get_inverse() == backend.received_commands[idx1].gate + + eng = MainEngine(backend=backend, engine_list=engine_list) + control = eng.allocate_qubit() + target = eng.allocate_qubit() + Rz(1.23456) | target + eng.flush() + idx0 = len(backend.received_commands) - 2 + idx1 = len(backend.received_commands) + Rz(1.23456) | target + eng.flush() + + assert isinstance(backend.received_commands[idx0].gate, Ry) + assert isinstance(backend.received_commands[idx1].gate, Ry) + assert backend.received_commands[idx0].gate.get_inverse() == backend.received_commands[idx1].gate + + +def _dummy_h2nothing_A(cmd): + qubit = cmd.qubits[0] + Ry(1.23456) | qubit + + +def test_chooser_Ry_reducer_unsupported_gate(): + backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet(rules=[DecompositionRule(H.__class__, _dummy_h2nothing_A)]) + + engine_list = [ + AutoReplacer(rule_set, chooser_Ry_reducer), + TagRemover(), + InstructionFilter(filter_gates), + ] + + eng = MainEngine(backend=backend, engine_list=engine_list) + qubit = eng.allocate_qubit() + H | qubit + eng.flush() + + for cmd in backend.received_commands: + print(cmd) + + assert isinstance(backend.received_commands[1].gate, Ry) + + +def test_chooser_Ry_reducer(): + # Without the chooser_Ry_reducer function, i.e. if the restricted gate set + # just picked the first option in each decomposition list, the circuit + # below would be decomposed into 8 single qubit gates and 1 two qubit + # gate. + # + # Including the Allocate, Measure and Flush commands, this would result in + # 13 commands. + # + # Using the chooser_Rx_reducer you get 10 commands, since you now have 4 + # single qubit gates and 1 two qubit gate. + + for engine_list, count in [ + ( + restrictedgateset.get_engine_list(one_qubit_gates=(Rx, Ry), two_qubit_gates=(Rxx,)), + 13, + ), + (get_engine_list(), 11), + ]: + backend = DummyEngine(save_commands=True) + eng = projectq.MainEngine(backend, engine_list, verbose=True) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + H | qubit1 + CNOT | (qubit1, qubit2) + Rz(0.2) | qubit1 + Measure | qubit1 + eng.flush() + + assert len(backend.received_commands) == count diff --git a/projectq/tests/__init__.py b/projectq/tests/__init__.py index ee1451dcd..7d98bf34e 100755 --- a/projectq/tests/__init__.py +++ b/projectq/tests/__init__.py @@ -11,3 +11,5 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +"""ProjectQ testing module.""" diff --git a/projectq/tests/_factoring_test.py b/projectq/tests/_factoring_test.py index 7e9135911..541c1492c 100755 --- a/projectq/tests/_factoring_test.py +++ b/projectq/tests/_factoring_test.py @@ -17,19 +17,19 @@ import projectq.libs.math import projectq.setups.decompositions from projectq.backends._sim._simulator_test import sim -from projectq.cengines import (MainEngine, - AutoReplacer, - DecompositionRuleSet, - InstructionFilter, - LocalOptimizer, - TagRemover) +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + LocalOptimizer, + MainEngine, + TagRemover, +) from projectq.libs.math import MultiplyByConstantModN from projectq.meta import Control -from projectq.ops import (All, BasicMathGate, get_inverse, H, Measure, QFT, - Swap, X) +from projectq.ops import QFT, All, BasicMathGate, H, Measure, Swap, X, get_inverse -rule_set = DecompositionRuleSet(modules=(projectq.libs.math, - projectq.setups.decompositions)) +rule_set = DecompositionRuleSet(modules=(projectq.libs.math, projectq.setups.decompositions)) assert sim # Asserts to tools that the fixture import is used. @@ -44,13 +44,15 @@ def high_level_gates(eng, cmd): def get_main_engine(sim): - engine_list = [AutoReplacer(rule_set), - InstructionFilter(high_level_gates), - TagRemover(), - LocalOptimizer(3), - AutoReplacer(rule_set), - TagRemover(), - LocalOptimizer(3)] + engine_list = [ + AutoReplacer(rule_set), + InstructionFilter(high_level_gates), + TagRemover(), + LocalOptimizer(3), + AutoReplacer(rule_set), + TagRemover(), + LocalOptimizer(3), + ] return MainEngine(sim, engine_list) @@ -76,7 +78,7 @@ def test_factoring(sim): vec = cheat_tpl[1] for i in range(len(vec)): - if abs(vec[i]) > 1.e-8: + if abs(vec[i]) > 1.0e-8: assert ((i >> idx) & 1) == 0 Measure | ctrl_qubit @@ -93,13 +95,13 @@ def test_factoring(sim): idx = cheat_tpl[0][ctrl_qubit[0].id] vec = cheat_tpl[1] - probability = 0. + probability = 0.0 for i in range(len(vec)): - if abs(vec[i]) > 1.e-8: + if abs(vec[i]) > 1.0e-8: if ((i >> idx) & 1) == 0: - probability += abs(vec[i])**2 + probability += abs(vec[i]) ** 2 - assert probability == pytest.approx(.5) + assert probability == pytest.approx(0.5) Measure | ctrl_qubit All(Measure) | x diff --git a/projectq/types/__init__.py b/projectq/types/__init__.py index 1abaea68b..dd1bc28b5 100755 --- a/projectq/types/__init__.py +++ b/projectq/types/__init__.py @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module containing all basic types.""" + from ._qubit import BasicQubit, Qubit, Qureg, WeakQubitRef diff --git a/projectq/types/_qubit.py b/projectq/types/_qubit.py index 19a98b716..39631410b 100755 --- a/projectq/types/_qubit.py +++ b/projectq/types/_qubit.py @@ -11,36 +11,34 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ -This file defines BasicQubit, Qubit, WeakQubit and Qureg. +Definition of BasicQubit, Qubit, WeakQubit and Qureg classes. A Qureg represents a list of Qubit or WeakQubit objects. -Qubit represents a (logical-level) qubit with a unique index provided by the -MainEngine. Qubit objects are automatically deallocated if they go out of -scope and intented to be used within Qureg objects in user code. +A Qubit represents a (logical-level) qubit with a unique index provided by the MainEngine. Qubit objects are +automatically deallocated if they go out of scope and intended to be used within Qureg objects in user code. Example: .. code-block:: python from projectq import MainEngine + eng = MainEngine() qubit = eng.allocate_qubit() -qubit is a Qureg of size 1 with one Qubit object which is deallocated once -qubit goes out of scope. +qubit is a Qureg of size 1 with one Qubit object which is deallocated once qubit goes out of scope. -WeakQubit are used inside the Command object and are not automatically -deallocated. +WeakQubit are used inside the Command object and are not automatically deallocated. """ -class BasicQubit(object): +class BasicQubit: """ BasicQubit objects represent qubits. They have an id and a reference to the owning engine. """ + def __init__(self, engine, idx): """ Initialize a BasicQubit object. @@ -53,29 +51,15 @@ def __init__(self, engine, idx): self.engine = engine def __str__(self): - """ - Return string representation of this qubit. - """ + """Return string representation of this qubit.""" return str(self.id) def __bool__(self): - """ - Access the result of a previous measurement and return False / True - (0 / 1) - """ + """Access the result of a previous measurement and return False / True (0 / 1).""" return self.engine.main_engine.get_measurement_result(self) - def __nonzero__(self): - """ - Access the result of a previous measurement for Python 2.7. - """ - return self.__bool__() - def __int__(self): - """ - Access the result of a previous measurement and return as integer - (0 / 1). - """ + """Access the result of a previous measurement and return as integer (0 / 1).""" return int(bool(self)) def __eq__(self, other): @@ -87,19 +71,13 @@ def __eq__(self, other): """ if self.id == -1: return self is other - return (isinstance(other, BasicQubit) and - self.id == other.id and - self.engine == other.engine) - - def __ne__(self, other): - return not self.__eq__(other) + return isinstance(other, BasicQubit) and self.id == other.id and self.engine == other.engine def __hash__(self): """ Return the hash of this qubit. - Hash definition because of custom __eq__. - Enables storing a qubit in, e.g., a set. + Hash definition because of custom __eq__. Enables storing a qubit in, e.g., a set. """ if self.id == -1: return object.__hash__(self) @@ -110,24 +88,19 @@ class Qubit(BasicQubit): """ Qubit class. - Represents a (logical-level) qubit with a unique index provided by the - MainEngine. Once the qubit goes out of scope (and is garbage-collected), - it deallocates itself automatically, allowing automatic resource - management. + Represents a (logical-level) qubit with a unique index provided by the MainEngine. Once the qubit goes out of scope + (and is garbage-collected), it deallocates itself automatically, allowing automatic resource management. - Thus the qubit is not copyable; only returns a reference to the same - object. + Thus the qubit is not copyable; only returns a reference to the same object. """ + def __del__(self): - """ - Destroy the qubit and deallocate it (automatically). - """ + """Destroy the qubit and deallocate it (automatically).""" if self.id == -1: return - # If a user directly calls this function, then the qubit gets id == -1 - # but stays in active_qubits as it is not yet deleted, hence remove - # it manually (if the garbage collector calls this function, then the - # WeakRef in active qubits is already gone): + # If a user directly calls this function, then the qubit gets id == -1 but stays in active_qubits as it is not + # yet deleted, hence remove it manually (if the garbage collector calls this function, then the WeakRef in + # active qubits is already gone): if self in self.engine.main_engine.active_qubits: self.engine.main_engine.active_qubits.remove(self) weak_copy = WeakQubitRef(self.engine, self.id) @@ -139,8 +112,7 @@ def __copy__(self): Non-copyable (returns reference to self). Note: - To prevent problems with automatic deallocation, qubits are not - copyable! + To prevent problems with automatic deallocation, qubits are not copyable! """ return self @@ -149,75 +121,60 @@ def __deepcopy__(self, memo): Non-deepcopyable (returns reference to self). Note: - To prevent problems with automatic deallocation, qubits are not - deepcopyable! + To prevent problems with automatic deallocation, qubits are not deepcopyable! """ return self -class WeakQubitRef(BasicQubit): +class WeakQubitRef(BasicQubit): # pylint: disable=too-few-public-methods """ WeakQubitRef objects are used inside the Command object. - Qubits feature automatic deallocation when destroyed. WeakQubitRefs, on - the other hand, do not share this feature, allowing to copy them and pass - them along the compiler pipeline, while the actual qubit objects may be - garbage-collected (and, thus, cleaned up early). Otherwise there is no - difference between a WeakQubitRef and a Qubit object. + Qubits feature automatic deallocation when destroyed. WeakQubitRefs, on the other hand, do not share this feature, + allowing to copy them and pass them along the compiler pipeline, while the actual qubit objects may be + garbage-collected (and, thus, cleaned up early). Otherwise there is no difference between a WeakQubitRef and a Qubit + object. """ - pass class Qureg(list): """ Quantum register class. - Simplifies accessing measured values for single-qubit registers (no []- - access necessary) and enables pretty-printing of general quantum registers - (call Qureg.__str__(qureg)). + Simplifies accessing measured values for single-qubit registers (no []- access necessary) and enables + pretty-printing of general quantum registers (call Qureg.__str__(qureg)). """ + def __bool__(self): """ Return measured value if Qureg consists of 1 qubit only. Raises: - Exception if more than 1 qubit resides in this register (then you - need to specify which value to get using qureg[???]) + Exception if more than 1 qubit resides in this register (then you need to specify which value to get using + qureg[???]) """ if len(self) == 1: return bool(self[0]) - else: - raise Exception("__bool__(qureg): Quantum register contains more " - "than 1 qubit. Use __bool__(qureg[idx]) instead.") + raise Exception( + "__bool__(qureg): Quantum register contains more than 1 qubit. Use __bool__(qureg[idx]) instead." + ) def __int__(self): """ Return measured value if Qureg consists of 1 qubit only. Raises: - Exception if more than 1 qubit resides in this register (then you - need to specify which value to get using qureg[???]) + Exception if more than 1 qubit resides in this register (then you need to specify which value to get using + qureg[???]) """ if len(self) == 1: return int(self[0]) - else: - raise Exception("__int__(qureg): Quantum register contains more " - "than 1 qubit. Use __bool__(qureg[idx]) instead.") - - def __nonzero__(self): - """ - Return measured value if Qureg consists of 1 qubit only for Python 2.7. - - Raises: - Exception if more than 1 qubit resides in this register (then you - need to specify which value to get using qureg[???]) - """ - return int(self) != 0 + raise Exception( + "__int__(qureg): Quantum register contains more than 1 qubit. Use __bool__(qureg[idx]) instead." + ) def __str__(self): - """ - Get string representation of a quantum register. - """ + """Get string representation of a quantum register.""" if len(self) == 0: return "Qureg[]" @@ -232,25 +189,19 @@ def __str__(self): count += 1 continue - out_list.append('{}-{}'.format(start_id, start_id + count - 1) - if count > 1 - else '{}'.format(start_id)) + out_list.append(f'{start_id}-{start_id + count - 1}' if count > 1 else f'{start_id}') start_id = qubit_id count = 1 - return "Qureg[{}]".format(', '.join(out_list)) + return f"Qureg[{', '.join(out_list)}]" @property def engine(self): - """ - Return owning engine. - """ + """Return owning engine.""" return self[0].engine @engine.setter def engine(self, eng): - """ - Set owning engine. - """ + """Set owning engine.""" for qb in self: qb.engine = eng diff --git a/projectq/types/_qubit_test.py b/projectq/types/_qubit_test.py index 7de66c9c0..fb5fb394b 100755 --- a/projectq/types/_qubit_test.py +++ b/projectq/types/_qubit_test.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for projectq.types._qubits.""" from copy import copy, deepcopy @@ -45,9 +44,7 @@ def test_basic_qubit_measurement(): assert int(qubit1) == 1 # Testing functions for python 2 and python 3 assert not qubit0.__bool__() - assert not qubit0.__nonzero__() assert qubit1.__bool__() - assert qubit1.__nonzero__() @pytest.mark.parametrize("id0, id1, expected", [(0, 0, True), (0, 1, False)]) @@ -75,8 +72,7 @@ def test_basic_qubit_hash(): assert a == c and hash(a) == hash(c) # For performance reasons, low ids should not collide. - assert len(set(hash(_qubit.BasicQubit(fake_engine, e)) - for e in range(100))) == 100 + assert len({hash(_qubit.BasicQubit(fake_engine, e)) for e in range(100)}) == 100 # Important that weakref.WeakSet in projectq.cengines._main.py works. # When id is -1, expect reference equality. @@ -88,7 +84,7 @@ def test_basic_qubit_hash(): @pytest.fixture def mock_main_engine(): - class MockMainEngine(object): + class MockMainEngine: def __init__(self): self.num_calls = 0 self.active_qubits = set() @@ -165,9 +161,7 @@ def test_qureg_measure_if_qubit(): assert int(qureg1) == 1 # Testing functions for python 2 and python 3 assert not qureg0.__bool__() - assert not qureg0.__nonzero__() assert qureg1.__bool__() - assert qureg1.__nonzero__() def test_qureg_measure_exception(): @@ -178,8 +172,6 @@ def test_qureg_measure_exception(): qureg.append(qubit) with pytest.raises(Exception): qureg.__bool__() - with pytest.raises(Exception): - qureg.__nonzero__() with pytest.raises(Exception): qureg.__int__() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..6e4686e23 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,207 @@ +[build-system] +requires = [ + 'setuptools>=61', + 'wheel', + 'pybind11>=2', + 'setuptools_scm[toml]>6' +] +build-backend = "setuptools.build_meta" + +[project] +name = 'projectq' +authors = [ + {name = 'ProjectQ', email = 'info@projectq.ch'} +] +description = 'ProjectQ - An open source software framework for quantum computing' +requires-python = '>= 3.8' +license = {text= 'Apache License Version 2.0'} +readme = 'README.rst' +classifiers = [ + 'License :: OSI Approved :: Apache Software License', + 'Topic :: Software Development :: Libraries :: Python Modules', + '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' +] +dynamic = ["version"] + +dependencies = [ + 'matplotlib >= 2.2.3', + 'networkx >= 2.4', + 'numpy>=1.21.5', + 'requests>=2.25.1', + 'scipy>=1.8.0' +] + +[project.urls] +'Homepage' = 'http://www.projectq.ch' +'Documentation' = 'https://projectq.readthedocs.io/en/latest/' +'Issue Tracker' = 'https://github.com/ProjectQ-Framework/ProjectQ/' + +[project.optional-dependencies] + +azure-quantum = [ + 'azure-quantum' +] + +braket = [ + 'boto3' +] + +revkit = [ + 'revkit == 3.0a2.dev2', + 'dormouse' +] + +test = [ + 'flaky>=3.7.0', + 'mock', + 'pytest >= 6.0', + 'pytest-cov', + 'pytest-mock' +] + +docs = [ + 'sphinx>=4.3.2', + 'sphinx_rtd_theme' +] + +# ============================================================================== + +[tool.black] + + line-length = 120 + target-version = ['py38','py39','py310','py311','py312'] + skip-string-normalization = true + + +[tool.check-manifest] +ignore = [ + 'PKG-INFO', + '*.egg-info', + '*.egg-info/*', + 'setup.cfg', + '.hgtags', + '.hgsigs', + '.hgignore', + '.gitignore', + '.bzrignore', + '.gitattributes', + '.github/*', + '.travis.yml', + 'Jenkinsfile', + '*.mo', + '.clang-format', + '.gitmodules', + 'requirements.txt', + 'requirements_tests.txt', + 'VERSION.txt', + '.editorconfig', + '*.yml', + '*.yaml', + 'docs/*', + 'docs/images/*', + 'examples/*', + ] + + + +[tool.coverage] + [tool.coverage.run] + omit = [ + '*_test.py', + '*_fixtures.py' + ] + + +[tool.pylint] + [tool.pylint.master] + ignore-patterns = [ + '__init__.py', + '.*_test.py', + '.*_fixtures.py', + '.*flycheck.*.py', + ] + + extension-pkg-whitelist = [ + 'math', + 'cmath', + 'unicodedata', + 'revkit' + ] + extension-pkg-allow-list = [ + 'math', + 'cmath', + 'unicodedata', + 'revkit' + ] + + [tool.pylint.basic] + good-names = ['qb', 'id', 'i', 'j', 'k', 'N', 'op', 'X', 'Y', 'Z', 'R', 'C', 'CRz', 'Zero', 'One'] + + [tool.pylint.format] + max-line-length = 120 + + [tool.pylint.reports] + msg-template = '{path}:{line}: [{msg_id}, {obj}] {msg} ({symbol})' + + [tool.pylint.similarities] + min-similarity-lines = 20 + + [tool.pylint.messages_control] + disable = [ + 'expression-not-assigned', + 'pointless-statement', + 'fixme', + 'unspecified-encoding', + 'R0801', + ] + + [tool.pylint.typecheck] + ignored-modules = ['boto3', 'botocore', 'sympy'] + + +[tool.pytest.ini_options] + +minversion = '6.0' +addopts = '-pno:warnings' +testpaths = ['projectq'] +ignore-glob = ['*flycheck*.py'] +mock_use_standalone_module = true + +[tool.doc8] + +verbose = 0 +max_line_length = 120 + +[tool.isort] + +profile = "black" + +[tool.setuptools_scm] + +write_to = 'VERSION.txt' +write_to_template = '{version}' +local_scheme = 'no-local-version' + +[tool.cibuildwheel] + +archs = ['auto64'] +build-frontend = 'build' +build-verbosity = 1 +skip = 'pp* *-musllinux*' +environment = { PROJECTQ_DISABLE_ARCH_NATIVE='1', PROJECTQ_CI_BUILD='1', OMP_NUM_THREADS='1' } + +before-test = [ + 'cd {package}', + 'python setup.py gen_reqfile', + 'python -m pip install -r requirements.txt --only-binary :all:', +] + +test-command = 'python {package}/examples/grover.py' + +# Normal options, etc. +manylinux-x86_64-image = 'manylinux2014' diff --git a/pytest.ini b/pytest.ini deleted file mode 100755 index e5ac5982e..000000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -testpaths = projectq diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index 903d45bdc..000000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -numpy -future -pytest>=3.1 -pybind11>=2.2.3 -requests -scipy -networkx diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..c5248b57f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,20 @@ +[options] + +zip_safe = False +packages = find: + +# ============================================================================== + +[flake8] + +max-line-length = 120 +ignore = E203,W503,E800 +exclude = + .git, + __pycache__, + build, + dist, + __init__.py +docstring-quotes = """ + +# ============================================================================== diff --git a/setup.py b/setup.py index 604f006af..24a647bd4 100755 --- a/setup.py +++ b/setup.py @@ -1,172 +1,810 @@ -from setuptools import setup, Extension, find_packages, Feature -from setuptools.command.build_ext import build_ext -import sys +# Some of the setup.py code is inspired or copied from SQLAlchemy + +# SQLAlchemy was created by Michael Bayer. + +# Major contributing authors include: + +# - Michael Bayer +# - Jason Kirtland +# - Gaetan de Menten +# - Diana Clarke +# - Michael Trier +# - Philip Jenvey +# - Ants Aasma +# - Paul Johnston +# - Jonathan Ellis +# - Damien Nguyen (ProjectQ) + +# Copyright 2005-2020 SQLAlchemy and ProjectQ authors and contributors (see above) + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +"""Setup.py file.""" + +# pylint: disable=deprecated-module,attribute-defined-outside-init + import os -import setuptools +import platform +import subprocess +import sys +import tempfile +from operator import itemgetter +from pathlib import Path + +from setuptools import Command +from setuptools import Distribution as _Distribution +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext + +try: + from setuptools._distutils.errors import ( + CCompilerError, + CompileError, + DistutilsError, + ) + from setuptools._distutils.spawn import DistutilsExecError, find_executable, spawn + from setuptools.errors import PlatformError + + _SETUPTOOL_IMPORT_ERROR = None + +except ImportError as setuptools_import_error: + _SETUPTOOL_IMPORT_ERROR = setuptools_import_error + + +try: + import setuptools_scm # noqa: F401 # pylint: disable=unused-import + + _HAS_SETUPTOOLS_SCM = True +except ImportError: + _HAS_SETUPTOOLS_SCM = False +try: + import tomllib -# This reads the __version__ variable from projectq/_version.py -exec(open('projectq/_version.py').read()) + def parse_toml(filename): + """Parse a TOML file.""" + with open(str(filename), 'rb') as toml_file: + return tomllib.load(toml_file) -# Readme file as long_description: -long_description = open('README.rst').read() +except ImportError: + try: + import toml + + def parse_toml(filename): + """Parse a TOML file.""" + return toml.load(filename) + + except ImportError: + + def _find_toml_section_end(lines, start): + """Find the index of the start of the next section.""" + return ( + next(filter(itemgetter(1), enumerate(line.startswith('[') for line in lines[start + 1 :])))[0] + + start + + 1 + ) + + def _parse_list(lines): + """Parse a TOML list into a Python list.""" + # NB: This function expects the TOML list to be formatted like so (ignoring leading and trailing spaces): + # name = [ + # '...', + # ] + # Any other format is not supported. + name = None + elements = [] + + for idx, line in enumerate(lines): + if name is None and not line.startswith("'"): + name = line.split('=')[0].strip() + continue + if line.startswith("]"): + return (name, elements, idx + 1) + elements.append(line.rstrip(',').strip("'").strip('"')) + + raise RuntimeError(f'Failed to locate closing "]" for {name}') + + def parse_toml(filename): + """Very simple parser routine for pyproject.toml.""" + result = {'project': {'optional-dependencies': {}}} + with open(filename) as toml_file: + lines = [line.strip() for line in toml_file.readlines()] + lines = [line for line in lines if line and not line.startswith('#')] + + start = lines.index('[project]') + project_data = lines[start : _find_toml_section_end(lines, start)] + + start = lines.index('[project.optional-dependencies]') + optional_dependencies = lines[start + 1 : _find_toml_section_end(lines, start)] + + idx = 0 + N = len(project_data) + while idx < N: + line = project_data[idx] + shift = 1 + if line.startswith('name'): + result['project']['name'] = line.split('=')[1].strip().strip("'") + elif line.startswith('dependencies'): + (name, pkgs, shift) = _parse_list(project_data[idx:]) + result['project'][name] = pkgs + idx += shift -# Read in requirements.txt -with open('requirements.txt', 'r') as f_requirements: - requirements = f_requirements.readlines() -requirements = [r.strip() for r in requirements] + idx = 0 + N = len(optional_dependencies) + while idx < N: + (opt_name, opt_pkgs, shift) = _parse_list(optional_dependencies[idx:]) + result['project']['optional-dependencies'][opt_name] = opt_pkgs + idx += shift + return result -class get_pybind_include(object): - """Helper class to determine the pybind11 include path - The purpose of this class is to postpone importing pybind11 - until it is actually installed, so that the ``get_include()`` - method can be invoked. """ +# ============================================================================== +# Helper functions and classes + + +class Pybind11Include: # pylint: disable=too-few-public-methods + """ + Helper class to determine the pybind11 include path. + + The purpose of this class is to postpone importing pybind11 until it is actually installed, so that the + ``get_include()`` method can be invoked. + """ def __init__(self, user=False): + """Initialize a Pybind11Include object.""" self.user = user def __str__(self): - import pybind11 + """Conversion to string.""" + import pybind11 # pylint: disable=import-outside-toplevel + return pybind11.get_include(self.user) -cppsim = Feature( - 'C++ Simulator', - standard=True, - ext_modules=[ - Extension( - 'projectq.backends._sim._cppsim', - ['projectq/backends/_sim/_cppsim.cpp'], - include_dirs=[ - # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True) - ], - language='c++' - ), - ], -) +def important_msgs(*msgs): + """Print an important message.""" + print('*' * 75) + for msg in msgs: + print(msg) + print('*' * 75) -def has_flag(compiler, flagname=None): - """ - Return a boolean indicating whether a flag name is supported on the - specified compiler. - """ - import tempfile - f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('int main (int argc, char **argv) { return 0; }') - f.close() - ret = True - try: - if flagname is None: - compiler.compile([f.name]) - else: - compiler.compile([f.name], extra_postargs=[flagname]) - except: - ret = False - os.unlink(f.name) - return ret +def status_msgs(*msgs): + """Print a status message.""" + print('-' * 75) + for msg in msgs: + print('# INFO: ', msg) + print('-' * 75) -def knows_intrinsics(compiler): - """ - Return a boolean indicating whether the compiler can handle intrinsics. - """ - import tempfile - f = tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) - f.write('#include \nint main (int argc, char **argv) ' - '{ __m256d neg = _mm256_set1_pd(1.0); }') - f.close() - ret = True +def compiler_test( + compiler, + flagname=None, + link_executable=False, + link_shared_lib=False, + include='', + body='', + compile_postargs=None, + link_postargs=None, +): # pylint: disable=too-many-arguments,too-many-branches + """Return a boolean indicating whether a flag name is supported on the specified compiler.""" + fname = None + with tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) as temp: + temp.write(f'{include}\nint main (int argc, char **argv) {{ {body} return 0; }}') + fname = temp.name + + if compile_postargs is None: + compile_postargs = [flagname] if flagname is not None else None + elif flagname is not None: + compile_postargs.append(flagname) + try: - compiler.compile([f.name], extra_postargs=['-march=native']) - except setuptools.distutils.errors.CompileError: - ret = False - os.unlink(f.name) - return ret + if compiler.compiler_type == 'msvc': + olderr = os.dup(sys.stderr.fileno()) + err = open('err.txt', 'w') # pylint: disable=consider-using-with + os.dup2(err.fileno(), sys.stderr.fileno()) + + obj_file = compiler.compile([fname], extra_postargs=compile_postargs) + if not os.path.exists(obj_file[0]): + raise RuntimeError('') + if link_executable: + compiler.link_executable(obj_file, os.path.join(tempfile.mkdtemp(), 'test'), extra_postargs=link_postargs) + elif link_shared_lib: + if sys.platform == 'win32': + lib_name = os.path.join(tempfile.mkdtemp(), 'test.dll') + else: + lib_name = os.path.join(tempfile.mkdtemp(), 'test.so') + compiler.link_shared_lib(obj_file, lib_name, extra_postargs=link_postargs) + + if compiler.compiler_type == 'msvc': + err.close() + os.dup2(olderr, sys.stderr.fileno()) + with open('err.txt') as err_file: + if err_file.readlines(): + raise RuntimeError('') + except Exception: # pylint: disable=broad-except + return False + else: + return True + finally: + os.unlink(fname) + + +def _fix_macosx_header_paths(*args): + # Fix path to SDK headers if necessary + _MACOSX_XCODE_REF_PATH = ( # pylint: disable=invalid-name + '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer' + ) + _MACOSX_DEVTOOLS_REF_PATH = '/Library/Developer/CommandLineTools/' # pylint: disable=invalid-name + _has_xcode = os.path.exists(_MACOSX_XCODE_REF_PATH) + _has_devtools = os.path.exists(_MACOSX_DEVTOOLS_REF_PATH) + if not _has_xcode and not _has_devtools: + important_msgs('ERROR: Must install either Xcode or CommandLineTools!') + raise BuildFailed() + + for compiler_args in args: + for idx, item in enumerate(compiler_args): + if not _has_xcode and _MACOSX_XCODE_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_XCODE_REF_PATH, _MACOSX_DEVTOOLS_REF_PATH) + + if not _has_devtools and _MACOSX_DEVTOOLS_REF_PATH in item: + compiler_args[idx] = item.replace(_MACOSX_DEVTOOLS_REF_PATH, _MACOSX_XCODE_REF_PATH) + + +# ============================================================================== + + +class BuildFailed(Exception): + """Extension raised if the build fails for any reason.""" + + def __init__(self): + """Initialize a BuildFailed exception object.""" + super().__init__() + self.cause = sys.exc_info()[1] # work around py 2/3 different syntax + + +# ------------------------------------------------------------------------------ +# Python build related variable + +cpython = platform.python_implementation() == 'CPython' +ext_errors = () +if _SETUPTOOL_IMPORT_ERROR is None: + ext_errors = (CCompilerError, DistutilsError, CompileError, DistutilsExecError) +if sys.platform == 'win32': + # 2.6's distutils.msvc9compiler can raise an IOError when failing to + # find the compiler + ext_errors += (IOError,) + +# ============================================================================== +# ProjectQ C++ extensions + +ext_modules = [ + Extension( + 'projectq.backends._sim._cppsim', + ['projectq/backends/_sim/_cppsim.cpp'], + include_dirs=[ + # Path to pybind11 headers + Pybind11Include(), + Pybind11Include(user=True), + ], + language='c++', + ), +] + +# ============================================================================== class BuildExt(build_ext): """A custom build extension for adding compiler-specific options.""" + c_opts = { 'msvc': ['/EHsc'], 'unix': [], } + user_options = build_ext.user_options + [ + ( + 'gen-compiledb', + None, + 'Generate a compile_commands.json alongside the compilation implies (-n/--dry-run)', + ), + ] + + boolean_options = build_ext.boolean_options + ['gen-compiledb'] + + def initialize_options(self): + """Initialize this command's options.""" + build_ext.initialize_options(self) + self.gen_compiledb = None + + def finalize_options(self): + """Finalize this command's options.""" + build_ext.finalize_options(self) + if self.gen_compiledb: + self.dry_run = True + + def run(self): + """Execute this command.""" + if _SETUPTOOL_IMPORT_ERROR is not None: + raise _SETUPTOOL_IMPORT_ERROR + + try: + build_ext.run(self) + except PlatformError as err: + raise BuildFailed() from err + def build_extensions(self): + """Build the individual C/C++ extensions.""" + self._configure_compiler() + + for ext in self.extensions: + ext.extra_compile_args = self.opts + ext.extra_link_args = self.link_opts + + if self.compiler.compiler_type == 'unix' and self.gen_compiledb: + compile_commands = [] + for ext in self.extensions: + commands = self._get_compilation_commands(ext) + for cmd, src in commands: + compile_commands.append( + { + 'directory': os.path.dirname(os.path.abspath(__file__)), + 'command': cmd, + 'file': os.path.abspath(src), + } + ) + + import json # pylint: disable=import-outside-toplevel + + with open( + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'compile_commands.json'), + 'w', + ) as json_file: + json.dump(compile_commands, json_file, sort_keys=True, indent=4) + + try: + build_ext.build_extensions(self) + except ext_errors as err: + raise BuildFailed() from err + except ValueError as err: + # this can happen on Windows 64 bit, see Python issue 7511 + if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 + raise BuildFailed() from err + raise + + def _get_compilation_commands(self, ext): + # pylint: disable=protected-access + ( + _, + objects, + extra_postargs, + pp_opts, + build, + ) = self.compiler._setup_compile( + outdir=self.build_temp, + sources=ext.sources, + macros=ext.define_macros, + incdirs=ext.include_dirs, + extra=ext.extra_compile_args, + depends=ext.depends, + ) + + cc_args = self.compiler._get_cc_args(pp_opts=pp_opts, debug=self.debug, before=None) + compiler_so = self.compiler.compiler_so + compiler_so[0] = find_executable(compiler_so[0]) + + commands = [] + for obj in objects: + try: + src, ext = build[obj] + except KeyError: + continue + + commands.append( + ( + ' '.join( + compiler_so + cc_args + [os.path.abspath(src), "-o", os.path.abspath(obj)] + extra_postargs + ), + src, + ) + ) + return commands + + def _configure_compiler(self): + # Force dry_run = False to allow for compiler feature testing + dry_run_old = self.compiler.dry_run + self.compiler.dry_run = False + + if ( + int(os.environ.get('PROJECTQ_CLEANUP_COMPILER_FLAGS', 0)) + and self.compiler.compiler_type == 'unix' + and sys.platform != 'darwin' + ): + self._cleanup_compiler_flags() + if sys.platform == 'darwin': - self.c_opts['unix'] += ['-mmacosx-version-min=10.7'] - if has_flag(self.compiler, '-stdlib=libc++'): + _fix_macosx_header_paths(self.compiler.compiler, self.compiler.compiler_so) + + if compiler_test(self.compiler, '-stdlib=libc++'): self.c_opts['unix'] += ['-stdlib=libc++'] - ct = self.compiler.compiler_type - opts = self.c_opts.get(ct, []) + compiler_type = self.compiler.compiler_type + self.opts = self.c_opts.get(compiler_type, []) + self.link_opts = [] + + if not compiler_test(self.compiler): + important_msgs( + 'ERROR: something is wrong with your C++ compiler.\nFailed to compile a simple test program!' + ) + raise BuildFailed() + + # ------------------------------ + + status_msgs('Configuring OpenMP') + self._configure_openmp() + status_msgs('Configuring compiler intrinsics') + self._configure_intrinsics() + status_msgs('Configuring C++ standard') + self._configure_cxx_standard() + + # ------------------------------ + # Other compiler tests + + status_msgs('Other compiler tests') + self.compiler.define_macro('VERSION_INFO', f'"{self.distribution.get_version()}"') + if compiler_type == 'unix' and compiler_test(self.compiler, '-fvisibility=hidden'): + self.opts.append('-fvisibility=hidden') + + self.compiler.dry_run = dry_run_old + status_msgs('Finished configuring compiler!') - if not has_flag(self.compiler): - self.warning("Something is wrong with your C++ compiler.\n" - "Failed to compile a simple test program!\n") + def _configure_openmp(self): + if self.compiler.compiler_type == 'msvc': return - openmp = '' - if has_flag(self.compiler, '-fopenmp'): - openmp = '-fopenmp' - elif has_flag(self.compiler, '-qopenmp'): - openmp = '-qopenmp' - if ct == 'msvc': - openmp = '' # supports only OpenMP 2.0 - - if knows_intrinsics(self.compiler): - opts.append('-DINTRIN') - if ct == 'msvc': - opts.append('/arch:AVX') - else: - opts.append('-march=native') + kwargs = { + 'link_shared_lib': True, + 'include': '#include ', + 'body': 'int a = omp_get_num_threads(); ++a;', + } - opts.append(openmp) - if ct == 'unix': - if not has_flag(self.compiler, '-std=c++11'): - self.warning("Compiler needs to have C++11 support!") + for flag in ['-openmp', '-fopenmp', '-qopenmp', '/Qopenmp']: + if compiler_test(self.compiler, flag, link_postargs=[flag], **kwargs): + self.opts.append(flag) + self.link_opts.append(flag) return - opts.append('-DVERSION_INFO="%s"' - % self.distribution.get_version()) - opts.append('-std=c++11') - if has_flag(self.compiler, '-fvisibility=hidden'): - opts.append('-fvisibility=hidden') - elif ct == 'msvc': - opts.append('/DVERSION_INFO=\\"%s\\"' - % self.distribution.get_version()) - for ext in self.extensions: - ext.extra_compile_args = opts - ext.extra_link_args = [openmp] + flag = '-fopenmp' + if sys.platform == 'darwin' and compiler_test(self.compiler, flag): + try: + llvm_root = subprocess.check_output(['brew', '--prefix', 'llvm']).decode('utf-8')[:-1] + compiler_root = subprocess.check_output(['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from HomeBrew + if llvm_root in compiler_root: + l_arg = f'-L{llvm_root}/lib' + if compiler_test(self.compiler, flag, link_postargs=[l_arg, flag], **kwargs): + self.opts.append(flag) + self.link_opts.extend((l_arg, flag)) + return + except subprocess.CalledProcessError: + pass + + try: + # Only relevant for MacPorts users with clang-3.7 + port_path = subprocess.check_output(['which', 'port']).decode('utf-8')[:-1] + macports_root = os.path.dirname(os.path.dirname(port_path)) + compiler_root = subprocess.check_output(['which', self.compiler.compiler[0]]).decode('utf-8')[:-1] + + # Only add the flag if the compiler we are using is the one + # from MacPorts + if macports_root in compiler_root: + inc_dir = f'{macports_root}/include/libomp' + lib_dir = f'{macports_root}/lib/libomp' + c_arg = '-I' + inc_dir + l_arg = '-L' + lib_dir + + if compiler_test(self.compiler, flag, compile_postargs=[c_arg], link_postargs=[l_arg], **kwargs): + self.compiler.add_include_dir(inc_dir) + self.compiler.add_library_dir(lib_dir) + return + except subprocess.CalledProcessError: + pass + + important_msgs('WARNING: compiler does not support OpenMP!') + + def _configure_intrinsics(self): + flags = [ + '-march=native', + '-mavx2', + '/arch:AVX2', + '/arch:CORE-AVX2', + '/arch:AVX', + ] + + if int(os.environ.get('PROJECTQ_NOINTRIN', '0')) or ( + sys.platform == 'darwin' + and platform.machine() == 'arm64' + and (sys.version_info.major == 3 and sys.version_info.minor < 9) + ): + important_msgs( + 'Either requested no-intrinsics or detected potentially unsupported Python version on ' + 'Apple Silicon: disabling intrinsics' + ) + self.compiler.define_macro('NOINTRIN') + return + if os.environ.get('PROJECTQ_DISABLE_ARCH_NATIVE'): + flags = flags[1:] + + for flag in flags: + if compiler_test( + self.compiler, + flagname=flag, + include='#include ', + body='__m256d neg = _mm256_set1_pd(1.0); (void)neg;', + ): + self.opts.append(flag) + self.compiler.define_macro("INTRIN") + break + + for flag in ['-ffast-math', '-fast', '/fast', '/fp:precise']: + if compiler_test(self.compiler, flagname=flag): + self.opts.append(flag) + break + + def _configure_cxx_standard(self): + if self.compiler.compiler_type == 'msvc': + return + + cxx_standards = [17, 14, 11] + if sys.platform == 'darwin': + major_version = int(platform.mac_ver()[0].split('.')[0]) + minor_version = int(platform.mac_ver()[0].split('.')[1]) + if major_version <= 10 and minor_version < 14: + cxx_standards = [year for year in cxx_standards if year < 17] + + for year in cxx_standards: + flag = f'-std=c++{year}' + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + flag = f'/Qstd=c++{year}' + if compiler_test(self.compiler, flag): + self.opts.append(flag) + return + + important_msgs('ERROR: compiler needs to have at least C++11 support!') + raise BuildFailed() + + def _cleanup_compiler_flags(self): + status_msgs('INFO: Performing compiler flags cleanup') + compiler_exe = self.compiler.compiler[0] + compiler_exe_so = self.compiler.compiler_so[0] + linker_so = self.compiler.linker_so[0] + compiler_flags = set(self.compiler.compiler[1:]) + compiler_so_flags = set(self.compiler.compiler_so[1:]) + linker_so_flags = set(self.compiler.linker_so[1:]) + + all_common_flags = compiler_flags & compiler_so_flags & linker_so_flags + common_compiler_flags = (compiler_flags & compiler_so_flags) - all_common_flags + + compiler_flags = compiler_flags - common_compiler_flags - all_common_flags + compiler_so_flags = compiler_so_flags - common_compiler_flags - all_common_flags + + flags = [] + for flag in common_compiler_flags: + compiler = type(self.compiler)() + compiler.set_executables(compiler=compiler_exe, compiler_so=compiler_exe_so, linker_so=linker_so) + + compiler.debug_print(f'INFO: trying out {flag}') + if compiler_test(compiler, flag, link_shared_lib=True, compile_postargs=['-fPIC']): + flags.append(flag) + else: + important_msgs(f'WARNING: ignoring unsupported compiler flag: {flag}') + + self.compiler.compiler = [compiler_exe] + list(compiler_flags) + self.compiler.compiler_so = [compiler_exe_so] + list(compiler_so_flags) + self.compiler.linker_so = [linker_so] + list(linker_so_flags - all_common_flags) + + self.compiler.compiler.extend(flags) + self.compiler.compiler_so.extend(flags) + + flags = [] + for flag in all_common_flags: + if compiler_test(self.compiler, flag): + flags.append(flag) + else: + important_msgs(f'WARNING: ignoring unsupported compiler flag: {flag}') + + self.compiler.compiler.extend(flags) + self.compiler.compiler_so.extend(flags) + self.compiler.linker_so.extend(flags) + + +# ------------------------------------------------------------------------------ + + +class ClangTidy(Command): + """A custom command to run Clang-Tidy on all C/C++ source files.""" + + description = 'run Clang-Tidy on all C/C++ source files' + user_options = [('warning-as-errors', None, 'Warning as errors')] + boolean_options = ['warning-as-errors'] + + sub_commands = [('build_ext', None)] + + def initialize_options(self): + """Initialize this command's options.""" + self.warning_as_errors = None + + def finalize_options(self): + """Finalize this command's options.""" + + def run(self): + """Execute this command.""" + # Ideally we would use self.run_command(command) but we need to ensure + # that --dry-run --gen-compiledb are passed to build_ext regardless of + # other arguments + command = 'build_ext' + # distutils.log.info("running %s --dry-run --gen-compiledb", command) + cmd_obj = self.get_finalized_command(command) + cmd_obj.dry_run = True + cmd_obj.gen_compiledb = True try: - build_ext.build_extensions(self) - except setuptools.distutils.errors.CompileError: - self.warning("") - - def warning(self, warning_text): - raise Exception(warning_text + "\nCould not install the C++-Simulator." - "\nProjectQ will default to the (slow) Python " - "simulator.\nUse --without-cppsimulator to skip " - "building the (faster) C++ version of the simulator.") - - -setup( - name='projectq', - version=__version__, - author='ProjectQ', - author_email='info@projectq.ch', - url='http://www.projectq.ch', - description=('ProjectQ - ' - 'An open source software framework for quantum computing'), - long_description=long_description, - features={'cppsimulator': cppsim}, - install_requires=requirements, - cmdclass={'build_ext': BuildExt}, - zip_safe=False, - license='Apache 2', - packages=find_packages() -) + cmd_obj.run() + self.distribution.have_run[command] = 1 + except BuildFailed as err: + # distutils.log.error('build_ext --dry-run --gen-compiledb command failed!') + raise RuntimeError('build_ext --dry-run --gen-compiledb command failed!') from err + + command = ['clang-tidy'] + if self.warning_as_errors: + command.append('--warnings-as-errors=*') + for ext in self.distribution.ext_modules: + command.extend(os.path.abspath(p) for p in ext.sources) + spawn(command, dry_run=self.dry_run) + + +# ------------------------------------------------------------------------------ + + +class GenerateRequirementFile(Command): + """A custom command to list the dependencies of the current.""" + + description = 'List the dependencies of the current package' + user_options = [ + ('include-all-extras', None, 'Include all "extras_require" into the list'), + ('include-extras=', None, 'Include some of extras_requires into the list (comma separated)'), + ] + + boolean_options = ['include-all-extras'] + + def initialize_options(self): + """Initialize this command's options.""" + self.include_extras = None + self.include_all_extras = None + self.extra_pkgs = [] + self.dependencies = [] + + def finalize_options(self): + """Finalize this command's options.""" + include_extras = self.include_extras.split(',') if self.include_extras else [] + pyproject_toml = parse_toml(Path(__file__).parent / 'pyproject.toml') + + for name, pkgs in pyproject_toml['project']['optional-dependencies'].items(): + if self.include_all_extras or name in include_extras: + self.extra_pkgs.extend(pkgs) + + self.dependencies = self.distribution.install_requires + if not self.dependencies: + self.dependencies = pyproject_toml['project']['dependencies'] + + def run(self): + """Execute this command.""" + with open('requirements.txt', 'w') as req_file: + for pkg in self.dependencies: + req_file.write(f'{pkg}\n') + req_file.write('\n') + for pkg in self.extra_pkgs: + req_file.write(f'{pkg}\n') + + +# ------------------------------------------------------------------------------ + + +class Distribution(_Distribution): + """Distribution class.""" + + def has_ext_modules(self): + """Return whether this distribution has some external modules.""" + # We want to always claim that we have ext_modules. This will be fine + # if we don't actually have them (such as on PyPy) because nothing + # will get built, however we don't want to provide an overally broad + # Wheel package when building a wheel without C support. This will + # ensure that Wheel knows to treat us as if the build output is + # platform specific. + return True + + +# ============================================================================== + + +def run_setup(with_cext): + """Run the setup() function.""" + kwargs = {} + if with_cext: + kwargs['ext_modules'] = ext_modules + else: + kwargs['ext_modules'] = [] + + # NB: Workaround for people calling setup.py without a proper environment containing setuptools-scm + # This can typically be the case when calling the `gen_reqfile` or `clang_tidy`commands + if not _HAS_SETUPTOOLS_SCM: + kwargs['version'] = '0.0.0' + + setup( + cmdclass={ + 'build_ext': BuildExt, + 'clang_tidy': ClangTidy, + 'gen_reqfile': GenerateRequirementFile, + }, + distclass=Distribution, + **kwargs, + ) + + +# ============================================================================== + +if not cpython: + run_setup(False) + important_msgs( + 'WARNING: C/C++ extensions are not supported on some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + ) +elif os.environ.get('PROJECTQ_DISABLE_CEXT'): + run_setup(False) + important_msgs( + 'PROJECTQ_DISABLE_CEXT is set; not attempting to build C/C++ extensions.', + 'Plain-Python build succeeded.', + ) + +else: + try: + run_setup(True) + except BuildFailed as exc: + if os.environ.get('PROJECTQ_CI_BUILD'): + raise exc + + important_msgs( + exc.cause, + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Failure information, if any, is above.', + 'Retrying the build without the C/C++ extensions now.', + ) + + run_setup(False) + + important_msgs( + 'WARNING: Some C/C++ extensions could not be compiled, ' + + 'some features are disabled (e.g. C++ simulator).', + 'Plain-Python build succeeded.', + )