diff --git a/.github/workflows/build-test b/.github/workflows/build-test new file mode 100644 index 00000000..d1147b3d --- /dev/null +++ b/.github/workflows/build-test @@ -0,0 +1,65 @@ +#!/bin/bash +set -evu + +# Usage: +# +# build-test [mypy|nomypy] +# +# Arguments: +# - mypy: include mypy check ("mypy" or "nomypy") +# +# Environment variables used: +# - GITHUB_WORKSPACE: workspace directory +# +# WARNING: running this locally will delete any local files that +# aren't strictly part of the git tree, including gitignored files! + +MODULE=pytket-cuquantum + +MYPY=$1 + +PLAT=`python -c 'import platform; print(platform.system())'` + +PYVER=`python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))'` + +git clean -dfx + +echo "Module to test: ${MODULE}" + +MODULEDIR="${GITHUB_WORKSPACE}" + +ARTIFACTSDIR=${GITHUB_WORKSPACE}/wheelhouse + +rm -rf ${ARTIFACTSDIR} && mkdir ${ARTIFACTSDIR} + +python -m pip install --upgrade pip wheel build + +# Generate and install the package +python -m build +for w in dist/*.whl ; do + python -m pip install $w + cp $w ${ARTIFACTSDIR} +done + +# Test and mypy: +if [[ "${MYPY}" = "mypy" ]] +then + python -m pip install --upgrade mypy +fi + +# Currently all tests depend on cuQuantum, so are disabled +#cd ${GITHUB_WORKSPACE}/tests +# +#python -m pip install --pre -r test-requirements.txt +# +## update the pytket version to the lastest (pre) release +#python -m pip install --upgrade --pre pytket~=1.0 +# +#pytest --doctest-modules +# +#cd .. + +if [[ "${MYPY}" = "mypy" ]] +then + ${GITHUB_WORKSPACE}/mypy-check ${GITHUB_WORKSPACE} +fi \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 00000000..3bbbb76c --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,42 @@ +name: Build and test + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - develop + - 'wheel/**' + - 'runci/**' + release: + types: + - created + - edited + schedule: + # 04:00 every Tuesday morning + - cron: '0 4 * * 2' + +jobs: + cuquantum-checks: + name: cuQuantum - Build and test module + strategy: + matrix: + os: ['ubuntu-22.04', 'macos-12', 'windows-2022'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: '0' + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/* + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Build and test including remote checks (3.9) mypy + shell: bash + if: (matrix.os == 'macos-12') && (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'release' || github.event_name == 'schedule' ) + run: | + chmod +x ./.github/workflows/build-test + ./.github/workflows/build-test mypy \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..74edd9b5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint python projects + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - develop + - 'wheel/**' + - 'runci/**' + +jobs: + lint: + + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Update pip + run: pip install --upgrade pip + - name: Install black and pylint + run: pip install black~=22.3 pylint~=2.13,!=2.13.6 + - name: Check files are formatted with black + run: | + black --check . + - name: Run pylint + run: | + pylint --recursive=y */ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f9b8f80d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.eggs +*.egg-info +build +dist +*.pyc +.vscode +.mypy_cache +.hypothesis +obj +docs/extensions +.ipynb_checkpoints +pytket/extensions/cuquantum/_metadata.py \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..c2a5a497 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,55 @@ +[MASTER] +max-line-length=88 +output-format=colorized +score=no +reports=no +disable=all +enable= + anomalous-backslash-in-string, + assert-on-tuple, + bad-indentation, + bad-option-value, + bad-reversed-sequence, + bad-super-call, + consider-merging-isinstance, + continue-in-finally, + dangerous-default-value, + duplicate-argument-name, + expression-not-assigned, + function-redefined, + inconsistent-mro, + init-is-generator, + line-too-long, + lost-exception, + missing-kwoa, + mixed-line-endings, + not-callable, + no-value-for-parameter, + nonexistent-operator, + not-in-loop, + pointless-statement, + redefined-builtin, + return-arg-in-generator, + return-in-init, + return-outside-function, + simplifiable-if-statement, + syntax-error, + too-many-function-args, + trailing-whitespace, + undefined-variable, + unexpected-keyword-arg, + unhashable-dict-key, + unnecessary-pass, + unreachable, + unrecognized-inline-option, + unused-import, + unnecessary-semicolon, + unused-variable, + unused-wildcard-import, + wildcard-import, + wrong-import-order, + wrong-import-position, + yield-outside-function + +# Ignore long lines containing URLs or pylint or mypy directives. +ignore-long-lines=^(.*#\w*pylint: disable.*|.*# type: ignore.*|\s*(# )??)$ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..7d8b9627 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include _metadata.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..17a57b80 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Pytket Extensions + +This repository contains the pytket-cuquantum extension, using Quantinuum's +[pytket](https://cqcl.github.io/tket/pytket/api/index.html) quantum SDK. + +# pytket-cuquantum + +[Pytket](https://cqcl.github.io/tket/pytket/api/index.html) is a python module for interfacing +with tket, a quantum computing toolkit and optimisation compiler developed by Quantinuum. + +[cuQuantum](https://docs.nvidia.com/cuda/cuquantum/index.html) SDK is a high-performance library +aimed at quantum circuit simulations on the NVIDIA GPU chips, consisting of two major components: + - cuStateVec: a high-performance library for state vector computations. + - cuTensorNet: a high-performance library for tensor network computations. + +Both components have both C and Python API. + +`pytket-cuquantum` is an extension to `pytket` that allows `pytket` circuits and expectation values to be +run on the cuQuantum simulators via interfaces to [cuQuantum Python](https://docs.nvidia.com/cuda/cuquantum/python/index.html). + +Currently, only an interface to [cuTensorNet](https://docs.nvidia.com/cuda/cuquantum/cutensornet/index.html) (via its [Python API](https://docs.nvidia.com/cuda/cuquantum/python/api/index.html)) is implemented. + +## Getting started + +`pytket-cuquantum` is available for Python 3.9 and 3.10, on Linux, MacOS and +Windows. + +Currently only installation in editable mode from source is available (see below). + +## Bugs, support and feature requests + +Please file bugs and feature requests on the Github +[issue tracker](https://github.com/CQCL/pytket-cuquantum/issues). + +## Development + +To install an extension in editable mode, simply change to its subdirectory +within the `modules` directory, and run: + +```shell +pip install -e . +``` + +## Contributing + +Pull requests are welcome. To make a PR, first fork the repo, make your proposed +changes on the `develop` branch, and open a PR from your fork. If it passes +tests and is accepted after review, it will be merged in. + +### Code style + +#### Docstrings + +We use the Google style docstrings, please see this +[page](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for +reference. + +#### Formatting + +All code should be formatted using +[black](https://black.readthedocs.io/en/stable/), with default options. This is +checked on the CI. The CI is currently using version 22.12.0. You can install it +(as well as pylint as described below) by running from the root package folder: + +```shell +pip install -r lint-requirements.txt +``` + +#### Type annotation + +On the CI, [mypy](https://mypy.readthedocs.io/en/stable/) is used as a static +type checker and all submissions must pass its checks. You should therefore run +`mypy` locally on any changed files before submitting a PR. Because of the way +extension modules embed themselves into the `pytket` namespace this is a little +complicated, but it should be sufficient to run the script `mypy-check` +and passing as a single argument the root directory of the module to test. The directory +path should end with a `/`. For example, to run mypy on all Python files in this +repository, when in the root folder, run: + +```shell +./mypy-check ./ +``` +The script requires `mypy` 0.800 or above. + +#### Linting + +We use [pylint](https://pypi.org/project/pylint/) on the CI to check compliance +with a set of style requirements (listed in `.pylintrc`). You should run +`pylint` over any changed files before submitting a PR, to catch any issues. + +### Tests + +To run the tests for a module: + +1. `cd` into that module's `tests` directory; +2. ensure you have installed `pytest` and any other modules listed in +the `test-requirements.txt` file (all via `pip`); +3. run `pytest`. + +When adding a new feature, please add a test for it. When fixing a bug, please +add a test that demonstrates the fix. diff --git a/_metadata.py b/_metadata.py new file mode 100644 index 00000000..a38ad3ce --- /dev/null +++ b/_metadata.py @@ -0,0 +1,2 @@ +__extension_version__ = "0.1.0" +__extension_name__ = "pytket-cuquantum" diff --git a/lint-requirements.txt b/lint-requirements.txt new file mode 100644 index 00000000..1e43bf10 --- /dev/null +++ b/lint-requirements.txt @@ -0,0 +1,2 @@ +black~=22.3 +pylint~=2.13,!=2.13.6 \ No newline at end of file diff --git a/mypy-check b/mypy-check new file mode 100755 index 00000000..98eb4785 --- /dev/null +++ b/mypy-check @@ -0,0 +1,30 @@ +#!/bin/bash +set -evu + +# single argument = root directory of module to test +# Requires mypy >= 0.800 + +MODULEDIR=$1 + +# set MYPYPATH +MYPYPATH="." +for MOD in $(ls -d "$MODULEDIR"*/); do + MOD_PATH="$(cd "$MOD" && pwd)" + MYPYPATH="$MYPYPATH:$MOD_PATH" +done +export MYPYPATH="$MYPYPATH" + +ROOT_INIT_FILE=$(python -c "from importlib.util import find_spec; print(find_spec('pytket').origin)") + +# remove pytket root init file +mv "$ROOT_INIT_FILE" "$ROOT_INIT_FILE.ignore" + +set +e +mypy --config-file=mypy.ini --no-incremental -p pytket -p tests +STATUS=$? +set -e + +# reset init file +mv "$ROOT_INIT_FILE.ignore" "$ROOT_INIT_FILE" + +exit $STATUS \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..74c92e3f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,28 @@ +[mypy] +python_version = 3.9 +warn_unused_configs = True + +disallow_untyped_decorators = False +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True + +no_implicit_optional = True +strict_optional = True +namespace_packages = True + +check_untyped_defs = True + +warn_redundant_casts = True +warn_unused_ignores = False +warn_no_return = False +warn_return_any = True +warn_unreachable = True + +[mypy-pytest.*] +ignore_missing_imports = True +ignore_errors = True + +[mypy-lark.*] +ignore_missing_imports = True +ignore_errors = True \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..e69de29b diff --git a/pytket/extensions/cuquantum/__init__.py b/pytket/extensions/cuquantum/__init__.py new file mode 100644 index 00000000..c79377ac --- /dev/null +++ b/pytket/extensions/cuquantum/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2019 Cambridge Quantum Computing +# +# 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 for conversion from tket primitives to cuQuantum primitives.""" + +# _metadata.py is copied to the folder after installation. +from .tensor_network_convert import ( + TensorNetwork, + PauliOperatorTensorNetwork, + ExpectationValueTensorNetwork, + tk_to_tensor_network, +) diff --git a/pytket/extensions/cuquantum/backends/__init__.py b/pytket/extensions/cuquantum/backends/__init__.py new file mode 100644 index 00000000..488b784d --- /dev/null +++ b/pytket/extensions/cuquantum/backends/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2019-2023 Cambridge Quantum Computing +# +# 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. +"""Backend for utilising the cuQuantum simulator directly from pytket""" + +from .cutensornet_backend import CuTensorNetBackend diff --git a/pytket/extensions/cuquantum/backends/cutensornet_backend.py b/pytket/extensions/cuquantum/backends/cutensornet_backend.py new file mode 100644 index 00000000..e0bfe431 --- /dev/null +++ b/pytket/extensions/cuquantum/backends/cutensornet_backend.py @@ -0,0 +1,293 @@ +# Copyright 2019-2023 Cambridge Quantum Computing +# +# 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. + +"""Methods to allow tket circuits to be run on the cuTensorNet simulator.""" + +import warnings + +try: + import cuquantum as cq # type: ignore +except ImportError: + warnings.warn("local settings failed to import cuquantum", ImportWarning) +from logging import warning +from typing import List, Union, Optional, Sequence +from uuid import uuid4 +import numpy as np +from sympy import Expr # type: ignore +from pytket.circuit import Circuit, OpType # type: ignore +from pytket.backends import ResultHandle, CircuitStatus, StatusEnum, CircuitNotRunError +from pytket.backends.backend import KwargTypes, Backend, BackendResult +from pytket.backends.backendinfo import BackendInfo +from pytket.backends.resulthandle import _ResultIdTuple +from pytket.extensions.cuquantum.tensor_network_convert import ( + TensorNetwork, + ExpectationValueTensorNetwork, + tk_to_tensor_network, +) +from pytket.predicates import Predicate, GateSetPredicate, NoClassicalBitsPredicate # type: ignore +from pytket.passes import ( # type: ignore + BasePass, + SequencePass, + DecomposeBoxes, + SynthesiseTket, + FullPeepholeOptimise, + RebaseCustom, + SquashCustom, +) +from pytket.utils.operators import QubitPauliOperator + + +# TODO: this is temporary - probably don't need it eventually? +def _sq(a: Expr, b: Expr, c: Expr) -> Circuit: + circ = Circuit(1) + if c != 0: + circ.Rz(c, 0) + if b != 0: + circ.Rx(b, 0) + if a != 0: + circ.Rz(a, 0) + return circ + + +class CuTensorNetBackend(Backend): + """A pytket Backend wrapping around the cuTensorNet simulator.""" + + _supports_state = True + _supports_expectation = True + _persistent_handles = False + + # TODO: add self._backend_info? + def __init__(self) -> None: + """Constructs a new cuTensorNet backend object.""" + super().__init__() + + @property + def _result_id_type(self) -> _ResultIdTuple: + return (str,) + + # TODO: return some info? Should it return self._backend_info instantiated on + # construction? + @property + def backend_info(self) -> Optional[BackendInfo]: + """Returns information on the backend.""" + return None + + # TODO: Surely we can allow for more gate sets - needs thorough testing though. + @property + def required_predicates(self) -> List[Predicate]: + """Returns the minimum set of predicates that a circuit must satisfy. + + Predicates need to be satisfied before the circuit can be successfully run on + this backend. + + Returns: + List of required predicates. + """ + preds = [ + NoClassicalBitsPredicate(), + GateSetPredicate( + { + OpType.Rx, + OpType.Ry, + OpType.Rz, + OpType.ZZMax, + } + ), + ] + return preds + + # TODO: also probably needs improvement. + def rebase_pass(self) -> BasePass: + """Defines rebasing method. + + Returns: + Custom rebase pass object. + """ + cx_circ = Circuit(2) + cx_circ.Sdg(0) + cx_circ.V(1) + cx_circ.Sdg(1) + cx_circ.Vdg(1) + cx_circ.add_gate(OpType.ZZMax, [0, 1]) + cx_circ.Vdg(1) + cx_circ.Sdg(1) + cx_circ.add_phase(0.5) + return RebaseCustom( + {OpType.Rx, OpType.Ry, OpType.Rz, OpType.ZZMax}, cx_circ, _sq + ) + + # TODO: same as above? + def default_compilation_pass(self, optimisation_level: int = 1) -> BasePass: + """Returns a default compilation pass. + + A suggested compilation pass that will guarantee the resulting circuit + will be suitable to run on this backend with as few preconditions as + possible. + + Args: + optimisation_level: The level of optimisation to perform during + compilation. Level 0 just solves the device constraints without + optimising. Level 1 additionally performs some light optimisations. + Level 2 adds more intensive optimisations that can increase compilation + time for large circuits. Defaults to 1. + Returns: + Compilation pass guaranteeing required predicates. + """ + assert optimisation_level in range(3) + squash = SquashCustom({OpType.Rz, OpType.Rx, OpType.Ry}, _sq) + seq = [DecomposeBoxes()] # Decompose boxes into basic gates + if optimisation_level == 1: + seq.append(SynthesiseTket()) # Optional fast optimisation + elif optimisation_level == 2: + seq.append(FullPeepholeOptimise()) # Optional heavy optimisation + seq.append(self.rebase_pass()) # Map to target gate set + if optimisation_level != 0: + seq.append( + squash + ) # Optionally simplify 1qb gate chains within this gate set + return SequencePass(seq) + + def circuit_status(self, handle: ResultHandle) -> CircuitStatus: + """Returns circuit status object. + + Returns: + CircuitStatus object. + + Raises: + CircuitNotRunError: if there is no handle object in cache. + """ + if handle in self._cache: + return CircuitStatus(StatusEnum.COMPLETED) + raise CircuitNotRunError(handle) + + def process_circuits( + self, + circuits: Sequence[Circuit], + n_shots: Optional[Union[int, Sequence[int]]] = None, + valid_check: bool = True, + **kwargs: KwargTypes, + ) -> List[ResultHandle]: + """Submits circuits to the backend for running. + + The results will be stored in the backend's result cache to be retrieved by the + corresponding get_ method. + + Args: + circuits: List of circuits to be submitted. + n_shots: Number of shots in case of shot-based calculation. + valid_check: Whether to check for circuit correctness. + + Returns: + Results handle objects. + + Raises: + TypeError: If global phase is dependent on a symbolic parameter. + """ + circuit_list = list(circuits) + if valid_check: + self._check_all_circuits(circuit_list) + handle_list = [] + for circuit in circuit_list: + state_tnet = tk_to_tensor_network(circuit) + state_tnet.append( + list(range(1, circuit.n_qubits + 1)) + ) # This ensures the right order. + state = cq.contract(*state_tnet).flatten() + try: # This constraint (from pytket-Qulacs) seems reasonable? + phase = float(circuit.phase) + coeff = np.exp(phase * np.pi * 1j) + state *= coeff # type: ignore + except TypeError: + warning( + "Global phase is dependent on a symbolic parameter, so cannot " + "adjust for phase" + ) + # Qubits order: + implicit_perm = circuit.implicit_qubit_permutation() + res_qubits = [ + implicit_perm[qb] for qb in sorted(circuit.qubits, reverse=False) + ] # reverse was set to True in the pytket-example but this fails tests. + # The below line is as per pytket-Qulacs, but this alone failt the implicit + # permutation test result. + # res_qubits = sorted(circuit.qubits, reverse=False) + handle = ResultHandle(str(uuid4())) + self._cache[handle] = { + "result": BackendResult(q_bits=res_qubits, state=state) + } + handle_list.append(handle) + return handle_list + + # TODO: this should be optionally parallelised with MPI + # (both wrt Pauli strings and contraction itself). + def get_operator_expectation_value( + self, + state_circuit: Circuit, + operator: QubitPauliOperator, + valid_check: bool = True, + ) -> float: + """Calculates expectation value of an operator using cuTensorNet contraction. + + Args: + state_circuit: Circuit representing state. + operator: Operator which expectation value is to be calculated. + valid_check: Whether to perform circuit validity check. + + Returns: + Real part of the expectation value. + """ + if valid_check: + self._check_all_circuits([state_circuit]) + + expectation = 0 + for qos, coeff in operator._dict.items(): + ket_network = TensorNetwork(state_circuit) + bra_network = ket_network.dagger() + expectation_value_network = ExpectationValueTensorNetwork( + bra_network, qos, ket_network + ) + if isinstance(coeff, Expr): + numeric_coeff = complex(coeff.evalf()) # type: ignore + else: + numeric_coeff = complex(coeff) + expectation_term = numeric_coeff * cq.contract( + *expectation_value_network.cuquantum_interleaved + ) + expectation += expectation_term + return expectation.real + + def get_circuit_overlap( + self, + circuit_ket: Circuit, + circuit_bra: Optional[Circuit] = None, + valid_check: bool = True, + ) -> float: + """Calculates an overlap of two states represented by two circuits. + + Args: + circuit_bra: Circuit representing the bra state. + circuit_ket: Circuit representing the ket state. + valid_check: Whether to perform circuit validity check. + + Returns: + Overlap value. + """ + if circuit_bra is None: + circuit_bra = circuit_ket + if valid_check: + self._check_all_circuits([circuit_bra, circuit_ket]) + + ket_net = TensorNetwork(circuit_ket) + overlap_net_interleaved = ket_net.vdot(TensorNetwork(circuit_bra)) + overlap: float = cq.contract(*overlap_net_interleaved) + return overlap diff --git a/pytket/extensions/cuquantum/py.typed b/pytket/extensions/cuquantum/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pytket/extensions/cuquantum/tensor_network_convert.py b/pytket/extensions/cuquantum/tensor_network_convert.py new file mode 100644 index 00000000..76d20651 --- /dev/null +++ b/pytket/extensions/cuquantum/tensor_network_convert.py @@ -0,0 +1,568 @@ +# Copyright 2019-2023 Cambridge Quantum Computing +# +# 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. + +"""Tools to convert tket circuit to tensor network to be contracted with cuTensorNet.""" + +from collections import defaultdict +import logging +from logging import Logger +from typing import List, Tuple, Union, Any, DefaultDict +import networkx as nx # type: ignore +from networkx.classes.reportviews import OutMultiEdgeView, OutMultiEdgeDataView # type: ignore +import numpy as np +from numpy.typing import NDArray +from pytket.utils import Graph +from pytket.pauli import QubitPauliString # type: ignore +from pytket.circuit import Circuit +from pytket.utils import permute_rows_cols_in_unitary + + +# TODO: decide whether to use logger. +def set_logger( + logger_name: str, + level: int = logging.INFO, + fmt: str = "%(name)s - %(levelname)s - %(message)s", +) -> Logger: + """Initialises and configures a logger object. + + Args: + logger_name: Name for the logger object. + level: Logger output level. + fmt: Logger output format. + + Returns: + New configured logger object. + """ + logger = logging.getLogger(logger_name) + logger.setLevel(level) + logger.propagate = False + if not logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(level) + formatter = logging.Formatter(fmt) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +class TensorNetwork: + """Responsible for converting pytket circuit to a tensor network and handling it.""" + + def __init__( + self, circuit: Circuit, adj: bool = False, loglevel: int = logging.INFO + ) -> None: + """Constructs a tensor network from a pytket circuit. + + Resulting tensor network in einsum notation suitable to use with cuTensorNet. + + Args: + circuit: A pytket circuit to be converted to a tensor network. + adj: Whether to create an adjoint representation of the original circuit. + loglevel: Internal logger output level. + """ + self._logger = set_logger("TensorNetwork", loglevel) + self._circuit = circuit + self._network = Graph(circuit).as_nx() + self._node_tensors = self._assign_node_tensors(adj=adj) + if adj: + self._node_tensor_indices, self.sticky_indices = self._get_tn_indices( + self._network, adj=adj + ) + else: + self._node_tensor_indices, self.sticky_indices = self._get_tn_indices( + self._network + ) + self._cuquantum_interleaved = self._make_interleaved() + + @property + def cuquantum_interleaved(self) -> list: + """Returns an interleaved format of the circuit tensor network.""" + return self._cuquantum_interleaved + + def _get_gate_tensors(self, adj: bool = False) -> DefaultDict[Any, List[Any]]: + """Computes and stores tensors for each gate type from the circuit. + + The unitaries are reshaped into tensors of bond dimension two prior to being + stored. + + Args: + adj: Whether an adjoint representation of the original circuit is to be + created. + + Returns: + A map between the gate type and corresponding tensor representation(s). + + Note: + The returned map values are lists and may contain more than one + representation - for >1-qubit gates, different topologies (e.g. upward and + downward) are taken into account. + """ + name_set = {com.op.get_name() for com in self._circuit.get_commands()} + gate_tensors = defaultdict(list) + for i in name_set: + for com in self._circuit.get_commands(): + if i == com.op.get_name(): + if adj: + gate_tensors[i].append( + com.op.get_unitary() + .T.conjugate() + .reshape([2] * (2 * com.op.n_qubits)) + ) + self._logger.debug( + f"Adding unitary: \n {com.op.get_unitary().T.conjugate()}" + ) + else: + gate_tensors[i].append( + com.op.get_unitary().reshape([2] * (2 * com.op.n_qubits)) + ) + self._logger.debug(f"Adding unitary: \n {com.op.get_unitary()}") + # Add a unitary for a gate pointing "upwards" (e.g. CX[1, 0]) + if com.op.n_qubits > 1: + com_qix = [self._circuit.qubits.index(qb) for qb in com.args] + self._logger.debug(f"command qubit indices: {com_qix}") + com_qix_compressed = [i for i, _ in enumerate(com_qix)] + self._logger.debug( + f"command qubit indices compressed: {com_qix_compressed}" + ) + com_qix_permut = list(reversed(com_qix_compressed)) + self._logger.debug( + f"command qubit indices compressed permuted:" + f" {com_qix_permut}" + ) + # TODO: check type inconsistency and remove type ignore + # statements + if adj: + gate_tensors[i].append( + permute_rows_cols_in_unitary( + com.op.get_unitary(), com_qix_permut # type: ignore + ) + .T.conjugate() + .reshape([2] * (2 * com.op.n_qubits)) + ) + self._logger.debug( + f"Adding unitary: \n {permute_rows_cols_in_unitary(com.op.get_unitary(), com_qix_permut).T.conjugate()}" # type: ignore + ) + else: + gate_tensors[i].append( + permute_rows_cols_in_unitary( + com.op.get_unitary(), com_qix_permut # type: ignore + ).reshape([2] * (2 * com.op.n_qubits)) + ) + self._logger.debug( # type: ignore + f"Adding unitary: \n {permute_rows_cols_in_unitary(com.op.get_unitary(),com_qix_permut)}" # type: ignore + ) + break + self._logger.debug(f"Gate tensors: \n{gate_tensors}\n") + return gate_tensors + + def _assign_node_tensors(self, adj: bool = False) -> List[Any]: + """Creates a list of tensors representing circuit gates (tensor network nodes). + + Args: + adj: Whether an adjoint representation of the original circuit is to be + created. + + Returns: + List of tensors representing circuit gates (tensor network nodes) in the + reversed order of circuit graph nodes. + """ + self._gate_tensors = self._get_gate_tensors(adj=adj) + node_tensors = [] + self._input_nodes = [] + self._output_nodes = [] + for i, node in reversed(list(enumerate(self._network.nodes(data=True)))): + if node[1]["desc"] not in ("Input", "Output"): + n_out_edges = len(list(self._network.out_edges(node[0]))) + if n_out_edges > 1: + src_ports = [ + edge[-1]["src_port"] + for edge in self._network.out_edges(node[0], data=True) + ] + unit_idx = [ + edge[-1]["unit_id"] + for edge in self._network.out_edges(node[0], data=True) + ] + # Detect if this is a reversed gate (pointing upward) + self._logger.debug(f"src_ports: {src_ports}, unit_idx: {unit_idx}") + self._logger.debug( + f"src_ports relation: {src_ports[0] < src_ports[1]}" + ) + self._logger.debug( + f"unit_idx relation: {unit_idx[0] < unit_idx[1]}" + ) + self._logger.debug( + f"criteria: " + f"{(src_ports[0] < src_ports[1]) != (unit_idx[0] < unit_idx[1])}" # pylint: disable=line-too-long + ) + if (src_ports[0] < src_ports[1]) != (unit_idx[0] < unit_idx[1]): + node_tensors.append(self._gate_tensors[node[1]["desc"]][1]) + self._logger.debug(f"Adding an upward gate tensor") + else: + node_tensors.append(self._gate_tensors[node[1]["desc"]][0]) + self._logger.debug(f"Adding a downward gate tensor") + else: + node_tensors.append(self._gate_tensors[node[1]["desc"]][0]) + self._logger.debug(f"Adding a 1-qubit gate tensor") + else: + if node[1]["desc"] == "Output": + self._output_nodes.append(i) + if node[1]["desc"] == "Input": + self._input_nodes.append(i) + node_tensors.append(np.array([1, 0], dtype="complex128")) + if adj: + node_tensors.reverse() + self._logger.debug(f"Node tensors: \n{node_tensors}\n") + + return node_tensors + + def _get_tn_indices( + self, net: nx.MultiDiGraph, adj: bool = False + ) -> Tuple[List[Any], List[Any]]: + """Computes indices of the edges of the tensor network nodes (tensors). + + Indices are computed such that they range from high (for circuit leftmost gates) + absolute values to |1|. Sign of the indices is negative if an adjoint + representation of the circuit is to be constructed. The outward or "sticky" + indices (for circuit rightmost gates) are sorted (and possibly swapped with + inner indices) such that they match qubit indices (+1) in the graph. Remaining + indices follow the graph edges reverse order (except for the swapped ones). + + Indices of the tensors dimensions to be contracted along must match, so they are + ordered consistently for each tensor. + + Lists of indices for each tensor (node) are stored in the same order in which + the tensors themselves are stored. + + Args: + net: Graph, representing the current circuit. + adj: Whether an adjoint representation of the original circuit is to be + created. + + Returns: + A list of lists of tensor network nodes edges (tensors dimensions) indices + and a list of outward ("sticky") indices along which there will be no + contraction. + """ + sign = -1 if adj else 1 + self._logger.debug(f"Network nodes: \n{net.nodes(data=True)}") + self._logger.debug(f"Network edges: \n{net.edges(data=True)}") + # There can be several identical edges for which we need different indices + edge_indices = defaultdict(list) + n_edges = nx.number_of_edges(net) + # Append tuples of inverse edge indices (starting from 1) and qubit indices + # to each edge entry + for i, (e, ed) in enumerate(zip(net.edges(), net.edges(data=True))): + edge_indices[e].append((sign * (n_edges - i), int(ed[-1]["unit_id"] / 2))) + self._logger.debug(f"Network edge indices: \n {edge_indices}") + nodes_out = self._output_nodes + # Check if need to swap indices for outward indices + for node in nodes_out: + prenode = next(net.predecessors(node)) + eid = edge_indices[(prenode, node)][0][0] + qid = edge_indices[(prenode, node)][0][1] + if ( + eid - sign * 1 != sign * qid + ): # Edge indexing starts from 1 or -1, qubit from 0 + lswap = False + # expensive: + for edge, idx_lst in edge_indices.items(): + for i, (ei, qi) in enumerate(idx_lst): + if ei - sign * 1 == sign * qid: + self._logger.debug( + f"Swapping indices of edges {edge} and " + f"({prenode, node})!" + ) + edge_indices[(prenode, node)] = [ + (edge_indices[edge][i][0], qid) + ] + edge_indices[edge][i] = (eid, qi) + lswap = True + break + if lswap: + break + self._logger.debug( + f"Network edge indices after swaps (if any): \n {edge_indices}" + ) + # Store the "sticky" indices + sticky_indices = [] + for edge in net.edges(): + for node in nodes_out: + if node in edge: + for ei, qi in edge_indices[edge]: + sticky_indices.append(ei) + sticky_indices.sort(key=abs) + self._logger.debug(f"sticky (outer) edge indices: \n {sticky_indices}") + # Assign correctly ordered indices to tensors (nodes) and store their lists in + # the same order as we store tensors themselves. + tn_indices = [] + for node in reversed(list(net.nodes)): + if node in nodes_out: + continue + self._logger.debug(f"Node: {node}") + num_edges = len(list(net.in_edges(node))) + len(list(net.out_edges(node))) + in_edges_data = net.in_edges(node, data=True) + out_edges_data = net.out_edges(node, data=True) + in_edges = net.in_edges(node) + out_edges = net.out_edges(node) + self._logger.debug(f"in_edges: {in_edges}") + self._logger.debug(f"out_edges: {out_edges}") + ordered_edges = [0] * num_edges + if num_edges > 2: + ordered_out_edges = self._order_edges_for_multiqubit_gate( + edge_indices, out_edges, out_edges_data, 0, self._logger + ) + ordered_in_edges = self._order_edges_for_multiqubit_gate( + edge_indices, + in_edges, + in_edges_data, + int(num_edges / 2), + self._logger, + ) + for loc_idx, edge_idx in ordered_in_edges.items(): + ordered_edges[loc_idx] = edge_idx + for loc_idx, edge_idx in ordered_out_edges.items(): + ordered_edges[loc_idx] = edge_idx + + else: + ordered_edges[0] = edge_indices[list(out_edges)[0]][0][0] + if in_edges: + ordered_edges[1] = edge_indices[list(in_edges)[0]][0][0] + if adj and len(ordered_edges) > 1: + m = int(len(ordered_edges) / 2) + ordered_edges[:m], ordered_edges[m:] = ( + ordered_edges[m:], + ordered_edges[:m], + ) + self._logger.debug(f"New node edges: \n {ordered_edges}") + tn_indices.append(ordered_edges) + if adj: + tn_indices.reverse() + self._logger.debug(f"Final TN edges: \n {tn_indices}") + return tn_indices, sticky_indices + + @staticmethod + def _order_edges_for_multiqubit_gate( + edge_indices: DefaultDict[Any, List[Tuple[Any, int]]], + edges: OutMultiEdgeView, + edges_data: OutMultiEdgeDataView, + offset: int, + logger: Logger, + ) -> dict: + """Returns a map from local tensor indices to global edges indices. + + Aimed at multi-qubit gates. + + This map assures correct ordering of edge indices for each multi-qubit gate + tensor representation within the tensor network (which is important for the + correct contraction). It should be called separately for the "incoming" and + "outgoing" edges of a node (dimensions of a tensor, representing a gate). + + Args: + edge_indices: a map from pytket graph edges (tuples of two integers, + representing adjacent nodes) to a list of tuples, containing an assigned + edge index and a corresponding qubit index. + edges: pytket graph edges (list of tuples of two integers). + edges_data: pytket graph edges with metadata (list of tuples of two integers + and a dict). + offset: an integer offset, being 0 if the "incoming" edges are to be mapped, + or half the number of edges of the node (dimensions of a tensor) if the + "outgoing" edges are to be mapped. + logger: a logger object. + """ + gate_edges_ordered = {} + qi_to_local_ei = {} + qis = [] + for edge_data in edges_data: + logger.debug(f"Edge data: {edge_data}") + logger.debug(f"Qubit id: {int(edge_data[-1]['unit_id'] / 2)}") + qis.append(int(edge_data[-1]["unit_id"] / 2)) + qis.sort() + for i, qi in enumerate(qis): + qi_to_local_ei[qi] = offset + i + logger.debug(f"Qubit to local edge index map: {qi_to_local_ei}") + for edge_data, edge in zip(edges_data, edges): + qi = int(edge_data[-1]["unit_id"] / 2) + if len(edge_indices[edge]) == 1: + gate_edges_ordered[qi_to_local_ei[qi]] = edge_indices[edge][0][0] + else: + for e, q in edge_indices[edge]: + if q == qi: + gate_edges_ordered[qi_to_local_ei[qi]] = e + break + return gate_edges_ordered + + def _make_interleaved(self) -> list: + """Returns an interleaved form of a tensor network. + + The format is suitable as an input for the cuQuantum-Python `contract` function. + + Combines the list of tensor representations of circuit gates and corresponding + edges indices, that must have been constructed in the same order. + + Returns: + A list of interleaved tensors (ndarrays) and lists of corresponding edges + indices. + """ + tn_interleaved = [] + for tensor, indices in zip(self._node_tensors, self._node_tensor_indices): + tn_interleaved.append(tensor) + tn_interleaved.append(indices) + self._logger.debug(f"cuQuantum input list: \n{input}") + return tn_interleaved + + def dagger(self) -> "TensorNetwork": + """Constructs an adjoint of a tensor network object. + + Returns: + A new TensorNetwork object, containing an adjoint representation of the + input object. + """ + tn_dagger = TensorNetwork(self._circuit.copy(), adj=True) + self._logger.debug( + f"dagger cuquantum input list: \n{tn_dagger._cuquantum_interleaved}" + ) + return tn_dagger + + def vdot(self, tn_other: "TensorNetwork") -> list: + """Returns a tensor network representing an overlap of two circuits. + + An adjoint representation of `tn_other` is obtained first (with the indices + having negative sign). Then the two tensor networks are concatenated, separated + by a single layer of unit matrices. The "sticky" indices of the two tensor + networks connect with their counterparts via those unit matrices. + + Args: + tn_other: a TensorNetwork object representing a circuit, an overlap with + which is to be calculated. + + Returns: + A tensor network in an interleaved form, representing an overlap of two + circuits. + """ + tn_other_adj = tn_other.dagger() + i_mat = np.array([[1, 0], [0, 1]], dtype="complex128") + connector = [ + f(x) # type: ignore + for x in self.sticky_indices + for f in (lambda x: i_mat, lambda x: [-x, x]) + ] + tn_concatenated = tn_other_adj.cuquantum_interleaved + tn_concatenated.extend(connector) + tn_concatenated.extend(self.cuquantum_interleaved) + self._logger.debug(f"Overlap input list: \n{tn_concatenated}") + return tn_concatenated + + +class PauliOperatorTensorNetwork: + """Handles a tensor network representing a Pauli operator string.""" + + PAULI = { + "X": np.array([[0, 1], [1, 0]], dtype="complex128"), + "Y": np.array([[0, -1j], [1j, 0]], dtype="complex128"), + "Z": np.array([[1, 0], [0, -1]], dtype="complex128"), + "I": np.array([[1, 0], [0, 1]], dtype="complex128"), + } + + def __init__( + self, paulis: QubitPauliString, ket: TensorNetwork, loglevel: int = logging.INFO + ) -> None: + """Constructs a tensor network representing a Pauli operator string. + + Contains a single layer of unitaries, corresponding to the provided Pauli string + operators and identity matrices. + + Takes a circuit tensor network as input and uses its "sticky" indices to assign + indices to the unitaries in the network - the "incoming" indices have negative + sign and "outgoing" - positive sign. + + Args: + paulis: Pauli operators string. + ket: Tensor network object representing a certain circuit. + loglevel: Logger verbosity level. + """ + self._logger = set_logger("PauliOperatorTensorNetwork", loglevel) + self._pauli_tensors = [self.PAULI[pauli.name] for pauli in paulis.map.values()] + self._logger.debug(f"Pauli tensors: {self._pauli_tensors}") + qubit_ids = [qubit.to_list()[1][0] + 1 for qubit in paulis.map.keys()] + qubit_to_pauli = { + qubit: pauli_tensor + for (qubit, pauli_tensor) in zip(qubit_ids, self._pauli_tensors) + } + self._logger.debug(f"qubit to Pauli mapping: {qubit_to_pauli}") + self._cuquantum_interleaved = [ + f(x) # type: ignore + for x in ket.sticky_indices + for f in ( + lambda x: qubit_to_pauli[x] if (x in qubit_ids) else self.PAULI["I"], + lambda x: [-x, x], + ) + ] + self._logger.debug(f"Pauli TN: {self.cuquantum_interleaved}") + + @property + def cuquantum_interleaved(self) -> list: + """Returns an interleaved format of the circuit tensor network.""" + return self._cuquantum_interleaved + + +class ExpectationValueTensorNetwork: + """Handles a tensor network representing an expectation value.""" + + def __init__( + self, bra: TensorNetwork, paulis: QubitPauliString, ket: TensorNetwork + ) -> None: + """Constructs a tensor network representing expectation value. + + Simply concatenates input tensor networks for bra and ket circuits and a string + of Pauli operators in-between. + + Args: + bra: Tensor network object representing a bra circuit. + ket: Tensor network object representing a ket circuit. + paulis: Pauli operator string. + """ + self._bra = bra + self._ket = ket + self._operator = PauliOperatorTensorNetwork(paulis, ket) + self._cuquantum_interleaved = self._make_interleaved() + + @property + def cuquantum_interleaved(self) -> list: + """Returns an interleaved format of the circuit tensor network.""" + return self._cuquantum_interleaved + + def _make_interleaved(self) -> list: + """Concatenates the tensor networks elements of the expectation value. + + Returns: + A tensor network representing expectation value in the interleaved format + (list). + """ + tn_concatenated = self._bra.cuquantum_interleaved + tn_concatenated.extend(self._operator.cuquantum_interleaved) + tn_concatenated.extend(self._ket.cuquantum_interleaved) + return tn_concatenated + + +def tk_to_tensor_network(tkc: Circuit) -> List[Union[NDArray, List]]: + """Converts pytket circuit into a tensor network. + + Args: + tkc: Circuit. + + Returns: + A tensor network representing the input circuit in the interleaved format + (list). + """ + return TensorNetwork(tkc).cuquantum_interleaved diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..ecd345db --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +# Copyright 2020-2023 Cambridge Quantum Computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import shutil +import os +from setuptools import setup, find_namespace_packages # type: ignore + +metadata: dict = {} +with open("_metadata.py") as fp: + exec(fp.read(), metadata) +shutil.copy( + "_metadata.py", + os.path.join("pytket", "extensions", "cuquantum", "_metadata.py"), +) + + +setup( + name="pytket-cuquantum", + version=metadata["__extension_version__"], + author="TKET development team", + author_email="tket-support@cambridgequantum.com", + python_requires=">=3.9", + project_urls={ + "Documentation": "https://cqcl.github.io/pytket-cuquantum/api/index.html", + "Source": "https://github.com/CQCL/pytket-cuquantum", + "Tracker": "https://github.com/CQCL/pytket-cuquantum/issues", + }, + description="Extension for pytket, providing access to the cuQuantum simulators", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + license="Apache 2", + packages=find_namespace_packages(include=["pytket.*"]), + include_package_data=True, + install_requires=[ + "pytket ~= 1.11" + ], # TODO: how to account for cuQuantum requirement? + classifiers=[ + "Environment :: Console", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + ], + zip_safe=False, +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..deda84e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,72 @@ +import pytest +from pytket.circuit import Circuit + + +@pytest.fixture +def q2_x0() -> Circuit: + circuit = Circuit(2) + circuit.X(0) + return circuit + + +@pytest.fixture +def q2_x1() -> Circuit: + circuit = Circuit(2) + circuit.X(1) + return circuit + + +@pytest.fixture +def q2_v0() -> Circuit: + circuit = Circuit(2) + circuit.V(0) + return circuit + + +@pytest.fixture +def q2_x0cx01() -> Circuit: + circuit = Circuit(2) + circuit.X(0).CX(0, 1) + return circuit + + +@pytest.fixture +def q2_x1cx10x1() -> Circuit: + circuit = Circuit(2) + circuit.X(1).CX(1, 0).X(1) + return circuit + + +@pytest.fixture +def q2_x0cx01cx10() -> Circuit: + circuit = Circuit(2) + circuit.X(0).CX(0, 1).CX(1, 0) + return circuit + + +@pytest.fixture +def q2_v0cx01cx10() -> Circuit: + circuit = Circuit(2) + circuit.V(0).CX(0, 1).CX(1, 0) + return circuit + + +@pytest.fixture +def q2_hadamard_test() -> Circuit: + circuit = Circuit(2) + circuit.H(0).CRx(0.5, 0, 1).H(0) + return circuit + + +@pytest.fixture +def q3_v0cx02() -> Circuit: + circuit = Circuit(3) + circuit.V(0).CX(0, 2) + return circuit + + +@pytest.fixture +def q3_cx01cz12x1rx0() -> Circuit: + circuit = Circuit(3) + circuit.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0) + return circuit diff --git a/tests/test-requirements.txt b/tests/test-requirements.txt new file mode 100644 index 00000000..408d40a8 --- /dev/null +++ b/tests/test-requirements.txt @@ -0,0 +1,2 @@ +pytest +pytest-lazy-fixture \ No newline at end of file diff --git a/tests/test_cutensornet_backend.py b/tests/test_cutensornet_backend.py new file mode 100644 index 00000000..84a529e9 --- /dev/null +++ b/tests/test_cutensornet_backend.py @@ -0,0 +1,116 @@ +import numpy as np +import pytest +from pytket.circuit import Circuit, BasisOrder, Unitary1qBox, OpType # type: ignore +from pytket.passes import CliffordSimp # type: ignore +from pytket.pauli import QubitPauliString, Pauli # type: ignore +from pytket.utils.operators import QubitPauliOperator +from pytket import Qubit # type: ignore +from pytket.extensions.cuquantum.backends import CuTensorNetBackend + + +def test_bell() -> None: + c = Circuit(2) + c.H(0) + c.CX(0, 1) + b = CuTensorNetBackend() + c = b.get_compiled_circuit(c) + h = b.process_circuit(c) + assert np.allclose( + b.get_result(h).get_state(), np.asarray([1, 0, 0, 1]) * 1 / np.sqrt(2) + ) + + +def test_basisorder() -> None: + c = Circuit(2) + c.X(1) + b = CuTensorNetBackend() + c = b.get_compiled_circuit(c) + h = b.process_circuit(c) + r = b.get_result(h) + assert np.allclose(r.get_state(), np.asarray([0, 1, 0, 0])) + assert np.allclose(r.get_state(basis=BasisOrder.dlo), np.asarray([0, 0, 1, 0])) + + +def test_implicit_perm() -> None: + c = Circuit(2) + c.CX(0, 1) + c.CX(1, 0) + c.Ry(0.1, 1) + c1 = c.copy() + CliffordSimp().apply(c1) + b = CuTensorNetBackend() + c = b.get_compiled_circuit(c, optimisation_level=1) + c1 = b.get_compiled_circuit(c1, optimisation_level=1) + assert c.implicit_qubit_permutation() != c1.implicit_qubit_permutation() + h, h1 = b.process_circuits([c, c1]) + r, r1 = b.get_results([h, h1]) + for bo in [BasisOrder.ilo, BasisOrder.dlo]: + s = r.get_state(basis=bo) + s1 = r1.get_state(basis=bo) + assert np.allclose(s, s1) + + +def test_compilation_pass() -> None: + b = CuTensorNetBackend() + for opt_level in range(3): + c = Circuit(2) + c.CX(0, 1) + u = np.asarray([[0, 1], [-1j, 0]]) + c.add_unitary1qbox(Unitary1qBox(u), 1) + c.CX(0, 1) + c.add_gate(OpType.CRz, 0.35, [1, 0]) + assert not (b.valid_circuit(c)) + c = b.get_compiled_circuit(c, optimisation_level=opt_level) + assert b.valid_circuit(c) + + +def test_invalid_measures() -> None: + c = Circuit(2) + c.H(0).CX(0, 1).measure_all() + b = CuTensorNetBackend() + c = b.get_compiled_circuit(c) + assert not (b.valid_circuit(c)) + + +def test_expectation_value() -> None: + c = Circuit(2) + c.H(0) + c.CX(0, 1) + op = QubitPauliOperator( + { + QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): 1.0, + QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.3, + QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Y}): 0.8j, + QubitPauliString({Qubit(0): Pauli.Y}): -0.4j, + } + ) + b = CuTensorNetBackend() + c = b.get_compiled_circuit(c) + expval = b.get_operator_expectation_value(c, op) + assert np.isclose(expval, 1.3) + + +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q2_x0"), # type: ignore + pytest.lazy_fixture("q2_x1"), # type: ignore + pytest.lazy_fixture("q2_v0"), # type: ignore + pytest.lazy_fixture("q2_x0cx01"), # type: ignore + pytest.lazy_fixture("q2_x1cx10x1"), # type: ignore + pytest.lazy_fixture("q2_x0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q3_v0cx02"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + ], +) +def test_compile_convert_statevec_overlap(circuit: Circuit) -> None: + b = CuTensorNetBackend() + c = b.get_compiled_circuit(circuit) + h = b.process_circuit(c) + assert np.allclose( + b.get_result(h).get_state(), np.array([circuit.get_statevector()]) + ) + ovl = b.get_circuit_overlap(c) + assert ovl == pytest.approx(1.0) diff --git a/tests/test_tensor_network_convert.py b/tests/test_tensor_network_convert.py new file mode 100644 index 00000000..0a9fb4dc --- /dev/null +++ b/tests/test_tensor_network_convert.py @@ -0,0 +1,56 @@ +from typing import List, Union +import warnings +import numpy as np +from numpy.typing import NDArray +import pytest + +try: + import cuquantum as cq # type: ignore +except ImportError: + warnings.warn("local settings failed to import cuquantum", ImportWarning) +from pytket.circuit import Circuit + +from pytket.extensions.cuquantum.tensor_network_convert import ( # type: ignore + tk_to_tensor_network, + TensorNetwork, +) + + +def state_contract(tn: List[Union[NDArray, List]], nqubit: int) -> NDArray: + """Calls cuQuantum contract function to contract an input state tensor network.""" + state_tn = tn.copy() + state_tn.append(list(range(1, nqubit + 1))) # This ensures the right ordering + state: NDArray = cq.contract(*state_tn).flatten() + return state + + +def circuit_overlap_contract(circuit_ket: Circuit) -> float: + """Calculates an overlap of a state circuit with its adjoint.""" + ket_net = TensorNetwork(circuit_ket) + overlap_net_interleaved = ket_net.vdot(TensorNetwork(circuit_ket)) + overlap: float = cq.contract(*overlap_net_interleaved) + return overlap + + +@pytest.mark.parametrize( + "circuit", + [ + pytest.lazy_fixture("q2_x0"), # type: ignore + pytest.lazy_fixture("q2_x1"), # type: ignore + pytest.lazy_fixture("q2_v0"), # type: ignore + pytest.lazy_fixture("q2_x0cx01"), # type: ignore + pytest.lazy_fixture("q2_x1cx10x1"), # type: ignore + pytest.lazy_fixture("q2_x0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_v0cx01cx10"), # type: ignore + pytest.lazy_fixture("q2_hadamard_test"), # type: ignore + pytest.lazy_fixture("q3_v0cx02"), # type: ignore + pytest.lazy_fixture("q3_cx01cz12x1rx0"), # type: ignore + ], +) +def test_convert_statevec_overlap(circuit: Circuit) -> None: + tn = tk_to_tensor_network(circuit) + result_cu = state_contract(tn, circuit.n_qubits).flatten().round(10) + state_vector = np.array([circuit.get_statevector()]) + assert np.allclose(result_cu, state_vector) + ovl = circuit_overlap_contract(circuit) + assert ovl == pytest.approx(1.0)