diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f384d36a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: pip + directory: "/" + schedule: + interval: "daily" + groups: + python-packages: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..558b0333 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +# Description + +Please summarise the changes. + +# Related issues + +Please mention any github issues addressed by this PR. + +# Checklist + +- [ ] I have performed a self-review of my code. +- [ ] I have commented hard-to-understand parts of my code. +- [ ] I have made corresponding changes to the public API documentation. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] I have updated the changelog with any user-facing changes. diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index cf742cf8..e4661dc1 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -26,12 +26,12 @@ jobs: os: ['ubuntu-22.04', 'macos-12'] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 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 + uses: actions/setup-python@v5 with: python-version: '3.9' - name: Build and mypy (3.9) @@ -42,7 +42,7 @@ jobs: ./.github/workflows/build-test mypy - name: Set up Python 3.10 if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' || github.event_name == 'schedule' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Build (3.10) @@ -51,7 +51,7 @@ jobs: run: | chmod +x ./.github/workflows/build-test ./.github/workflows/build-test nomypy - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: github.event_name == 'release' || contains(github.ref, 'refs/heads/wheel') with: name: artefacts @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Download all wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: wheelhouse - name: Put them all in the dist folder @@ -94,15 +94,15 @@ jobs: needs: publish_to_pypi runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: '0' - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Download all wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: wheelhouse - name: Install pip, wheel @@ -119,7 +119,7 @@ jobs: mkdir extensions ./build-docs -d ${GITHUB_WORKSPACE}/.github/workflows/docs/extensions/api - name: Upload docs as artefact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: .github/workflows/docs/extensions @@ -137,4 +137,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 \ No newline at end of file + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8afea444..1f624bb6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,9 +12,9 @@ jobs: name: build docs runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Upgrade pip and install wheel @@ -30,7 +30,7 @@ jobs: cd .github/workflows/docs mkdir extensions ./build-docs -d ${GITHUB_WORKSPACE}/.github/workflows/docs/extensions - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: pytket-extension-docs path: .github/workflows/docs/extensions/ \ No newline at end of file diff --git a/.github/workflows/docs/build-docs b/.github/workflows/docs/build-docs index 5a2a4080..6166e0bd 100755 --- a/.github/workflows/docs/build-docs +++ b/.github/workflows/docs/build-docs @@ -10,8 +10,11 @@ import sys DOCS_DIR = Path(sys.argv[0]).absolute().parent MODULES_DIR = DOCS_DIR.parent.parent.parent -PYTKET_DOCS_LINK = "https://cqcl.github.io/tket/pytket/api/index.html" -PYTKET_EX_DOCS_LINK = "https://cqcl.github.io/pytket-extensions/api/index.html" +TKET_EXAMPLES_LINK = "https://tket.quantinuum.com/examples/" +TKET_MANUAL_LINK = "https://tket.quantinuum.com/user-manual/" +TKET_WEBSITE_LINK = "https://tket.quantinuum.com/" +PYTKET_DOCS_LINK = "https://tket.quantinuum.com/api-docs/" +PYTKET_EX_DOCS_LINK = "https://tket.quantinuum.com/api-docs/extensions.html" PYTKET_QULACS_PYPI_LINK = "https://pypi.org/project/pytket-cutensornet/" PYTKET_QULACS_GITHUB = "https://github.com/CQCL/pytket-cutensornet" MODULE = "cutensornet" @@ -49,10 +52,13 @@ def build_module_docs(): with open(mod_docs / "intro.txt", "r") as f: content = f.readlines() content.append( - "\n.. toctree::\n\t:caption: More documentation:\n\t:maxdepth: 1\n\n" + "\n.. toctree::\n\t:caption: pytket documentation:\n\t:maxdepth: 1\n\n" ) - content.append(f"\tpytket <{PYTKET_DOCS_LINK}>\n") + content.append(f"\tpytket API docs <{PYTKET_DOCS_LINK}>\n") content.append(f"\tpytket extensions <{PYTKET_EX_DOCS_LINK}>\n") + content.append(f"\tManual <{TKET_MANUAL_LINK}>\n") + content.append(f"\tExample notebooks <{TKET_EXAMPLES_LINK}>\n") + content.append(f"\tTKET website <{TKET_WEBSITE_LINK}>\n") content.append( "\n.. toctree::\n\t:caption: Links:\n\t:maxdepth: 1\n\n" ) diff --git a/.github/workflows/docs/conf.py b/.github/workflows/docs/conf.py index 6623061b..748ab387 100644 --- a/.github/workflows/docs/conf.py +++ b/.github/workflows/docs/conf.py @@ -45,7 +45,7 @@ # -- Extension configuration ------------------------------------------------- -pytketdoc_base = "https://cqcl.github.io/tket/pytket/api/" +pytketdoc_base = "https://tket.quantinuum.com/api-docs/" intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 74edd9b5..2356b943 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.x - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Update pip diff --git a/README.md b/README.md index 1c8fec4f..cca55e5b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,12 @@ -# Pytket Extensions - -This repository contains the pytket-cutensornet extension, using Quantinuum's -[pytket](https://cqcl.github.io/tket/pytket/api/index.html) quantum SDK. - # pytket-cutensornet -[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. +[Pytket](https://tket.quantinuum.com/api-docs/index.html) is a python module for interfacing +with tket, a quantum computing toolkit and optimising compiler developed by Quantinuum. -[cuTensorNet](https://docs.nvidia.com/cuda/cuquantum/cutensornet/index.html) is a +[cuTensorNet](https://docs.nvidia.com/cuda/cuquantum/latest/cutensornet/index.html) is a high-performance library for tensor network computations, developed by NVIDIA. -It is part of the [cuQuantum](https://docs.nvidia.com/cuda/cuquantum/index.html) SDK - +It is part of the [cuQuantum](https://docs.nvidia.com/cuda/cuquantum/latest/index.html) SDK - 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. @@ -21,7 +16,7 @@ Both components have both C and Python API. `pytket-cutensornet` is an extension to `pytket` that allows `pytket` circuits and expectation values to be simulated using `cuTensorNet` via an interface to -[cuQuantum Python](https://docs.nvidia.com/cuda/cuquantum/python/index.html). +[cuQuantum Python](https://docs.nvidia.com/cuda/cuquantum/latest/cutensornet/index.html). Currently, only single-GPU calculations are supported, but a multi-GPU execution will be implemented in the due course using `mpi4py` library. @@ -29,13 +24,16 @@ implemented in the due course using `mpi4py` library. ## Getting started `pytket-cutensornet` is available for Python 3.9, 3.10 and 3.11 on Linux. -In order to use it, you need access to a Linux machine with either `Volta`, `Ampere` -or `Hopper` GPU and first install `cuQuantum Python` following their installation -[instructions](https://docs.nvidia.com/cuda/cuquantum/python/README.html#installation). +In order to use it, you need access to a Linux machine with an NVIDIA GPU of +Compute Capability +7.0 (check it [here](https://developer.nvidia.com/cuda-gpus)) and first +install `cuQuantum Python` following their installation +[instructions](https://docs.nvidia.com/cuda/cuquantum/latest/python/README.html#installation). This will include the necessary dependencies such as CUDA toolkit. Then, to install `pytket-cutensornet`, run: -```pip install pytket-cutensornet``` +```shell +pip install pytket-cutensornet +``` ## Bugs, support and feature requests diff --git a/_metadata.py b/_metadata.py index 9669f116..500ed3c5 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.4.0" +__extension_version__ = "0.5.0" __extension_name__ = "pytket-cutensornet" diff --git a/docs/changelog.rst b/docs/changelog.rst index 892af085..3f4f6046 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ~~~~~~~~~ +0.5.0 (December 2023) +--------------------- + +* ``MPS`` simulation with fixed ``truncation_fidelity`` now uses the corresponding truncation primitive from cuQuantum (v23.10). + 0.4.0 (October 2023) -------------------- diff --git a/examples/mps_tutorial.ipynb b/examples/mps_tutorial.ipynb index 237504e7..ed8bf238 100644 --- a/examples/mps_tutorial.ipynb +++ b/examples/mps_tutorial.ipynb @@ -34,7 +34,7 @@ "source": [ "# Introduction\n", "\n", - "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://cqcl.github.io/pytket-cutensornet/api/index.html.\n", + "This notebook provides examples of the usage of the MPS functionalities of `pytket_cutensornet`. For more information, see the docs at https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html.\n", "\n", "A Matrix Product State (MPS) represents a state on `n` qubits as a list of `n` tensors connected in a line as show below:\n", "\n", diff --git a/lint-requirements.txt b/lint-requirements.txt index 1e43bf10..3d9b6a8e 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,2 +1,2 @@ -black~=22.3 -pylint~=2.13,!=2.13.6 \ No newline at end of file +black~=23.12 +pylint~=3.0 \ No newline at end of file diff --git a/pytket/extensions/cutensornet/mps/mps_gate.py b/pytket/extensions/cutensornet/mps/mps_gate.py index 93961bf2..a5e322da 100644 --- a/pytket/extensions/cutensornet/mps/mps_gate.py +++ b/pytket/extensions/cutensornet/mps/mps_gate.py @@ -15,8 +15,6 @@ import warnings import logging -import numpy as np # type: ignore - try: import cupy as cp # type: ignore except ImportError: @@ -93,6 +91,8 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: Returns: ``self``, to allow for method chaining. """ + options = {"handle": self._lib.handle, "device_id": self._lib.device_id} + l_pos = min(positions) r_pos = max(positions) @@ -147,142 +147,39 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: gate_tensor, self.tensors[l_pos], self.tensors[r_pos], - options={"handle": self._lib.handle, "device_id": self._lib.device_id}, + options=options, optimize={"path": [(0, 1), (0, 1)]}, ) self._logger.debug(f"Intermediate tensor of size (MiB)={T.nbytes / 2**20}") # Get the template of the MPS tensors involved L = self.tensors[l_pos] - l_shape = list(L.shape) R = self.tensors[r_pos] - r_shape = list(R.shape) if self._cfg.truncation_fidelity < 1: - # Carry out SVD decomposition first with NO truncation - # to figure out where to apply the dimension cutoff. - # Then, apply S normalisation and contraction of S and L manually. - # - # TODO: As soon as cuQuantum 23.09 is released, replace this - # unintuitive code with a simple update to SVDConfig so that it - # uses REL_SUM2_CUTOFF. Then the code in the `else` block should - # be run; i.e. use standard cuTensorNet API to do the SVD - # including normalisation and contraction of S with L. + # Apply SVD decomposition to truncate as much as possible before exceeding + # a `discarded_weight_cutoff` of `1 - self._cfg.truncation_fidelity`. self._logger.debug( f"Truncating to target fidelity={self._cfg.truncation_fidelity}" ) - options = {"handle": self._lib.handle, "device_id": self._lib.device_id} - svd_method = tensor.SVDMethod(abs_cutoff=self._cfg.zero) - L, S, R = tensor.decompose( - "acLR->asL,scR", T, method=svd_method, options=options - ) - - # Use the fact that the entries of S are sorted in decreasing - # order and calculate the number of singular values `new_dim` to - # keep so that - # sum([s**2 for s in S']) - # truncation_fidelity <= ------------------------- - # sum([s**2 for s in S]) - # - # where S is the list of original singular values and S' is the set of - # singular values that remain after truncation (before normalisation). - denom = float(sum(cp.square(S))) # Element-wise squaring - numer = 0.0 - old_dim = new_dim - new_dim = 0 - - # Take singular values until we surpass the target fidelity - while self._cfg.truncation_fidelity > numer / denom: - numer += float(S[new_dim] ** 2) - new_dim += 1 - this_fidelity = numer / denom - - # Reshape tensors down to `new_dim` for the virtual bond - # No data is copied or moved around, we're changing the ndarray bounds - l_shape[-2] = new_dim - # pylint: disable = unexpected-keyword-arg # Disable pylint for next line - L = cp.ndarray( - l_shape, - dtype=self._cfg._complex_t, - memptr=L.data, - strides=L.strides, - ) - r_shape[0] = new_dim - # pylint: disable = unexpected-keyword-arg # Disable pylint for next line - R = cp.ndarray( - r_shape, - dtype=self._cfg._complex_t, - memptr=R.data, - strides=R.strides, - ) - # pylint: disable = unexpected-keyword-arg # Disable pylint for next line - S = cp.ndarray(new_dim, dtype=self._cfg._real_t, memptr=S.data) - - # Normalise - S *= np.sqrt(1 / this_fidelity) - - # Contract S into L - S = S.astype(dtype=self._cfg._complex_t, copy=False) - # Use some einsum index magic: since the virtual bond "s" appears in the - # list of bonds of the output, it is not summed over. - # This causes S to act as the intended diagonal matrix. - L = cq.contract( - "asL,s->asL", - L, - S, - options={"handle": self._lib.handle, "device_id": self._lib.device_id}, - optimize={"path": [(0, 1)]}, - ) - - # We multiply the fidelity of the current step to the overall fidelity - # to keep track of a lower bound for the fidelity. - self.fidelity *= this_fidelity - - # Report to logger - self._logger.debug(f"Truncation done. Truncation fidelity={this_fidelity}") - self._logger.debug( - f"Reduced virtual bond dimension from {old_dim} to {new_dim}." + svd_method = tensor.SVDMethod( + abs_cutoff=self._cfg.zero, + discarded_weight_cutoff=1 - self._cfg.truncation_fidelity, + partition="U", # Contract S directly into U (named L in our case) + normalization="L2", # Sum of squares singular values must equal 1 ) elif new_dim > self._cfg.chi: # Apply SVD decomposition and truncate up to a `max_extent` (for the shared - # bond) of `self._cfg.chi`. Ask cuTensorNet to contract S directly into the - # L tensor and normalise the singular values so that the sum of its squares - # is equal to one (i.e. the MPS is a normalised state after truncation). + # bond) of `self._cfg.chi`. self._logger.debug(f"Truncating to (or below) chosen chi={self._cfg.chi}") - options = {"handle": self._lib.handle, "device_id": self._lib.device_id} svd_method = tensor.SVDMethod( abs_cutoff=self._cfg.zero, max_extent=self._cfg.chi, partition="U", # Contract S directly into U (named L in our case) - normalization="L2", # Sum of squares equal 1 - ) - - L, S, R, svd_info = tensor.decompose( - "acLR->asL,scR", T, method=svd_method, options=options, return_info=True - ) - assert S is None # Due to "partition" option in SVDMethod - - # discarded_weight is calculated within cuTensorNet as: - # sum([s**2 for s in S']) - # discarded_weight = 1 - ------------------------- - # sum([s**2 for s in S]) - # where S is the list of original singular values and S' is the set of - # singular values that remain after truncation (before normalisation). - # It can be shown that the fidelity ||^2 (for |phi> and |psi> - # unit vectors before and after truncation) is equal to 1 - disc_weight. - # - # We multiply the fidelity of the current step to the overall fidelity - # to keep track of a lower bound for the fidelity. - this_fidelity = 1.0 - svd_info.discarded_weight - self.fidelity *= this_fidelity - - # Report to logger - self._logger.debug(f"Truncation done. Truncation fidelity={this_fidelity}") - self._logger.debug( - f"Reduced virtual bond dimension from {new_dim} to {R.shape[0]}." + normalization="L2", # Sum of squares singular values must equal 1 ) else: @@ -299,22 +196,41 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: # since canonicalisation is just meant to detect the optimal singular values # to truncate, but if we find values that are essentially zero, we are safe # to remove them. - options = {"handle": self._lib.handle, "device_id": self._lib.device_id} svd_method = tensor.SVDMethod( abs_cutoff=self._cfg.zero, partition="U", # Contract S directly into U (named L in our case) normalization=None, # Without canonicalisation we must not normalise ) - L, S, R = tensor.decompose( - "acLR->asL,scR", T, method=svd_method, options=options - ) - assert S is None # Due to "partition" option in SVDMethod - # Report to logger + # Apply the SVD decomposition using the configuration defined above + L, S, R, svd_info = tensor.decompose( + "acLR->asL,scR", T, method=svd_method, options=options, return_info=True + ) + assert S is None # Due to "partition" option in SVDMethod + + # Update fidelity if there was some truncation (of non-zero singular values) + if new_dim > self._cfg.chi or self._cfg.truncation_fidelity < 1: + # discarded_weight is calculated within cuTensorNet as: + # sum([s**2 for s in S']) + # discarded_weight = 1 - ------------------------- + # sum([s**2 for s in S]) + # where S is the list of original singular values and S' is the set of + # singular values that remain after truncation (before normalisation). + # It can be shown that the fidelity ||^2 (for |phi> and |psi> + # unit vectors before and after truncation) is equal to 1 - disc_weight. + # + # We multiply the fidelity of the current step to the overall fidelity + # to keep track of a lower bound for the fidelity. + this_fidelity = 1.0 - svd_info.discarded_weight + self.fidelity *= this_fidelity + self._logger.debug(f"Truncation done. Truncation fidelity={this_fidelity}") + + else: self._logger.debug(f"Truncation done. Fidelity estimate unchanged.") - self._logger.debug( - f"Reduced virtual bond dimension from {new_dim} to {R.shape[0]}." - ) + + self._logger.debug( + f"Reduced virtual bond dimension from {new_dim} to {R.shape[0]}." + ) self.tensors[l_pos] = L self.tensors[r_pos] = R diff --git a/setup.py b/setup.py index 592d71f8..e849b2c4 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ author_email="tket-support@cambridgequantum.com", python_requires=">=3.9", project_urls={ - "Documentation": "https://cqcl.github.io/pytket-cutensornet/api/index.html", + "Documentation": "https://tket.quantinuum.com/extensions/pytket-cutensornet/api/index.html", "Source": "https://github.com/CQCL/pytket-cutensornet", "Tracker": "https://github.com/CQCL/pytket-cutensornet/issues", }, @@ -42,7 +42,7 @@ license="Apache 2", packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, - install_requires=["pytket ~= 1.11"], + install_requires=["pytket ~= 1.22"], classifiers=[ "Environment :: Console", "Programming Language :: Python :: 3.9",