diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f95d55e5..b8b2e1c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,8 @@ jobs: - name: Prepare env run: | - python setup.py gen_reqfile --include-extras + python setup.py gen_reqfile --include-extras=test,braket,revkit python -m pip install -r requirements.txt --prefer-binary - python -m pip install -r requirements_tests.txt --prefer-binary python -m pip install coveralls - name: Setup annotations on Linux @@ -62,7 +61,7 @@ jobs: run: python -m pip install pytest-github-actions-annotate-failures - name: Build and install package - run: python -m pip install -ve .[braket] + run: python -m pip install -ve .[braket,revkit,test] - name: Pytest run: | @@ -125,15 +124,14 @@ jobs: - name: Prepare Python env run: | - python3 setup.py gen_reqfile --include-extras + python3 setup.py gen_reqfile --include-extras=test,braket python3 -m pip install -r requirements.txt --prefer-binary - python3 -m pip install -r requirements_tests.txt --prefer-binary - name: Upgrade pybind11 and flaky run: python3 -m pip install --upgrade pybind11 flaky --prefer-binary - name: Build and install package - run: python3 -m pip install -ve .[braket] + run: python3 -m pip install -ve .[braket,test] - name: Pytest run: | @@ -170,15 +168,14 @@ jobs: - name: Prepare Python env run: | - python3 setup.py gen_reqfile --include-extras + python3 setup.py gen_reqfile --include-extras=test,braket python3 -m pip install -r requirements.txt --prefer-binary - python3 -m pip install -r requirements_tests.txt --prefer-binary - name: Upgrade pybind11 and flaky run: python3 -m pip install --upgrade pybind11 flaky --prefer-binary - name: Build and install package - run: python3 -m pip install -ve .[braket] + run: python3 -m pip install -ve .[braket,test] - name: Pytest run: | @@ -244,12 +241,11 @@ jobs: - name: Install dependencies run: | - python3 setup.py gen_reqfile --include-extras + python3 setup.py gen_reqfile --include-extras=test,braket python3 -m pip install -r requirements.txt --prefer-binary - python3 -m pip install -r requirements_tests.txt --prefer-binary - name: Build and install package - run: python3 -m pip install -ve .[braket] + run: python3 -m pip install -ve .[braket,test] - name: Pytest run: | @@ -283,10 +279,7 @@ jobs: - name: Install docs & setup requirements run: | - python3 setup.py gen_reqfile --include-extras - python3 -m pip install -r requirements.txt --prefer-binary - python3 -m pip install -r docs/requirements.txt --prefer-binary - python3 -m pip install . + python3 -m pip install .[docs] - name: Build docs run: python3 -m sphinx -b html docs docs/.build @@ -334,12 +327,11 @@ jobs: - name: Prepare env run: | - python setup.py gen_reqfile --include-extras + python setup.py gen_reqfile --include-extras=test,braket python -m pip install -r requirements.txt --prefer-binary - python -m pip install -r requirements_tests.txt --prefer-binary - name: Build and install package - run: python -m pip install -ve .[braket] + run: python -m pip install -ve .[braket,test] - name: Run all checks run: | diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 6a7989adc..8717a479a 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -27,6 +27,33 @@ jobs: git fetch --prune --unshallow git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - 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/} + + 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/} + + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Set tag for setuptools-scm + run: git tag v${RELEASE_VERSION} master + - name: Build wheels uses: joerick/cibuildwheel@v1.11.1 env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 523c4e98e..03afd8fe9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,9 @@ # # See https://github.com/pre-commit/pre-commit +ci: + skip: [check-manifest] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 @@ -25,7 +28,6 @@ repos: - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending - - id: requirements-txt-fixer - id: trailing-whitespace - id: fix-encoding-pragma @@ -35,12 +37,6 @@ repos: hooks: - id: remove-tabs -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - exclude: ^(docs/.*|tools/.*)$ - - repo: https://github.com/psf/black rev: 21.5b1 hooks: @@ -49,13 +45,20 @@ repos: # This is a slow hook, so only run this if --hook-stage manual is passed stages: [manual] +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + exclude: ^(docs/.*|tools/.*)$ + - repo: https://github.com/pre-commit/mirrors-pylint rev: 'v3.0.0a3' hooks: - id: pylint - args: ['--score=n', '--exit-zero'] + args: ['--score=n'] # This is a slow hook, so only run this if --hook-stage manual is passed stages: [manual] + additional_dependencies: ['pybind11>=2.6', 'numpy', 'requests', 'boto3', 'matplotlib', 'networkx'] - repo: https://github.com/mgedmin/check-manifest rev: "0.46" diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..9199f30a8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +# .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/CHANGELOG.md b/CHANGELOG.md index c3962290e..59c7baec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,31 +8,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +### Changed +### Deprecated +### Fixed +### Removed +### Repository + +## [0.6.0] - 2021-06-23 + +### Added + +- Added backend for IonQ. +- Added support for state-dependent qubit control + +### Changed -- Support for GitHub Actions - * Build and testing on various plaforms and compilers - * Automatic draft of new release - * Automatic publication of new release once ready - * Automatic upload of releases artifacts to PyPi and GitHub -- Use ``setuptools-scm`` for versioning -- Added ``.editorconfig` file -- Added ``pyproject.toml`` and ``setup.cfg`` -- Added CHANGELOG.md -- Added backend for IonQ. -- Added support for state-dependent qubit control +- 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 +- 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. ### Removed - Compatibility with Python 2.7 +- Support for multi-qubit measurement gates; use `All(Measure)` instead ### Repository -- Updated cibuildwheels action to v1.11.1 -- Updated thomaseizinger/create-pull-request action to v1.1.0 +- 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 plaforms 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 @@ -71,3 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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/0.6.0...HEAD + +[0.6.0]: https://github.com/ProjectQ-Framework/ProjectQ/compare/0.5.1...0.6.0 diff --git a/MANIFEST.in b/MANIFEST.in index aa5655eab..30dbe21d7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,6 @@ include CHANGELOG.md include MANIFEST.in include NOTICE include README.rst -include requirements*.txt include setup.py include setup.cfg include pyproject.toml diff --git a/README.rst b/README.rst index c93c2165b..3e24d7332 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://coveralls.io/repos/github/ProjectQ-Framework/ProjectQ/badge.svg - :target: https://coveralls.io/github/ProjectQ-Framework/ProjectQ +.. image:: https://badge.fury.io/py/projectq.svg + :target: https://badge.fury.io/py/projectq -.. image:: https://readthedocs.org/projects/projectq/badge/?version=latest - :target: http://projectq.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status +.. 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://badge.fury.io/py/projectq.svg - :target: https://badge.fury.io/py/projectq +.. image:: https://coveralls.io/repos/github/ProjectQ-Framework/ProjectQ/badge.svg + :alt: Coverage Status + :target: https://coveralls.io/github/ProjectQ-Framework/ProjectQ -.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-brightgreen.svg +.. image:: https://readthedocs.org/projects/projectq/badge/?version=latest + :target: http://projectq.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status ProjectQ is an open source effort for quantum computing. diff --git a/docs/conf.py b/docs/conf.py index 5097d7854..46527b4fa 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - #!/usr/bin/env python3 # -*- coding: utf-8 -*- # @@ -18,7 +16,8 @@ # 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. -# +# pylint: skip-file + import os import sys diff --git a/docs/package_description.py b/docs/package_description.py index a782a1aa8..6beeff848 100644 --- a/docs/package_description.py +++ b/docs/package_description.py @@ -1,13 +1,31 @@ # -*- coding: utf-8 -*- +# 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 inspect import sys import os -class PackageDescription(object): +class PackageDescription: # pylint: disable=too-many-instance-attributes,too-few-public-methods + """Class representing a package description""" + package_list = [] - def __init__( + def __init__( # pylint: disable=too-many-arguments self, pkg_name, desc='', @@ -80,7 +98,10 @@ def __init__( ] self.members.sort(key=lambda x: x[0].lower()) - def get_ReST(self): + 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)) @@ -164,5 +185,4 @@ def get_ReST(self): new_lines.append(' {}'.format(param)) new_lines.append('') - assert not new_lines[-1] return new_lines[:-1] diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 82133027c..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx -sphinx_rtd_theme diff --git a/examples/aqt.py b/examples/aqt.py index e2fb6af12..e5fb6f658 100644 --- a/examples/aqt.py +++ b/examples/aqt.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import matplotlib.pyplot as plt import getpass diff --git a/examples/bellpair_circuit.py b/examples/bellpair_circuit.py index fff144602..d652ebc33 100755 --- a/examples/bellpair_circuit.py +++ b/examples/bellpair_circuit.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import matplotlib.pyplot as plt from projectq import MainEngine diff --git a/examples/control_tester.py b/examples/control_tester.py index 94833a70e..5e671fafe 100755 --- a/examples/control_tester.py +++ b/examples/control_tester.py @@ -12,6 +12,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. +# pylint: skip-file + from projectq.cengines import MainEngine from projectq.meta import Control diff --git a/examples/gate_zoo.py b/examples/gate_zoo.py index 6abe51c00..404d9822b 100644 --- a/examples/gate_zoo.py +++ b/examples/gate_zoo.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import os import sys diff --git a/examples/grover.py b/examples/grover.py index d845d148a..feff9ef58 100755 --- a/examples/grover.py +++ b/examples/grover.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import math from projectq import MainEngine diff --git a/examples/hws4.py b/examples/hws4.py index 3b0ab9a0b..14a4fe551 100644 --- a/examples/hws4.py +++ b/examples/hws4.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from projectq.cengines import MainEngine from projectq.ops import All, H, X, Measure from projectq.meta import Compute, Uncompute diff --git a/examples/hws6.py b/examples/hws6.py index 6a17b532f..38892f371 100644 --- a/examples/hws6.py +++ b/examples/hws6.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from projectq.cengines import MainEngine from projectq.ops import All, H, X, Measure from projectq.meta import Compute, Uncompute, Dagger diff --git a/examples/ibm.py b/examples/ibm.py index 6bc2913e9..6914d051f 100755 --- a/examples/ibm.py +++ b/examples/ibm.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import matplotlib.pyplot as plt import getpass diff --git a/examples/ionq.py b/examples/ionq.py index 1eaf2d136..8ca8cc66b 100644 --- a/examples/ionq.py +++ b/examples/ionq.py @@ -12,6 +12,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. +# pylint: skip-file + """Example of a basic entangling operation using an IonQBackend.""" import getpass diff --git a/examples/ionq_bv.py b/examples/ionq_bv.py index a1135db8c..61aa3c16e 100644 --- a/examples/ionq_bv.py +++ b/examples/ionq_bv.py @@ -12,6 +12,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. +# pylint: skip-file + """Example of a basic Bernstein-Vazirani circuit using an IonQBackend.""" import getpass diff --git a/examples/ionq_half_adder.py b/examples/ionq_half_adder.py index 33fb995e0..798ed4ac4 100644 --- a/examples/ionq_half_adder.py +++ b/examples/ionq_half_adder.py @@ -12,6 +12,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. +# pylint: skip-file + """Example of a basic 'half-adder' circuit using an IonQBackend""" import getpass diff --git a/examples/quantum_random_numbers.py b/examples/quantum_random_numbers.py index 76d51e178..796603d37 100755 --- a/examples/quantum_random_numbers.py +++ b/examples/quantum_random_numbers.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from projectq.ops import H, Measure from projectq import MainEngine diff --git a/examples/quantum_random_numbers_ibm.py b/examples/quantum_random_numbers_ibm.py index 15bf9b80c..77e427434 100755 --- a/examples/quantum_random_numbers_ibm.py +++ b/examples/quantum_random_numbers_ibm.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + import projectq.setups.ibm from projectq.ops import H, Measure from projectq import MainEngine diff --git a/examples/shor.py b/examples/shor.py index c3066e780..f604abb25 100755 --- a/examples/shor.py +++ b/examples/shor.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from __future__ import print_function import math diff --git a/examples/teleport.py b/examples/teleport.py index 499767868..2a6b964da 100755 --- a/examples/teleport.py +++ b/examples/teleport.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from projectq.ops import CNOT, H, Measure, Rz, X, Z from projectq import MainEngine from projectq.meta import Dagger, Control diff --git a/examples/teleport_circuit.py b/examples/teleport_circuit.py index bf1ff5e0d..1f002b915 100755 --- a/examples/teleport_circuit.py +++ b/examples/teleport_circuit.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: skip-file + from projectq import MainEngine from projectq.backends import CircuitDrawer diff --git a/projectq/backends/_aqt/__init__.py b/projectq/backends/_aqt/__init__.py index 08893cf17..7c5dcb45e 100644 --- a/projectq/backends/_aqt/__init__.py +++ b/projectq/backends/_aqt/__init__.py @@ -13,4 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""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 index df5a41a8c..e250e239c 100644 --- a/projectq/backends/_aqt/_aqt.py +++ b/projectq/backends/_aqt/_aqt.py @@ -20,6 +20,7 @@ from projectq.cengines import BasicEngine from projectq.meta import get_control_count, LogicalQubitIDTag from projectq.ops import Rx, Ry, Rxx, Measure, Allocate, Barrier, Deallocate, FlushGate +from projectq.types import WeakQubitRef from ._aqt_http_client import send, retrieve @@ -38,11 +39,10 @@ def _format_counts(samples, length): counts[h_result] = 1 else: counts[h_result] += 1 - counts = {k: v for k, v in sorted(counts.items(), key=lambda item: item[0])} - return counts + return dict(sorted(counts.items(), key=lambda item: item[0])) -class AQTBackend(BasicEngine): +class AQTBackend(BasicEngine): # pylint: disable=too-many-instance-attributes """ The AQT Backend class, which stores the circuit, transforms it to the appropriate data format, and sends the circuit through the AQT API. @@ -58,7 +58,7 @@ def __init__( num_retries=3000, interval=1, retrieve_execution=None, - ): + ): # pylint: disable=too-many-arguments """ Initialize the Backend object. @@ -142,14 +142,12 @@ def _store(self, cmd): if gate == Deallocate: 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 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: logical_id = qb_id self._mapper.append(qb_id) @@ -190,13 +188,13 @@ def _logical_to_physical(self, qb_id): "was eliminated during optimization.".format(qb_id) ) return mapping[qb_id] - except AttributeError: + except AttributeError as err: if qb_id not in self._mapper: raise RuntimeError( "Unknown qubit id {}. Please make sure " "eng.flush() was called and that the qubit " "was eliminated during optimization.".format(qb_id) - ) + ) from err return qb_id def get_probabilities(self, qureg): @@ -264,7 +262,6 @@ def _run(self): info, device=self.device, token=self._token, - shots=self._num_runs, num_retries=self._num_retries, interval=self._interval, verbose=self._verbose, @@ -281,37 +278,33 @@ def _run(self): self._num_runs = len(res) counts = _format_counts(res, n_qubit) # Determine random outcome - P = random.random() + 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 >= 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, qubit_id): - self.id = qubit_id - - # register measurement result + # 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(QB(qubit_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. + Receives 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 diff --git a/projectq/backends/_aqt/_aqt_http_client.py b/projectq/backends/_aqt/_aqt_http_client.py index 05fa0220a..a38b69329 100644 --- a/projectq/backends/_aqt/_aqt_http_client.py +++ b/projectq/backends/_aqt/_aqt_http_client.py @@ -29,8 +29,10 @@ class AQT(Session): + """Class managing the session to AQT's APIs""" + def __init__(self): - super(AQT, self).__init__() + super().__init__() self.backends = dict() self.timeout = 5.0 self.token = None @@ -57,7 +59,15 @@ def update_devices_list(self, verbose=False): print(self.backends) def is_online(self, device): - # useless at the moment, may change if API evolves + """ + 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): @@ -75,7 +85,7 @@ def can_run_experiment(self, info, device): nb_qubit_needed = info['nq'] return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed - def _authenticate(self, token=None): + def authenticate(self, token=None): """ Args: token (str): AQT user API token. @@ -85,14 +95,15 @@ def _authenticate(self, token=None): self.headers.update({'Ocp-Apim-Subscription-Key': token, 'SDK': 'ProjectQ'}) self.token = token - def _run(self, info, device): + 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(AQT, self).put(urljoin(_API_URL, self.backends[device]['url']), data=argument) + 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': @@ -100,8 +111,12 @@ def _run(self, info, device): execution_id = r_json["id"] return execution_id - def _get_result(self, device, execution_id, num_retries=3000, interval=1, verbose=False): - + 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("Waiting for results. [Job ID: {}]".format(execution_id)) @@ -116,7 +131,7 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover for retries in range(num_retries): argument = {'id': execution_id, 'access_token': self.token} - req = super(AQT, self).put(urljoin(_API_URL, self.backends[device]['url']), data=argument) + 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: @@ -131,7 +146,7 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover # available if not self.is_online(device): # pragma: no cover raise DeviceOfflineError( - "Device went offline. The ID of " "your submitted job is {}.".format(execution_id) + "Device went offline. The ID of your submitted job is {}.".format(execution_id) ) finally: @@ -142,11 +157,11 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover class DeviceTooSmall(Exception): - pass + """Exception raised if the device is too small to run the circuit""" class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" def show_devices(verbose=False): @@ -165,7 +180,7 @@ def show_devices(verbose=False): return aqt_session.backends -def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): +def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): # pylint: disable=too-many-arguments """ Retrieves a previously run job by its ID. @@ -178,9 +193,9 @@ def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): (list) samples form the AQT server """ aqt_session = AQT() - aqt_session._authenticate(token) + 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) + res = aqt_session.get_result(device, jobid, num_retries=num_retries, interval=interval, verbose=verbose) return res @@ -188,11 +203,10 @@ def send( info, device='aqt_simulator', token=None, - shots=100, num_retries=100, interval=1, verbose=False, -): +): # pylint: disable=too-many-arguments """ Sends cicruit through the AQT API and runs the quantum circuit. @@ -200,8 +214,6 @@ def send( 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. - shots (int): Number of runs of the same circuit to collect - statistics. max for AQT is 200. 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). @@ -217,7 +229,7 @@ def send( print("- Authenticating...") if token is not None: print('user API token: ' + token) - aqt_session._authenticate(token) + aqt_session.authenticate(token) # check if the device is online aqt_session.update_devices_list(verbose) @@ -238,10 +250,10 @@ def send( raise DeviceTooSmall("Device is too small.") if verbose: print("- Running code: {}".format(info)) - execution_id = aqt_session._run(info, device) + execution_id = aqt_session.run(info, device) if verbose: print("- Waiting for results...") - res = aqt_session._get_result( + res = aqt_session.get_result( device, execution_id, num_retries=num_retries, @@ -260,3 +272,4 @@ def send( 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 index 401c35e5c..64e50fee2 100644 --- a/projectq/backends/_aqt/_aqt_http_client_test.py +++ b/projectq/backends/_aqt/_aqt_http_client_test.py @@ -34,7 +34,7 @@ def test_is_online(): token = 'access' aqt_session = _aqt_http_client.AQT() - aqt_session._authenticate(token) + aqt_session.authenticate(token) aqt_session.update_devices_list() assert aqt_session.is_online('aqt_simulator') assert aqt_session.is_online('aqt_simulator_noise') @@ -48,7 +48,7 @@ def test_show_devices(): assert len(device_list) == 3 -def test_send_too_many_qubits(monkeypatch): +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]], ' @@ -58,11 +58,10 @@ def test_send_too_many_qubits(monkeypatch): 'backend': {'name': 'aqt_simulator'}, } token = "access" - shots = 1 # Code to test: with pytest.raises(_aqt_http_client.DeviceTooSmall): - _aqt_http_client.send(info, device="aqt_simulator", token=token, shots=shots, verbose=True) + _aqt_http_client.send(info, device="aqt_simulator", token=token, verbose=True) def test_send_real_device_online_verbose(monkeypatch): @@ -83,7 +82,6 @@ def test_send_real_device_online_verbose(monkeypatch): 'backend': {'name': 'aqt_simulator'}, } token = "access" - shots = 1 execution_id = '3' result_ready = [False] result = "my_result" @@ -139,7 +137,7 @@ def user_password_input(prompt): monkeypatch.setattr("getpass.getpass", user_password_input) # Code to test: - res = _aqt_http_client.send(info, device="aqt_simulator", token=None, shots=shots, verbose=True) + res = _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) assert res == result @@ -157,7 +155,6 @@ def user_password_input(prompt): return token monkeypatch.setattr("getpass.getpass", user_password_input) - shots = 1 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]], ' @@ -166,7 +163,7 @@ def user_password_input(prompt): 'shots': 1, 'backend': {'name': 'aqt_simulator'}, } - _aqt_http_client.send(info, device="aqt_simulator", token=None, shots=shots, verbose=True) + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) def test_send_that_errors_are_caught2(monkeypatch): @@ -183,7 +180,6 @@ def user_password_input(prompt): return token monkeypatch.setattr("getpass.getpass", user_password_input) - shots = 1 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]], ' @@ -192,7 +188,7 @@ def user_password_input(prompt): 'shots': 1, 'backend': {'name': 'aqt_simulator'}, } - _aqt_http_client.send(info, device="aqt_simulator", token=None, shots=shots, verbose=True) + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) def test_send_that_errors_are_caught3(monkeypatch): @@ -209,7 +205,6 @@ def user_password_input(prompt): return token monkeypatch.setattr("getpass.getpass", user_password_input) - shots = 1 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]], ' @@ -218,7 +213,7 @@ def user_password_input(prompt): 'shots': 1, 'backend': {'name': 'aqt_simulator'}, } - _aqt_http_client.send(info, device="aqt_simulator", token=None, shots=shots, verbose=True) + _aqt_http_client.send(info, device="aqt_simulator", token=None, verbose=True) def test_send_that_errors_are_caught4(monkeypatch): @@ -230,7 +225,6 @@ def test_send_that_errors_are_caught4(monkeypatch): } info = {'circuit': '[]', 'nq': 3, 'shots': 1, 'backend': {'name': 'aqt_simulator'}} token = "access" - shots = 1 execution_id = '123e' def mocked_requests_put(*args, **kwargs): @@ -265,7 +259,6 @@ def raise_for_status(self): device="aqt_simulator", token=token, num_retries=10, - shots=shots, verbose=True, ) @@ -288,7 +281,6 @@ def test_timeout_exception(monkeypatch): 'backend': {'name': 'aqt_simulator'}, } token = "access" - shots = 1 execution_id = '123e' tries = [0] @@ -338,7 +330,6 @@ def user_password_input(prompt): device="aqt_simulator", token=tok, num_retries=10, - shots=shots, verbose=True, ) assert "123e" in str(excinfo.value) # check that job id is in exception diff --git a/projectq/backends/_awsbraket/__init__.py b/projectq/backends/_awsbraket/__init__.py index db8d7a6ff..2d01597e0 100644 --- a/projectq/backends/_awsbraket/__init__.py +++ b/projectq/backends/_awsbraket/__init__.py @@ -13,18 +13,19 @@ # 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 - import warnings - warnings.warn( - "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])" - ) + class AWSBraketBackend: # pylint: disable=too-few-public-methods + """Dummy class""" - # Make sure that the symbol is defined - class AWSBraketBackend: - pass + def __init__(self, *args, **kwargs): + 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 index 7f158afa1..9580c36f0 100755 --- a/projectq/backends/_awsbraket/_awsbraket.py +++ b/projectq/backends/_awsbraket/_awsbraket.py @@ -48,11 +48,10 @@ from ._awsbraket_boto3_client import send, retrieve -class AWSBraketBackend(BasicEngine): +class AWSBraketBackend(BasicEngine): # pylint: disable=too-many-instance-attributes """ - 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. + 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__( @@ -66,30 +65,24 @@ def __init__( 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: + 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" """ BasicEngine.__init__(self) @@ -136,37 +129,29 @@ def __init__( self._circuittail = ']}' - def is_available(self, cmd): + 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 avialable 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 + - "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 + "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 + - "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 @@ -264,12 +249,11 @@ def _reset(self): self._clear = True self._measured_ids = [] - def _store(self, cmd): + 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. + Translates the command and stores it in a local variable (self._circuit) in JSON format. Args: cmd: Command to store @@ -281,7 +265,9 @@ def _store(self, cmd): return num_controls = get_control_count(cmd) - gate_type = type(gate) if not isinstance(gate, DaggeredGate) else type(gate._gate) + gate_type = ( + type(gate) if not isinstance(gate, DaggeredGate) else type(gate._gate) # pylint: disable=protected-access + ) if self._clear: self._probabilities = dict() @@ -293,7 +279,6 @@ def _store(self, cmd): self._allocated_qubits.add(cmd.qubits[0][0].id) 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 tag in cmd.tags: @@ -343,22 +328,21 @@ def _logical_to_physical(self, qb_id): mapping = self.main_engine.mapper.current_mapping if qb_id not in mapping: raise RuntimeError( - "Unknown qubit id {} in current mapping. Please make sure " - "eng.flush() was called and that the qubit " - "was eliminated during optimization.".format(qb_id) + ( + "Unknown qubit id {} in current mapping. Please make sure eng.flush() was called and that the" + "qubit was eliminated during optimization." + ).format(qb_id) ) 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. + 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. Args: qureg (list): Quantum register determining the order of the @@ -376,16 +360,13 @@ def get_probabilities(self, qureg): 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 retreived 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. + 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 retreived 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: @@ -394,9 +375,10 @@ def get_probabilities(self, qureg): probability_dict = dict() for state in self._probabilities: mapped_state = ['0'] * len(qureg) - for i, _ in enumerate(qureg): - assert self._logical_to_physical(qureg[i].id) < len(state) - mapped_state[i] = state[self._logical_to_physical(qureg[i].id)] + for i, qubit in enumerate(qureg): + if self._logical_to_physical(qubit.id) >= len(state): # pragma: no cover + raise IndexError('Physical ID {} > length of internal probabilities array'.format(qubit.id)) + mapped_state[i] = state[self._logical_to_physical(qubit.id)] probability = self._probabilities[state] mapped_state = "".join(mapped_state) if mapped_state not in probability_dict: @@ -409,26 +391,23 @@ 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 + 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 + # 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). + # 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, - taskArn=self._retrieve_execution, + task_arn=self._retrieve_execution, num_retries=self._num_retries, interval=self._interval, verbose=self._verbose, @@ -457,14 +436,14 @@ def _run(self): counts = res # Determine random outcome - P = random.random() + random_outcome = random.random() p_sum = 0.0 measured = "" for state in counts: probability = counts[state] p_sum += probability star = "" - if p_sum >= P and measured == "": + if p_sum >= random_outcome and measured == "": measured = state star = "*" self._probabilities[state] = probability @@ -479,8 +458,7 @@ def _run(self): def receive(self, command_list): """ - Receives a command list and, for each command, stores it until - completion. + Receives a command list and, for each command, stores it until completion. Args: command_list: List of commands to execute diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client.py index 533557653..392d1af0e 100755 --- a/projectq/backends/_awsbraket/_awsbraket_boto3_client.py +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client.py @@ -21,14 +21,14 @@ """ import getpass -import signal +import json import re +import signal import time + import boto3 import botocore -import json - class AWSBraket: """ @@ -41,7 +41,7 @@ def __init__(self): self._credentials = dict() self._s3_folder = [] - def _authenticate(self, credentials=None): + def authenticate(self, credentials=None): """ Args: credentials (dict): mapping the AWS key credentials as the @@ -53,16 +53,16 @@ def _authenticate(self, credentials=None): self._credentials = credentials - def _get_s3_folder(self, s3_folder=None): + def get_s3_folder(self, s3_folder=None): """ Args: s3_folder (list): contains the S3 bucket and directory to store the results. """ if s3_folder is None: # pragma: no cover - S3Bucket = input("Enter the S3 Bucket configured in Braket: ") - S3Directory = input("Enter the Directory created in the S3 Bucket: ") - s3_folder = [S3Bucket, S3Directory] + 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 @@ -96,38 +96,38 @@ def get_list_devices(self, verbose=False): if result['deviceType'] not in ['QPU', 'SIMULATOR']: continue if result['deviceType'] == 'QPU': - deviceCapabilities = json.loads( + device_capabilities = json.loads( client.get_device(deviceArn=result['deviceArn'])['deviceCapabilities'] ) self.backends[result['deviceName']] = { - 'nq': deviceCapabilities['paradigm']['qubitCount'], - 'coupling_map': deviceCapabilities['paradigm']['connectivity']['connectivityGraph'], - 'version': deviceCapabilities['braketSchemaHeader']['version'], + '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': deviceCapabilities['deviceParameters']['properties']['braketSchemaHeader'][ + 'deviceParameters': device_capabilities['deviceParameters']['properties']['braketSchemaHeader'][ 'const' ], - 'deviceModelParameters': deviceCapabilities['deviceParameters']['definitions'][ + 'deviceModelParameters': device_capabilities['deviceParameters']['definitions'][ 'GateModelParameters' ]['properties']['braketSchemaHeader']['const'], } # Unfortunatelly the Capabilities schemas are not homogeneus # for real devices and simulators elif result['deviceType'] == 'SIMULATOR': - deviceCapabilities = json.loads( + device_capabilities = json.loads( client.get_device(deviceArn=result['deviceArn'])['deviceCapabilities'] ) self.backends[result['deviceName']] = { - 'nq': deviceCapabilities['paradigm']['qubitCount'], + 'nq': device_capabilities['paradigm']['qubitCount'], 'coupling_map': {}, - 'version': deviceCapabilities['braketSchemaHeader']['version'], + 'version': device_capabilities['braketSchemaHeader']['version'], 'location': 'us-east-1', 'deviceArn': result['deviceArn'], - 'deviceParameters': deviceCapabilities['deviceParameters']['properties']['braketSchemaHeader'][ + 'deviceParameters': device_capabilities['deviceParameters']['properties']['braketSchemaHeader'][ 'const' ], - 'deviceModelParameters': deviceCapabilities['deviceParameters']['definitions'][ + 'deviceModelParameters': device_capabilities['deviceParameters']['definitions'][ 'GateModelParameters' ]['properties']['braketSchemaHeader']['const'], } @@ -170,7 +170,7 @@ def can_run_experiment(self, info, device): nb_qubit_needed = info['nq'] return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed - def _run(self, info, device): + def run(self, info, device): """ Run the quantum code to the AWS Braket selected device. @@ -180,7 +180,7 @@ def _run(self, info, device): device (str): name of the device to use Returns: - taskArn (str): The Arn of the task + task_arn (str): The Arn of the task """ @@ -219,8 +219,10 @@ def _run(self, info, device): return response['quantumTaskArn'] - def _get_result(self, execution_id, num_retries=30, interval=1, verbose=False): - + 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("Waiting for results. [Job Arn: {}]".format(execution_id)) @@ -248,14 +250,14 @@ def _calculate_measurement_probs(measurements): measurements_probabilities = {} for i in range(total_unique_mes): strqubits = '' - for nq in range(len_qubits): - strqubits += str(unique_mes[i][nq]) + 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 taskArn itself + # The region_name is obtained from the task_arn itself region_name = re.split(':', execution_id)[3] client_braket = boto3.client( 'braket', @@ -321,11 +323,11 @@ def _calculate_measurement_probs(measurements): class DeviceTooSmall(Exception): - pass + """Exception raised if the device is too small to run the circuit""" class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" def show_devices(credentials=None, verbose=False): @@ -342,21 +344,21 @@ def show_devices(credentials=None, verbose=False): (list) list of available devices and their properties """ awsbraket_session = AWSBraket() - awsbraket_session._authenticate(credentials=credentials) + awsbraket_session.authenticate(credentials=credentials) return awsbraket_session.get_list_devices(verbose=verbose) # TODO: Create a Show Online properties per device -def retrieve(credentials, taskArn, num_retries=30, interval=1, verbose=False): +def retrieve(credentials, task_arn, num_retries=30, interval=1, verbose=False): """ Retrieves a job/task by its Arn. Args: credentials (dict): Dictionary storing the AWS credentials with keys AWS_ACCESS_KEY_ID and AWS_SECRET_KEY. - taskArn (str): The Arn of the task to retreive + task_arn (str): The Arn of the task to retreive Returns: (dict) measurement probabilities from the result @@ -368,18 +370,20 @@ def retrieve(credentials, taskArn, num_retries=30, interval=1, verbose=False): print("- Authenticating...") if credentials is not None: print("AWS credentials: " + credentials['AWS_ACCESS_KEY_ID'] + ", " + credentials['AWS_SECRET_KEY']) - awsbraket_session._authenticate(credentials=credentials) - res = awsbraket_session._get_result(taskArn, num_retries=num_retries, interval=interval, verbose=verbose) + 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 ", taskArn) + print("- Unable to locate the job with Arn ", task_arn) print(error, error_code) raise -def send(info, device, credentials, s3_folder, num_retries=30, interval=1, verbose=False): +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 +): """ Sends cicruit through the Boto3 SDK and runs the quantum circuit. @@ -404,8 +408,8 @@ def send(info, device, credentials, s3_folder, num_retries=30, interval=1, verbo print("- Authenticating...") if credentials is not None: print("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) + 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) @@ -429,12 +433,12 @@ def send(info, device, credentials, s3_folder, num_retries=30, interval=1, verbo raise DeviceTooSmall("Device is too small.") if verbose: print("- Running code: {}".format(info)) - taskArn = awsbraket_session._run(info, device) - print("Your task Arn is: {}. Make note of that for future reference".format(taskArn)) + task_arn = awsbraket_session.run(info, device) + print("Your task Arn is: {}. Make note of that for future reference".format(task_arn)) if verbose: print("- Waiting for results...") - res = awsbraket_session._get_result(taskArn, num_retries=num_retries, interval=interval, verbose=verbose) + res = awsbraket_session.get_result(task_arn, num_retries=num_retries, interval=interval, verbose=verbose) if verbose: print("- Done.") return res diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py index 5faf939a8..267e982ca 100644 --- a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py @@ -103,11 +103,11 @@ def test_retrieve(mocker, var_status, var_result, retrieve_setup): mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) if var_status == 'completed': - res = _awsbraket_boto3_client.retrieve(credentials=creds, taskArn=arntask) + 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, taskArn=arntask, num_retries=2) + _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask, num_retries=2) print(exinfo.value) if var_status == 'failed': assert ( @@ -146,7 +146,7 @@ def test_retrieve_devicetypes(mocker, retrieve_devicetypes_setup): 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, taskArn=arntask) + res = _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask) assert res == res_completed @@ -294,7 +294,7 @@ def test_retrieve_error_arn_not_exist(mocker, var_error, arntask, creds): mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) with pytest.raises(botocore.exceptions.ClientError): - _awsbraket_boto3_client.retrieve(credentials=creds, taskArn=arntask) + _awsbraket_boto3_client.retrieve(credentials=creds, task_arn=arntask) # ============================================================================== diff --git a/projectq/backends/_circuits/__init__.py b/projectq/backends/_circuits/__init__.py index 79ce3a298..8985f0fe0 100755 --- a/projectq/backends/_circuits/__init__.py +++ b/projectq/backends/_circuits/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module for exporting/printing quantum circuits""" + from ._to_latex import to_latex from ._plot import to_draw diff --git a/projectq/backends/_circuits/_drawer.py b/projectq/backends/_circuits/_drawer.py index fd86c92a4..bea56f40b 100755 --- a/projectq/backends/_circuits/_drawer.py +++ b/projectq/backends/_circuits/_drawer.py @@ -21,10 +21,12 @@ from projectq.cengines import LastEngineException, BasicEngine from projectq.ops import FlushGate, Measure, Allocate, Deallocate from projectq.meta import get_control_count -from projectq.backends._circuits import to_latex +from ._to_latex import to_latex -class CircuitItem(object): +class CircuitItem: + """Item of a quantum circuit to draw""" + def __init__(self, gate, lines, ctrl_lines): """ Initialize a circuit item. @@ -216,6 +218,7 @@ def _print_cmd(self, cmd): 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: @@ -227,19 +230,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 not in ('0', '1', 1, 0): + meas = None + while meas not in ('0', '1', 1, 0): prompt = "Input measurement result (0 or 1) for qubit " + str(qubit) + ": " - m = input(prompt) + 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] diff --git a/projectq/backends/_circuits/_drawer_matplotlib.py b/projectq/backends/_circuits/_drawer_matplotlib.py index c26bd0af4..ee83c6023 100644 --- a/projectq/backends/_circuits/_drawer_matplotlib.py +++ b/projectq/backends/_circuits/_drawer_matplotlib.py @@ -24,7 +24,7 @@ from projectq.cengines import LastEngineException, BasicEngine from projectq.ops import FlushGate, Measure, Allocate, Deallocate from projectq.meta import get_control_count -from projectq.backends._circuits import to_draw +from ._plot import to_draw # ============================================================================== @@ -98,7 +98,7 @@ def is_available(self, cmd): except LastEngineException: return True - def _process(self, cmd): + def _process(self, cmd): # pylint: disable=too-many-branches """ Process the command cmd and stores it in the internal storage @@ -109,18 +109,20 @@ def _process(self, cmd): 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: - self._map[qubit_id] = qubit_id - self._qubit_lines[qubit_id] = [] + 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: - 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: @@ -152,14 +154,14 @@ def _process(self, cmd): if len(targets) + len(controls) > 1: max_depth = max(len(self._qubit_lines[qubit_id]) for qubit_id in self._qubit_lines) - for qubit_id in itertools.chain(targets, controls): - depth = len(self._qubit_lines[qubit_id]) - self._qubit_lines[qubit_id] += [None] * (max_depth - depth) + 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 qubit_id == ref_qubit_id: - self._qubit_lines[qubit_id].append((gate_str, targets, controls)) + if qb_id == ref_qubit_id: + self._qubit_lines[qb_id].append((gate_str, targets, controls)) else: - self._qubit_lines[qubit_id].append(None) + self._qubit_lines[qb_id].append(None) def receive(self, command_list): """ diff --git a/projectq/backends/_circuits/_drawer_matplotlib_test.py b/projectq/backends/_circuits/_drawer_matplotlib_test.py index 600ff051c..9c52ad34f 100644 --- a/projectq/backends/_circuits/_drawer_matplotlib_test.py +++ b/projectq/backends/_circuits/_drawer_matplotlib_test.py @@ -16,6 +16,8 @@ Tests for projectq.backends.circuits._drawer.py. """ +import pytest + from projectq import MainEngine from projectq.cengines import DummyEngine from projectq.ops import H, X, Rx, CNOT, Swap, Measure, Command, BasicGate @@ -49,6 +51,11 @@ def test_drawer_measurement(): 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._process(Command(engine=eng, gate=Measure, qubits=([qb1],), controls=[qb2])) + class MockEngine(object): def is_available(self, cmd): diff --git a/projectq/backends/_circuits/_drawer_test.py b/projectq/backends/_circuits/_drawer_test.py index e308bbf45..26ef4974b 100755 --- a/projectq/backends/_circuits/_drawer_test.py +++ b/projectq/backends/_circuits/_drawer_test.py @@ -19,7 +19,8 @@ import pytest from projectq import MainEngine -from projectq.ops import H, X, CNOT, Measure +from projectq.ops import H, X, CNOT, Measure, Command +from projectq.types import WeakQubitRef import projectq.backends._circuits._drawer as _drawer from projectq.backends._circuits._drawer import CircuitItem, CircuitDrawer @@ -80,6 +81,11 @@ def test_drawer_measurement(): 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(): drawer = CircuitDrawer() diff --git a/projectq/backends/_circuits/_plot.py b/projectq/backends/_circuits/_plot.py index 97db7646c..0d3673fc6 100644 --- a/projectq/backends/_circuits/_plot.py +++ b/projectq/backends/_circuits/_plot.py @@ -296,7 +296,9 @@ def resize_figure(fig, axes, width, height, plot_params): axes.set_ylim(0, new_limits[1]) -def draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, plot_params): +def draw_gates( # pylint: disable=too-many-arguments + axes, qubit_lines, drawing_order, gate_grid, wire_grid, plot_params +): """ Draws the gates. @@ -326,7 +328,9 @@ def draw_gates(axes, qubit_lines, drawing_order, gate_grid, wire_grid, plot_para ) -def draw_gate(axes, gate_str, gate_pos, target_wires, targets_order, control_wires, plot_params): +def draw_gate( + axes, gate_str, gate_pos, target_wires, targets_order, control_wires, plot_params +): # pylint: disable=too-many-arguments """ Draws a single gate at a given location. @@ -482,7 +486,9 @@ def draw_measure_gate(axes, gate_pos, wire_pos, plot_params): axes.add_collection(gate) -def multi_qubit_gate(axes, gate_str, gate_pos, wire_pos_min, wire_pos_max, plot_params): +def multi_qubit_gate( # pylint: disable=too-many-arguments + axes, gate_str, gate_pos, wire_pos_min, wire_pos_max, plot_params +): """ Draws a multi-target qubit gate. diff --git a/projectq/backends/_circuits/_to_latex.py b/projectq/backends/_circuits/_to_latex.py index 37cc17758..4b1a568fa 100755 --- a/projectq/backends/_circuits/_to_latex.py +++ b/projectq/backends/_circuits/_to_latex.py @@ -13,6 +13,8 @@ # 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, @@ -27,25 +29,41 @@ ) +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. + + 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 to_latex(circuit, drawing_order=None, draw_gates_in_parallel=True): """ Translates 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. + 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. + 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} - 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 @@ -67,7 +85,7 @@ class name string as a key. Every gate can have its own width, height, pre text = _header(settings) text += _body(circuit, settings, drawing_order, draw_gates_in_parallel=draw_gates_in_parallel) - text += _footer(settings) + text += _footer() return text @@ -146,7 +164,7 @@ def _header(settings): "blur,fit,decorations.pathreplacing,shapes}\n\n" ) - init = "\\begin{document}\n" "\\begin{tikzpicture}[scale=0.8, transform shape]\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," @@ -199,17 +217,14 @@ def _header(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. + 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. @@ -237,7 +252,7 @@ def _body(circuit, settings, drawing_order=None, draw_gates_in_parallel=True): return "".join(code) -def _footer(settings): +def _footer(): """ Return the footer of the Latex document. @@ -247,10 +262,9 @@ def _footer(settings): 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, ... """ @@ -269,14 +283,14 @@ def __init__(self, settings, 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, draw_gates_in_parallel=True): + 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. @@ -285,9 +299,8 @@ def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): 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]) @@ -302,24 +315,24 @@ def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): all_lines = lines + ctrl_lines all_lines.remove(line) # remove current line - for ll in all_lines: + for _line in all_lines: gate_idx = 0 - while not (circuit[ll][gate_idx] == cmds[i]): + while not circuit[_line][gate_idx] == cmds[i]: gate_idx += 1 - tikz_code.append(self.to_tikz(ll, circuit, gate_idx)) + tikz_code.append(self.to_tikz(_line, circuit, gate_idx)) # we are taking care of gate 0 (the current one) - circuit[ll] = circuit[ll][1:] + circuit[_line] = circuit[_line][1:] all_lines = lines + ctrl_lines pos = max([self.pos[ll] for ll in range(min(all_lines), max(all_lines) + 1)]) - for ll in range(min(all_lines), max(all_lines) + 1): - self.pos[ll] = pos + self._gate_pre_offset(gate) + for _line in range(min(all_lines), max(all_lines) + 1): + self.pos[_line] = pos + self._gate_pre_offset(gate) connections = "" - for ll in all_lines: - connections += self._line(self.op_count[ll] - 1, self.op_count[ll], line=ll) + 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 @@ -338,8 +351,8 @@ def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): add_str = self._sqrtswap_gate(lines, ctrl_lines, daggered=True) elif gate == Measure: # draw measurement gate - for ll in lines: - op = self._op(ll) + for _line in lines: + op = self._op(_line) width = self._gate_width(Measure) height = self._gate_height(Measure) shift0 = 0.07 * height @@ -357,15 +370,15 @@ def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): "{shift1}cm]{op}.north east);" ).format( op=op, - pos=self.pos[ll], - line=ll, + pos=self.pos[_line], + line=_line, shift0=shift0, shift1=shift1, shift2=shift2, ) - self.op_count[ll] += 1 - self.pos[ll] += self._gate_width(gate) + self._gate_offset(gate) - self.is_quantum[ll] = False + 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}}{}$}};" @@ -401,41 +414,22 @@ def to_tikz(self, line, circuit, end=None, draw_gates_in_parallel=True): # 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 ll in lines: - self.is_quantum[ll] = 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 ll in range(len(self.pos)): - if ll != line: - self.pos[ll] = self.pos[line] + for _line in range(len(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. @@ -445,7 +439,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() @@ -453,11 +448,11 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): gate_str = "" for line in lines: op = self._op(line) - w = "{}cm".format(0.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 = "{}cm".format(0.5 * gate_width) + blc = "[xshift=-{w},yshift=-{w}]{op}.center".format(w=width, op=op) + trc = "[xshift={w},yshift={w}]{op}.center".format(w=width, op=op) + tlc = "[xshift=-{w},yshift={w}]{op}.center".format(w=width, op=op) + brc = "[xshift={w},yshift=-{w}]{op}.center".format(w=width, op=op) swap_style = "swapstyle,edgestyle" if self.settings['gate_shadow']: swap_style += ",shadowed" @@ -467,10 +462,10 @@ def _sqrtswap_gate(self, lines, ctrl_lines, daggered): "\\draw[{swap_style}] ({s3})--({s4});" ).format( op=op, - s1=s1, - s2=s2, - s3=s3, - s4=s4, + s1=blc, + s2=trc, + s3=tlc, + s4=brc, line=line, pos=self.pos[line], swap_style=swap_style, @@ -511,7 +506,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. @@ -521,7 +516,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() @@ -529,11 +525,11 @@ def _swap_gate(self, lines, ctrl_lines): gate_str = "" for line in lines: op = self._op(line) - w = "{}cm".format(0.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 = "{}cm".format(0.5 * gate_width) + blc = "[xshift=-{w},yshift=-{w}]{op}.center".format(w=width, op=op) + trc = "[xshift={w},yshift={w}]{op}.center".format(w=width, op=op) + tlc = "[xshift=-{w},yshift={w}]{op}.center".format(w=width, op=op) + brc = "[xshift={w},yshift=-{w}]{op}.center".format(w=width, op=op) swap_style = "swapstyle,edgestyle" if self.settings['gate_shadow']: swap_style += ",shadowed" @@ -543,10 +539,10 @@ def _swap_gate(self, lines, ctrl_lines): "\\draw[{swap_style}] ({s3})--({s4});" ).format( op=op, - s1=s1, - s2=s2, - s3=s3, - s4=s4, + s1=blc, + s2=trc, + s3=tlc, + s4=brc, line=line, pos=self.pos[line], swap_style=swap_style, @@ -580,7 +576,8 @@ 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) @@ -611,7 +608,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) @@ -637,7 +633,7 @@ 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'] @@ -654,7 +650,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'] @@ -672,7 +668,7 @@ def _gate_offset(self, gate): (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'] @@ -689,7 +685,7 @@ 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: @@ -727,11 +723,10 @@ def _op(self, line, op=None, offset=0): op = self.op_count[line] return "line{}_gate{}".format(line, 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 + 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: @@ -747,30 +742,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.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): + + 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. @@ -792,7 +787,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)) diff --git a/projectq/backends/_circuits/_to_latex_test.py b/projectq/backends/_circuits/_to_latex_test.py index c6032109a..2d2246114 100755 --- a/projectq/backends/_circuits/_to_latex_test.py +++ b/projectq/backends/_circuits/_to_latex_test.py @@ -18,6 +18,8 @@ import copy +import pytest + from projectq import MainEngine from projectq.ops import ( BasicGate, @@ -45,7 +47,7 @@ def test_tolatex(): _to_latex._header = lambda x: "H" _to_latex._body = lambda x, settings, drawing_order, draw_gates_in_parallel: x - _to_latex._footer = lambda x: "F" + _to_latex._footer = lambda: "F" latex = _to_latex.to_latex("B") assert latex == "HBF" @@ -183,6 +185,27 @@ def test_body(): 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, []) diff --git a/projectq/backends/_ibm/__init__.py b/projectq/backends/_ibm/__init__.py index 216a0e418..21c3b1789 100755 --- a/projectq/backends/_ibm/__init__.py +++ b/projectq/backends/_ibm/__init__.py @@ -13,4 +13,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 c34057817..a020d2a80 100755 --- a/projectq/backends/_ibm/_ibm.py +++ b/projectq/backends/_ibm/_ibm.py @@ -19,11 +19,12 @@ from projectq.cengines import BasicEngine from projectq.meta import get_control_count, LogicalQubitIDTag, has_negative_control from projectq.ops import NOT, H, Rx, Ry, Rz, Measure, Allocate, Deallocate, Barrier, FlushGate +from projectq.types import WeakQubitRef from ._ibm_http_client import send, retrieve -class IBMBackend(BasicEngine): +class IBMBackend(BasicEngine): # pylint: disable=too-many-instance-attributes """ The IBM Backend class, which stores the circuit, transforms it to JSON, and sends the circuit through the IBM API. @@ -39,7 +40,7 @@ def __init__( num_retries=3000, interval=1, retrieve_execution=None, - ): + ): # pylint: disable=too-many-arguments """ Initialize the Backend object. @@ -61,7 +62,8 @@ def __init__( 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 @@ -92,17 +94,12 @@ def is_available(self, cmd): if has_negative_control(cmd): return False - g = cmd.gate + gate = cmd.gate - if g == NOT and get_control_count(cmd) == 1: - return True + if get_control_count(cmd) == 1: + return gate == NOT if get_control_count(cmd) == 0: - if g == H: - 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): @@ -115,7 +112,7 @@ def _reset(self): 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. @@ -124,6 +121,9 @@ 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._clear = False @@ -140,13 +140,13 @@ def _store(self, cmd): return if gate == Measure: - assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 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 @@ -162,7 +162,6 @@ def _store(self, cmd): self.qasm += 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({})'} u_name = {'Rx': 'u3', 'Ry': 'u3', 'Rz': 'u1'} @@ -177,7 +176,6 @@ def _store(self, cmd): self.qasm += "\n{} q[{}];".format(gate_qasm, qb_pos) self._json.append({'qubits': [qb_pos], 'name': gate_name, 'params': params}) elif gate == H: - assert get_control_count(cmd) == 0 qb_pos = cmd.qubits[0][0].id self.qasm += "\nu2(0,pi/2) q[{}];".format(qb_pos) self._json.append({'qubits': [qb_pos], 'name': 'u2', 'params': [0, 3.141592653589793]}) @@ -192,7 +190,6 @@ 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( @@ -234,8 +231,8 @@ def get_probabilities(self, qureg): probability_dict = dict() for state in self._probabilities: mapped_state = ['0'] * len(qureg) - for i in range(len(qureg)): - mapped_state[i] = state[self._logical_to_physical(qureg[i].id)] + for i, val in enumerate(qureg): + mapped_state[i] = state[self._logical_to_physical(val.id)] probability = self._probabilities[state] mapped_state = "".join(mapped_state) if mapped_state not in probability_dict: @@ -244,7 +241,7 @@ def get_probabilities(self, qureg): probability_dict[mapped_state] += probability return probability_dict - def _run(self): + def _run(self): # pylint: disable=too-many-locals """ Run the circuit. @@ -254,7 +251,7 @@ def _run(self): # 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) + self.qasm += "\nmeasure q[{0}] -> c[{0}];".format(qb_loc) self._json.append({'qubits': [qb_loc], 'name': 'measure', 'memory': [qb_loc]}) # return if no operations / measurements have been performed. if self.qasm == "": @@ -288,7 +285,7 @@ def _run(self): ) counts = res['data']['counts'] # Determine random outcome - P = random.random() + random_outcome = random.random() p_sum = 0.0 measured = "" for state in counts: @@ -299,30 +296,26 @@ def _run(self): 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 - - # 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. + Receives 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 diff --git a/projectq/backends/_ibm/_ibm_http_client.py b/projectq/backends/_ibm/_ibm_http_client.py index 83d8ec58d..323256de2 100755 --- a/projectq/backends/_ibm/_ibm_http_client.py +++ b/projectq/backends/_ibm/_ibm_http_client.py @@ -13,6 +13,9 @@ # 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 does not exist and has to be deduced from the qiskit code # source at: https://github.com/Qiskit/qiskit-ibmq-provider @@ -39,7 +42,7 @@ class IBMQ(Session): """ def __init__(self, **kwargs): - super(IBMQ, self).__init__(**kwargs) # Python 2 compatibility + super().__init__(**kwargs) self.backends = dict() self.timeout = 5.0 @@ -57,7 +60,7 @@ def get_list_devices(self, verbose=False): """ list_device_url = 'Network/ibm-q/Groups/open/Projects/main/devices/v/1' argument = {'allow_redirects': True, 'timeout': (self.timeout, None)} - request = super(IBMQ, self).get(urljoin(_API_URL, list_device_url), **argument) + request = super().get(urljoin(_API_URL, list_device_url), **argument) request.raise_for_status() r_json = request.json() self.backends = dict() @@ -104,7 +107,7 @@ def can_run_experiment(self, info, device): nb_qubit_needed = info['nq'] return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed - def _authenticate(self, token=None): + def authenticate(self, token=None): """ Args: token (str): IBM quantum experience user API token. @@ -119,12 +122,12 @@ def _authenticate(self, token=None): 'json': {'apiToken': token}, 'timeout': (self.timeout, None), } - request = super(IBMQ, self).post(_AUTH_API_URL, **args) + 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): + def run(self, info, device): # pylint: disable=too-many-locals """ Run the quantum code to the IBMQ machine. Update since September 2020: only protocol available is what they call @@ -153,7 +156,7 @@ def _run(self, info, device): }, 'timeout': (self.timeout, None), } - request = super(IBMQ, self).post( + request = super().post( urljoin(_API_URL, 'Network/ibm-q/Groups/open/Projects/main/Jobs'), **json_step1, ) @@ -196,7 +199,7 @@ def _run(self, info, device): 'params': {'access_token': None}, 'timeout': (5.0, None), } - request = super(IBMQ, self).put(upload_url, **json_step2) + request = super().put(upload_url, **json_step2) request.raise_for_status() # STEP3: CONFIRM UPLOAD @@ -206,12 +209,17 @@ def _run(self, info, device): _API_URL, 'Network/ibm-q/Groups/open/Projects/main/Jobs/' + str(execution_id) + '/jobDataUploaded', ) - request = super(IBMQ, self).post(upload_data_url, **json_step3) + 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): + 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 @@ -229,7 +237,7 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover # STEP5: WAIT FOR THE JOB TO BE RUN json_step5 = {'allow_redirects': True, 'timeout': (self.timeout, None)} - request = super(IBMQ, self).get(urljoin(_API_URL, job_status_url), **json_step5) + request = super().get(urljoin(_API_URL, job_status_url), **json_step5) request.raise_for_status() r_json = request.json() acceptable_status = ['VALIDATING', 'VALIDATED', 'RUNNING'] @@ -239,7 +247,7 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover 'allow_redirects': True, 'timeout': (self.timeout, None), } - request = super(IBMQ, self).get( + request = super().get( urljoin(_API_URL, job_status_url + '/resultDownloadUrl'), **json_step6, ) @@ -252,13 +260,13 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover 'params': {'access_token': None}, 'timeout': (self.timeout, None), } - request = super(IBMQ, self).get(r_json['url'], **json_step7) + 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(IBMQ, self).post( + request = super().post( urljoin(_API_URL, job_status_url + '/resultDownloaded'), **json_step8, ) @@ -285,11 +293,11 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover class DeviceTooSmall(Exception): - pass + """Exception raised if the device is too small to run the circuit""" class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" def show_devices(token=None, verbose=False): @@ -305,11 +313,11 @@ def show_devices(token=None, verbose=False): (list) list of available devices and their properties """ ibmq_session = IBMQ() - ibmq_session._authenticate(token=token) + 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): +def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): # pylint: disable=too-many-arguments """ Retrieves a previously run job by its ID. @@ -322,9 +330,9 @@ def retrieve(device, token, jobid, num_retries=3000, interval=1, verbose=False): (dict) result form the IBMQ server """ ibmq_session = IBMQ() - ibmq_session._authenticate(token) + 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) + res = ibmq_session.get_result(device, jobid, num_retries=num_retries, interval=interval, verbose=verbose) return res @@ -336,7 +344,7 @@ def send( num_retries=3000, interval=1, verbose=False, -): +): # pylint: disable=too-many-arguments """ Sends QASM through the IBM API and runs the quantum circuit. @@ -362,7 +370,7 @@ def send( print("- Authenticating...") if token is not None: print('user API token: ' + token) - ibmq_session._authenticate(token) + ibmq_session.authenticate(token) # check if the device is online ibmq_session.get_list_devices(verbose) @@ -384,10 +392,10 @@ def send( raise DeviceTooSmall("Device is too small.") if verbose: print("- Running code: {}".format(info)) - execution_id = ibmq_session._run(info, device) + execution_id = ibmq_session.run(info, device) if verbose: print("- Waiting for results...") - res = ibmq_session._get_result( + res = ibmq_session.get_result( device, execution_id, num_retries=num_retries, @@ -406,3 +414,4 @@ def send( except KeyError as err: print("- Failed to parse response:") print(err) + return None diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index 204878276..c16dbe461 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -18,6 +18,7 @@ import math from projectq.backends._ibm import _ibm from projectq.cengines import MainEngine, BasicMapperEngine, DummyEngine +from projectq.meta import LogicalQubitIDTag from projectq.ops import ( All, Allocate, @@ -382,3 +383,22 @@ def mock_send(*args, **kwargs): 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 index 5269e8f23..dfc37dc08 100644 --- a/projectq/backends/_ionq/__init__.py +++ b/projectq/backends/_ionq/__init__.py @@ -13,6 +13,8 @@ # 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 index 6dfd52d20..191d3bd9b 100644 --- a/projectq/backends/_ionq/_ionq.py +++ b/projectq/backends/_ionq/_ionq.py @@ -81,7 +81,7 @@ def _rearrange_result(input_result, length): return ''.join(bin_input)[::-1] -class IonQBackend(BasicEngine): +class IonQBackend(BasicEngine): # pylint: disable=too-many-instance-attributes """Backend for building circuits and submitting them to the IonQ API.""" def __init__( @@ -94,7 +94,7 @@ def __init__( num_retries=3000, interval=1, retrieve_execution=None, - ): + ): # pylint: disable=too-many-arguments """Constructor for the IonQBackend. Args: @@ -196,7 +196,6 @@ def _store(self, cmd): # Create a measurement. if gate == Measure: - assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 logical_id = cmd.qubits[0][0].id for tag in cmd.tags: if isinstance(tag, LogicalQubitIDTag): @@ -211,7 +210,7 @@ def _store(self, cmd): gate_name = GATE_MAP.get(gate_type) # Daggered gates get special treatment. if isinstance(gate, DaggeredGate): - gate_name = GATE_MAP[type(gate._gate)] + 'i' + gate_name = 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: @@ -301,7 +300,7 @@ def get_probabilities(self, qureg): probability_dict[mapped_state] = probability_dict.get(mapped_state, 0) + probability return probability_dict - def _run(self): + 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: @@ -341,7 +340,7 @@ def _run(self): self._measured_ids = measured_ids = res['meas_qubit_ids'] # Determine random outcome from probable states. - P = random.random() + random_outcome = random.random() p_sum = 0.0 measured = "" star = "" @@ -353,7 +352,7 @@ def _run(self): state = _rearrange_result(int(state_int), num_measured) probability = probable_outcomes[state_int] p_sum += probability - if p_sum >= P and measured == "" or (idx == len(states) - 1): + if p_sum >= random_outcome and measured == "" or (idx == len(states) - 1): measured = state star = "*" self._probabilities[state] = probability diff --git a/projectq/backends/_ionq/_ionq_http_client.py b/projectq/backends/_ionq/_ionq_http_client.py index 8bcab285f..8f45389b9 100644 --- a/projectq/backends/_ionq/_ionq_http_client.py +++ b/projectq/backends/_ionq/_ionq_http_client.py @@ -38,7 +38,7 @@ class IonQ(Session): """A requests.Session based HTTP client for the IonQ API.""" def __init__(self, verbose=False): - super(IonQ, self).__init__() + super().__init__() self.backends = dict() self.timeout = 5.0 self.token = None @@ -93,7 +93,7 @@ def can_run_experiment(self, info, device): nb_qubit_needed = info['nq'] return nb_qubit_needed <= nb_qubit_max, nb_qubit_max, nb_qubit_needed - def _authenticate(self, token=None): + 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. @@ -108,7 +108,7 @@ def _authenticate(self, token=None): self.headers.update({'Authorization': 'apiKey {}'.format(token)}) self.token = token - def _run(self, info, device): + def run(self, info, device): """Run a circuit from ``info`` on the specified ``device``. Args: @@ -140,7 +140,7 @@ def _run(self, info, device): # _API_URL[:-1] strips the trailing slash. # TODO: Add comprehensive error parsing for non-200 responses. - req = super(IonQ, self).post(_API_URL[:-1], json=argument) + req = super().post(_API_URL[:-1], json=argument) req.raise_for_status() # Process the response. @@ -164,7 +164,7 @@ def _run(self, info, device): ) ) - def _get_result(self, device, execution_id, num_retries=3000, interval=1): + 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: @@ -203,7 +203,7 @@ def _handle_sigint_during_get_result(*_): # pragma: no cover try: for retries in range(num_retries): - req = super(IonQ, self).get(urljoin(_API_URL, execution_id)) + req = super().get(urljoin(_API_URL, execution_id)) req.raise_for_status() r_json = req.json() status = r_json['status'] @@ -261,7 +261,7 @@ def retrieve( num_retries=3000, interval=1, verbose=False, -): +): # pylint: disable=too-many-arguments """Retrieve an already submitted IonQ job. Args: @@ -279,9 +279,9 @@ def retrieve( dict: A dict with job submission results. """ ionq_session = IonQ(verbose=verbose) - ionq_session._authenticate(token) + ionq_session.authenticate(token) ionq_session.update_devices_list() - res = ionq_session._get_result( + res = ionq_session.get_result( device, jobid, num_retries=num_retries, @@ -297,7 +297,7 @@ def send( 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:: @@ -334,7 +334,7 @@ def send( print("- Authenticating...") if verbose and token is not None: # pragma: no cover print('user API token: ' + token) - ionq_session._authenticate(token) + ionq_session.authenticate(token) # check if the device is online ionq_session.update_devices_list() @@ -356,10 +356,10 @@ def send( raise DeviceTooSmall("Device is too small.") if verbose: # pragma: no cover print("- Running code: {}".format(info)) - execution_id = ionq_session._run(info, device) + execution_id = ionq_session.run(info, device) if verbose: # pragma: no cover print("- Waiting for results...") - res = ionq_session._get_result( + res = ionq_session.get_result( device, execution_id, num_retries=num_retries, @@ -383,7 +383,7 @@ def send( err_json['error'], err_json['message'], ) - ) + ) from err # Else, just print: print("- There was an error running your code:") @@ -391,6 +391,7 @@ def send( except requests.exceptions.RequestException as err: print("- Looks like something is wrong with server:") print(err) + return None __all__ = [ diff --git a/projectq/backends/_ionq/_ionq_http_client_test.py b/projectq/backends/_ionq/_ionq_http_client_test.py index 7b09a00f8..c1586569d 100644 --- a/projectq/backends/_ionq/_ionq_http_client_test.py +++ b/projectq/backends/_ionq/_ionq_http_client_test.py @@ -35,7 +35,7 @@ def no_requests(monkeypatch): def test_authenticate(): ionq_session = _ionq_http_client.IonQ() - ionq_session._authenticate('NotNone') + ionq_session.authenticate('NotNone') assert 'Authorization' in ionq_session.headers assert ionq_session.token == 'NotNone' assert ionq_session.headers['Authorization'] == 'apiKey NotNone' @@ -49,13 +49,13 @@ def user_password_input(prompt): monkeypatch.setattr('getpass.getpass', user_password_input) ionq_session = _ionq_http_client.IonQ() with pytest.raises(RuntimeError) as excinfo: - ionq_session._authenticate() + ionq_session.authenticate() assert str(excinfo.value) == 'An authentication token is required!' def test_is_online(): ionq_session = _ionq_http_client.IonQ() - ionq_session._authenticate('not none') + ionq_session.authenticate('not none') ionq_session.update_devices_list() assert ionq_session.is_online('ionq_simulator') assert ionq_session.is_online('ionq_qpu') diff --git a/projectq/backends/_ionq/_ionq_mapper.py b/projectq/backends/_ionq/_ionq_mapper.py index 9b7450b0d..24c330f0f 100644 --- a/projectq/backends/_ionq/_ionq_mapper.py +++ b/projectq/backends/_ionq/_ionq_mapper.py @@ -20,6 +20,8 @@ class BoundedQubitMapper(BasicMapperEngine): + """Maps logical qubits to a fixed number of hardware qubits""" + def __init__(self, max_qubits): super().__init__() self._qubit_idx = 0 diff --git a/projectq/backends/_printer.py b/projectq/backends/_printer.py index 69223b706..fc91ae839 100755 --- a/projectq/backends/_printer.py +++ b/projectq/backends/_printer.py @@ -13,8 +13,8 @@ # 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). +Contains a compiler engine which prints commands to stdout prior to sending them on to the next engines (see +CommandPrinter). """ import sys @@ -28,8 +28,8 @@ class CommandPrinter(BasicEngine): """ - CommandPrinter is a compiler engine which prints commands to stdout prior - to sending them on to the next compiler engine. + 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): @@ -37,14 +37,10 @@ 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) self._accept_input = accept_input @@ -53,15 +49,13 @@ def __init__(self, accept_input=True, default_measure=False, in_place=False): 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). + 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,27 +64,28 @@ 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 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: + meas = None + while meas not in ('0', '1', 1, 0): prompt = "Input measurement result (0 or 1) for qubit " + str(qubit) + ": " - m = input(prompt) + 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: @@ -98,7 +93,7 @@ def _print_cmd(self, cmd): 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) + self.main_engine.set_measurement_result(qubit, meas) else: if self._in_place: # pragma: no cover sys.stdout.write("\0\r\t\x1b[K" + str(cmd) + "\r") diff --git a/projectq/backends/_printer_test.py b/projectq/backends/_printer_test.py index 872423e26..3c06b3ed1 100755 --- a/projectq/backends/_printer_test.py +++ b/projectq/backends/_printer_test.py @@ -59,6 +59,16 @@ def test_command_printer_accept_input(monkeypatch): 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()]) diff --git a/projectq/backends/_resource.py b/projectq/backends/_resource.py index 97317778f..558b298bf 100755 --- a/projectq/backends/_resource.py +++ b/projectq/backends/_resource.py @@ -13,8 +13,8 @@ # 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. +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. """ from projectq.cengines import BasicEngine, LastEngineException @@ -25,21 +25,16 @@ 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): @@ -59,16 +54,14 @@ def __init__(self): 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). + 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) @@ -77,13 +70,15 @@ 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): + def _add_cmd(self, cmd): # pylint: disable=too-many-branches """ Add a gate to the count. """ @@ -142,9 +137,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 = [] @@ -172,13 +166,11 @@ def __str__(self): 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 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/_sim/__init__.py b/projectq/backends/_sim/__init__.py index 513e6d4b0..c0d0d5d3f 100755 --- a/projectq/backends/_sim/__init__.py +++ b/projectq/backends/_sim/__init__.py @@ -13,5 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module dedicated to simulation""" + from ._simulator import Simulator from ._classical_simulator import ClassicalSimulator diff --git a/projectq/backends/_sim/_classical_simulator.py b/projectq/backends/_sim/_classical_simulator.py index 3d93c1467..308987ca8 100755 --- a/projectq/backends/_sim/_classical_simulator.py +++ b/projectq/backends/_sim/_classical_simulator.py @@ -26,9 +26,8 @@ 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): @@ -48,17 +47,15 @@ def _convert_logical_to_mapped_qubit(self, qubit): 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 + return qubit def read_bit(self, qubit): """ Reads 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. @@ -71,17 +68,15 @@ def read_bit(self, 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 + 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. @@ -92,37 +87,34 @@ def write_bit(self, 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] + 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. + Returns 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. 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): @@ -138,23 +130,21 @@ def read_register(self, 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 + 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. 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 = [] @@ -166,41 +156,40 @@ def _write_mapped_register(self, mapped_qureg, value): """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) + or isinstance(cmd.gate, (BasicMathGate, FlushGate, XGate)) ) def receive(self, command_list): + """Forward 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: @@ -221,7 +210,8 @@ def _handle(self, cmd): 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)) diff --git a/projectq/backends/_sim/_classical_simulator_test.py b/projectq/backends/_sim/_classical_simulator_test.py index f9c33dc90..8a35d2159 100755 --- a/projectq/backends/_sim/_classical_simulator_test.py +++ b/projectq/backends/_sim/_classical_simulator_test.py @@ -164,6 +164,15 @@ def test_write_register_value_error_exception(mapper): # noqa: F811 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())]) diff --git a/projectq/backends/_sim/_cppkernels/simulator.hpp b/projectq/backends/_sim/_cppkernels/simulator.hpp index 19e4b173c..1a84723f7 100755 --- a/projectq/backends/_sim/_cppkernels/simulator.hpp +++ b/projectq/backends/_sim/_cppkernels/simulator.hpp @@ -455,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; diff --git a/projectq/backends/_sim/_pysim.py b/projectq/backends/_sim/_pysim.py index 3900b4b7b..54860cafd 100755 --- a/projectq/backends/_sim/_pysim.py +++ b/projectq/backends/_sim/_pysim.py @@ -19,31 +19,29 @@ """ import random -import numpy as _np import os +import numpy as _np _USE_REFCHECK = True if 'TRAVIS' 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) @@ -54,16 +52,13 @@ def __init__(self, rnd_seed, *args, **kwargs): 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) @@ -78,10 +73,10 @@ def measure_qubits(self, ids): Returns: List of measurement results (containing either True or False). """ - P = random.random() + 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 @@ -92,82 +87,78 @@ 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.0 - for i in range(len(self._state)): + for i, _state in enumerate(self._state): if (mask & i) != val: self._state[i] = 0.0 else: - nrm += _np.abs(self._state[i]) ** 2 + nrm += _np.abs(_state) ** 2 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, refcheck=_USE_REFCHECK) - def get_classical_value(self, ID, tol=1.0e-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: + 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 down + return state_down - def deallocate_qubit(self, ID): + 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) k = 0 - for i in range((1 << pos) * int(cv), len(self._state), (1 << (pos + 1))): + 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 @@ -175,7 +166,7 @@ def deallocate_qubit(self, ID): 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 @@ -194,15 +185,14 @@ def _get_control_mask(self, ctrlids): 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) @@ -217,16 +207,16 @@ def emulate_math(self, f, qubit_ids, ctrlqubit_ids): for i in range(0, len(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] + 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] = self._state[i] else: newstate[i] = self._state[i] @@ -272,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. @@ -285,135 +274,119 @@ 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: + 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]] + 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 in range(len(self._state)): + 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. + 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()." + "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. + 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. - 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]) # rescale the operator by s: - s = int(op_nrm + 1.0) - 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.0 while nrm_change > 1.0e-12: - coeff = (-time * 1j) / float(s * (j + 1)) + 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. + Applies 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. + Applies 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))): @@ -421,40 +394,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. + Applies 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) @@ -463,18 +434,18 @@ 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): + 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 " + "set_wavefunction(): Invalid mapping provided. Please make sure all qubits have been " "allocated previously (call eng.flush())." ) @@ -493,18 +464,18 @@ def collapse_wavefunction(self, ids, values): 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]): + 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 " + "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]] + for i, qubit_id in enumerate(ids): + pos = self._map[qubit_id] mask |= 1 << pos val |= int(values[i]) << pos nrm = 0.0 @@ -524,9 +495,8 @@ def run(self): """ Dummy function to implement 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. (Helper function for time evolution & expectation) @@ -540,6 +510,8 @@ def _apply_term(self, term, ids, ctrlids=[]): 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) diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 3647136b6..cfc2b56ec 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -104,14 +104,13 @@ def is_available(self, cmd): cmd.gate == Measure or cmd.gate == Allocate or cmd.gate == Deallocate - or isinstance(cmd.gate, BasicMathGate) - or isinstance(cmd.gate, TimeEvolution) + 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 AttributeError: @@ -133,8 +132,7 @@ def _convert_logical_to_mapped_qureg(self, qureg): 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): """ @@ -335,7 +333,7 @@ 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/ @@ -350,12 +348,13 @@ def _handle(self, cmd): """ 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: @@ -366,23 +365,23 @@ def _handle(self, cmd): 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 ( + 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) if FALLBACK_TO_PYSIM: math_fun = cmd.gate.get_math_function(cmd.qubits) @@ -410,13 +409,13 @@ def _handle(self, cmd): 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 + 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) + 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: " diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 0d3cae90f..4e6001e35 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -232,6 +232,11 @@ def test_simulator_functional_measurement(sim): 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, []) @@ -606,6 +611,10 @@ 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.0 All(H) | qubits[1:] diff --git a/projectq/cengines/__init__.py b/projectq/cengines/__init__.py index d81b59cee..9b25fde78 100755 --- a/projectq/cengines/__init__.py +++ b/projectq/cengines/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module containing all compiler engines""" + from ._basics import BasicEngine, LastEngineException, ForwarderEngine from ._cmdmodifier import CommandModifier from ._basicmapper import BasicMapperEngine diff --git a/projectq/cengines/_basicmapper.py b/projectq/cengines/_basicmapper.py index 0a70b9b26..876717138 100644 --- a/projectq/cengines/_basicmapper.py +++ b/projectq/cengines/_basicmapper.py @@ -15,37 +15,43 @@ """ Defines 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.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) + 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,8 +73,6 @@ 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)] @@ -82,5 +86,6 @@ def add_logical_id(command, old_tags=deepcopy(cmd.tags)): self.send([new_cmd]) def receive(self, command_list): + """Forward all commands to the next engine.""" for cmd in command_list: self._send_cmd_with_mapped_ids(cmd) diff --git a/projectq/cengines/_basics.py b/projectq/cengines/_basics.py index 9ac53bb6f..72ddcfaab 100755 --- a/projectq/cengines/_basics.py +++ b/projectq/cengines/_basics.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing the basic definition of a compiler engine""" + from projectq.ops import Allocate, Deallocate from projectq.types import Qubit, Qureg, WeakQubitRef from projectq.ops import Command @@ -20,35 +22,28 @@ 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, + super().__init__( ( - "\nERROR: Sending to next engine failed. " - "{} as last engine?\nIf this is legal, " - "please override 'isAvailable' to adapt its" - " behavior." + "\nERROR: Sending to next engine failed. {} as last engine?\nIf this is legal, please override" + "'isAvailable' to adapt its behavior." ).format(engine.__class__.__name__), ) -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 + 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. + This information is provided by the MainEngine, which initializes all further engines. Attributes: next_engine (BasicEngine): Next compiler engine (or the back-end). @@ -60,8 +55,7 @@ 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 @@ -69,9 +63,8 @@ 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). + 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. @@ -80,13 +73,11 @@ 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): """ @@ -117,7 +108,7 @@ 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 DirtyQubitTag # pylint: disable=import-outside-toplevel if self.is_meta_tag_supported(DirtyQubitTag): cmd.tags += [DirtyQubitTag()] @@ -126,23 +117,21 @@ 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. @@ -152,7 +141,7 @@ def deallocate_qubit(self, qubit): if qubit.id == -1: raise ValueError("Already deallocated.") - from projectq.meta import DirtyQubitTag + from projectq.meta import DirtyQubitTag # pylint: disable=import-outside-toplevel is_dirty = qubit.id in self.main_engine.dirty_qubits self.send( @@ -173,14 +162,12 @@ def is_meta_tag_supported(self, 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 while engine is not None: @@ -202,11 +189,10 @@ def send(self, 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): @@ -215,11 +201,10 @@ def __init__(self, engine, cmd_mod_fun=None): 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: diff --git a/projectq/cengines/_cmdmodifier.py b/projectq/cengines/_cmdmodifier.py index faf2440c6..c988cccd6 100755 --- a/projectq/cengines/_cmdmodifier.py +++ b/projectq/cengines/_cmdmodifier.py @@ -13,17 +13,17 @@ # 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). +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). """ -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. + 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): @@ -31,8 +31,7 @@ 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 @@ -42,17 +41,15 @@ def cmd_mod_fun(cmd): 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 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/_ibm5qubitmapper.py b/projectq/cengines/_ibm5qubitmapper.py index 3b75c329b..4f9d093e5 100755 --- a/projectq/cengines/_ibm5qubitmapper.py +++ b/projectq/cengines/_ibm5qubitmapper.py @@ -17,12 +17,14 @@ """ 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 ._basicmapper import BasicMapperEngine + + class IBM5QubitMapper(BasicMapperEngine): """ Mapper for the 5-qubit IBM backend. @@ -44,7 +46,7 @@ def __init__(self, connections=None): Resets the mapping. """ - BasicMapperEngine.__init__(self) + super().__init__() self.current_mapping = dict() self._reset() self._cmds = [] diff --git a/projectq/cengines/_linearmapper.py b/projectq/cengines/_linearmapper.py index cd2d1149d..911b5e975 100644 --- a/projectq/cengines/_linearmapper.py +++ b/projectq/cengines/_linearmapper.py @@ -15,17 +15,14 @@ """ 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 copy import deepcopy -from projectq.cengines import BasicMapperEngine from projectq.meta import LogicalQubitIDTag from projectq.ops import ( Allocate, @@ -38,14 +35,15 @@ ) from projectq.types import WeakQubitRef +from ._basicmapper import BasicMapperEngine + def return_swap_depth(swaps): """ Returns 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. @@ -62,30 +60,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. - 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. """ @@ -99,7 +94,7 @@ 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 @@ -121,38 +116,26 @@ def is_available(self, cmd): 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): """ Builds 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 compilicated 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 @@ -183,7 +166,7 @@ def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, stored_comma if len(qubit_ids) > 2 or len(qubit_ids) == 0: raise Exception("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) @@ -221,29 +204,30 @@ def return_new_mapping(num_qubits, cyclic, currently_allocated_ids, stored_comma ) @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. - 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 segements 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: @@ -328,30 +312,25 @@ def _process_two_qubit_gate(num_qubits, cyclic, qubit0, qubit1, active_qubits, s 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. - 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 @@ -427,13 +406,11 @@ def _odd_even_transposition_sort_swaps(self, old_mapping, new_mapping): 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 @@ -446,10 +423,11 @@ def _odd_even_transposition_sort_swaps(self, old_mapping, new_mapping): 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 @@ -471,7 +449,7 @@ def _odd_even_transposition_sort_swaps(self, old_mapping, new_mapping): 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. @@ -529,7 +507,7 @@ def _send_possible_commands(self): mapped_ids = list(mapped_ids) 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: @@ -543,15 +521,13 @@ 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. - 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: @@ -581,9 +557,9 @@ def _run(self): 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 @@ -613,24 +589,21 @@ def _run(self): # 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." + "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). + Receives 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/_main.py b/projectq/cengines/_main.py index 475207dd1..2c961b9a6 100755 --- a/projectq/cengines/_main.py +++ b/projectq/cengines/_main.py @@ -21,28 +21,38 @@ import traceback import weakref -from projectq.cengines import BasicEngine, BasicMapperEngine from projectq.ops import Command, FlushGate from projectq.types import WeakQubitRef from projectq.backends import Simulator +from ._basics import BasicEngine +from ._basicmapper import BasicMapperEngine + 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 _ErrorEngine: # pylint: disable=too-few-public-methods + """ + Fake compiler engine class only used to ensure gracious failure when an exception occurs in the MainEngine + constructor. + """ + + def receive(self, command_list): # pylint: disable=unused-argument + """No-op""" -class MainEngine(BasicEngine): +class MainEngine(BasicEngine): # pylint: disable=too-many-instance-attributes """ - The MainEngine class provides all functionality of the main compiler - engine. + 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). + 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). @@ -58,14 +68,13 @@ def __init__(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). @@ -103,12 +112,18 @@ def __init__(self, backend=None, engine_list=None, verbose=False): LocalOptimizer(3)] eng = MainEngine(Simulator(), engines) """ - BasicEngine.__init__(self) + super().__init__() + self.active_qubits = weakref.WeakSet() + self._measurements = dict() + self.dirty_qubits = set() + self.verbose = verbose + self.main_engine = self 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" @@ -116,9 +131,11 @@ def __init__(self, backend=None, engine_list=None, verbose=False): "E.g. MainEngine(backend=Simulator) instead of \n" " 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() @@ -127,26 +144,29 @@ 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: + self.next_engine = _ErrorEngine() raise UnsupportedEngineError("More than one mapper engine is not supported.") else: + 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(set(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" @@ -161,11 +181,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): @@ -173,9 +188,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 @@ -188,8 +202,7 @@ 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) @@ -202,23 +215,19 @@ def set_measurement_result(self, qubit, value): """ 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 + setMeasurementResult). Args: qubit (BasicQubit): Qubit of which to get the measurement result. @@ -236,17 +245,13 @@ def get_measurement_result(self, 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): """ @@ -276,28 +281,25 @@ def send(self, command_list): """ try: self.next_engine.receive(command_list) - except Exception: + 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 [qb for qb in self.active_qubits if qb is not None]: diff --git a/projectq/cengines/_main_test.py b/projectq/cengines/_main_test.py index 078ccad6e..3178cd5c7 100755 --- a/projectq/cengines/_main_test.py +++ b/projectq/cengines/_main_test.py @@ -95,7 +95,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() diff --git a/projectq/cengines/_manualmapper.py b/projectq/cengines/_manualmapper.py index 75364efdb..4af0122ac 100755 --- a/projectq/cengines/_manualmapper.py +++ b/projectq/cengines/_manualmapper.py @@ -15,45 +15,42 @@ """ Contains a compiler engine to add mapping information """ -from projectq.cengines import BasicMapperEngine +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() 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/_optimize.py b/projectq/cengines/_optimize.py index cd9c9b7b0..39762bd6a 100755 --- a/projectq/cengines/_optimize.py +++ b/projectq/cengines/_optimize.py @@ -16,60 +16,68 @@ Contains a local optimizer engine. """ -from projectq.cengines import BasicEngine +import warnings + from projectq.ops import FlushGate, FastForwardingGate, 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. + 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) + super().__init__() self._l = dict() # dict of lists containing operations for each qubit - self._m = m # wait for m gates before sending on + + 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): + 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] # temporary label for readability - for i in range(min(n, len(il))): # loop over first n operations + 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] 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:] + 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?).") @@ -77,19 +85,19 @@ def _send_qubit_pipeline(self, idx, n): # --> 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] @@ -101,8 +109,8 @@ def _get_gate_indices(self, idx, i, IDs): cmd = self._l[idx][i] 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 @@ -125,12 +133,11 @@ def _optimize(self, idx, lim=None): # 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 in range(len(qubitids)): + for j, qubit_id in enumerate(qubitids): new_list = ( - self._l[qubitids[j]][0 : gid[j]] # noqa: E203 - + self._l[qubitids[j]][gid[j] + 1 :] # noqa: E203 + self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 1 :] # noqa: E203 # noqa: E203 ) - self._l[qubitids[j]] = new_list + self._l[qubitids[j]] = new_list # pylint: disable=undefined-loop-variable i = 0 limit -= 1 continue @@ -145,17 +152,16 @@ def _optimize(self, idx, lim=None): # 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)): + for j, qubit_id in enumerate(qubitids): new_list = ( - self._l[qubitids[j]][0 : gid[j]] # noqa: E203 - + self._l[qubitids[j]][gid[j] + 2 :] # noqa: E203 + self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 # noqa: E203 ) - self._l[qubitids[j]] = new_list + self._l[qubit_id] = new_list i = 0 limit -= 2 continue @@ -169,18 +175,18 @@ def _optimize(self, idx, lim=None): 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 + for j, qubit_id in enumerate(qubitids): + self._l[qubit_id][gid[j]] = merged_command new_list = ( - self._l[qubitids[j]][0 : gid[j] + 1] # noqa: E203 - + self._l[qubitids[j]][gid[j] + 2 :] # noqa: E203 + self._l[qubit_id][0 : gid[j] + 1] # noqa: E203 + + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 ) - self._l[qubitids[j]] = new_list + self._l[qubit_id] = new_list i = 0 limit -= 1 continue @@ -197,13 +203,13 @@ def _check_and_send(self): """ for i in self._l: if ( - len(self._l[i]) >= self._m + 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) + 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() @@ -221,10 +227,10 @@ def _cache_cmd(self, cmd): 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() @@ -240,10 +246,11 @@ def receive(self, command_list): self._send_qubit_pipeline(idx, len(self._l[idx])) new_dict = dict() for idx in self._l: - if len(self._l[idx]) > 0: + if len(self._l[idx]) > 0: # pragma: no cover new_dict[idx] = self._l[idx] self._l = new_dict - assert self._l == dict() + if self._l != dict(): # 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 f4cc651ff..fc2ac96c0 100755 --- a/projectq/cengines/_optimize_test.py +++ b/projectq/cengines/_optimize_test.py @@ -15,6 +15,9 @@ """Tests for projectq.cengines._optimize.py.""" import math + +import pytest + from projectq import MainEngine from projectq.cengines import DummyEngine from projectq.ops import ( @@ -31,8 +34,20 @@ from projectq.cengines import _optimize +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 + + 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 @@ -63,7 +78,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 @@ -78,7 +93,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 @@ -93,7 +108,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) @@ -121,7 +136,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 @@ -136,7 +151,7 @@ def test_local_optimizer_mergeable_gates(): def test_local_optimizer_identity_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 diff --git a/projectq/cengines/_replacer/_decomposition_rule.py b/projectq/cengines/_replacer/_decomposition_rule.py index b53016eed..f7bee3d67 100755 --- a/projectq/cengines/_replacer/_decomposition_rule.py +++ b/projectq/cengines/_replacer/_decomposition_rule.py @@ -13,14 +13,16 @@ # 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: +class DecompositionRule: # pylint: disable=too-few-public-methods """ A rule for breaking down specific gates into sequences of simpler gates. """ diff --git a/projectq/cengines/_replacer/_decomposition_rule_set.py b/projectq/cengines/_replacer/_decomposition_rule_set.py index 45c004e6c..aba4eba6b 100755 --- a/projectq/cengines/_replacer/_decomposition_rule_set.py +++ b/projectq/cengines/_replacer/_decomposition_rule_set.py @@ -13,6 +13,8 @@ # 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 @@ -40,6 +42,9 @@ def __init__(self, rules=None, modules=None): ) def add_decomposition_rules(self, rules): + """ + Add some decomposition rules to a decomposition rule set + """ for rule in rules: self.add_decomposition_rule(rule) @@ -57,7 +62,7 @@ def add_decomposition_rule(self, rule): self.decompositions[cls].append(decomp_obj) -class ModuleWithDecompositionRuleSet: # pragma: no cover +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. @@ -72,7 +77,7 @@ def __init__(self, all_defined_decomposition_rules): self.all_defined_decomposition_rules = all_defined_decomposition_rules -class _Decomposition(object): +class _Decomposition: # pylint: disable=too-few-public-methods """ The Decomposition class can be used to register a decomposition rule (by calling register_decomposition) diff --git a/projectq/cengines/_replacer/_replacer.py b/projectq/cengines/_replacer/_replacer.py index 07ea5d3fb..0a9a4d684 100755 --- a/projectq/cengines/_replacer/_replacer.py +++ b/projectq/cengines/_replacer/_replacer.py @@ -26,34 +26,32 @@ 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). + 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. + 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) 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. + 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. @@ -72,15 +70,14 @@ 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). + 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_rule_se, decomposition_chooser=lambda cmd, decomposition_list: decomposition_list[0], ): """ @@ -109,13 +106,12 @@ def decomposition_chooser(cmd, decomp_list): """ BasicEngine.__init__(self) 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). + 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. @@ -123,7 +119,7 @@ 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: # First check for a decomposition rules of the gate class, then @@ -133,7 +129,7 @@ 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 + 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. @@ -152,13 +148,13 @@ def _process_command(self, cmd): if level < len(gate_mro): class_name = gate_mro[level].__name__ try: - potential_decomps = [d for d in rules[class_name]] + potential_decomps = 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) + 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 @@ -170,9 +166,9 @@ def _process_command(self, cmd): 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) + for decomp in potential_decomps: + if decomp.check(cmd): + decomp_list.append(decomp) if len(decomp_list) != 0: break diff --git a/projectq/cengines/_swapandcnotflipper.py b/projectq/cengines/_swapandcnotflipper.py index 481dd7365..dfb87b7a3 100755 --- a/projectq/cengines/_swapandcnotflipper.py +++ b/projectq/cengines/_swapandcnotflipper.py @@ -19,10 +19,12 @@ """ 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 ._basics import BasicEngine, ForwarderEngine +from ._cmdmodifier import CommandModifier + class SwapAndCNOTFlipper(BasicEngine): """ @@ -46,7 +48,7 @@ def __init__(self, connectivity): 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): @@ -59,7 +61,7 @@ def is_available(self, cmd): """ return self._is_swap(cmd) or self.next_engine.is_available(cmd) - def _is_cnot(self, cmd): + def _is_cnot(self, cmd): # pylint: disable=no-self-use """ Check if the command corresponds to a CNOT (controlled NOT gate). @@ -68,7 +70,7 @@ def _is_cnot(self, cmd): """ return isinstance(cmd.gate, NOT.__class__) and get_control_count(cmd) == 1 - def _is_swap(self, cmd): + def _is_swap(self, cmd): # pylint: disable=no-self-use """ Check if the command corresponds to a Swap gate. @@ -133,7 +135,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]] diff --git a/projectq/cengines/_swapandcnotflipper_test.py b/projectq/cengines/_swapandcnotflipper_test.py index fdea4fdd7..61256aa52 100755 --- a/projectq/cengines/_swapandcnotflipper_test.py +++ b/projectq/cengines/_swapandcnotflipper_test.py @@ -18,9 +18,11 @@ from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import All, H, CNOT, X, Swap +from projectq.ops import All, H, CNOT, X, Swap, Command from projectq.meta import Control, Compute, Uncompute, ComputeTag, UncomputeTag -from projectq.cengines import _swapandcnotflipper +from projectq.types import WeakQubitRef + +from . import _swapandcnotflipper def test_swapandcnotflipper_missing_connection(): @@ -31,6 +33,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() diff --git a/projectq/cengines/_tagremover.py b/projectq/cengines/_tagremover.py index 213f2ed6c..60da9b0d8 100755 --- a/projectq/cengines/_tagremover.py +++ b/projectq/cengines/_tagremover.py @@ -13,25 +13,24 @@ # 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, ...) +Contains a TagRemover engine, which 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). + 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) + 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. @@ -39,19 +38,21 @@ def __init__(self, tags=[ComputeTag, UncomputeTag]): 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('tags should be a list! Got: {}'.format(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 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 9f84fce7e..d22369762 100755 --- a/projectq/cengines/_tagremover_test.py +++ b/projectq/cengines/_tagremover_test.py @@ -14,6 +14,8 @@ # limitations under the License. """Tests for projectq.cengines._tagremover.py.""" +import pytest + from projectq import MainEngine from projectq.meta import ComputeTag, UncomputeTag from projectq.ops import Command, H @@ -27,6 +29,11 @@ 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("")]) diff --git a/projectq/cengines/_testengine.py b/projectq/cengines/_testengine.py index 106cee637..eace5744b 100755 --- a/projectq/cengines/_testengine.py +++ b/projectq/cengines/_testengine.py @@ -15,25 +15,34 @@ """TestEngine and DummyEngine.""" from copy import deepcopy -from projectq.cengines import BasicEngine 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. + 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. """ def __init__(self): - BasicEngine.__init__(self) + 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] maxidx = int(0) @@ -43,7 +52,7 @@ def cache_cmd(self, cmd): # if so, increase size of list to account for these qubits 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 @@ -51,17 +60,19 @@ def cache_cmd(self, cmd): self._l[qubit_id] += [cmd] def receive(self, command_list): + """ + Receives 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): return False @@ -69,7 +80,7 @@ def __eq__(self, other): if len(self._l[i]) != 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]): + if not _compare_cmds(self._l[i][j], other._l[i][j]): return False return True @@ -90,11 +101,10 @@ 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): @@ -105,17 +115,23 @@ def __init__(self, save_commands=False): 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): + """ + Receives 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/_twodmapper.py b/projectq/cengines/_twodmapper.py index 2c068c4ad..f3bf4b5d3 100644 --- a/projectq/cengines/_twodmapper.py +++ b/projectq/cengines/_twodmapper.py @@ -15,12 +15,10 @@ """ 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 @@ -29,7 +27,6 @@ import networkx as nx -from projectq.cengines import BasicMapperEngine, LinearMapper, return_swap_depth from projectq.meta import LogicalQubitIDTag from projectq.ops import ( AllocateQubitGate, @@ -41,12 +38,15 @@ from projectq.types import WeakQubitRef -class GridMapper(BasicMapperEngine): +from ._basicmapper import BasicMapperEngine +from ._linearmapper import LinearMapper, return_swap_depth + + +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 | | @@ -54,39 +54,33 @@ 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__( + def __init__( # pylint: disable=too-many-arguments self, num_rows, num_columns, mapped_ids_to_backend_ids=None, storage=1000, - optimization_function=lambda x: return_swap_depth(x), + optimization_function=return_swap_depth, num_optimization_steps=50, ): """ @@ -95,27 +89,19 @@ def __init__( 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 @@ -193,25 +179,19 @@ def is_available(self, cmd): 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. - 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: @@ -236,8 +216,7 @@ def _return_new_mapping(self): 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 @@ -254,8 +233,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 = [] @@ -301,30 +279,29 @@ def _sort_within_columns(self, final_positions, key): 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 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): + class Position: # pylint: disable=too-few-public-methods """Custom Container.""" - def __init__( + def __init__( # pylint: disable=too-many-arguments self, current_row, current_column, @@ -375,20 +352,19 @@ def __init__( 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 + # 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 @@ -398,8 +374,7 @@ def __init__( 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) @@ -423,7 +398,7 @@ def __init__( 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: @@ -440,12 +415,11 @@ def __init__( 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. - 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: @@ -508,10 +482,8 @@ def _send_possible_commands(self): 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: @@ -520,15 +492,13 @@ 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. - 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: @@ -573,9 +543,9 @@ def _run(self): 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 @@ -609,24 +579,21 @@ def _run(self): # 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." + "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). + Receives 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/libs/__init__.py b/projectq/libs/__init__.py index 16fc4afdf..0690f2b10 100755 --- a/projectq/libs/__init__.py +++ b/projectq/libs/__init__.py @@ -12,3 +12,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/_histogram.py b/projectq/libs/hist/_histogram.py index 849bddbd4..85c0be1b3 100644 --- a/projectq/libs/hist/_histogram.py +++ b/projectq/libs/hist/_histogram.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Functions to plot a histogram of measured data""" + from __future__ import print_function import matplotlib.pyplot as plt @@ -39,11 +41,11 @@ def histogram(backend, qureg): Don't forget to call eng.flush() before using this function. """ qubit_list = [] - for q in qureg: - if isinstance(q, list): - qubit_list.extend(q) + for qb in qureg: + if isinstance(qb, list): + qubit_list.extend(qb) else: - qubit_list.append(q) + qubit_list.append(qb) if len(qubit_list) > 5: print('Warning: For {0} qubits there are 2^{0} different outcomes'.format(len(qubit_list))) diff --git a/projectq/libs/math/_constantmath.py b/projectq/libs/math/_constantmath.py index f8f693d85..25eb6def7 100755 --- a/projectq/libs/math/_constantmath.py +++ b/projectq/libs/math/_constantmath.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing constant math quantum operations""" + import math try: @@ -26,7 +28,7 @@ # 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. @@ -38,24 +40,25 @@ def add_constant(eng, c, quint): 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. """ - 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 @@ -64,7 +67,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] @@ -72,12 +75,12 @@ 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., @@ -86,29 +89,34 @@ def mul_by_constant_modN(eng, c, N, quint_in): (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 4de8e430d..83eaf060e 100755 --- a/projectq/libs/math/_constantmath_test.py +++ b/projectq/libs/math/_constantmath_test.py @@ -54,6 +54,18 @@ def eng(): 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) diff --git a/projectq/libs/math/_default_rules.py b/projectq/libs/math/_default_rules.py index 89e3548e5..60e25dd8b 100755 --- a/projectq/libs/math/_default_rules.py +++ b/projectq/libs/math/_default_rules.py @@ -59,31 +59,31 @@ 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): @@ -92,17 +92,17 @@ def _replace_addquantum(cmd): quint_a = cmd.qubits[0] quint_b = cmd.qubits[1] if len(cmd.qubits) == 3: - c = cmd.qubits[2] - add_quantum(eng, quint_a, quint_b, c) + 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: - c = cmd.qubits[2] + carry = cmd.qubits[2] with Control(eng, cmd.control_qubits): - quantum_conditional_add_carry(eng, quint_a, quint_b, cmd.control_qubits, c) + 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) @@ -126,10 +126,10 @@ def _replace_comparator(cmd): eng = cmd.engine quint_a = cmd.qubits[0] quint_b = cmd.qubits[1] - c = cmd.qubits[2] + carry = cmd.qubits[2] with Control(eng, cmd.control_qubits): - comparator(eng, quint_a, quint_b, c) + comparator(eng, quint_a, quint_b, carry) def _replace_quantumdivision(cmd): diff --git a/projectq/libs/math/_gates.py b/projectq/libs/math/_gates.py index 794281432..45b2bc1fe 100755 --- a/projectq/libs/math/_gates.py +++ b/projectq/libs/math/_gates.py @@ -33,7 +33,7 @@ class AddConstant(BasicMathGate): 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. @@ -44,7 +44,7 @@ def __init__(self, a): corresponding function, so it can be emulated efficiently. """ BasicMathGate.__init__(self, lambda x: ((x + a),)) - self.a = a + self.a = a # pylint: disable=invalid-name def get_inverse(self): """ @@ -65,7 +65,7 @@ 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. @@ -118,7 +118,7 @@ def __init__(self, a, N): corresponding function, so it can be emulated efficiently. """ BasicMathGate.__init__(self, lambda x: ((x + a) % N,)) - self.a = a + self.a = a # pylint: disable=invalid-name self.N = N def __str__(self): @@ -141,7 +141,7 @@ 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. @@ -195,7 +195,7 @@ class MultiplyByConstantModN(BasicMathGate): * 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. @@ -208,7 +208,7 @@ def __init__(self, a, N): corresponding function, so it can be emulated efficiently. """ BasicMathGate.__init__(self, lambda x: ((a * x) % N,)) - self.a = a + self.a = a # pylint: disable=invalid-name self.N = N def __str__(self): @@ -258,12 +258,12 @@ def __ne__(self, other): return not self.__eq__(other) def get_math_function(self, qubits): - n = len(qubits[0]) + n_qubits = len(qubits[0]) - def math_fun(a): + def math_fun(a): # pylint: disable=invalid-name a[1] = a[0] + a[1] - if len(bin(a[1])[2:]) > n: - a[1] = a[1] % (2 ** n) + 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 @@ -296,7 +296,7 @@ def __str__(self): return "_InverseAddQuantum" def get_math_function(self, qubits): - def math_fun(a): + def math_fun(a): # pylint: disable=invalid-name if len(a) == 3: # Flip the last bit of the carry register a[2] ^= 1 @@ -326,11 +326,11 @@ class SubtractQuantumGate(BasicMathGate): def __init__(self): """ - Initializes the gate to its base class, BasicMathGate, with the - corresponding function, so it can be emulated efficiently. + Initializes the gate to its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. """ - def subtract(a, b): + def subtract(a, b): # pylint: disable=invalid-name return (a, b - a) BasicMathGate.__init__(self, subtract) @@ -349,8 +349,7 @@ def __ne__(self, other): 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 AddQuantum @@ -360,8 +359,7 @@ def get_inverse(self): class ComparatorQuantumGate(BasicMathGate): """ - Flips a compare qubit if the binary value of first imput is higher than - the second input. + Flips a compare qubit if the binary value of first imput 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 @@ -379,11 +377,12 @@ class ComparatorQuantumGate(BasicMathGate): def __init__(self): """ - Initializes the gate and its base class, BasicMathGate, with the - corresponding function, so it can be emulated efficiently. + Initializes the gate and its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. """ - def compare(a, b, c): + def compare(a, b, c): # pylint: disable=invalid-name + # pylint: disable=invalid-name if b < a: if c == 0: c = 1 @@ -417,11 +416,9 @@ def get_inverse(self): class DivideQuantumGate(BasicMathGate): """ - Divides 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. + Divides 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. @@ -444,17 +441,16 @@ class DivideQuantumGate(BasicMathGate): def __init__(self): """ - Initializes the gate and its base class, BasicMathGate, with the - corresponding function, so it can be emulated efficiently. + Initializes 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) - else: - quotient = remainder + dividend // divisor - return ((dividend - (quotient * divisor)), quotient, divisor) + quotient = remainder + dividend // divisor + return ((dividend - (quotient * divisor)), quotient, divisor) BasicMathGate.__init__(self, division) @@ -479,8 +475,7 @@ def __ne__(self, other): class _InverseDivideQuantumGate(BasicMathGate): """ - Internal gate glass to support emulation for inverse - division. + Internal gate glass to support emulation for inverse division. """ def __init__(self): @@ -500,11 +495,10 @@ def __str__(self): class MultiplyQuantumGate(BasicMathGate): """ - Multiplies 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. + Multiplies 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 @@ -520,11 +514,11 @@ class MultiplyQuantumGate(BasicMathGate): def __init__(self): """ - Initializes the gate and its base class, BasicMathGate, with the - corresponding function, so it can be emulated efficiently. + Initializes the gate and its base class, BasicMathGate, with the corresponding function, so it can be emulated + efficiently. """ - def multiply(a, b, c): + def multiply(a, b, c): # pylint: disable=invalid-name return (a, b, c + a * b) BasicMathGate.__init__(self, multiply) @@ -550,12 +544,11 @@ def get_inverse(self): class _InverseMultiplyQuantumGate(BasicMathGate): """ - Internal gate glass to support emulation for inverse - multiplication. + Internal gate glass to support emulation for inverse multiplication. """ def __init__(self): - def inverse_multiplication(a, b, c): + def inverse_multiplication(a, b, c): # pylint: disable=invalid-name return (a, b, c - a * b) BasicMathGate.__init__(self, inverse_multiplication) diff --git a/projectq/libs/math/_quantummath.py b/projectq/libs/math/_quantummath.py index 8e9acfcc2..bb86329fd 100644 --- a/projectq/libs/math/_quantummath.py +++ b/projectq/libs/math/_quantummath.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Definition of some mathematical quantum operations""" + from projectq.ops import All, X, CNOT from projectq.meta import Control from ._gates import AddQuantum, SubtractQuantum @@ -38,43 +40,44 @@ def add_quantum(eng, quint_a, quint_b, carry=None): .. rubric:: References - Quantum addition using ripple carry from: - https://arxiv.org/pdf/0910.2530.pdf + Quantum addition using ripple carry from: https://arxiv.org/pdf/0910.2530.pdf """ # pylint: disable = pointless-statement - assert len(quint_a) == len(quint_b) - assert carry and len(carry) == 1 or not carry + 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 = len(quint_a) + 1 + n_qubits = len(quint_a) + 1 - for i in range(1, n - 1): + for i in range(1, n_qubits - 1): CNOT | (quint_a[i], quint_b[i]) if carry: - CNOT | (quint_a[n - 2], carry) + CNOT | (quint_a[n_qubits - 2], carry) - for j in range(n - 3, 0, -1): + for j in range(n_qubits - 3, 0, -1): CNOT | (quint_a[j], quint_a[j + 1]) - for k in range(0, n - 2): + 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 - 2], quint_b[n - 2]]): + with Control(eng, [quint_a[n_qubits - 2], quint_b[n_qubits - 2]]): X | carry - for l in range(n - 2, 0, -1): # noqa: E741 - CNOT | (quint_a[l], quint_b[l]) - with Control(eng, [quint_a[l - 1], quint_b[l - 1]]): - X | quint_a[l] + 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 m in range(1, n - 2): - CNOT | (quint_a[m], quint_a[m + 1]) + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) - for n in range(0, n - 1): - CNOT | (quint_a[n], quint_b[n]) + 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): @@ -91,11 +94,9 @@ def subtract_quantum(eng, quint_a, quint_b): 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. + 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. @@ -107,31 +108,32 @@ def subtract_quantum(eng, quint_a, quint_b): """ # pylint: disable = pointless-statement, expression-not-assigned - assert len(quint_a) == len(quint_b) - n = len(quint_a) + 1 + 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 - 1): + for i in range(1, n_qubits - 1): CNOT | (quint_a[i], quint_b[i]) - for j in range(n - 3, 0, -1): + for j in range(n_qubits - 3, 0, -1): CNOT | (quint_a[j], quint_a[j + 1]) - for k in range(0, n - 2): + for k in range(0, n_qubits - 2): with Control(eng, [quint_a[k], quint_b[k]]): X | (quint_a[k + 1]) - for l in range(n - 2, 0, -1): # noqa: E741 - CNOT | (quint_a[l], quint_b[l]) - with Control(eng, [quint_a[l - 1], quint_b[l - 1]]): - X | quint_a[l] + 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 m in range(1, n - 2): - CNOT | (quint_a[m], quint_a[m + 1]) + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) - for n in range(0, n - 1): - CNOT | (quint_a[n], quint_b[n]) + for n_qubits in range(0, n_qubits - 1): + CNOT | (quint_a[n_qubits], quint_b[n_qubits]) All(X) | quint_b @@ -148,7 +150,8 @@ def inverse_add_quantum_carry(eng, quint_a, quint_b): # pylint: disable = pointless-statement, expression-not-assigned # pylint: disable = unused-argument - assert len(quint_a) == len(quint_b[0]) + 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] @@ -167,8 +170,7 @@ def comparator(eng, quint_a, quint_b, comp): 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) + (only works if quint_a and quint_b are the same size and the comparator is 1 qubit) Args: eng (MainEngine): ProjectQ MainEngine @@ -177,48 +179,47 @@ def comparator(eng, quint_a, quint_b, comp): 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. + 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 - assert len(quint_a) == len(quint_b) - assert len(comp) == 1 + 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 = len(quint_a) + 1 + n_qubits = len(quint_a) + 1 All(X) | quint_b - for i in range(1, n - 1): + for i in range(1, n_qubits - 1): CNOT | (quint_a[i], quint_b[i]) - CNOT | (quint_a[n - 2], comp) + CNOT | (quint_a[n_qubits - 2], comp) - for j in range(n - 3, 0, -1): + for j in range(n_qubits - 3, 0, -1): CNOT | (quint_a[j], quint_a[j + 1]) - for k in range(0, n - 2): + 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 - 2], quint_b[n - 2]]): + with Control(eng, [quint_a[n_qubits - 2], quint_b[n_qubits - 2]]): X | comp - for k in range(0, n - 2): + 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 - 3, 0, -1): + for j in range(n_qubits - 3, 0, -1): CNOT | (quint_a[j], quint_a[j + 1]) - for i in range(1, n - 1): + for i in range(1, n_qubits - 1): CNOT | (quint_a[i], quint_b[i]) All(X) | quint_b @@ -249,35 +250,37 @@ def quantum_conditional_add(eng, quint_a, quint_b, conditional): """ # pylint: disable = pointless-statement, expression-not-assigned - assert len(quint_a) == len(quint_b) - assert len(conditional) == 1 + 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 = len(quint_a) + 1 + n_qubits = len(quint_a) + 1 - for i in range(1, n - 1): + for i in range(1, n_qubits - 1): CNOT | (quint_a[i], quint_b[i]) - for i in range(n - 2, 1, -1): + for i in range(n_qubits - 2, 1, -1): CNOT | (quint_a[i - 1], quint_a[i]) - for k in range(0, n - 2): + 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 - 2], conditional[0]]): - X | quint_b[n - 2] + with Control(eng, [quint_a[n_qubits - 2], conditional[0]]): + X | quint_b[n_qubits - 2] - for l in range(n - 2, 0, -1): # noqa: E741 - with Control(eng, [quint_a[l - 1], quint_b[l - 1]]): - X | quint_a[l] - with Control(eng, [quint_a[l - 1], conditional[0]]): - X | (quint_b[l - 1]) + 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 m in range(1, n - 2): - CNOT | (quint_a[m], quint_a[m + 1]) + for j in range(1, n_qubits - 2): + CNOT | (quint_a[j], quint_a[j + 1]) - for o in range(1, n - 1): - CNOT | (quint_a[o], quint_b[o]) + for k in range(1, n_qubits - 1): + CNOT | (quint_a[k], quint_b[k]) def quantum_division(eng, dividend, remainder, divisor): @@ -302,36 +305,35 @@ def quantum_division(eng, dividend, remainder, divisor): Quantum Restoring Integer Division from: https://arxiv.org/pdf/1609.01241.pdf. """ - # pylint: disable = pointless-statement, expression-not-assigned - # The circuit consits of three parts # i) leftshift # ii) subtraction # iii) conditional add operation. - assert len(dividend) == len(remainder) == len(divisor) + if not len(dividend) == len(remainder) == len(divisor): + raise ValueError('Size mismatch in dividend, divisor and remainder!') j = len(remainder) - n = len(dividend) + n_dividend = len(dividend) while j != 0: combined_reg = [] - combined_reg.append(dividend[n - 1]) + combined_reg.append(dividend[n_dividend - 1]) - for i in range(0, n - 1): + for i in range(0, n_dividend - 1): combined_reg.append(remainder[i]) - SubtractQuantum | (divisor[0:n], combined_reg) - CNOT | (combined_reg[n - 1], remainder[n - 1]) - with Control(eng, remainder[n - 1]): - AddQuantum | (divisor[0:n], combined_reg) - X | remainder[n - 1] + 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 - 1]) - dividend.insert(0, remainder[n]) - del remainder[n] - del dividend[n] + remainder.insert(0, dividend[n_dividend - 1]) + dividend.insert(0, remainder[n_dividend]) + del remainder[n_dividend] + del dividend[n_dividend] j -= 1 @@ -348,14 +350,13 @@ def inverse_quantum_division(eng, remainder, quotient, divisor): remainder (list): Quantum register (or list of qubits) divisor (list): Quantum register (or list of qubits) """ - # pylint: disable = pointless-statement, expression-not-assigned - - assert len(remainder) == len(quotient) == len(divisor) + if not len(quotient) == len(remainder) == len(divisor): + raise ValueError('Size mismatch in quotient, divisor and remainder!') j = 0 - n = len(quotient) + n_quotient = len(quotient) - while j != n: + while j != n_quotient: X | quotient[0] with Control(eng, quotient[0]): SubtractQuantum | (divisor, remainder) @@ -363,14 +364,14 @@ def inverse_quantum_division(eng, remainder, quotient, divisor): AddQuantum | (divisor, remainder) - remainder.insert(n, quotient[0]) - quotient.insert(n, remainder[0]) + 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): +def quantum_conditional_add_carry(eng, quint_a, quint_b, ctrl, z): # pylint: disable=invalid-name """ Adds up two quantum integers if the control qubit is |1>, i.e., @@ -396,53 +397,53 @@ def quantum_conditional_add_carry(eng, quint_a, quint_b, ctrl, z): .. rubric:: References - Quantum conditional add with no input carry from: - https://arxiv.org/pdf/1706.05113.pdf + Quantum conditional add with no input carry from: https://arxiv.org/pdf/1706.05113.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(ctrl) != 1: + raise ValueError('Only a single control qubit is allowed!') + if len(z) != 2: + raise ValueError('Z quantum register must have 2 qubits!') - assert len(quint_a) == len(quint_b) - assert len(ctrl) == 1 - assert len(z) == 2 + n_a = len(quint_a) - n = len(quint_a) - - for i in range(1, n): + for i in range(1, n_a): CNOT | (quint_a[i], quint_b[i]) - with Control(eng, [quint_a[n - 1], ctrl[0]]): + with Control(eng, [quint_a[n_a - 1], ctrl[0]]): X | z[0] - for j in range(n - 2, 0, -1): + for j in range(n_a - 2, 0, -1): CNOT | (quint_a[j], quint_a[j + 1]) - for k in range(0, n - 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 - 1], quint_a[n - 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 - 1], quint_a[n - 1]]): + with Control(eng, [quint_b[n_a - 1], quint_a[n_a - 1]]): X | z[1] - for l in range(n - 1, 0, -1): # noqa: E741 - with Control(eng, [ctrl[0], quint_a[l]]): - X | quint_b[l] - with Control(eng, [quint_a[l - 1], quint_b[l - 1]]): - X | quint_a[l] + 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 m in range(1, n - 1): - CNOT | (quint_a[m], quint_a[m + 1]) + for j in range(1, n_a - 1): + CNOT | (quint_a[j], quint_a[j + 1]) - for n in range(1, n): - CNOT | (quint_a[n], quint_b[n]) + 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): @@ -451,8 +452,7 @@ def quantum_multiplication(eng, quint_a, quint_b, product): |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). + (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 @@ -462,37 +462,37 @@ def quantum_multiplication(eng, quint_a, quint_b, product): the result Notes: - Ancilla: 2n + 1, size: 7n^2 - 9n + 4, toffoli: 5n^2 - 4n, - depth: 3n^2 - 2. + 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. """ - # pylint: disable = pointless-statement, expression-not-assigned + n_a = len(quint_a) - assert len(quint_a) == len(quint_b) - n = len(quint_a) - assert len(product) == ((2 * n) + 1) + 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): + 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 - 1)], # noqa: E203 - product[1:n], - [product[n + 1], product[n + 2]], + 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): + for j in range(2, n_a): with Control(eng, quint_b[j]): AddQuantum | ( - quint_a[0 : (n - 1)], # noqa: E203 - product[(0 + j) : (n - 1 + j)], # noqa: E203 - [product[n + j], product[n + j + 1]], + 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]], ) @@ -502,37 +502,36 @@ def inverse_quantum_multiplication(eng, quint_a, quint_b, product): |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) + (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 + product (list): Quantum register (or list of qubits) storing the result """ - # pylint: disable = pointless-statement, expression-not-assigned + n_a = len(quint_a) - assert len(quint_a) == len(quint_b) - n = len(quint_a) - assert len(product) == ((2 * n) + 1) + 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): + for j in range(2, n_a): with Control(eng, quint_b[j]): SubtractQuantum | ( - quint_a[0 : (n - 1)], # noqa: E203 - product[(0 + j) : (n - 1 + j)], # noqa: E203 - [product[n + j], product[n + j + 1]], + 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): + 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 - 1)], # noqa: E203 - product[1:n], - [product[n + 1], product[n + 2]], + 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 index ac936691a..c51fd81b6 100644 --- a/projectq/libs/math/_quantummath_test.py +++ b/projectq/libs/math/_quantummath_test.py @@ -31,7 +31,7 @@ MultiplyQuantum, ) -from projectq.meta import Control, Compute, Uncompute +from projectq.meta import Control, Compute, Uncompute, Dagger def print_all_probabilities(eng, qureg): @@ -74,6 +74,55 @@ def eng(): ) +@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) @@ -162,6 +211,11 @@ def test_quantumsubtraction(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([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 @@ -201,11 +255,48 @@ def test_comparator(eng): 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) @@ -242,6 +333,35 @@ def test_quantumdivision(eng): 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) diff --git a/projectq/libs/revkit/__init__.py b/projectq/libs/revkit/__init__.py index 8f422f811..8cbf8bc17 100644 --- a/projectq/libs/revkit/__init__.py +++ b/projectq/libs/revkit/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Module containing code to interface with RevKit""" + from ._permutation import PermutationOracle from ._control_function import ControlFunctionOracle from ._phase import PhaseOracle diff --git a/projectq/libs/revkit/_control_function.py b/projectq/libs/revkit/_control_function.py index a64e749eb..d80613cf8 100644 --- a/projectq/libs/revkit/_control_function.py +++ b/projectq/libs/revkit/_control_function.py @@ -13,12 +13,15 @@ # 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. @@ -57,15 +60,15 @@ 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() @@ -81,12 +84,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" - ) + ) from err + # pylint: disable=invalid-name # convert qubits to tuple qs = [] diff --git a/projectq/libs/revkit/_permutation.py b/projectq/libs/revkit/_permutation.py index cc4891f95..71384f828 100644 --- a/projectq/libs/revkit/_permutation.py +++ b/projectq/libs/revkit/_permutation.py @@ -13,12 +13,14 @@ # 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. @@ -60,13 +62,14 @@ def __or__(self, qubits): 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" - ) + ) from err + # pylint: disable=invalid-name # convert qubits to flattened list qs = BasicGate.make_tuple_of_qureg(qubits) qs = sum(qs, []) diff --git a/projectq/libs/revkit/_phase.py b/projectq/libs/revkit/_phase.py index f30d72191..d50539d92 100644 --- a/projectq/libs/revkit/_phase.py +++ b/projectq/libs/revkit/_phase.py @@ -13,12 +13,14 @@ # 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. @@ -61,15 +63,15 @@ 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() @@ -83,13 +85,14 @@ def __or__(self, qubits): 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" - ) + ) from err + # pylint: disable=invalid-name # convert qubits to tuple qs = [] for item in BasicGate.make_tuple_of_qureg(qubits): @@ -105,7 +108,7 @@ def __or__(self, qubits): revkit.tt(table="{0:#0{1}x}".format(self.function, hex_length)) # 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): diff --git a/projectq/libs/revkit/_utils.py b/projectq/libs/revkit/_utils.py index 5871d7d6d..573451bf6 100644 --- a/projectq/libs/revkit/_utils.py +++ b/projectq/libs/revkit/_utils.py @@ -14,13 +14,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'. 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 + exec(code) diff --git a/projectq/meta/__init__.py b/projectq/meta/__init__.py index ce321f0f0..27c20b835 100755 --- a/projectq/meta/__init__.py +++ b/projectq/meta/__init__.py @@ -13,8 +13,8 @@ # 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., +The projectq.meta package features 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)) diff --git a/projectq/meta/_compute.py b/projectq/meta/_compute.py index d2adbf16a..f09eb8514 100755 --- a/projectq/meta/_compute.py +++ b/projectq/meta/_compute.py @@ -15,22 +15,21 @@ """ Compute, Uncompute, 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 + """Exception raised when the lifetime of a qubit is problematic within a loop""" class NoComputeSectionError(Exception): @@ -38,10 +37,8 @@ class NoComputeSectionError(Exception): Exception raised if uncompute is called but no compute section found. """ - pass - -class ComputeTag(object): +class ComputeTag: """ Compute meta tag. """ @@ -53,7 +50,7 @@ def __ne__(self, other): return not self.__eq__(other) -class UncomputeTag(object): +class UncomputeTag: """ Uncompute meta tag. """ @@ -65,6 +62,17 @@ def __ne__(self, other): return not self.__eq__(other) +def _add_uncompute_tag(cmd): + """ + Modify the command tags, inserting an UncomputeTag. + + Args: + cmd (Command): Command to modify. + """ + cmd.tags.append(UncomputeTag()) + return cmd + + class ComputeEngine(BasicEngine): """ Adds Compute-tags to all commands and stores them (to later uncompute them @@ -82,31 +90,19 @@ def __init__(self): 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 @@ -131,16 +127,20 @@ def run_uncompute(self): 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())]) + 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() 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 @@ -149,7 +149,7 @@ 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) @@ -166,7 +166,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: @@ -183,7 +183,7 @@ def add_uncompute(command, old_tags=deepcopy(cmd.tags)): 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())]) + self.send([_add_uncompute_tag(cmd.get_inverse())]) else: # Process commands by replacing each local qubit from @@ -195,19 +195,18 @@ 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): @@ -218,9 +217,8 @@ def end_compute(self): 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. + 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. @@ -270,7 +268,7 @@ def receive(self, command_list): self.send([cmd]) -class Compute(object): +class Compute: """ Start a compute-section. @@ -283,9 +281,8 @@ class Compute(object): 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: @@ -314,12 +311,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): @@ -327,8 +322,7 @@ 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 @@ -337,13 +331,13 @@ def __enter__(self): 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): # 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. @@ -357,8 +351,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): @@ -366,13 +360,13 @@ 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): # first, remove the compute engine @@ -391,11 +385,11 @@ 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): # 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. @@ -411,7 +405,7 @@ def __exit__(self, type, value, traceback): drop_engine_after(self.engine) -def Uncompute(engine): +def Uncompute(engine): # pylint: disable=invalid-name """ Uncompute automatically. diff --git a/projectq/meta/_control.py b/projectq/meta/_control.py index 704c76374..97a25af90 100755 --- a/projectq/meta/_control.py +++ b/projectq/meta/_control.py @@ -24,11 +24,11 @@ """ 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 ._compute import ComputeTag, UncomputeTag from ._util import insert_engine, drop_engine_after -from projectq.ops import CtrlAll def canonical_ctrl_state(ctrl_state, num_qubits): @@ -45,6 +45,7 @@ def canonical_ctrl_state(ctrl_state, num_qubits): 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 significan bit being 0. + This means in particular that the followings are equivalent: .. code-block:: python @@ -85,6 +86,19 @@ def canonical_ctrl_state(ctrl_state, num_qubits): 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 + + class ControlEngine(BasicEngine): """ Adds control qubits to all commands that have no compute / uncompute tags. @@ -102,29 +116,18 @@ def __init__(self, qubits, ctrl_state=CtrlAll.One): self._qubits = qubits self._state = ctrl_state - 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 - def _handle_command(self, cmd): - if not self._has_compute_uncompute_tag(cmd) and not isinstance(cmd.gate, ClassicalInstructionGate): + 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): + """Forward all commands to the next engine.""" 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. @@ -151,7 +154,8 @@ def __init__(self, engine, qubits, ctrl_state=CtrlAll.One): ... """ 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 @@ -159,10 +163,10 @@ def __init__(self, engine, qubits, ctrl_state=CtrlAll.One): def __enter__(self): if len(self._qubits) > 0: - ce = ControlEngine(self._qubits, self._state) - 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): # remove control handler from engine list (i.e. skip it) if len(self._qubits) > 0: drop_engine_after(self.engine) diff --git a/projectq/meta/_control_test.py b/projectq/meta/_control_test.py index cc79c4591..eadadad34 100755 --- a/projectq/meta/_control_test.py +++ b/projectq/meta/_control_test.py @@ -82,10 +82,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", ctrl_state=CtrlAll.One) - 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(): @@ -114,6 +113,9 @@ def test_control(): 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) diff --git a/projectq/meta/_dagger.py b/projectq/meta/_dagger.py index bbe2a3d54..86d28857c 100755 --- a/projectq/meta/_dagger.py +++ b/projectq/meta/_dagger.py @@ -28,7 +28,7 @@ class QubitManagementError(Exception): - pass + """Exception raised when the lifetime of a qubit is problematic within a loop""" class DaggerEngine(BasicEngine): @@ -78,7 +78,7 @@ def receive(self, command_list): self._commands.extend(command_list) -class Dagger(object): +class Dagger: """ Invert an entire code block. @@ -132,11 +132,11 @@ def __enter__(self): 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): # 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/_dirtyqubit.py b/projectq/meta/_dirtyqubit.py index 3a3ec455a..a26cc574a 100755 --- a/projectq/meta/_dirtyqubit.py +++ b/projectq/meta/_dirtyqubit.py @@ -17,7 +17,7 @@ """ -class DirtyQubitTag(object): +class DirtyQubitTag: """ Dirty qubit meta tag """ diff --git a/projectq/meta/_logicalqubit.py b/projectq/meta/_logicalqubit.py index 63ee60514..218456bae 100644 --- a/projectq/meta/_logicalqubit.py +++ b/projectq/meta/_logicalqubit.py @@ -17,7 +17,7 @@ """ -class LogicalQubitIDTag(object): +class LogicalQubitIDTag: """ LogicalQubitIDTag for a mapped qubit to annotate a MeasureGate. diff --git a/projectq/meta/_loop.py b/projectq/meta/_loop.py index d3e61ab5a..f563bb2f0 100755 --- a/projectq/meta/_loop.py +++ b/projectq/meta/_loop.py @@ -31,10 +31,10 @@ class QubitManagementError(Exception): - pass + """Exception raised when the lifetime of a qubit is problematic within a loop""" -class LoopTag(object): +class LoopTag: """ Loop meta tag """ @@ -99,7 +99,7 @@ def run(self): " ...\n" " del qubit[0]\n" ) - if not self._next_engines_support_loop_tag: + 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: @@ -130,7 +130,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. @@ -147,6 +147,7 @@ def receive(self, command_list): unroll or, if there is a LoopTag-handling engine, add the 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 @@ -185,7 +186,7 @@ def receive(self, command_list): self._refs_to_local_qb[qubit.id].append(qubit) -class Loop(object): +class Loop: """ Loop n times over an entire code block. @@ -196,8 +197,8 @@ class Loop(object): # [quantum gates to be executed 4 times] 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**: @@ -248,7 +249,7 @@ def __enter__(self): 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): if self.num != 1: # remove loop handler from engine list (i.e. skip it) self._loop_eng.run() diff --git a/projectq/meta/_util.py b/projectq/meta/_util.py index ffbc3dc20..4dab11ede 100755 --- a/projectq/meta/_util.py +++ b/projectq/meta/_util.py @@ -13,6 +13,10 @@ # 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): """ diff --git a/projectq/ops/__init__.py b/projectq/ops/__init__.py index 21e4e4031..81b3313ac 100755 --- a/projectq/ops/__init__.py +++ b/projectq/ops/__init__.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ProjectQ module containing all basic gates (operations)""" + from ._basics import ( NotMergeable, NotInvertible, diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 6d224b98b..6c9943c78 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -31,15 +31,15 @@ needs to be made explicitely, while for one argument it is optional. """ -import math from copy import deepcopy +import math +import unicodedata import numpy as np from projectq.types import BasicQubit from ._command import Command, apply_command -import unicodedata ANGLE_PRECISION = 12 ANGLE_TOLERANCE = 10 ** -ANGLE_PRECISION @@ -49,23 +49,18 @@ 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 (or where it 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 (or where the inverse is not implemented + (yet)). """ - pass - -class BasicGate(object): +class BasicGate: """ Base class of all gates. (Don't use it directly but derive from it) """ @@ -84,8 +79,7 @@ def __init__(self): 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 @@ -97,8 +91,8 @@ def __init__(self): 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 @@ -106,7 +100,7 @@ def __init__(self): """ self.interchangeable_qubit_indices = [] - def get_inverse(self): + def get_inverse(self): # pylint: disable=no-self-use """ Return the inverse gate. @@ -117,7 +111,7 @@ def get_inverse(self): """ raise NotInvertible("BasicGate: No get_inverse() implemented.") - def get_merged(self, other): + def get_merged(self, other): # pylint: disable=no-self-use """ Return this gate merged with another gate. @@ -133,9 +127,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] @@ -143,9 +136,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], ) @@ -154,20 +146,19 @@ 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)): + for i, qubit in enumerate(qubits): if isinstance(qubits[i], BasicQubit): - qubits[i] = [qubits[i]] + qubits[i] = [qubit] return tuple(qubits) @@ -186,7 +177,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): @@ -201,8 +193,8 @@ 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) @@ -211,17 +203,14 @@ def __eq__(self, other): """ Equality comparision - 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. + 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 - elif isinstance(other, MatrixGate): + if isinstance(other, MatrixGate): return NotImplemented - else: - return False + return False def __ne__(self, other): return not self.__eq__(other) @@ -229,19 +218,21 @@ def __ne__(self, other): def __str__(self): raise NotImplementedError('This gate does not implement __str__.') - def to_string(self, symbols): + def to_string(self, symbols): # pylint: disable=unused-argument """ String representation - Achieve same function as str() but can be extended for configurable - representation + Achieve same function as str() but can be extended for configurable representation """ return str(self) def __hash__(self): return hash(str(self)) - def is_identity(self): + def is_identity(self): # pylint: disable=no-self-use + """ + Return True if the gate is an identity gate. In this base class, always returns False. + """ return False @@ -250,9 +241,8 @@ class MatrixGate(BasicGate): Defines 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. + 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: @@ -274,18 +264,24 @@ def __init__(self, matrix=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): """ Equality comparision - Return True only if both gates have a matrix respresentation and the - matrices are (approximately) equal. Otherwise return False. + Return True only if both gates have a matrix respresentation and the matrices are (approximately) + equal. Otherwise return False. """ if not hasattr(other, 'matrix'): return False @@ -307,12 +303,11 @@ def get_inverse(self): 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 @@ -329,11 +324,9 @@ class BasicRotationGate(BasicGate): """ Defines a base class of a rotation gate. - 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): @@ -366,9 +359,8 @@ 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. + 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 = "(" + str(round(self.angle / math.pi, 3)) + unicodedata.lookup("GREEK SMALL LETTER PI") + ")" @@ -395,22 +387,19 @@ def get_inverse(self): """ 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. @@ -423,8 +412,7 @@ def __eq__(self, other): """Return True if same class and same rotation angle.""" if isinstance(other, self.__class__): return self.angle == other.angle - else: - return False + return False def __ne__(self, other): return not self.__eq__(other) @@ -443,11 +431,9 @@ class BasicPhaseGate(BasicGate): """ Defines a base class of a phase gate. - 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): @@ -489,20 +475,17 @@ def tex_str(self): 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. @@ -522,8 +505,7 @@ def __eq__(self, other): """Return True if same class and same rotation angle.""" if isinstance(other, self.__class__): return self.angle == other.angle - else: - return False + return False def __ne__(self, other): return not self.__eq__(other) @@ -533,32 +515,26 @@ def __hash__(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 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 @@ -567,15 +543,13 @@ 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 @@ -583,11 +557,10 @@ class BasicMathGate(BasicGate): def add(x): 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 @@ -597,14 +570,11 @@ def multiply(a,b,c) 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 @@ -613,9 +583,8 @@ def add(a,b): return (a,a+b) BasicMathGate.__init__(self, 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 + 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 @@ -636,25 +605,24 @@ def math_fun(a): """ BasicGate.__init__(self) - def math_function(x): - return list(math_fun(*x)) + def math_function(arg): + return list(math_fun(*arg)) self._math_function = math_function def __str__(self): 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). + 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 a47a28441..7fa40643b 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -23,6 +23,7 @@ from projectq.ops import Command, X from projectq import MainEngine from projectq.cengines import DummyEngine +from projectq.types import WeakQubitRef from projectq.ops import _basics @@ -78,6 +79,15 @@ def test_basic_gate_generate_command(main_engine): 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()]) diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 8cd4061d4..621bee320 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -21,14 +21,12 @@ 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 @@ -40,11 +38,11 @@ """ from copy import deepcopy +from enum import IntEnum import itertools import projectq from projectq.types import WeakQubitRef, Qureg -from enum import IntEnum class IncompatibleControlState(Exception): @@ -52,10 +50,10 @@ class IncompatibleControlState(Exception): Exception thrown when trying to set two incompatible states for a control qubit. """ - pass - class CtrlAll(IntEnum): + """Enum type to initialise the control state of qubits""" + Zero = 0 One = 1 @@ -64,8 +62,7 @@ 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 @@ -74,55 +71,43 @@ 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=(), control_state=CtrlAll.One): + 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. - control_state(int,str,projectq.meta.CtrlAll) - Control state for any control qubits + 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) @@ -136,10 +121,12 @@ def __init__(self, engine, gate, qubits, controls=(), tags=(), control_state=Ctr @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): @@ -156,12 +143,10 @@ 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, @@ -218,8 +203,8 @@ def _order_qubits(self, qubits): for old_positions in interchangeable_qubit_indices: 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 @@ -255,6 +240,7 @@ def control_qubits(self, qubits): @property def control_state(self): + """Returns the state of the control qubits (ie. either positively- or negatively-controlled)""" return self._control_state @control_state.setter @@ -266,7 +252,7 @@ def control_state(self, state): state (int,str,projectq.meta.CtrtAll): state of control qubit (ie. positive or negative) """ # NB: avoid circular imports - from projectq.meta import canonical_ctrl_state + from projectq.meta import canonical_ctrl_state # pylint: disable=import-outside-toplevel self._control_state = canonical_ctrl_state(state, len(self._control_qubits)) @@ -284,9 +270,10 @@ def add_control_qubits(self, qubits, state=CtrlAll.One): control qubits. """ # NB: avoid circular imports - from projectq.meta import canonical_ctrl_state + from projectq.meta import canonical_ctrl_state # pylint: disable=import-outside-toplevel - assert isinstance(qubits, list) + 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)) @@ -297,7 +284,6 @@ def add_control_qubits(self, qubits, state=CtrlAll.One): # 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)) - assert len(set(qubits)) == 1 # This should be by design... if len(set(states)) != 1: raise IncompatibleControlState( 'Control qubits {} cannot have conflicting control states: {}'.format(list(qubits), states) diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index eb4cb6681..1228f135a 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -196,6 +196,9 @@ def test_commmand_add_control_qubits_one(main_engine, 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', diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py index 44300f854..a041c18f9 100755 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -43,11 +43,9 @@ import math import cmath -import warnings import numpy as np -from projectq.ops import get_inverse from ._basics import ( BasicGate, SelfInverseGate, @@ -57,6 +55,7 @@ FastForwardingGate, ) from ._command import apply_command +from ._metagates import get_inverse class HGate(SelfInverseGate): @@ -67,6 +66,7 @@ def __str__(self): @property def matrix(self): + """Access to the matrix property of this gate""" return 1.0 / cmath.sqrt(2.0) * np.matrix([[1, 1], [1, -1]]) @@ -82,6 +82,7 @@ def __str__(self): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[0, 1], [1, 0]]) @@ -97,6 +98,7 @@ def __str__(self): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[0, -1j], [1j, 0]]) @@ -112,6 +114,7 @@ def __str__(self): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[1, 0], [0, -1]]) @@ -124,6 +127,7 @@ class SGate(BasicGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[1, 0], [0, 1j]]) def __str__(self): @@ -141,6 +145,7 @@ class TGate(BasicGate): @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): @@ -158,9 +163,13 @@ class SqrtXGate(BasicGate): @property def matrix(self): + """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): + def tex_str(self): # pylint: disable=no-self-use + """ + Return the Latex string representation of a SqrtXGate. + """ return r'$\sqrt{X}$' def __str__(self): @@ -183,6 +192,7 @@ def __str__(self): @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], @@ -207,6 +217,7 @@ def __str__(self): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix( [ [1, 0, 0, 0], @@ -240,6 +251,7 @@ class Ph(BasicPhaseGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[cmath.exp(1j * self.angle), 0], [0, cmath.exp(1j * self.angle)]]) @@ -248,6 +260,7 @@ class Rx(BasicRotationGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix( [ [math.cos(0.5 * self.angle), -1j * math.sin(0.5 * self.angle)], @@ -261,6 +274,7 @@ class Ry(BasicRotationGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix( [ [math.cos(0.5 * self.angle), -math.sin(0.5 * self.angle)], @@ -274,6 +288,7 @@ class Rz(BasicRotationGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix( [ [cmath.exp(-0.5 * 1j * self.angle), 0], @@ -287,6 +302,7 @@ class Rxx(BasicRotationGate): @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)], @@ -302,6 +318,7 @@ class Ryy(BasicRotationGate): @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)], @@ -317,6 +334,7 @@ class Rzz(BasicRotationGate): @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], @@ -332,6 +350,7 @@ class R(BasicPhaseGate): @property def matrix(self): + """Access to the matrix property of this gate""" return np.matrix([[1, 0], [0, cmath.exp(1j * self.angle)]]) @@ -340,9 +359,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 @@ -377,13 +395,8 @@ 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` diff --git a/projectq/ops/_metagates.py b/projectq/ops/_metagates.py index 6f38648ba..59fafb7d3 100644 --- a/projectq/ops/_metagates.py +++ b/projectq/ops/_metagates.py @@ -23,8 +23,7 @@ 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) """ @@ -36,20 +35,16 @@ class ControlQubitError(Exception): Exception thrown when wrong number of control qubits are supplied. """ - pass - 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 +52,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): @@ -91,13 +85,11 @@ def tex_str(self): """ if hasattr(self._gate, 'tex_str'): return self._gate.tex_str() + r"${}^\dagger$" - else: - return str(self._gate) + r"${}^\dagger$" + return str(self._gate) + r"${}^\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 @@ -116,8 +108,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 @@ -158,10 +149,8 @@ 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 @@ -207,12 +196,10 @@ def get_inverse(self): 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 @@ -238,7 +225,7 @@ def __or__(self, qubits): "the required number of control quregs." ) - import projectq.meta + import projectq.meta # pylint: disable=import-outside-toplevel with projectq.meta.Control(gate_quregs[0][0].engine, ctrl): self._gate | tuple(gate_quregs) @@ -251,27 +238,26 @@ def __ne__(self, other): return not self.__eq__(other) -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) """ - 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 @@ -291,8 +277,7 @@ def __str__(self): 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)) @@ -305,9 +290,12 @@ def __ne__(self, other): def __or__(self, qubits): """Applies the gate to every qubit in the quantum register 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 diff --git a/projectq/ops/_metagates_test.py b/projectq/ops/_metagates_test.py index 52b9e00d1..3c99780fc 100755 --- a/projectq/ops/_metagates_test.py +++ b/projectq/ops/_metagates_test.py @@ -34,10 +34,22 @@ ClassicalInstructionGate, All, ) +from projectq.types import WeakQubitRef from projectq.ops import _metagates +def test_tensored_gate_invalid(): + qb0 = WeakQubitRef(None, idx=0) + qb1 = WeakQubitRef(None, idx=1) + + 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()]) diff --git a/projectq/ops/_qaagate.py b/projectq/ops/_qaagate.py index a7a6c438a..9dea09bef 100755 --- a/projectq/ops/_qaagate.py +++ b/projectq/ops/_qaagate.py @@ -13,22 +13,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains the definition of the quantum amplitude amplification gate""" + from ._basics import BasicGate class QAA(BasicGate): """ - Quantum Aplitude Aplification gate. + Quantum Aplitude 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) + (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 wich the QAA algorithm is executed - is the one resulting of aplying the Algorithm on the |0> state. + 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 wich the QAA algorithm is executed is the one + resulting of aplying the Algorithm on the |0> state. Example: .. code-block:: python @@ -60,16 +59,14 @@ def func_oracle(eng,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` + 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_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 + qaa_ancilla: auxiliary qubit that helps to invert the amplitude of the "good" states """ diff --git a/projectq/ops/_qftgate.py b/projectq/ops/_qftgate.py index 8b22bcc87..45eaec9bf 100755 --- a/projectq/ops/_qftgate.py +++ b/projectq/ops/_qftgate.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains the definition of the QFT gate""" + from ._basics import BasicGate diff --git a/projectq/ops/_qpegate.py b/projectq/ops/_qpegate.py index 43834d55f..c806c61a0 100755 --- a/projectq/ops/_qpegate.py +++ b/projectq/ops/_qpegate.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains the definition of the quantum phase estimation gate""" + from ._basics import BasicGate diff --git a/projectq/ops/_qubit_operator.py b/projectq/ops/_qubit_operator.py index 621d50d77..19f1301e0 100644 --- a/projectq/ops/_qubit_operator.py +++ b/projectq/ops/_qubit_operator.py @@ -13,6 +13,7 @@ # 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 cmath import copy @@ -44,7 +45,7 @@ class QubitOperatorError(Exception): - pass + """Exception raised when a QubitOperator is instantiated with some invalid data""" class QubitOperator(BasicGate): @@ -55,63 +56,51 @@ class QubitOperator(BasicGate): coefficent * 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. + 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. + 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: + 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.j) | qureg # Applies X to qubit 0 and 5 - # with an additional global phase - # of 1.j + QubitOperator('X0 X5', 1.j) | 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.0): + def __init__(self, term=None, coefficient=1.0): # pylint: disable=too-many-branches """ Inits a QubitOperator. - 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 @@ -123,28 +112,22 @@ def __init__(self, term=None, coefficient=1.0): 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. + 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. + 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. @@ -155,7 +138,7 @@ def __init__(self, term=None, coefficient=1.0): self.terms = {} if term is None: return - elif isinstance(term, tuple): + if isinstance(term, tuple): if term == (): self.terms[()] = coefficient else: @@ -176,10 +159,10 @@ def __init__(self, term=None, coefficient=1.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 @@ -195,8 +178,8 @@ def __init__(self, term=None, coefficient=1.0): 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. + Eliminates 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 @@ -214,11 +197,9 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12): """ Returns 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. @@ -227,10 +208,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)): @@ -241,7 +222,7 @@ def isclose(self, other, rel_tol=1e-12, abs_tol=1e-12): return False return True - def __or__(self, qubits): + def __or__(self, qubits): # pylint: disable=too-many-locals """ Operator| overload which enables the following syntax: @@ -317,7 +298,7 @@ def __or__(self, qubits): # Check that Qureg has enough qubits: num_qubits = len(qubits[0]) non_trivial_qubits = set() - for index, action in term: + 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.") @@ -335,11 +316,10 @@ def __or__(self, qubits): # 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 + for i, qubit in enumerate(non_trivial_qubits): + new_index[qubit] = i new_qubitoperator = QubitOperator() - assert len(new_qubitoperator.terms) == 0 - 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_qubitoperator.terms[new_term] = coefficient new_qubits = [qubits[0][i] for i in non_trivial_qubits] # Apply new gate @@ -373,10 +353,9 @@ def get_merged(self, other): """ if isinstance(other, self.__class__) and len(other.terms) == 1 and len(self.terms) == 1: return self * other - else: - raise NotMergeable() + raise NotMergeable() - def __imul__(self, multiplier): + def __imul__(self, multiplier): # pylint: disable=too-many-locals,too-many-branches """ In-place multiply (*=) terms with scalar or QubitOperator. @@ -390,7 +369,7 @@ def __imul__(self, multiplier): return self # Handle QubitOperator. - elif isinstance(multiplier, QubitOperator): + if isinstance(multiplier, QubitOperator): # pylint: disable=too-many-nested-blocks result_terms = dict() for left_term in self.terms: for right_term in multiplier.terms: @@ -442,8 +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): """ @@ -458,12 +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): """ @@ -587,9 +564,10 @@ def __str__(self): tmp_string += ' X{}'.format(operator[0]) elif operator[1] == 'Y': tmp_string += ' Y{}'.format(operator[0]) - else: - assert operator[1] == 'Z' + elif operator[1] == 'Z': tmp_string += ' Z{}'.format(operator[0]) + else: # pragma: no cover + raise ValueError('Internal compiler error: operator must be one of X, Y, Z!') string_rep += '{} +\n'.format(tmp_string) return string_rep[:-3] diff --git a/projectq/ops/_shortcuts.py b/projectq/ops/_shortcuts.py index 0e06586cb..0defb4e22 100755 --- a/projectq/ops/_shortcuts.py +++ b/projectq/ops/_shortcuts.py @@ -25,9 +25,9 @@ def CRz(angle): """ - Shortcut for C(Rz(angle), n=1). + Shortcut for C(Rz(angle), n_qubits=1). """ - return C(Rz(angle), n=1) + return C(Rz(angle), n_qubits=1) CNOT = CX = C(NOT) diff --git a/projectq/ops/_state_prep.py b/projectq/ops/_state_prep.py index af6dce015..d86824bf8 100644 --- a/projectq/ops/_state_prep.py +++ b/projectq/ops/_state_prep.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains the definition of the state preparation gate""" + from ._basics import BasicGate diff --git a/projectq/ops/_time_evolution.py b/projectq/ops/_time_evolution.py index d80d7388b..80e051f70 100644 --- a/projectq/ops/_time_evolution.py +++ b/projectq/ops/_time_evolution.py @@ -13,17 +13,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains the definition of the time evolution gate""" + import copy -from projectq.ops import Ph 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): @@ -54,8 +56,7 @@ 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: @@ -63,10 +64,8 @@ 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) if not isinstance(time, (float, int)): @@ -93,27 +92,23 @@ 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. @@ -131,8 +126,7 @@ def get_merged(self, other): # 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): """ @@ -145,8 +139,7 @@ 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: @@ -156,11 +149,10 @@ def __or__(self, qubits): 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 @@ -170,8 +162,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) @@ -185,7 +177,7 @@ 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.") @@ -194,12 +186,11 @@ def __or__(self, qubits): # 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 + 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] diff --git a/projectq/ops/_uniformly_controlled_rotation.py b/projectq/ops/_uniformly_controlled_rotation.py index e8d8ccaee..c5ae74229 100644 --- a/projectq/ops/_uniformly_controlled_rotation.py +++ b/projectq/ops/_uniformly_controlled_rotation.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Contains uniformly controlled rotation gates""" + import math from ._basics import ANGLE_PRECISION, ANGLE_TOLERANCE, BasicGate, NotMergeable @@ -22,11 +24,9 @@ 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. + 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 @@ -36,15 +36,13 @@ class UniformlyControlledRy(BasicGate): 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. + 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. + angles(list[float]): Rotation angles. Ry(angles[k]) is applied conditioned on the control qubits being in + state k. """ def __init__(self, angles): @@ -73,8 +71,7 @@ def __eq__(self, other): """Return True if same class, same rotation angles.""" if isinstance(other, self.__class__): return self.angles == other.angles - else: - return False + return False def __ne__(self, other): return not self.__eq__(other) @@ -87,11 +84,9 @@ 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. + 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 @@ -101,10 +96,9 @@ class UniformlyControlledRz(BasicGate): 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. + 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 @@ -113,7 +107,7 @@ class UniformlyControlledRz(BasicGate): """ def __init__(self, angles): - BasicGate.__init__(self) + super().__init__() rounded_angles = [] for angle in angles: new_angle = round(float(angle) % (4.0 * math.pi), ANGLE_PRECISION) @@ -138,8 +132,7 @@ def __eq__(self, other): """Return True if same class, same rotation angles.""" if isinstance(other, self.__class__): return self.angles == other.angles - else: - return False + return False def __ne__(self, other): return not self.__eq__(other) diff --git a/projectq/setups/__init__.py b/projectq/setups/__init__.py index 16fc4afdf..f279b3d1d 100755 --- a/projectq/setups/__init__.py +++ b/projectq/setups/__init__.py @@ -12,3 +12,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..a50110ec1 --- /dev/null +++ b/projectq/setups/_utils.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# 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 + +from projectq.cengines import ( + AutoReplacer, + DecompositionRuleSet, + InstructionFilter, + LocalOptimizer, + TagRemover, +) +from projectq.ops import ClassicalInstructionGate, CNOT, ControlledGate, Swap, QFT, get_inverse, BasicMathGate +import projectq.libs.math +import projectq.setups.decompositions + + +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)): + """ + Returns 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. + + 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 index 78849dc98..bd4ff862e 100644 --- a/projectq/setups/aqt.py +++ b/projectq/setups/aqt.py @@ -31,6 +31,9 @@ def get_engine_list(token=None, device=None): + """ + Return the default list of compiler engine for the AQT plaftorm + """ # Access to the hardware properties via show_devices # Can also be extended to take into account gate fidelities, new available # gate, etc.. @@ -65,8 +68,8 @@ def get_engine_list(token=None, device=None): class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" class DeviceNotHandledError(Exception): - pass + """Exception raised if a selected device is cannot handle the circuit""" diff --git a/projectq/setups/awsbraket.py b/projectq/setups/awsbraket.py index c5ec54a12..e9558a1d1 100644 --- a/projectq/setups/awsbraket.py +++ b/projectq/setups/awsbraket.py @@ -45,6 +45,9 @@ 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.. @@ -81,7 +84,8 @@ def get_engine_list(credentials=None, device=None): other_gates=(Barrier,), ) return setup + raise RuntimeError('Unsupported device type: {}!'.format(device)) # pragma: no cover class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" diff --git a/projectq/setups/decompositions/amplitudeamplification.py b/projectq/setups/decompositions/amplitudeamplification.py index 6472639db..7e2a171b0 100644 --- a/projectq/setups/decompositions/amplitudeamplification.py +++ b/projectq/setups/decompositions/amplitudeamplification.py @@ -77,7 +77,7 @@ def func_oracle(eng,system_qubits,qaa_ancilla): from projectq.ops import QAA -def _decompose_QAA(cmd): +def _decompose_QAA(cmd): # pylint: disable=invalid-name """Decompose the Quantum Amplitude Apmplification algorithm as a gate.""" eng = cmd.engine @@ -86,24 +86,24 @@ def _decompose_QAA(cmd): qaa_ancilla = cmd.qubits[1] # The Oracle and the Algorithm - Oracle = cmd.gate.oracle - A = cmd.gate.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) + oracle(eng, system_qubits, qaa_ancilla) # Apply the inversion of the Algorithm, # the inversion of the aplitude of |0> and the Algorithm with Compute(eng): with Dagger(eng): - A(eng, system_qubits) + 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 - A(eng, system_qubits) + alg(eng, system_qubits) Ph(math.pi) | system_qubits[0] diff --git a/projectq/setups/decompositions/arb1qubit2rzandry.py b/projectq/setups/decompositions/arb1qubit2rzandry.py index ba20c98a7..fadc006d2 100644 --- a/projectq/setups/decompositions/arb1qubit2rzandry.py +++ b/projectq/setups/decompositions/arb1qubit2rzandry.py @@ -48,16 +48,12 @@ 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 + 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. @@ -75,7 +71,7 @@ def _test_parameters(matrix, a, b_half, c_half, d_half): Returns: True if matrix elements of U and `matrix` are TOLERANCE close. """ - U = [ + 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), @@ -85,10 +81,10 @@ def _test_parameters(matrix, a, b_half, c_half, d_half): cmath.exp(1j * (a + b_half + d_half)) * math.cos(c_half), ], ] - return numpy.allclose(U, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) + return numpy.allclose(unitary, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) -def _find_parameters(matrix): +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 @@ -114,11 +110,11 @@ def _find_parameters(matrix): # 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.0 + 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]) + 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), @@ -143,11 +139,11 @@ def _find_parameters(matrix): # 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.0 + 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 + 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), @@ -172,9 +168,9 @@ def _find_parameters(matrix): # 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.0 + 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]) # yapf: disable possible_d_half = [two_d/4. % (2*math.pi), @@ -219,7 +215,7 @@ 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): diff --git a/projectq/setups/decompositions/barrier.py b/projectq/setups/decompositions/barrier.py index 5f4c05439..f3e94f408 100755 --- a/projectq/setups/decompositions/barrier.py +++ b/projectq/setups/decompositions/barrier.py @@ -22,12 +22,11 @@ from projectq.ops import BarrierGate -def _decompose_barrier(cmd): +def _decompose_barrier(cmd): # pylint: disable=unused-argument """Throw out all barriers if they are not supported.""" - pass -def _recognize_barrier(cmd): +def _recognize_barrier(cmd): # pylint: disable=unused-argument """Recognize all barriers.""" return True diff --git a/projectq/setups/decompositions/carb1qubit2cnotrzandry.py b/projectq/setups/decompositions/carb1qubit2cnotrzandry.py index e0fb14780..b78d20556 100644 --- a/projectq/setups/decompositions/carb1qubit2cnotrzandry.py +++ b/projectq/setups/decompositions/carb1qubit2cnotrzandry.py @@ -38,15 +38,13 @@ def _recognize_carb1qubit(cmd): """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 + 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. @@ -63,7 +61,7 @@ def _test_parameters(matrix, a, b, c_half): Returns: True if matrix elements of V and `matrix` are TOLERANCE close. """ - V = [ + v_matrix = [ [ -math.sin(c_half) * cmath.exp(1j * a), cmath.exp(1j * (a - b)) * math.cos(c_half), @@ -73,10 +71,10 @@ def _test_parameters(matrix, a, b, c_half): cmath.exp(1j * a) * math.sin(c_half), ], ] - return numpy.allclose(V, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) + return numpy.allclose(v_matrix, matrix, rtol=10 * TOLERANCE, atol=TOLERANCE) -def _recognize_v(matrix): +def _recognize_v(matrix): # pylint: disable=too-many-branches """ Recognizes a matrix which can be written in the following form: @@ -94,71 +92,65 @@ def _recognize_v(matrix): # 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.0 + 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) + return (a, b, c_half) + raise RuntimeError('Case matrix[0][0]==0 should work in all cases, but did not!') # pragma: no cover - elif abs(matrix[0][1]) < TOLERANCE: + 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.0 - b = 0 + 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] - found = False + 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.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 - else: - a = two_a / 2.0 - 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])) - # yapf: disable - 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)] - # yapf: enable - 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])) + # yapf: disable + 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)] + # yapf: enable + 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). @@ -195,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): @@ -212,9 +204,9 @@ 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 + 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): diff --git a/projectq/setups/decompositions/cnot2rxx.py b/projectq/setups/decompositions/cnot2rxx.py index 92b3a0598..1a37ff045 100644 --- a/projectq/setups/decompositions/cnot2rxx.py +++ b/projectq/setups/decompositions/cnot2rxx.py @@ -20,13 +20,14 @@ Registers 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, Rxx, Ry, Rx, X -import math -def _decompose_cnot2rxx_M(cmd): +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 @@ -38,7 +39,7 @@ def _decompose_cnot2rxx_M(cmd): Ry(-1 * math.pi / 2) | ctrl[0] -def _decompose_cnot2rxx_P(cmd): +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 diff --git a/projectq/setups/decompositions/cnu2toffoliandcu.py b/projectq/setups/decompositions/cnu2toffoliandcu.py index faedeba56..fd1d0e2b5 100644 --- a/projectq/setups/decompositions/cnu2toffoliandcu.py +++ b/projectq/setups/decompositions/cnu2toffoliandcu.py @@ -25,7 +25,7 @@ from projectq.ops import BasicGate, Toffoli, XGate -def _recognize_CnU(cmd): +def _recognize_CnU(cmd): # pylint: disable=invalid-name """ Recognize an arbitrary gate which has n>=2 control qubits, except a Toffoli gate. @@ -38,7 +38,7 @@ def _recognize_CnU(cmd): return False -def _decompose_CnU(cmd): +def _decompose_CnU(cmd): # pylint: disable=invalid-name """ Decompose a multi-controlled gate U with n control qubits into a single- controlled U. @@ -50,16 +50,16 @@ def _decompose_CnU(cmd): qubits = cmd.qubits ctrl_qureg = cmd.control_qubits gate = cmd.gate - n = get_control_count(cmd) + n_controls = get_control_count(cmd) # specialized for X-gate - if gate == XGate() and n > 2: - n -= 1 - ancilla_qureg = eng.allocate_qureg(n - 1) + 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): + for ctrl_index in range(2, n_controls): Toffoli | ( ctrl_qureg[ctrl_index], ancilla_qureg[ctrl_index - 2], diff --git a/projectq/setups/decompositions/crz2cxandrz.py b/projectq/setups/decompositions/crz2cxandrz.py index 98e2f342e..013fdb978 100755 --- a/projectq/setups/decompositions/crz2cxandrz.py +++ b/projectq/setups/decompositions/crz2cxandrz.py @@ -23,20 +23,20 @@ from projectq.ops import NOT, Rz, C -def _decompose_CRz(cmd): +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): +def _recognize_CRz(cmd): # pylint: disable=invalid-name """Recognize the controlled Rz gate.""" return get_control_count(cmd) >= 1 diff --git a/projectq/setups/decompositions/entangle.py b/projectq/setups/decompositions/entangle.py index 59943ab1d..918be886e 100755 --- a/projectq/setups/decompositions/entangle.py +++ b/projectq/setups/decompositions/entangle.py @@ -26,13 +26,13 @@ def _decompose_entangle(cmd): """Decompose the entangle gate.""" - qr = cmd.qubits[0] + 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 diff --git a/projectq/setups/decompositions/globalphase.py b/projectq/setups/decompositions/globalphase.py index 3b546a361..69e38f081 100755 --- a/projectq/setups/decompositions/globalphase.py +++ b/projectq/setups/decompositions/globalphase.py @@ -23,12 +23,11 @@ from projectq.ops import Ph -def _decompose_PhNoCtrl(cmd): +def _decompose_PhNoCtrl(cmd): # pylint: disable=invalid-name,unused-argument """Throw out global phases (no controls).""" - pass -def _recognize_PhNoCtrl(cmd): +def _recognize_PhNoCtrl(cmd): # pylint: disable=invalid-name """Recognize global phases (no controls).""" return get_control_count(cmd) == 0 diff --git a/projectq/setups/decompositions/h2rx.py b/projectq/setups/decompositions/h2rx.py index 68b7f6866..c9f27092a 100644 --- a/projectq/setups/decompositions/h2rx.py +++ b/projectq/setups/decompositions/h2rx.py @@ -27,7 +27,7 @@ from projectq.ops import Ph, Rx, Ry, H -def _decompose_h2rx_M(cmd): +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] @@ -36,7 +36,7 @@ def _decompose_h2rx_M(cmd): Ry(-1 * math.pi / 2) | qubit -def _decompose_h2rx_N(cmd): +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) @@ -46,7 +46,7 @@ def _decompose_h2rx_N(cmd): Rx(-1 * math.pi) | qubit -def _recognize_HNoCtrl(cmd): +def _recognize_HNoCtrl(cmd): # pylint: disable=invalid-name """For efficiency reasons only if no control qubits.""" return get_control_count(cmd) == 0 diff --git a/projectq/setups/decompositions/ph2r.py b/projectq/setups/decompositions/ph2r.py index 4f14e2c26..c72f459ee 100755 --- a/projectq/setups/decompositions/ph2r.py +++ b/projectq/setups/decompositions/ph2r.py @@ -24,7 +24,7 @@ from projectq.ops import Ph, R -def _decompose_Ph(cmd): +def _decompose_Ph(cmd): # pylint: disable=invalid-name """Decompose the controlled phase gate (C^nPh(phase)).""" ctrl = cmd.control_qubits gate = cmd.gate @@ -34,7 +34,7 @@ def _decompose_Ph(cmd): R(gate.angle) | ctrl[0] -def _recognize_Ph(cmd): +def _recognize_Ph(cmd): # pylint: disable=invalid-name """Recognize the controlled phase gate.""" return get_control_count(cmd) >= 1 diff --git a/projectq/setups/decompositions/phaseestimation.py b/projectq/setups/decompositions/phaseestimation.py index 1d7fc3f32..5059ad1ec 100644 --- a/projectq/setups/decompositions/phaseestimation.py +++ b/projectq/setups/decompositions/phaseestimation.py @@ -91,7 +91,7 @@ def two_qubit_gate(system_q, time): from projectq.ops import QPE -def _decompose_QPE(cmd): +def _decompose_QPE(cmd): # pylint: disable=invalid-name """Decompose the Quantum Phase Estimation gate.""" eng = cmd.engine @@ -103,20 +103,20 @@ def _decompose_QPE(cmd): Tensor(H) | qpe_ancillas # The Unitary Operator - U = cmd.gate.unitary + unitary = cmd.gate.unitary # Control U on the system_qubits - if callable(U): + if callable(unitary): # If U is a function - for i in range(len(qpe_ancillas)): - with Control(eng, qpe_ancillas[i]): - U(system_qubits, time=2 ** i) + for i, ancilla in enumerate(qpe_ancillas): + with Control(eng, ancilla): + unitary(system_qubits, time=2 ** i) else: - for i in range(len(qpe_ancillas)): + for i, ancilla in enumerate(qpe_ancillas): ipower = int(2 ** i) with Loop(eng, ipower): - with Control(eng, qpe_ancillas[i]): - U | system_qubits + with Control(eng, ancilla): + unitary | system_qubits # Inverse QFT on the ancillas get_inverse(QFT) | qpe_ancillas diff --git a/projectq/setups/decompositions/phaseestimation_test.py b/projectq/setups/decompositions/phaseestimation_test.py index 43aa5ec16..118641d7e 100644 --- a/projectq/setups/decompositions/phaseestimation_test.py +++ b/projectq/setups/decompositions/phaseestimation_test.py @@ -17,6 +17,7 @@ import cmath import numpy as np +from flaky import flaky import pytest from projectq.backends import Simulator @@ -34,6 +35,7 @@ import projectq.setups.decompositions.uniformlycontrolledr2cnot as ucr2cnot +@flaky(max_runs=5, min_passes=2) def test_simple_test_X_eigenvectors(): rule_set = DecompositionRuleSet(modules=[pe, dqft]) eng = MainEngine( @@ -43,7 +45,7 @@ def test_simple_test_X_eigenvectors(): ], ) results = np.array([]) - for i in range(100): + for i in range(150): autovector = eng.allocate_qureg(1) X | autovector H | autovector @@ -66,6 +68,7 @@ def test_simple_test_X_eigenvectors(): ) +@flaky(max_runs=5, min_passes=2) def test_Ph_eigenvectors(): rule_set = DecompositionRuleSet(modules=[pe, dqft]) eng = MainEngine( @@ -75,7 +78,7 @@ def test_Ph_eigenvectors(): ], ) results = np.array([]) - for i in range(100): + for i in range(150): autovector = eng.allocate_qureg(1) theta = cmath.pi * 2.0 * 0.125 unit = Ph(theta) @@ -103,6 +106,7 @@ def two_qubit_gate(system_q, time): 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( @@ -112,7 +116,7 @@ def test_2qubitsPh_andfunction_eigenvectors(): ], ) results = np.array([]) - for i in range(100): + for i in range(150): autovector = eng.allocate_qureg(2) X | autovector[0] ancillas = eng.allocate_qureg(3) diff --git a/projectq/setups/decompositions/qft2crandhadamard.py b/projectq/setups/decompositions/qft2crandhadamard.py index 5119e19ee..157f8e98a 100755 --- a/projectq/setups/decompositions/qft2crandhadamard.py +++ b/projectq/setups/decompositions/qft2crandhadamard.py @@ -29,7 +29,7 @@ from projectq.meta import Control -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): diff --git a/projectq/setups/decompositions/qubitop2onequbit.py b/projectq/setups/decompositions/qubitop2onequbit.py index 4e24effad..c4cc7873b 100644 --- a/projectq/setups/decompositions/qubitop2onequbit.py +++ b/projectq/setups/decompositions/qubitop2onequbit.py @@ -29,7 +29,8 @@ def _recognize_qubitop(cmd): def _decompose_qubitop(cmd): - assert len(cmd.qubits) == 1 + 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 diff --git a/projectq/setups/decompositions/qubitop2onequbit_test.py b/projectq/setups/decompositions/qubitop2onequbit_test.py index ddf542ffa..91d95b4d3 100644 --- a/projectq/setups/decompositions/qubitop2onequbit_test.py +++ b/projectq/setups/decompositions/qubitop2onequbit_test.py @@ -26,7 +26,8 @@ InstructionFilter, ) from projectq.meta import Control -from projectq.ops import All, Measure, Ph, QubitOperator, X, Y, Z +from projectq.ops import All, Measure, Ph, QubitOperator, X, Y, Z, Command +from projectq.types import WeakQubitRef import projectq.setups.decompositions.qubitop2onequbit as qubitop2onequbit @@ -54,6 +55,13 @@ def _decomp_gates(eng, cmd): 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))] diff --git a/projectq/setups/decompositions/r2rzandph.py b/projectq/setups/decompositions/r2rzandph.py index 2e7a94e22..dbd204721 100755 --- a/projectq/setups/decompositions/r2rzandph.py +++ b/projectq/setups/decompositions/r2rzandph.py @@ -24,7 +24,7 @@ from projectq.ops import Ph, Rz, R -def _decompose_R(cmd): +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 diff --git a/projectq/setups/decompositions/rx2rz.py b/projectq/setups/decompositions/rx2rz.py index 41f5a3c7f..eb64f63bf 100644 --- a/projectq/setups/decompositions/rx2rz.py +++ b/projectq/setups/decompositions/rx2rz.py @@ -34,7 +34,7 @@ def _decompose_rx(cmd): Uncompute(eng) -def _recognize_RxNoCtrl(cmd): +def _recognize_RxNoCtrl(cmd): # pylint: disable=invalid-name """For efficiency reasons only if no control qubits.""" return get_control_count(cmd) == 0 diff --git a/projectq/setups/decompositions/ry2rz.py b/projectq/setups/decompositions/ry2rz.py index aa2a0bd65..4dc3dca1c 100644 --- a/projectq/setups/decompositions/ry2rz.py +++ b/projectq/setups/decompositions/ry2rz.py @@ -36,7 +36,7 @@ def _decompose_ry(cmd): Uncompute(eng) -def _recognize_RyNoCtrl(cmd): +def _recognize_RyNoCtrl(cmd): # pylint: disable=invalid-name """For efficiency reasons only if no control qubits.""" return get_control_count(cmd) == 0 diff --git a/projectq/setups/decompositions/rz2rx.py b/projectq/setups/decompositions/rz2rx.py index afa96b436..380b14a19 100644 --- a/projectq/setups/decompositions/rz2rx.py +++ b/projectq/setups/decompositions/rz2rx.py @@ -28,7 +28,7 @@ from projectq.ops import Rx, Ry, Rz -def _decompose_rz2rx_P(cmd): +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] @@ -42,7 +42,7 @@ def _decompose_rz2rx_P(cmd): Uncompute(eng) -def _decompose_rz2rx_M(cmd): +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] @@ -56,7 +56,7 @@ def _decompose_rz2rx_M(cmd): Uncompute(eng) -def _recognize_RzNoCtrl(cmd): +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 diff --git a/projectq/setups/decompositions/sqrtswap2cnot.py b/projectq/setups/decompositions/sqrtswap2cnot.py index f2441c2b0..4c9ce919a 100644 --- a/projectq/setups/decompositions/sqrtswap2cnot.py +++ b/projectq/setups/decompositions/sqrtswap2cnot.py @@ -23,7 +23,11 @@ def _decompose_sqrtswap(cmd): """Decompose (controlled) swap gates.""" - assert len(cmd.qubits) == 2 and len(cmd.qubits[0]) == 1 and len(cmd.qubits[1]) == 1 + + 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] diff --git a/projectq/setups/decompositions/sqrtswap2cnot_test.py b/projectq/setups/decompositions/sqrtswap2cnot_test.py index 02dd3362d..ace87a94e 100644 --- a/projectq/setups/decompositions/sqrtswap2cnot_test.py +++ b/projectq/setups/decompositions/sqrtswap2cnot_test.py @@ -24,8 +24,8 @@ DummyEngine, InstructionFilter, ) - -from projectq.ops import All, Measure, SqrtSwap +from projectq.ops import All, Measure, SqrtSwap, Command +from projectq.types import WeakQubitRef import projectq.setups.decompositions.sqrtswap2cnot as sqrtswap2cnot @@ -36,6 +36,18 @@ def _decomp_gates(eng, cmd): 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) diff --git a/projectq/setups/decompositions/stateprep2cnot.py b/projectq/setups/decompositions/stateprep2cnot.py index 6073bcc14..c82bd62b9 100644 --- a/projectq/setups/decompositions/stateprep2cnot.py +++ b/projectq/setups/decompositions/stateprep2cnot.py @@ -29,12 +29,13 @@ ) -def _decompose_state_preparation(cmd): +def _decompose_state_preparation(cmd): # pylint: disable=too-many-locals """ Implements state preparation based on arXiv:quant-ph/0407010v1. """ eng = cmd.engine - assert len(cmd.qubits) == 1 + 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 @@ -52,17 +53,17 @@ def _decompose_state_preparation(cmd): phase_of_blocks = [] for amplitude in final_state: phase_of_blocks.append(cmath.phase(amplitude)) - for target_qubit in range(len(qureg)): + for qubit_idx, qubit in enumerate(qureg): angles = [] phase_of_next_blocks = [] - for block in range(2 ** (len(qureg) - target_qubit - 1)): + 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[(target_qubit + 1) :], # noqa: E203 - qureg[target_qubit], + qureg[(qubit_idx + 1) :], # noqa: E203 + qubit, ) phase_of_blocks = phase_of_next_blocks # Cancel global phase @@ -71,20 +72,20 @@ def _decompose_state_preparation(cmd): abs_of_blocks = [] for amplitude in final_state: abs_of_blocks.append(abs(amplitude)) - for target_qubit in range(len(qureg)): + for qubit_idx, qubit in enumerate(qureg): angles = [] abs_of_next_blocks = [] - for block in range(2 ** (len(qureg) - target_qubit - 1)): - a0 = abs_of_blocks[2 * block] - a1 = abs_of_blocks[2 * block + 1] + 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[(target_qubit + 1) :], # noqa: E203 - qureg[target_qubit], + qureg[(qubit_idx + 1) :], # noqa: E203 + qubit, ) abs_of_blocks = abs_of_next_blocks diff --git a/projectq/setups/decompositions/stateprep2cnot_test.py b/projectq/setups/decompositions/stateprep2cnot_test.py index 381414ac0..ffa510ce1 100644 --- a/projectq/setups/decompositions/stateprep2cnot_test.py +++ b/projectq/setups/decompositions/stateprep2cnot_test.py @@ -29,6 +29,14 @@ import projectq.setups.decompositions.stateprep2cnot as stateprep2cnot +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) diff --git a/projectq/setups/decompositions/time_evolution.py b/projectq/setups/decompositions/time_evolution.py index 6ca4789ce..1bce70fd5 100644 --- a/projectq/setups/decompositions/time_evolution.py +++ b/projectq/setups/decompositions/time_evolution.py @@ -33,15 +33,14 @@ def _recognize_time_evolution_commuting_terms(cmd): 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 @@ -60,7 +59,7 @@ 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. @@ -78,19 +77,22 @@ 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: @@ -112,8 +114,10 @@ def _decompose_time_evolution_individual_terms(cmd): H | qureg[index] elif action == 'Y': 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]) diff --git a/projectq/setups/decompositions/time_evolution_test.py b/projectq/setups/decompositions/time_evolution_test.py index 79397e07f..293aba089 100644 --- a/projectq/setups/decompositions/time_evolution_test.py +++ b/projectq/setups/decompositions/time_evolution_test.py @@ -31,16 +31,8 @@ DecompositionRuleSet, ) from projectq.meta import Control -from projectq.ops import ( - QubitOperator, - TimeEvolution, - ClassicalInstructionGate, - Ph, - Rx, - Ry, - All, - Measure, -) +from projectq.ops import QubitOperator, TimeEvolution, ClassicalInstructionGate, Ph, Rx, Ry, All, Measure, Command +from projectq.types import WeakQubitRef from . import time_evolution as te @@ -145,6 +137,28 @@ 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(tuple(), 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) diff --git a/projectq/setups/decompositions/toffoli2cnotandtgate.py b/projectq/setups/decompositions/toffoli2cnotandtgate.py index 94b42bcab..c3e794d75 100755 --- a/projectq/setups/decompositions/toffoli2cnotandtgate.py +++ b/projectq/setups/decompositions/toffoli2cnotandtgate.py @@ -28,23 +28,21 @@ def _decompose_toffoli(cmd): ctrl = cmd.control_qubits 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 diff --git a/projectq/setups/decompositions/uniformlycontrolledr2cnot.py b/projectq/setups/decompositions/uniformlycontrolledr2cnot.py index a1ce53d85..a04263b18 100644 --- a/projectq/setups/decompositions/uniformlycontrolledr2cnot.py +++ b/projectq/setups/decompositions/uniformlycontrolledr2cnot.py @@ -21,7 +21,9 @@ from projectq.ops import CNOT, Ry, Rz, UniformlyControlledRy, UniformlyControlledRz -def _apply_ucr_n(angles, ucontrol_qubits, target_qubit, eng, gate_class, rightmost_cnot): +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): diff --git a/projectq/setups/default.py b/projectq/setups/default.py index b31d98fcf..942b66894 100755 --- a/projectq/setups/default.py +++ b/projectq/setups/default.py @@ -26,5 +26,8 @@ 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)] diff --git a/projectq/setups/grid.py b/projectq/setups/grid.py index 9204bd7ae..49dd393fb 100644 --- a/projectq/setups/grid.py +++ b/projectq/setups/grid.py @@ -15,59 +15,17 @@ """ Defines 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, -) +from projectq.cengines import GridMapper +from projectq.ops import CNOT, 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 - - -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_rows, num_columns, one_qubit_gates="any", two_qubit_gates=(CNOT, Swap)): @@ -75,18 +33,14 @@ def get_engine_list(num_rows, num_columns, one_qubit_gates="any", two_qubit_gate Returns 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 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. 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, @@ -96,86 +50,18 @@ def get_engine_list(num_rows, num_columns, one_qubit_gates="any", two_qubit_gate 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/ibm.py b/projectq/setups/ibm.py index e1db71a9f..559f8efdc 100755 --- a/projectq/setups/ibm.py +++ b/projectq/setups/ibm.py @@ -36,6 +36,9 @@ 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.. @@ -110,15 +113,18 @@ def get_engine_list(token=None, device=None): class DeviceOfflineError(Exception): - pass + """Exception raised if a selected device is currently offline""" class DeviceNotHandledError(Exception): - pass + """Exception raised if a selected device is cannot handle the circuit""" def list2set(coupling_list): + """ + Convert a list() to a set() + """ result = [] - for el in coupling_list: - result.append(tuple(el)) + for element in coupling_list: + result.append(tuple(element)) return set(result) diff --git a/projectq/setups/ionq.py b/projectq/setups/ionq.py index c68251eb9..985faa19d 100644 --- a/projectq/setups/ionq.py +++ b/projectq/setups/ionq.py @@ -44,6 +44,9 @@ def get_engine_list(token=None, device=None): + """ + Return the default list of compiler engine for the IonQ platform + """ devices = show_devices(token) if not device or device not in devices: raise DeviceOfflineError("Error checking engine list: no '{}' devices available".format(device)) diff --git a/projectq/setups/linear.py b/projectq/setups/linear.py index 2f8c80e02..b0ff5a7e8 100644 --- a/projectq/setups/linear.py +++ b/projectq/setups/linear.py @@ -15,59 +15,16 @@ """ Defines 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 +from projectq.cengines import LinearMapper +from projectq.ops import CNOT, Swap -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 - - -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)): @@ -75,18 +32,14 @@ def get_engine_list(num_qubits, cyclic=False, one_qubit_gates="any", two_qubit_g Returns 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 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. 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, @@ -96,86 +49,18 @@ def get_engine_list(num_qubits, cyclic=False, one_qubit_gates="any", two_qubit_g 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/restrictedgateset.py b/projectq/setups/restrictedgateset.py index f12877bcb..c6d7f034f 100644 --- a/projectq/setups/restrictedgateset.py +++ b/projectq/setups/restrictedgateset.py @@ -35,48 +35,22 @@ ) from projectq.ops import ( BasicGate, - BasicMathGate, ClassicalInstructionGate, CNOT, ControlledGate, - get_inverse, - QFT, - Swap, ) +from ._utils import one_and_two_qubit_gates, high_level_gates -def high_level_gates(eng, cmd): + +def default_chooser(cmd, decomposition_list): # pylint: disable=unused-argument """ - Remove any MathGates. + Default chooser function for the AutoReplacer compiler engine. """ - 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): return decomposition_list[0] -def get_engine_list( +def get_engine_list( # pylint: disable=too-many-branches,too-many-statements one_qubit_gates="any", two_qubit_gates=(CNOT,), other_gates=(), @@ -164,11 +138,12 @@ def get_engine_list( 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_classes2.append(gate) elif isinstance(gate, BasicGate): if isinstance(gate, ControlledGate): - allowed_gate_instances2.append((gate._gate, gate._n)) + allowed_gate_instances2.append((gate._gate, gate._n)) # pylint: disable=protected-access else: allowed_gate_instances2.append((gate, 0)) else: @@ -177,11 +152,12 @@ def get_engine_list( 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) 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: @@ -193,26 +169,26 @@ def get_engine_list( 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 - elif isinstance(cmd.gate, allowed_gate_classes1) and len(all_qubits) == 1: + if isinstance(cmd.gate, allowed_gate_classes1) and len(all_qubits) == 1: return True - elif isinstance(cmd.gate, allowed_gate_classes2) and len(all_qubits) == 2: + if isinstance(cmd.gate, allowed_gate_classes2) and len(all_qubits) == 2: return True - elif cmd.gate in allowed_gate_instances1 and len(all_qubits) == 1: + if cmd.gate in allowed_gate_instances1 and len(all_qubits) == 1: return True - elif (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances2 and len(all_qubits) == 2: + if (cmd.gate, len(cmd.control_qubits)) in allowed_gate_instances2 and len(all_qubits) == 2: return True return False diff --git a/projectq/setups/trapped_ion_decomposer.py b/projectq/setups/trapped_ion_decomposer.py index 158073f93..4472b7c65 100644 --- a/projectq/setups/trapped_ion_decomposer.py +++ b/projectq/setups/trapped_ion_decomposer.py @@ -19,23 +19,21 @@ """ 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. +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. +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. +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.setups import restrictedgateset from projectq.ops import Rxx, Rx, Ry -from projectq.meta import get_control_count # ------------------chooser_Ry_reducer-------------------# # If the qubit is not in the prev_Ry_sign dictionary, then no decomposition @@ -51,7 +49,7 @@ # +1 -def chooser_Ry_reducer(cmd, decomposition_list): +def chooser_Ry_reducer(cmd, decomposition_list): # pylint: disable=invalid-name, too-many-return-statements """ Choose the decomposition so as to maximise Ry cancellations, based on the previous decomposition used for the given qubit. @@ -79,10 +77,9 @@ def chooser_Ry_reducer(cmd, decomposition_list): except IndexError: pass - local_prev_Ry_sign = prev_Ry_sign.setdefault(cmd.engine, dict()) + local_prev_Ry_sign = prev_Ry_sign.setdefault(cmd.engine, dict()) # pylint: disable=invalid-name if name == 'cnot2rxx': - assert get_control_count(cmd) == 1 ctrl_id = cmd.control_qubits[0].id if local_prev_Ry_sign.get(ctrl_id, -1) <= 0: @@ -100,7 +97,6 @@ def chooser_Ry_reducer(cmd, decomposition_list): if name == 'h2rx': qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] - assert len(qubit_id) == 1 # this should be a single qubit gate qubit_id = qubit_id[0] if local_prev_Ry_sign.get(qubit_id, 0) == 0: @@ -112,7 +108,6 @@ def chooser_Ry_reducer(cmd, decomposition_list): if name == 'rz2rx': qubit_id = [qb.id for qureg in cmd.qubits for qb in qureg] - assert len(qubit_id) == 1 # this should be a single qubit gate qubit_id = qubit_id[0] if local_prev_Ry_sign.get(qubit_id, -1) <= 0: diff --git a/projectq/types/__init__.py b/projectq/types/__init__.py index 96f7ecf24..d81996907 100755 --- a/projectq/types/__init__.py +++ b/projectq/types/__init__.py @@ -13,4 +13,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 96ffc2fe9..207efd08d 100755 --- a/projectq/types/_qubit.py +++ b/projectq/types/_qubit.py @@ -16,9 +16,8 @@ This file defines BasicQubit, Qubit, WeakQubit and Qureg. 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 intented to be used within Qureg objects in user code. Example: .. code-block:: python @@ -27,15 +26,13 @@ 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. @@ -61,21 +58,13 @@ def __str__(self): 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)) @@ -97,8 +86,7 @@ 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) @@ -109,13 +97,10 @@ 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): @@ -124,10 +109,9 @@ def __del__(self): """ 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 +123,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,33 +132,28 @@ 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): @@ -183,40 +161,28 @@ 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): """ diff --git a/projectq/types/_qubit_test.py b/projectq/types/_qubit_test.py index dfcad755c..54287749c 100755 --- a/projectq/types/_qubit_test.py +++ b/projectq/types/_qubit_test.py @@ -45,9 +45,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)]) @@ -164,9 +162,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(): @@ -177,8 +173,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 index e2d959ca0..9f58329ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,8 @@ build-backend = "setuptools.build_meta" '*.mo', '.clang-format', '.gitmodules', + 'requirements.txt', + 'requirements_tests.txt', 'VERSION.txt', '.editorconfig', '*.yml', @@ -49,22 +51,46 @@ build-backend = "setuptools.build_meta" [tool.pylint] - [tools.pylint.master] + [tool.pylint.master] ignore-patterns = [ - '__init__.py' + '__init__.py', + '.*_test.py', + '.*_fixtures.py', + '.*flycheck.*.py', + 'docs/.*', + 'examples/.*', ] - [tools.pylint.format] + 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 - [tools.pylint.reports] + [tool.pylint.reports] msg-template = '{path}:{line}: [{msg_id}, {obj}] {msg} ({symbol})' - [tools.pylint.messages_control] + [tool.pylint.similarities] + min-similarity-lines = 20 + + [tool.pylint.messages_control] disable = [ - 'invalid-name', 'expression-not-assigned', - 'pointless-statemen', + 'pointless-statement', + 'fixme' ] @@ -73,6 +99,7 @@ build-backend = "setuptools.build_meta" minversion = '6.0' addopts = '-pno:warnings' testpaths = ['projectq'] +ignore-glob = ['*flycheck*.py'] mock_use_standalone_module = true [tool.setuptools_scm] diff --git a/requirements_tests.txt b/requirements_tests.txt deleted file mode 100644 index ea01acbbc..000000000 --- a/requirements_tests.txt +++ /dev/null @@ -1,5 +0,0 @@ -flaky -mock -pytest >= 6.0 -pytest-cov -pytest-mock diff --git a/setup.cfg b/setup.cfg index 3d0ebb9c7..f01474cb7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,9 @@ classifier = zip_safe = False packages = find: python_requires = >= 3 +setup_requires = + setuptools_scm[toml] + pybind11 >= 2 install_requires = matplotlib >= 2.2.3 networkx >= 2 @@ -40,6 +43,20 @@ install_requires = [options.extras_require] braket = boto3 +revkit = + revkit == 3.0a2.dev2 + dormouse +test = + flaky + mock + pytest >= 6.0 + pytest-cov + pytest-mock + +docs = + sphinx + sphinx_rtd_theme + # ============================================================================== diff --git a/setup.py b/setup.py index 9be796a62..acb57eb05 100755 --- a/setup.py +++ b/setup.py @@ -36,8 +36,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import print_function -from setuptools import setup, Extension +"""Setup.py file""" + import distutils.log from distutils.cmd import Command from distutils.spawn import find_executable, spawn @@ -48,34 +48,39 @@ DistutilsExecError, DistutilsPlatformError, ) -from setuptools import Distribution as _Distribution -from setuptools.command.build_ext import build_ext -import sys import os -import subprocess import platform +import subprocess +import sys +import tempfile + +from setuptools import setup, Extension +from setuptools import Distribution as _Distribution +from setuptools.command.build_ext import build_ext # ============================================================================== # Helper functions and classes -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.""" +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): self.user = user def __str__(self): - import pybind11 + import pybind11 # pylint: disable=import-outside-toplevel return pybind11.get_include(self.user) def important_msgs(*msgs): + """ + Print an important message. + """ print('*' * 75) for msg in msgs: print(msg) @@ -83,22 +88,27 @@ def important_msgs(*msgs): def status_msgs(*msgs): + """ + Print a status message. + """ print('-' * 75) for msg in msgs: print('# INFO: ', msg) print('-' * 75) -def compiler_test(compiler, flagname=None, link=False, include='', body='', postargs=None): +def compiler_test( + compiler, flagname=None, link=False, include='', body='', postargs=None +): # pylint: disable=too-many-arguments """ 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('{}\nint main (int argc, char **argv) {{ {} return 0; }}'.format(include, body)) - f.close() + fname = None + with tempfile.NamedTemporaryFile('w', suffix='.cpp', delete=False) as temp: + temp.write('{}\nint main (int argc, char **argv) {{ {} return 0; }}'.format(include, body)) + fname = temp.name ret = True if postargs is None: @@ -111,10 +121,10 @@ def compiler_test(compiler, flagname=None, link=False, include='', body='', post if compiler.compiler_type == 'msvc': olderr = os.dup(sys.stderr.fileno()) - err = open('err.txt', 'w') + err = open('err.txt', 'w') # pylint: disable=consider-using-with os.dup2(err.fileno(), sys.stderr.fileno()) - obj_file = compiler.compile([f.name], extra_postargs=postargs) + obj_file = compiler.compile([fname], extra_postargs=postargs) if not os.path.exists(obj_file[0]): raise RuntimeError('') if link: @@ -128,37 +138,39 @@ def compiler_test(compiler, flagname=None, link=False, include='', body='', post raise RuntimeError('') except (CompileError, LinkError, RuntimeError): ret = False - os.unlink(f.name) + os.unlink(fname) return ret def _fix_macosx_header_paths(*args): # Fix path to SDK headers if necessary - _MACOSX_XCODE_REF_PATH = '/Applications/Xcode.app/Contents/' + 'Developer/Platforms/MacOSX.platform/' + 'Developer' - _MACOSX_DEVTOOLS_REF_PATH = '/Library/Developer/CommandLineTools/' + _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() - def _do_replace(idx, item): - 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) - for compiler_args in args: for idx, item in enumerate(compiler_args): - _do_replace(idx, item) + 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): + super().__init__() self.cause = sys.exc_info()[1] # work around py 2/3 different syntax @@ -181,8 +193,8 @@ def __init__(self): ['projectq/backends/_sim/_cppsim.cpp'], include_dirs=[ # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True), + Pybind11Include(), + Pybind11Include(user=True), ], language='c++', ), @@ -203,7 +215,7 @@ class BuildExt(build_ext): ( 'gen-compiledb', None, - 'Generate a compile_commands.json alongside the compilation ' 'implies (-n/--dry-run)', + 'Generate a compile_commands.json alongside the compilation implies (-n/--dry-run)', ), ] @@ -216,13 +228,13 @@ def initialize_options(self): def finalize_options(self): build_ext.finalize_options(self) if self.gen_compiledb: - self.dry_run = True + self.dry_run = True # pylint: disable=attribute-defined-outside-init def run(self): try: build_ext.run(self) - except DistutilsPlatformError: - raise BuildFailed() + except DistutilsPlatformError as err: + raise BuildFailed() from err def build_extensions(self): self._configure_compiler() @@ -244,26 +256,27 @@ def build_extensions(self): } ) - import json + import json # pylint: disable=import-outside-toplevel with open( os.path.join(os.path.dirname(os.path.abspath(__file__)), 'compile_commands.json'), 'w', - ) as fd: - json.dump(compile_commands, fd, sort_keys=True, indent=4) + ) as json_file: + json.dump(compile_commands, json_file, sort_keys=True, indent=4) try: build_ext.build_extensions(self) - except ext_errors: - raise BuildFailed() - except ValueError: + 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() + raise BuildFailed() from err raise def _get_compilation_commands(self, ext): - (macros, objects, extra_postargs, pp_opts, build,) = self.compiler._setup_compile( + # 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, @@ -294,6 +307,8 @@ def _get_compilation_commands(self, ext): return commands def _configure_compiler(self): + # pylint: disable=attribute-defined-outside-init + # Force dry_run = False to allow for compiler feature testing dry_run_old = self.compiler.dry_run self.compiler.dry_run = False @@ -311,8 +326,8 @@ def _configure_compiler(self): if compiler_test(self.compiler, '-stdlib=libc++'): self.c_opts['unix'] += ['-stdlib=libc++'] - ct = self.compiler.compiler_type - self.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): @@ -335,7 +350,7 @@ def _configure_compiler(self): status_msgs('Other compiler tests') self.compiler.define_macro('VERSION_INFO', '"{}"'.format(self.distribution.get_version())) - if ct == 'unix' and compiler_test(self.compiler, '-fvisibility=hidden'): + if compiler_type == 'unix' and compiler_test(self.compiler, '-fvisibility=hidden'): self.opts.append('-fvisibility=hidden') self.compiler.dry_run = dry_run_old @@ -503,10 +518,9 @@ def run(self): try: cmd_obj.run() self.distribution.have_run[command] = 1 - assert self.distribution.ext_modules - except BuildFailed: + 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!') + raise RuntimeError('build_ext --dry-run --gen-compiledb command failed!') from err command = ['clang-tidy'] if self.warning_as_errors: @@ -523,40 +537,52 @@ class GenerateRequirementFile(Command): """A custom command to list the dependencies of the current""" description = 'List the dependencies of the current package' - user_options = [('include-extras', None, 'Include "extras_require" into the list')] - boolean_options = ['include-extras'] + 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): self.include_extras = None + self.include_all_extras = None + self.extra_pkgs = [] def finalize_options(self): - pass + include_extras = self.include_extras.split(',') + + try: + for name, pkgs in self.distribution.extras_require.items(): + if self.include_all_extras or name in include_extras: + self.extra_pkgs.extend(pkgs) + + except TypeError: # Mostly for old setuptools (< 30.x) + for name, pkgs in self.distribution.command_options['options.extras_require'].items(): + if self.include_all_extras or name in include_extras: + self.extra_pkgs.extend(pkgs) def run(self): - with open('requirements.txt', 'w') as fd: + with open('requirements.txt', 'w') as req_file: try: for pkg in self.distribution.install_requires: - fd.write('{}\n'.format(pkg)) - if self.include_extras: - for name, pkgs in self.distribution.extras_require.items(): - for pkg in pkgs: - fd.write('{}\n'.format(pkg)) - + req_file.write('{}\n'.format(pkg)) except TypeError: # Mostly for old setuptools (< 30.x) for pkg in self.distribution.command_options['options']['install_requires']: - fd.write('{}\n'.format(pkg)) - if self.include_extras: - for name, pkgs in self.distribution.command_options['options.extras_require'].items(): - location, pkgs = pkgs - for pkg in pkgs.split(): - fd.write('{}\n'.format(pkg)) + req_file.write('{}\n'.format(pkg)) + req_file.write('\n') + for pkg in self.extra_pkgs: + req_file.write('{}\n'.format(pkg)) # ------------------------------------------------------------------------------ class Distribution(_Distribution): - def has_ext_modules(self): + """Distribution class""" + + def has_ext_modules(self): # pylint: disable=no-self-use + """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 @@ -570,6 +596,7 @@ def has_ext_modules(self): def run_setup(with_cext): + """Run the setup() function""" kwargs = {} if with_cext: kwargs['ext_modules'] = ext_modules