diff --git a/.github/workflows/build-test b/.github/workflows/build-test index 50f637e..7833968 100755 --- a/.github/workflows/build-test +++ b/.github/workflows/build-test @@ -51,6 +51,9 @@ 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 .. diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b81deba..862f0d1 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -9,6 +9,7 @@ on: branches: - develop - 'wheel/**' + - 'runci/**' release: types: - created @@ -35,7 +36,6 @@ jobs: fetch-depth: '0' - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/* - name: Set up Python 3.9 - if: github.event_name == 'pull_request' || github.event_name == 'release' || contains(github.ref, 'refs/heads/wheel') || github.event_name == 'schedule' uses: actions/setup-python@v4 with: python-version: '3.9' @@ -46,7 +46,7 @@ jobs: env: PYTKET_RUN_REMOTE_TESTS: 1 - name: Build and test including remote checks (3.9) nomypy - if: (matrix.os != 'macos-12') && (github.event_name == 'pull_request' || github.event_name == 'release' || contains(github.ref, 'refs/heads/wheel') || github.event_name == 'schedule') + 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') shell: bash run: | ./.github/workflows/build-test nomypy @@ -98,8 +98,8 @@ jobs: password: ${{ secrets.PYPI_PYTKET_IQM_API_TOKEN }} verbose: true - docs: - name: Build and publish docs + build_docs: + name: Build docs if: github.event_name == 'release' needs: publish_to_pypi runs-on: ubuntu-22.04 @@ -117,7 +117,7 @@ jobs: path: wheelhouse - name: Install pip, wheel run: pip install -U pip wheel - - name: Install extensions + - name: Install extension run: for w in `find wheelhouse/ -type f -name "*.whl"` ; do pip install $w ; done - name: Install docs dependencies run: | @@ -127,20 +127,24 @@ jobs: run: | cd .github/workflows/docs mkdir extensions - ./build-docs -d ${GITHUB_WORKSPACE}/.github/workflows/docs/extensions - - name: Configure git - run: | - git config --global user.email "tket-bot@cambridgequantum.com" - git config --global user.name "«$GITHUB_WORKFLOW» github action" - - name: Check out gh-pages branch - run: git checkout gh-pages - - name: Remove old docs - run: git rm -r --ignore-unmatch docs/api - - name: Add generated docs to repository - run: | - mkdir -p docs - mv .github/workflows/docs/extensions docs/api - git add -f docs/api - git commit --allow-empty -m "Add generated documentation." - - name: Publish docs - run: git push origin gh-pages:gh-pages + ./build-docs -d ${GITHUB_WORKSPACE}/.github/workflows/docs/extensions/api + - name: Upload docs as artefact + uses: actions/upload-pages-artifact@v1 + with: + path: .github/workflows/docs/extensions + + publish_docs: + name: Publish docs + if: github.event_name == 'release' + needs: build_docs + runs-on: ubuntu-22.04 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6600ab1..15eff11 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python 3.9 uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: '3.9' - name: Upgrade pip and install wheel run: pip install --upgrade pip wheel - name: Install pytket iqm diff --git a/.github/workflows/docs/Quantinuum_logo.png b/.github/workflows/docs/Quantinuum_logo.png index e15bc56..5569581 100644 Binary files a/.github/workflows/docs/Quantinuum_logo.png and b/.github/workflows/docs/Quantinuum_logo.png differ diff --git a/.github/workflows/docs/conf.py b/.github/workflows/docs/conf.py index fe9a193..c44f60b 100644 --- a/.github/workflows/docs/conf.py +++ b/.github/workflows/docs/conf.py @@ -10,9 +10,24 @@ "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", + "sphinx_copybutton", ] -html_theme = "sphinx_rtd_theme" +pygments_style = "borland" + +html_theme = "sphinx_book_theme" + +html_theme_options = { + "repository_url": "https://github.com/CQCL/pytket-iqm", + "use_repository_button": True, + "use_issues_button": True, +} + +html_static_path = ["_static"] + +html_css_files = ["custom.css"] + +html_logo = "Quantinuum_logo.png" # -- Extension configuration ------------------------------------------------- @@ -29,13 +44,14 @@ # The following code is for resolving broken hyperlinks in the doc. -from sphinx.application import Sphinx +import re +from typing import Any, Dict, List, Optional +from urllib.parse import urljoin + from docutils import nodes from docutils.nodes import Element, TextElement +from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment -from urllib.parse import urljoin -import re -from typing import Any, Dict, List, Optional # Mappings for broken hyperlinks that intersphinx cannot resolve external_url_mapping = { diff --git a/.github/workflows/docs/requirements.txt b/.github/workflows/docs/requirements.txt index 62096a9..31e9126 100644 --- a/.github/workflows/docs/requirements.txt +++ b/.github/workflows/docs/requirements.txt @@ -1,2 +1,3 @@ sphinx ~= 4.3.2 -sphinx_rtd_theme +sphinx_book_theme +sphinx-copybutton diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 8731c09..4798a2e 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Login - uses: atlassian/gajira-login@v2.0.0 + uses: atlassian/gajira-login@v3.0.1 env: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - name: Create Bug - uses: atlassian/gajira-create@v2.0.1 + uses: atlassian/gajira-create@v3.0.1 if: contains(github.event.issue.labels.*.name, 'bug') with: project: TKET @@ -24,7 +24,7 @@ jobs: summary: « [pytket-iqm] ${{ github.event.issue.title }}» description: ${{ github.event.issue.html_url }} - name: Create Task - uses: atlassian/gajira-create@v2.0.1 + uses: atlassian/gajira-create@v3.0.1 if: "! contains(github.event.issue.labels.*.name, 'bug')" with: project: TKET diff --git a/.gitignore b/.gitignore index 74ec74f..acc4832 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build dist *.pyc .vscode +.venv .mypy_cache .hypothesis obj diff --git a/.pylintrc b/.pylintrc index 6549ebb..7b25117 100644 --- a/.pylintrc +++ b/.pylintrc @@ -22,7 +22,6 @@ enable= line-too-long, lost-exception, missing-kwoa, - mixed-indentation, mixed-line-endings, not-callable, no-value-for-parameter, @@ -30,7 +29,6 @@ enable= not-in-loop, pointless-statement, redefined-builtin, - relative-import, return-arg-in-generator, return-in-init, return-outside-function, diff --git a/README.md b/README.md index e5ebabe..cb9fd6e 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ from pytket.extensions.iqm import IQMBackend from pytket.circuit import Circuit backend = IQMBackend( - url="https://cortex-demo.qc.iqm.fi", - auth_server_url="https://auth.demo.qc.iqm.fi", + url="https://demo.qc.iqm.fi/cocos", + auth_server_url="https://demo.qc.iqm.fi/auth", username="USERNAME", - password="PASSWORD", + password="PASSWORD", ) circuit = Circuit(3, 3) @@ -63,6 +63,10 @@ by providing the `arch` parameter to the `IQMBackend` constructor, but it genera does not make sense, since the IQM server reports the valid quantum architecture relevant to the given backend URL. +(Note: At the moment IQM does not provide a quantum computing service open to the +general public. Please contact our [sales team](https://www.meetiqm.com/contact/) +to set up your access to an IQM quantum computer.) + ## Bugs and feature requests Please file bugs and feature requests on the GitHub @@ -122,7 +126,7 @@ skipped. To enable them, set the following environment variables: ```shell export PYTKET_RUN_REMOTE_TESTS=1 -export PYTKET_REMOTE_IQM_AUTH_SERVER_URL=https://auth.demo.qc.iqm.fi +export PYTKET_REMOTE_IQM_AUTH_SERVER_URL=https://demo.qc.iqm.fi/auth export PYTKET_REMOTE_IQM_USERNAME=YOUR_USERNAME export PYTKET_REMOTE_IQM_PASSWORD=YOUR_PASSWORD ``` diff --git a/_metadata.py b/_metadata.py index 3284371..be52f94 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.5.0" +__extension_version__ = "0.6.0" __extension_name__ = "pytket-iqm" diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..732d595 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,44 @@ +.wy-side-nav-search, +.wy-nav-top { + background: #5A46BE; +} + +.wy-grid-for-nav, +.wy-body-for-nav, +.wy-nav-side, +.wy-side-scroll, +.wy-menu, +.wy-menu-vertical { + background-color: #FFFFFF; +} + +.wy-menu-vertical a:hover { + background-color: #d9d9d9; +} + +.caption-text { + color: #000000; +} + +.btn-link:visited, +.btn-link, +a:visited, +.a.reference.external, +.a.reference.internal, +.wy-menu-vertical a, +.wy-menu-vertical li, +.wy-menu-vertical ul, +.span.pre, +.sig-param, +.std.std-ref, +a { + color: #544d4d; +} + +:root { + --pst-color-inline-code: 199, 37, 78 !important; +} + +.sig-name { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 4854a54..987a58f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog ~~~~~~~~~ +0.6.0 (March 2023) +------------------ + +* Updated pytket version requirement to 1.13. +* Updated iqm-client version requirement to 11.8. +* New method ``IQMBackend.get_metadata()`` for reteieving metadata associated + with a ``ResultHandle``. + 0.5.0 (November 2022) --------------------- diff --git a/pytket/extensions/iqm/__init__.py b/pytket/extensions/iqm/__init__.py index 41298d7..0e42819 100644 --- a/pytket/extensions/iqm/__init__.py +++ b/pytket/extensions/iqm/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. diff --git a/pytket/extensions/iqm/backends/__init__.py b/pytket/extensions/iqm/backends/__init__.py index b48fa8f..32738f6 100644 --- a/pytket/extensions/iqm/backends/__init__.py +++ b/pytket/extensions/iqm/backends/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. diff --git a/pytket/extensions/iqm/backends/config.py b/pytket/extensions/iqm/backends/config.py index 49d38e7..fb717b9 100644 --- a/pytket/extensions/iqm/backends/config.py +++ b/pytket/extensions/iqm/backends/config.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. diff --git a/pytket/extensions/iqm/backends/iqm.py b/pytket/extensions/iqm/backends/iqm.py index f379c47..2c818a6 100644 --- a/pytket/extensions/iqm/backends/iqm.py +++ b/pytket/extensions/iqm/backends/iqm.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. @@ -13,12 +13,13 @@ # limitations under the License. import json -from typing import cast, Dict, List, Optional, Sequence, Union +from typing import cast, Dict, List, Optional, Sequence, Tuple, Union from uuid import UUID from iqm_client.iqm_client import Circuit as IQMCircuit from iqm_client.iqm_client import ( Instruction, IQMClient, + Metadata, ) import numpy as np from pytket.backends import Backend, CircuitStatus, ResultHandle, StatusEnum @@ -221,7 +222,9 @@ def process_circuits( instrs = _translate_iqm(c0) qm = {str(qb): _as_name(qb) for qb in c.qubits} iqmc = IQMCircuit( - name=c.name if c.name else f"circuit_{i}", instructions=instrs + name=c.name if c.name else f"circuit_{i}", + instructions=instrs, + metadata=None, ) run_id = self._client.submit_circuits( [iqmc], qubit_mapping=qm, shots=n_shots @@ -285,6 +288,39 @@ def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResul assert circuit_status.status is StatusEnum.ERROR raise RuntimeError(circuit_status.message) + def get_metadata(self, handle: ResultHandle, **kwargs: KwargTypes) -> Metadata: + """Return the metadata corresponding to the handle. + + Use keyword arguments to specify parameters to be used in retrieving + the metadata. + + * `timeout`: maximum time to wait for remote job to finish + + Example usage: + n_shots = 100 + backend.run_circuit(circuit, n_shots=n_shots, timeout=30) + handle = backend.process_circuits([circuit], n_shots=n_shots)[0] + result = backend.get_result(handle) + metadata = backend.get_metadata(handle) + print([qm.physical_name for qm in metadata.request.qubit_mapping]) + + :param handle: handle to results + :type handle: ResultHandle + :return: Metadata corresponding to handle + :rtype: Metadata + """ + self._check_handle_type(handle) + if handle in self._cache and "metadata" in self._cache[handle]: + return cast(Metadata, self._cache[handle]["metadata"]) + # Wait for job to finish, capture metadata and store it in cache + timeout = kwargs.get("timeout", 900) + run_id = UUID(bytes=cast(bytes, handle[0])) + run_result = self._client.wait_for_results( + run_id, timeout_secs=cast(float, timeout) + ) + self._cache[handle]["metadata"] = run_result.metadata + return cast(Metadata, self._cache[handle]["metadata"]) + def _as_node(qname: str) -> Node: assert qname.startswith("QB") @@ -298,7 +334,7 @@ def _as_name(qnode: Node) -> str: return f"QB{qnode.index[0] + 1}" -def _translate_iqm(circ: Circuit) -> List[Instruction]: +def _translate_iqm(circ: Circuit) -> Tuple[Instruction, ...]: """Convert a circuit in the IQM gate set to IQM list representation.""" instrs = [] for cmd in circ.get_commands(): @@ -310,18 +346,27 @@ def _translate_iqm(circ: Circuit) -> List[Instruction]: if optype == OpType.PhasedX: instr = Instruction( name="phased_rx", - qubits=[str(qbs[0])], + implementation=None, + qubits=(str(qbs[0]),), args={"angle_t": 0.5 * params[0], "phase_t": 0.5 * params[1]}, ) elif optype == OpType.CZ: - instr = Instruction(name="cz", qubits=[str(qbs[0]), str(qbs[1])], args={}) + instr = Instruction( + name="cz", + implementation=None, + qubits=(str(qbs[0]), str(qbs[1])), + args={}, + ) else: assert optype == OpType.Measure instr = Instruction( - name="measurement", qubits=[str(qbs[0])], args={"key": str(cbs[0])} + name="measurement", + implementation=None, + qubits=(str(qbs[0]),), + args={"key": str(cbs[0])}, ) instrs.append(instr) - return instrs + return tuple(instrs) def _iqm_rebase() -> BasePass: diff --git a/setup.py b/setup.py index 7651772..4c636aa 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. @@ -42,7 +42,7 @@ license="Apache 2", packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, - install_requires=["pytket ~= 1.8", "iqm-client ~= 9.1"], + install_requires=["pytket ~= 1.13", "iqm-client ~= 11.8"], classifiers=[ "Environment :: Console", "Programming Language :: Python :: 3.9", diff --git a/tests/backend_test.py b/tests/backend_test.py index 620a92d..ce37ee0 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 Cambridge Quantum Computing +# 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. @@ -13,10 +13,11 @@ # limitations under the License. import os +from uuid import UUID import pytest from requests import get from conftest import get_demo_url # type: ignore -from iqm_client.iqm_client import ClientAuthenticationError +from iqm_client.iqm_client import ClientAuthenticationError, Metadata, RunRequest from pytket.circuit import Circuit # type: ignore from pytket.backends import StatusEnum from pytket.extensions.iqm import IQMBackend @@ -32,20 +33,10 @@ @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_iqm(authenticated_iqm_backend: IQMBackend) -> None: +def test_iqm(authenticated_iqm_backend: IQMBackend, sample_circuit: Circuit) -> None: # Run a circuit on the demo device. b = authenticated_iqm_backend - c = Circuit(4, 4) - c.H(0) - c.CX(0, 1) - c.Rz(0.3, 2) - c.CSWAP(0, 1, 2) - c.CRz(0.4, 2, 3) - c.CY(1, 3) - c.ZZPhase(0.1, 2, 0) - c.Tdg(3) - c.measure_all() - c = b.get_compiled_circuit(c) + c = b.get_compiled_circuit(sample_circuit) n_shots = 10 res = b.run_circuit(c, n_shots=n_shots, timeout=30) shots = res.get_shots() @@ -54,24 +45,23 @@ def test_iqm(authenticated_iqm_backend: IQMBackend) -> None: assert sum(counts.values()) == n_shots +@pytest.mark.skipif(skip_remote_tests, reason=REASON) def test_invalid_cred(demo_url: str) -> None: with pytest.raises(ClientAuthenticationError): _ = IQMBackend( url=demo_url, - auth_server_url="https://auth.demo.qc.iqm.fi", + auth_server_url="https://demo.qc.iqm.fi/auth", username="invalid", password="invalid", ) @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_handles(authenticated_iqm_backend: IQMBackend) -> None: +def test_handles( + authenticated_iqm_backend: IQMBackend, sample_circuit: Circuit +) -> None: b = authenticated_iqm_backend - c = Circuit(2, 2) - c.H(0) - c.CX(0, 1) - c.measure_all() - c = b.get_compiled_circuit(c) + c = b.get_compiled_circuit(sample_circuit) n_shots = 5 res = b.run_circuit(c, n_shots=n_shots, timeout=30) shots = res.get_shots() @@ -89,20 +79,40 @@ def test_handles(authenticated_iqm_backend: IQMBackend) -> None: for handle in handles: assert b.circuit_status(handle).status == StatusEnum.COMPLETED for result in results: - assert result.get_shots().shape == (n_shots, 2) + assert result.get_shots().shape == (n_shots, c.n_qubits) @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_none_nshots(authenticated_iqm_backend: IQMBackend) -> None: +def test_metadata( + authenticated_iqm_backend: IQMBackend, sample_circuit: Circuit +) -> None: b = authenticated_iqm_backend - c = Circuit(2, 2) - c.H(0) - c.CX(0, 1) - c.measure_all() - c = b.get_compiled_circuit(c) - with pytest.raises(ValueError) as errorinfo: + c = b.get_compiled_circuit(sample_circuit) + n_shots = 5 + b.run_circuit(c, n_shots=n_shots, timeout=30) + handle = b.process_circuits([c], n_shots=n_shots)[0] + b.get_result(handle) + metadata = b.get_metadata(handle) + assert isinstance(metadata, Metadata) + assert isinstance(metadata.calibration_set_id, UUID) + assert isinstance(metadata.request, RunRequest) + assert len(metadata.request.circuits) == 1 + assert metadata.request.circuits[0].name == c.name + assert metadata.request.calibration_set_id is None + assert isinstance(metadata.request.qubit_mapping, list) + assert len(metadata.request.qubit_mapping) == c.n_qubits + assert metadata.request.shots == n_shots + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +def test_none_nshots( + authenticated_iqm_backend: IQMBackend, sample_circuit: Circuit +) -> None: + b = authenticated_iqm_backend + c = b.get_compiled_circuit(sample_circuit) + with pytest.raises(ValueError) as error_info: _ = b.process_circuits([c]) - assert "Parameter n_shots is required" in str(errorinfo.value) + assert "Parameter n_shots is required" in str(error_info.value) @pytest.mark.skipif(skip_remote_tests, reason=REASON) diff --git a/tests/conftest.py b/tests/conftest.py index f91c8f7..f5d9ddf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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. @@ -15,10 +15,11 @@ import os import pytest from pytket.extensions.iqm import IQMBackend +from pytket.circuit import Circuit def get_demo_url() -> str: - return "https://cortex-demo.qc.iqm.fi/" + return "https://demo.qc.iqm.fi/cocos" @pytest.fixture(name="demo_url", scope="session") @@ -40,3 +41,19 @@ def fixture_authenticated_iqm_backend() -> IQMBackend: username=os.getenv("PYTKET_REMOTE_IQM_USERNAME"), password=os.getenv("PYTKET_REMOTE_IQM_PASSWORD"), ) + + +@pytest.fixture(name="sample_circuit", scope="session") +def fixture_sample_circuit() -> Circuit: + c = Circuit(4, 4) + c.H(0) + c.CX(0, 1) + c.Rz(0.3, 2) + c.CSWAP(0, 1, 2) + c.CRz(0.4, 2, 3) + c.CY(1, 3) + c.ZZPhase(0.1, 2, 0) + c.Tdg(3) + c.measure_all() + c.name = "test_circuit" + return c diff --git a/tests/convert_test.py b/tests/convert_test.py index 83543ab..806ef1d 100644 --- a/tests/convert_test.py +++ b/tests/convert_test.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 Cambridge Quantum Computing +# 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.