diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml new file mode 100644 index 00000000..177ed867 --- /dev/null +++ b/.github/workflows/rules.yml @@ -0,0 +1,22 @@ +# A single CI script with github workflow +name: Tests + +on: + push: + pull_request: + types: [labeled] + +jobs: + build: + if: contains(github.event.pull_request.labels.*.name, 'run-workflow') || github.event_name == 'push' + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.7, 3.8, 3.9, "3.10"] + uses: qiboteam/workflows/.github/workflows/rules.yml@main + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + environment: "qibotn" + pip-extras: "analysis,tests" + secrets: inherit diff --git a/.gitignore b/.gitignore index 6a256b3f..68bc17f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,129 +1,160 @@ # Byte-compiled / optimized / DLL files - __pycache__/ - *.py[cod] - *$py.class - - # C extensions - *.so - - # Distribution / packaging - .Python - build/ - develop-eggs/ - dist/ - downloads/ - eggs/ - .eggs/ - lib/ - lib64/ - parts/ - sdist/ - var/ - wheels/ - pip-wheel-metadata/ - share/python-wheels/ - *.egg-info/ - .installed.cfg - *.egg - MANIFEST - - # PyInstaller - # Usually these files are written by a python script from a template - # before PyInstaller builds the exe, so as to inject date/other infos into it. - *.manifest - *.spec - - # Installer logs - pip-log.txt - pip-delete-this-directory.txt - - # Unit test / coverage reports - htmlcov/ - .tox/ - .nox/ - .coverage - .coverage.* - .cache - nosetests.xml - coverage.xml - *.cover - *.py,cover - .hypothesis/ - .pytest_cache/ - - # Translations - *.mo - *.pot - - # Django stuff: - *.log - local_settings.py - db.sqlite3 - db.sqlite3-journal - - # Flask stuff: - instance/ - .webassets-cache - - # Scrapy stuff: - .scrapy - - # Sphinx documentation - docs/_build/ - - # PyBuilder - target/ - - # Jupyter Notebook - .ipynb_checkpoints - - # IPython - profile_default/ - ipython_config.py - - # pyenv - .python-version - - # pipenv - # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. - # However, in case of collaboration, if having platform-specific dependencies or dependencies - # having no cross-platform support, pipenv may install dependencies that don't work, or not - # install all needed dependencies. - #Pipfile.lock - - # PEP 582; used by e.g. github.com/David-OConnor/pyflow - __pypackages__/ - - # Celery stuff - celerybeat-schedule - celerybeat.pid - - # SageMath parsed files - *.sage.py - - # Environments - .env - .venv - env/ - venv/ - ENV/ - env.bak/ - venv.bak/ - - # Spyder project settings - .spyderproject - .spyproject - - # Rope project settings - .ropeproject - - # mkdocs documentation - /site - - # mypy - .mypy_cache/ - .dmypy.json - dmypy.json - - # Pyre type checker - .pyre/ +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..177c408f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: debug-statements + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f2546e49 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.pylint.reports] +output-format = "colorized" + +[tool.pytest.ini_options] +testpaths = ["tests/"] +addopts = ["--cov=qibotn", "--cov-report=xml"] +env = ["D:NUMBA_DISABLE_JIT=1"] diff --git a/setup.py b/setup.py index da40cc4b..aefac894 100644 --- a/setup.py +++ b/setup.py @@ -1,33 +1,30 @@ -# Installation script for python from setuptools import setup, find_packages -import os import re +import pathlib +HERE = pathlib.Path(__file__).parent.absolute() PACKAGE = "qibotn" # Returns the qibotn version -def get_version(): - """ Gets the version from the package's __init__ file - if there is some problem, let it happily fail """ - VERSIONFILE = os.path.join("src", PACKAGE, "__init__.py") - initfile_lines = open(VERSIONFILE, "rt").readlines() - VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" - for line in initfile_lines: - mo = re.search(VSRE, line, re.M) - if mo: - return mo.group(1) +def version(): + """Gets the version from the package's __init__ file + if there is some problem, let it happily fail""" + version_file = HERE / "src" / PACKAGE / "__init__.py" + version_regex = r"^__version__ = ['\"]([^'\"]*)['\"]" + initfile = version_file.read_text(encoding="utf-8") + matched = re.search(version_regex, initfile, re.M) -# load long description from README -this_directory = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f: - long_description = f.read() + if matched is not None: + return matched.group(1) + return "0.0.0" +# load long description from README setup( name="qibotn", - version=get_version(), + version=version(), description="A tensor-network translation module for quantum computing", author="The Qibo team", author_email="", @@ -42,19 +39,22 @@ def get_version(): "Topic :: Scientific/Engineering :: Physics", ], install_requires=[ - "networkx>=2.3", - "opt_einsum>=3.2", - "autoray>=0.2.0", - "diskcache>=3.0", - "randomgen>=1.18", - "quimb", - "qibo" + "qibo>=0.1.10", + "qibojit>=0.0.7", + "quimb[tensor]>=1.4.0", ], extras_require={ - "docs": ["sphinx", "sphinx_rtd_theme", "recommonmark", "sphinxcontrib-bibtex", "sphinx_markdown_tables", "nbsphinx", "IPython", "doc2dash>=2.4.1", ], - "tests": ["pytest", "cirq", "ply", "sklearn", "dill", "coverage", "pytest-cov"], + "docs": [], + "tests": [ + "pytest>=7.2.0", + "pytest-cov>=4.0.0", + "pytest-env>=0.8.1", + ], + "analysis": [ + "pylint>=2.16.0", + ], }, python_requires=">=3.7.0", - long_description=long_description, - long_description_content_type='text/markdown', -) \ No newline at end of file + long_description=(HERE / "README.md").read_text(encoding="utf-8"), + long_description_content_type="text/markdown", +) diff --git a/src/qibotn/__main__.py b/src/qibotn/__main__.py new file mode 100644 index 00000000..8ed74396 --- /dev/null +++ b/src/qibotn/__main__.py @@ -0,0 +1,19 @@ +import argparse +from qibotn import qasm_quimb + + +def parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--nqubits", default=10, type=int, help="Number of quibits in the circuits." + ) + return parser.parse_args() + + +def main(args: argparse.Namespace): + print("Testing for %d nqubits" % (args.nqubits)) + qasm_quimb.eval_QI_qft(args.nqubits, args.qasm_circ, args.init_state) + + +if __name__ == "__main__": + main(parser()) diff --git a/src/qibotn/quimb.py b/src/qibotn/quimb.py new file mode 100644 index 00000000..8414540f --- /dev/null +++ b/src/qibotn/quimb.py @@ -0,0 +1,39 @@ +import numpy as np +import quimb.tensor as qtn +from qibo.models import Circuit as QiboCircuit + + +def from_qibo(circuit: QiboCircuit, psi0=None): + nqubits = circuit.nqubits + tncirc = qtn.Circuit(nqubits, psi0=psi0) + + for gate in circuit.queue: + tncirc.apply_gate( + gate.name, + *gate.parameters, + *gate.qubits, + parametrize=len(gate.parameters) > 0 + ) + + return tncirc + + +def init_state_tn(nqubits, init_state_sv): + dims = tuple(2 * np.ones(nqubits, dtype=int)) + + return qtn.tensor_1d.MatrixProductState.from_dense(init_state_sv, dims) + + +def eval(qasm: str, init_state, backend="numpy"): + """Evaluate QASM with Quimb + + backend (quimb): numpy, cupy, jax. Passed to ``opt_einsum``. + + """ + circuit = QiboCircuit.from_qasm(qasm) + init_state_mps = init_state_tn(circuit.nqubits, init_state) + circ_quimb = from_qibo(circuit, psi0=init_state_mps) + interim = circ_quimb.psi.full_simplify(seq="DRC") + amplitudes = interim.to_dense(backend=backend).flatten() + + return amplitudes diff --git a/tests/config.py b/tests/config.py new file mode 100644 index 00000000..74036736 --- /dev/null +++ b/tests/config.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Executor: + backend: str + platform: Optional[str] = None + + +qibo = Executor(backend="qibojit", platform="numpy") +quimb = Executor(backend="numpy") diff --git a/tests/test_qasm_quimb_backend.py b/tests/test_qasm_quimb_backend.py new file mode 100644 index 00000000..e9620187 --- /dev/null +++ b/tests/test_qasm_quimb_backend.py @@ -0,0 +1,60 @@ +import copy +import os +from timeit import default_timer as timer + +import config +import numpy as np +import pytest +import qibo +from qibo.models import QFT + + +def create_init_state(nqubits): + init_state = np.random.random(2**nqubits) + 1j * np.random.random(2**nqubits) + init_state = init_state / np.sqrt((np.abs(init_state) ** 2).sum()) + return init_state + + +def qibo_qft(nqubits, init_state, swaps): + circ_qibo = QFT(nqubits, swaps) + state_vec = np.array(circ_qibo(init_state)) + return circ_qibo, state_vec + + +def time(func): + start = timer() + res = func() + end = timer() + time = end - start + return time, res + + +@pytest.mark.parametrize("nqubits", [1, 2, 5, 10]) +def test_eval(nqubits: int): + # hack quimb to use the correct number of processes + # TODO: remove completely, or at least delegate to the backend + # implementation + os.environ["QUIMB_NUM_PROCS"] = str(os.cpu_count()) + import qibotn.quimb + + init_state = create_init_state(nqubits=nqubits) + init_state_tn = copy.deepcopy(init_state) + + # Test qibo + qibo.set_backend(backend=config.qibo.backend, platform=config.qibo.platform) + qibo_time, (qibo_circ, result_sv) = time( + lambda: qibo_qft(nqubits, init_state, swaps=True) + ) + + # Convert to qasm for other backends + qasm_circ = qibo_circ.to_qasm() + + # Test quimb + quimb_time, result_tn = time( + lambda: qibotn.quimb.eval( + qasm_circ, init_state_tn, backend=config.quimb.backend + ) + ) + + assert 1e-2 * qibo_time < quimb_time < 1e2 * qibo_time + assert np.allclose(result_sv, result_tn), "Resulting dense vectors do not match"