From d79d2ebb015a107d35d76f0f189828c0705ee2e3 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 11 Oct 2023 12:14:12 -0500 Subject: [PATCH 1/7] Initial import of pytemplate --- .github/workflows/python-app.yml | 46 +++++++++ .gitignore | 163 +++++++++++++++++++++---------- .pre-commit-config.yaml | 48 +++++++++ Makefile | 23 +++++ README.md | 27 +++++ docs/Makefile | 20 ++++ docs/source/conf.py | 37 +++++++ docs/source/index.rst | 51 ++++++++++ mypy.ini | 50 ++++++++++ phir/__init__.py | 1 + phir/main.py | 10 ++ phir/utils.py | 6 ++ pyproject.toml | 27 +++++ requirements.txt | 8 ++ ruff.toml | 60 ++++++++++++ tests/test_main.py | 8 ++ tests/test_utils.py | 10 ++ 17 files changed, 546 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/python-app.yml create mode 100644 .pre-commit-config.yaml create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/Makefile create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 mypy.ini create mode 100644 phir/__init__.py create mode 100644 phir/main.py create mode 100644 phir/utils.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 ruff.toml create mode 100644 tests/test_main.py create mode 100644 tests/test_utils.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..1bbf154 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,46 @@ +name: Python application + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -e . + - name: Pre-commit checks + run: | + python -m pip install pre-commit + pre-commit run --all-files + - name: Run tests + run: | + python -m pytest + # - name: Sphinx documentation build + # run: | + # make docs + # - name: Deploy to GitHub Pages + # uses: peaceiris/actions-gh-pages@v3 + # if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + # with: + # publish_branch: gh-pages + # github_token: ${{ secrets.GITHUB_TOKEN }} + # publish_dir: docs/build/html/ + # force_orphan: true diff --git a/.gitignore b/.gitignore index 2d52d6a..d930761 100644 --- a/.gitignore +++ b/.gitignore @@ -1,102 +1,167 @@ +# autogenerated by sphinx-napoleon +docs/source/modules.rst +docs/source/pytemplate.rst + +### Add custom ignores above this line ### + +## Copied over from https://github.com/github/gitignore/blob/main/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ -.pytest_cache/ *.py[cod] +*$py.class # C extensions *.so # Distribution / packaging .Python -.venv/ -env/ -bin/ build/ develop-eggs/ dist/ +downloads/ eggs/ +.eggs/ lib/ lib64/ parts/ sdist/ var/ - -! include/ -**/include/* -!**/cuquantum_wrapper/extern/**/include/* - -man/ -venv/ +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 -pip-selfcheck.json # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage +.coverage.* .cache nosetests.xml coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ # Translations *.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -.ropeproject +*.pot # Django stuff: *.log -*.pot +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache -.DS_Store +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ -# PyCharm -.idea/ - -# VSCode -.vscode/ - -# Pyenv -.python-version +# 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/ -# Jupyter -**/.ipynb_checkpoints +# Spyder project settings +.spyderproject +.spyproject -# Cython -*.pyc -*.pyd +# Rope project settings +.ropeproject -# linting -.pylintrc +# mkdocs documentation +/site -~* +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ +# Pyre type checker +.pyre/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +# pytype static type analyzer +.pytype/ -# These are backup files generated by rustfmt -**/*.rs.bk +# Cython debug symbols +cython_debug/ -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb \ No newline at end of file +# 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 0000000..5bd9030 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: check-added-large-files + # Python-specific + - id: check-ast + - id: check-docstring-first + - id: debug-statements + + - repo: https://github.com/crate-ci/typos + rev: v1.16.18 + hooks: + - id: typos + args: [] + + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + + - repo: https://github.com/keewis/blackdoc + rev: v0.3.8 + hooks: + - id: blackdoc + additional_dependencies: + - black==23.9.1 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.5.1' + hooks: + - id: mypy + pass_filenames: false + args: [--package=phir, --package=tests] + additional_dependencies: [ + pytest, + types-setuptools, + ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe5afc7 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: install dev tests lint docs clean build + +install: + pip install . + +dev: + pip install -e . + +tests: + pytest . + +lint: + pre-commit run --all-files + +docs: + sphinx-apidoc -f -o docs/source/ phir + sphinx-build -M html docs/source/ docs/build/ + +clean: + rm -rf *.egg-info dist build docs/build + +build: clean + python -m build --sdist -n diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e98663 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# PHIR: _[PECOS](https://github.com/PECOS-packages/PECOS) High-level Intermediate Representation_ + +See [PHIR specification](./phir_spec_qasm.md) for more. + +## Installation + +Clone the repository and run: + +```sh +python -m venv .venv +source .venv/bin/activate +pip install -U pip setuptools +pip install -r requirements.txt +pre-commit install +``` + +Then install the project using: + +```sh +pip install -e . +``` + +See `Makefile` for other useful commands. + +## Testing + +Just issue `pytest` from the root directory. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..a40eb43 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,37 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. +import pathlib +import sys + +sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "phir" +project_copyright = "2023, Author" +author = "Author" +release = "0.0.1" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", +] + +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..60e2610 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,51 @@ +phir +========== + +Demo Sphinx site for the `CQCL phir `_ project. + +---- + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + +Installation +------------ + +To install phir, simply run: + +.. code-block:: bash + + pip install phir + +Usage +----- + +To use phir, import the main module: + +.. code-block:: python + + from phir import main + + main.run() + +Utils +----- + +The utils module contains various utility functions that can be used in conjunction with phir. To use the utils module, import it like so: + +.. code-block:: python + + from phir import utils + + utils.do_something() + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..97b2183 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,50 @@ +# Global options: + +; See doc at https://mypy.readthedocs.io/en/stable/config_file.html +[mypy] +follow_imports = silent +python_version = 3.10 + +exclude = (?x)( + ^build/ + ) + +## Disallow dynamic typing +disallow_any_unimported = true +disallow_any_expr = true +disallow_any_decorated = true +disallow_any_explicit = true +disallow_any_generics = true +disallow_subclassing_any = true + +## Untyped definitions and calls +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true + +## Configuring warnings +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true + +## Miscellaneous strictness flags +local_partial_types = true +implicit_reexport = false +strict_equality = true +strict = true + +## Configuring error messages +; show_error_context = true +pretty = true + +## Miscellaneous +scripts_are_modules = true +warn_unused_configs = true + +# Per-module options: + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/phir/__init__.py b/phir/__init__.py new file mode 100644 index 0000000..1f33ecc --- /dev/null +++ b/phir/__init__.py @@ -0,0 +1 @@ +"""Init file for phir.""" diff --git a/phir/main.py b/phir/main.py new file mode 100644 index 0000000..9741a55 --- /dev/null +++ b/phir/main.py @@ -0,0 +1,10 @@ +"""This is the main module of the phir package.""" + + +def hello_world() -> str: + """Print 'Hello, world!' to the console.""" + hw = "Hello, World!" + return hw + + +print(hello_world()) diff --git a/phir/utils.py b/phir/utils.py new file mode 100644 index 0000000..3415d7d --- /dev/null +++ b/phir/utils.py @@ -0,0 +1,6 @@ +"""Utility functions for the phir package.""" + + +def add_numbers(a: int, b: int) -> int: + """Add two numbers and returns the result.""" + return a + b diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a7d9f8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "phir" +version = "0.0.1" +description = "A python library" +readme = "README.md" +requires-python = ">=3.10" +authors = [{name = "Kartik Singhal", email = "kartik.singhal@quantinuum.com" }] + +[project.optional-dependencies] +tests = ["pytest"] + +docs = ["sphinx", "sphinx-rtd-theme"] + +[tool.setuptools.packages.find] +where = ["."] + +[project.urls] +Repository = "https://github.com/CQCL/phir.git" + +[tool.pytest.ini_options] +pythonpath = [ + "." +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c156cf4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +black==23.9.1 +build==1.0.3 +mypy==1.6.0 +pre-commit==3.4.0 +pytest==7.4.2 +ruff==0.0.292 +sphinx==7.2.6 +wheel==0.41.2 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1d2caf5 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,60 @@ +# See https://docs.astral.sh/ruff/rules/ +target-version = "py310" + +line-length = 88 + +select = [ + "E", # pycodestyle Errors + "W", # pycodestyle Warnings + + "A", # flake8-builtins + "B", # flake8-Bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "COM", # flake8-commas + "D", # pydocstyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # pyFlakes + "FA", # flake8-future-annotations + "FIX", # flake8-fixme + "FLY", # flynt + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "N", # pep8-Naming + "NPY", # NumPy-specific + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "RSE", # flake8-raise + "RUF", # Ruff-specific + "S", # flake8-bandit (Security) + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TD", # flake8-todos + "TRY", # tryceratops + "UP", # pyupgrade + "YTT", # flake8-2020 +] + +[per-file-ignores] +"__init__.py" = ["F401"] # module imported but unused +"docs/*" = [ + "D100", # Missing docstring in public module + "INP001", # File * is part of an implicit namespace package. Add an `__init__.py`. +] +"tests/*" = [ + "INP001", + "S101", # Use of `assert` detected + "PLR2004", # Magic value used in comparison, consider replacing * with a constant variable + ] + +[pydocstyle] +convention = "google" diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..15b8207 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,8 @@ +"""Tests for phir.main module.""" + +from phir.main import hello_world + + +def test_hello_world(): + """Test the hello_world function.""" + assert hello_world() == "Hello, World!" diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4499f33 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,10 @@ +"""Tests for phir.utils.""" + +from phir.utils import add_numbers + + +def test_add(): + """Test the add function.""" + assert add_numbers(2, 3) == 5 + assert add_numbers(0, 0) == 0 + assert add_numbers(-1, 1) == 0 From c079150dff638823ec404ecec8b4d6a7ba12d411 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Thu, 12 Oct 2023 13:16:42 -0500 Subject: [PATCH 2/7] Add base model from pecos --- .pre-commit-config.yaml | 1 + mypy.ini | 6 ++ phir/main.py | 3 - phir/model.py | 134 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + requirements.txt | 1 + 6 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 phir/model.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bd9030..a51e08c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,7 @@ repos: pass_filenames: false args: [--package=phir, --package=tests] additional_dependencies: [ + pydantic, pytest, types-setuptools, ] diff --git a/mypy.ini b/mypy.ini index 97b2183..4f02c47 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4,6 +4,7 @@ [mypy] follow_imports = silent python_version = 3.10 +plugins = pydantic.mypy exclude = (?x)( ^build/ @@ -48,3 +49,8 @@ warn_unused_configs = true [mypy-tests.*] disallow_untyped_defs = false + +[pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true diff --git a/phir/main.py b/phir/main.py index 9741a55..61fecce 100644 --- a/phir/main.py +++ b/phir/main.py @@ -5,6 +5,3 @@ def hello_world() -> str: """Print 'Hello, world!' to the console.""" hw = "Hello, World!" return hw - - -print(hello_world()) diff --git a/phir/model.py b/phir/model.py new file mode 100644 index 0000000..c2747e0 --- /dev/null +++ b/phir/model.py @@ -0,0 +1,134 @@ +"""PHIR model lives here.""" + +import json + +from pydantic import BaseModel + +# Data Management + + +class Data(BaseModel): + """Data Management Base Class.""" + + metadata: dict | None = None + + +class DefineVar(Data): + """Defining Variables.""" + + data_type: str | type + variable: str + metadata: dict | None = None + + +class CVarDefine(DefineVar): + """Defining Classical Variables.""" + + data_type: str | type + variable: str + cvar_id: int + size: int + metadata: dict | None = None + + +class QVarDefine(DefineVar): + """Defining Quantum Variables.""" + + data_type: str | type + variable: str + size: int + qubit_ids: list[int] + metadata: dict | None = None + + +class ExportVar(Data): + """Exporting Classical Variables.""" + + variables: list[str] + to: list[str] | None = None + metadata: dict | None = None + + +# Operations + + +class Op(BaseModel): + """Operation Base Class.""" + + name: str + args: list | None = None + returns: list | None = None + metadata: dict | None = None + + +class QOp(Op): + """Quantum operation.""" + + name: str + args: list + returns: list | None = None + metadata: dict | None = None + + +class COp(Op): + """Classical operation.""" + + name: str + args: list + returns: list | None = None + metadata: dict | None = None + + +class FFCall(COp): + """External Classical Function Call.""" + + +class MOp(Op): + """Machine operation.""" + + +class EMOp(Op): + """Error model operation.""" + + +class SOp(Op): + """Simulation model.""" + + +# Blocks + + +class Block(BaseModel): + """General block type.""" + + metadata: dict | None = None + + +class SeqBlock(Block): + """A generic sequence block.""" + + ops: list + metadata: dict | None = None + + +class IfBlock(Block): + """If/else block.""" + + condition: COp + true_branch: list[COp] + false_branch: list | None = None + metadata: dict | None = None + + +class PHIR(BaseModel): + """PHIR model object.""" + + format_: str = "PHIR/JSON" + version: str = "0.1.0" + metadata: dict | None = None + ops: list[Data | Op | Block] + + +print( # noqa: T201 + json.dumps(PHIR.model_json_schema(), indent=2), # type: ignore [misc] +) diff --git a/pyproject.toml b/pyproject.toml index 8a7d9f8..8a6faef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ description = "A python library" readme = "README.md" requires-python = ">=3.10" authors = [{name = "Kartik Singhal", email = "kartik.singhal@quantinuum.com" }] +dependencies = ["pydantic"] [project.optional-dependencies] tests = ["pytest"] diff --git a/requirements.txt b/requirements.txt index c156cf4..0db6a86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ black==23.9.1 build==1.0.3 mypy==1.6.0 pre-commit==3.4.0 +pydantic==2.4.2 pytest==7.4.2 ruff==0.0.292 sphinx==7.2.6 From 8d5eb822bba46dc8ec7b111b7f16b06a7d09eb53 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Thu, 12 Oct 2023 15:31:50 -0500 Subject: [PATCH 3/7] Add example file and corresponding test --- phir/main.py | 7 --- phir/model.py | 8 +-- phir/utils.py | 6 -- tests/example.json | 148 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 8 --- tests/test_model.py | 18 ++++++ tests/test_utils.py | 10 --- 7 files changed, 167 insertions(+), 38 deletions(-) delete mode 100644 phir/main.py delete mode 100644 phir/utils.py create mode 100644 tests/example.json delete mode 100644 tests/test_main.py create mode 100644 tests/test_model.py delete mode 100644 tests/test_utils.py diff --git a/phir/main.py b/phir/main.py deleted file mode 100644 index 61fecce..0000000 --- a/phir/main.py +++ /dev/null @@ -1,7 +0,0 @@ -"""This is the main module of the phir package.""" - - -def hello_world() -> str: - """Print 'Hello, world!' to the console.""" - hw = "Hello, World!" - return hw diff --git a/phir/model.py b/phir/model.py index c2747e0..618beaa 100644 --- a/phir/model.py +++ b/phir/model.py @@ -1,6 +1,5 @@ """PHIR model lives here.""" -import json from pydantic import BaseModel @@ -120,15 +119,10 @@ class IfBlock(Block): metadata: dict | None = None -class PHIR(BaseModel): +class PHIRModel(BaseModel): """PHIR model object.""" format_: str = "PHIR/JSON" version: str = "0.1.0" metadata: dict | None = None ops: list[Data | Op | Block] - - -print( # noqa: T201 - json.dumps(PHIR.model_json_schema(), indent=2), # type: ignore [misc] -) diff --git a/phir/utils.py b/phir/utils.py deleted file mode 100644 index 3415d7d..0000000 --- a/phir/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Utility functions for the phir package.""" - - -def add_numbers(a: int, b: int) -> int: - """Add two numbers and returns the result.""" - return a + b diff --git a/tests/example.json b/tests/example.json new file mode 100644 index 0000000..2e55bb6 --- /dev/null +++ b/tests/example.json @@ -0,0 +1,148 @@ +{ + "format": "PHIR/JSON", + "version": "0.1.0", + "metadata": { + "program_name": "example_prog", + "description": "Program showing off PHIR", + "num_qubits": 10 + }, + "ops": [ + { + "data": "qvar_define", + "data_type": "qubits", + "variable": "q", + "size": 2 + }, + { + "data": "qvar_define", + "data_type": "qubits", + "variable": "w", + "size": 3 + }, + { + "data": "qvar_define", + "data_type": "qubits", + "variable": "d", + "size": 5 + }, + {"data": "cvar_define", "data_type": "i64", "variable": "m", "size": 2}, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 32 + }, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "b", + "size": 32 + }, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "c", + "size": 12 + }, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "d", + "size": 10 + }, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "e", + "size": 30 + }, + {"data": "cvar_define", "data_type": "i64", "variable": "f", "size": 5}, + { + "data": "cvar_define", + "data_type": "i64", + "variable": "g", + "size": 32 + }, + {"qop": "H", "args": [["q", 0]]}, + {"qop": "CX", "args": [[["q", 0], ["q", 1]]]}, + { + "qop": "Measure", + "args": [["q", 0], ["q", 1]], + "returns": [["m", 0], ["m", 1]] + }, + {"cop": "=", "args": [5], "returns": ["b"]}, + {"cop": "=", "args": [3], "returns": ["c"]}, + { + "cop": "ffcall", + "function": "add", + "args": ["b", "c"], + "returns": [["a", 0]] + }, + { + "block": "if", + "condition": {"cop": "==", "args": ["m", 1]}, + "true_branch": [ + { + "cop": "=", + "args": [ + { + "cop": "|", + "args": [ + {"cop": "^", "args": [["c", 2], "d"]}, + { + "cop": "+", + "args": [ + {"cop": "-", "args": ["e", 2]}, + {"cop": "&", "args": ["f", "g"]} + ] + } + ] + } + ], + "returns": ["a"] + } + ] + }, + { + "block": "if", + "condition": {"cop": "==", "args": ["m", 2]}, + "true_branch": [ + {"cop": "ffcall", "function": "sub", "args": ["d", "e"]} + ] + }, + { + "block": "if", + "condition": {"cop": ">", "args": ["a", 2]}, + "true_branch": [ + {"cop": "=", "args": ["c"], "returns": [7]}, + {"qop": "X", "args": [["w", 0]]}, + {"qop": "H", "args": [["w", 1]]}, + {"qop": "CX", "args": [[["w", 1], ["w", 2]]]}, + { + "qop": "Measure", + "args": [["w", 1], ["w", 2]], + "returns": [["g", 0], ["g", 1]] + } + ] + }, + { + "block": "if", + "condition": {"cop": "==", "args": [["a", 3], 1]}, + "true_branch": [ + { + "qop": "H", + "args": [["d", 0], ["d", 1], ["d", 2], ["d", 3], ["d", 4]] + } + ] + }, + { + "qop": "Measure", + "args": [["d", 0], ["d", 1], ["d", 2], ["d", 3], ["d", 4]], + "returns": [["f", 0], ["f", 1], ["f", 2], ["f", 3], ["f", 4]] + }, + { + "data": "cvar_export", + "variables": ["m", "a", "b", "c", "d", "e", "f", "g"] + } + ] +} diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index 15b8207..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Tests for phir.main module.""" - -from phir.main import hello_world - - -def test_hello_world(): - """Test the hello_world function.""" - assert hello_world() == "Hello, World!" diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..a9f5e89 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,18 @@ +"""Basic validation tests.""" + +import json + +from pydantic import ValidationError + +from phir.model import PHIRModel + + +def test_spec_example(): # noqa: D103 + # From https://github.com/CQCL/phir/blob/main/phir_spec_qasm.md#overall-phir-example-with-quantinuums-extended-openqasm-20 + with open("tests/example.json") as f: + data = json.load(f) + + try: + PHIRModel(**data) + except ValidationError as e: + print(e) # noqa: T201 diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 4499f33..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Tests for phir.utils.""" - -from phir.utils import add_numbers - - -def test_add(): - """Test the add function.""" - assert add_numbers(2, 3) == 5 - assert add_numbers(0, 0) == 0 - assert add_numbers(-1, 1) == 0 From 7d464d38aabb5bd25b34200769df0da6f6da6411 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 13 Oct 2023 11:27:36 -0500 Subject: [PATCH 4/7] Add CLI and improve the model --- .pre-commit-config.yaml | 1 + mypy.ini | 1 - phir/cli.py | 31 +++++++++++++++ phir/model.py | 86 ++++++++++++++++++++++++----------------- pyproject.toml | 12 +++--- requirements.txt | 1 + tests/test_model.py | 9 +---- 7 files changed, 92 insertions(+), 49 deletions(-) create mode 100644 phir/cli.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a51e08c..b7bd619 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,5 +45,6 @@ repos: additional_dependencies: [ pydantic, pytest, + rich, types-setuptools, ] diff --git a/mypy.ini b/mypy.ini index 4f02c47..ac057a5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,7 +14,6 @@ exclude = (?x)( disallow_any_unimported = true disallow_any_expr = true disallow_any_decorated = true -disallow_any_explicit = true disallow_any_generics = true disallow_subclassing_any = true diff --git a/phir/cli.py b/phir/cli.py new file mode 100644 index 0000000..2fbee5d --- /dev/null +++ b/phir/cli.py @@ -0,0 +1,31 @@ +"""PHIR validation driver.""" + +import argparse +import json +from pathlib import Path + +from pydantic import ValidationError +from rich import print + +from phir.model import PHIRModel + + +def main() -> None: + """CLI Entrypoint.""" + parser = argparse.ArgumentParser( + prog="phir", + description="Validates and pretty prints valid PHIR", + ) + parser.add_argument( + "jsonfile", + help="json file to validate against PHIR spec", + ) + args = parser.parse_args() + + with open(Path(args.jsonfile)) as f: # type: ignore [misc] + data = json.load(f) # type: ignore [misc] + + try: + print(PHIRModel.model_validate(data, strict=True)) # type: ignore [misc] + except ValidationError as e: + print(e) diff --git a/phir/model.py b/phir/model.py index 618beaa..4439b28 100644 --- a/phir/model.py +++ b/phir/model.py @@ -1,5 +1,8 @@ """PHIR model lives here.""" +from __future__ import annotations + +from typing import Any, Literal, TypeAlias from pydantic import BaseModel @@ -9,35 +12,32 @@ class Data(BaseModel): """Data Management Base Class.""" - metadata: dict | None = None + metadata: dict[str, Any] | None = None -class DefineVar(Data): +class VarDefine(Data): """Defining Variables.""" data_type: str | type + data: str variable: str - metadata: dict | None = None + size: int -class CVarDefine(DefineVar): +class CVarDefine(VarDefine): """Defining Classical Variables.""" - data_type: str | type - variable: str - cvar_id: int - size: int - metadata: dict | None = None + data: Literal["cvar_define"] + + cvar_id: int | None = None -class QVarDefine(DefineVar): +class QVarDefine(VarDefine): """Defining Quantum Variables.""" - data_type: str | type - variable: str - size: int - qubit_ids: list[int] - metadata: dict | None = None + data: Literal["qvar_define"] + + qubit_ids: list[int] | None = None class ExportVar(Data): @@ -45,54 +45,62 @@ class ExportVar(Data): variables: list[str] to: list[str] | None = None - metadata: dict | None = None +DataMgmt: TypeAlias = CVarDefine | QVarDefine | ExportVar + # Operations class Op(BaseModel): """Operation Base Class.""" - name: str - args: list | None = None - returns: list | None = None - metadata: dict | None = None + args: list[Any] | None = None + returns: list[Any] | None = None + metadata: dict[str, Any] | None = None class QOp(Op): """Quantum operation.""" - name: str - args: list - returns: list | None = None - metadata: dict | None = None + qop: str + args: list[list[str | int] | list[list[str | int]]] class COp(Op): """Classical operation.""" - name: str - args: list - returns: list | None = None - metadata: dict | None = None + cop: str + args: list[Any] class FFCall(COp): """External Classical Function Call.""" + cop: Literal["ffcall"] + function: str + class MOp(Op): """Machine operation.""" + mop: str + class EMOp(Op): """Error model operation.""" + # NOTE: unused + class SOp(Op): """Simulation model.""" + # NOTE: unused + + +OpType: TypeAlias = FFCall | COp | QOp | MOp + # Blocks @@ -100,23 +108,29 @@ class SOp(Op): class Block(BaseModel): """General block type.""" - metadata: dict | None = None + block: str + metadata: dict[str, Any] | None = None class SeqBlock(Block): """A generic sequence block.""" - ops: list - metadata: dict | None = None + block: Literal["sequence"] + + ops: list[OpType | BlockType] class IfBlock(Block): """If/else block.""" + block: Literal["if"] + condition: COp - true_branch: list[COp] - false_branch: list | None = None - metadata: dict | None = None + true_branch: list[OpType] + false_branch: list[OpType] | None = None + + +BlockType: TypeAlias = SeqBlock | IfBlock class PHIRModel(BaseModel): @@ -124,5 +138,5 @@ class PHIRModel(BaseModel): format_: str = "PHIR/JSON" version: str = "0.1.0" - metadata: dict | None = None - ops: list[Data | Op | Block] + metadata: dict[str, Any] | None = None + ops: list[DataMgmt | OpType | BlockType] diff --git a/pyproject.toml b/pyproject.toml index 8a6faef..0363e98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,19 +9,21 @@ description = "A python library" readme = "README.md" requires-python = ">=3.10" authors = [{name = "Kartik Singhal", email = "kartik.singhal@quantinuum.com" }] -dependencies = ["pydantic"] +dependencies = ["pydantic", "rich"] [project.optional-dependencies] tests = ["pytest"] +docs = ["sphinx", "sphinx-rtd-theme"] -docs = ["sphinx", "sphinx-rtd-theme"] - -[tool.setuptools.packages.find] -where = ["."] +[project.scripts] +phir = "phir.cli:main" [project.urls] Repository = "https://github.com/CQCL/phir.git" +[tool.setuptools.packages.find] +where = ["."] + [tool.pytest.ini_options] pythonpath = [ "." diff --git a/requirements.txt b/requirements.txt index 0db6a86..2a9e38b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ mypy==1.6.0 pre-commit==3.4.0 pydantic==2.4.2 pytest==7.4.2 +rich==13.6.0 ruff==0.0.292 sphinx==7.2.6 wheel==0.41.2 diff --git a/tests/test_model.py b/tests/test_model.py index a9f5e89..935d0e1 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -2,17 +2,12 @@ import json -from pydantic import ValidationError - from phir.model import PHIRModel def test_spec_example(): # noqa: D103 # From https://github.com/CQCL/phir/blob/main/phir_spec_qasm.md#overall-phir-example-with-quantinuums-extended-openqasm-20 with open("tests/example.json") as f: - data = json.load(f) + data = json.load(f) # type: ignore [misc] - try: - PHIRModel(**data) - except ValidationError as e: - print(e) # noqa: T201 + PHIRModel.model_validate(data) # type: ignore [misc] From 3485149559f0b478d9b92ce7bd4ecf14641f99a6 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 13 Oct 2023 12:17:07 -0500 Subject: [PATCH 5/7] Add description, update authors --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0363e98..03999fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,13 @@ build-backend = "setuptools.build_meta" [project] name = "phir" version = "0.0.1" -description = "A python library" +description = "A data model and validation tool for PHIR (PECOS High-level Intermediate Representation)." readme = "README.md" requires-python = ">=3.10" -authors = [{name = "Kartik Singhal", email = "kartik.singhal@quantinuum.com" }] +authors = [ + {name = "CiarĂ¡n Ryan-Anderson", email = "ciaran.ryan-anderson@quantinuum.com" }, + {name = "Kartik Singhal", email = "kartik.singhal@quantinuum.com" }, +] dependencies = ["pydantic", "rich"] [project.optional-dependencies] From be59f14da089a031632c36faacc891c500fb02c1 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 13 Oct 2023 12:55:34 -0500 Subject: [PATCH 6/7] Remove unneeded fields from model, add version and schema args --- phir/cli.py | 35 +++++++++++++++++++++++++++++------ phir/model.py | 4 ---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/phir/cli.py b/phir/cli.py index 2fbee5d..8a6607c 100644 --- a/phir/cli.py +++ b/phir/cli.py @@ -1,7 +1,11 @@ """PHIR validation driver.""" +# mypy: disable-error-code="misc" + import argparse +import importlib.metadata import json +import sys from pathlib import Path from pydantic import ValidationError @@ -18,14 +22,33 @@ def main() -> None: ) parser.add_argument( "jsonfile", + nargs="?", help="json file to validate against PHIR spec", ) + parser.add_argument( + "-s", + "--schema", + action="store_true", + default=False, + help="dump JSON schema of the PHIR model and exit", + ) + parser.add_argument( + "-v", + "--version", + action="version", + version=f'{importlib.metadata.version("phir")}', + ) args = parser.parse_args() - with open(Path(args.jsonfile)) as f: # type: ignore [misc] - data = json.load(f) # type: ignore [misc] + if args.schema: + print(json.dumps(PHIRModel.model_json_schema(), indent=2)) + sys.exit(1) + + if args.jsonfile: + with open(Path(args.jsonfile)) as f: + data = json.load(f) - try: - print(PHIRModel.model_validate(data, strict=True)) # type: ignore [misc] - except ValidationError as e: - print(e) + try: + print(PHIRModel.model_validate(data, strict=True)) + except ValidationError as e: + print(e) diff --git a/phir/model.py b/phir/model.py index 4439b28..c680101 100644 --- a/phir/model.py +++ b/phir/model.py @@ -29,16 +29,12 @@ class CVarDefine(VarDefine): data: Literal["cvar_define"] - cvar_id: int | None = None - class QVarDefine(VarDefine): """Defining Quantum Variables.""" data: Literal["qvar_define"] - qubit_ids: list[int] | None = None - class ExportVar(Data): """Exporting Classical Variables.""" From 86b199a44106c55be3b579c45ef5b0b143fa4d57 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 13 Oct 2023 13:11:22 -0500 Subject: [PATCH 7/7] Show help when no arguments passed to CLI --- phir/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phir/cli.py b/phir/cli.py index 8a6607c..25ebec6 100644 --- a/phir/cli.py +++ b/phir/cli.py @@ -52,3 +52,7 @@ def main() -> None: print(PHIRModel.model_validate(data, strict=True)) except ValidationError as e: print(e) + + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1)